简书地址 : H.264 Data Structure
博客地址 : H.264 Data Structure
掘金地址 : H.264 Data Structure
Why ?
相信在你的电脑里,一定存有一些已经下载好的视频文件,如果你硬说没有,那我相信你曾经总有吧?曾经也没有?那我想对你说曾经免费的时候你不下载,直到电影都收费才后悔那些年错过下载的大片。
好了,言归正传,在日常我们一定见过很多后缀为avi, mp4, rmvb, flv等格式的视频文件。而很少有人真正挖掘这些文件到底是什么?其实以上格式都是封装视频的封装格式。
什么是封装格式 ?
把音频数据和视频数据打包成一个文件的规范。不用封装格式差距不大, 各有千秋。
从视频播放器播放一个互联网上的视频文件
需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文件则不需要解协议,其他步骤相同。
- 解协议:流媒体协议的数据,解析为标准的相应的封装格式数据
- 解封装:将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。
- 解码:就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。
- 视音频同步:就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。
为什么要对视频数据进行编码
视频编码的主要作用是将视频像素数据(RGB,YUV等)压缩成视频码流,从而降低视频的数据量。举个例子:比如当前手机的屏幕分辨率是1280 * 720(即我们平时在视频软件中可选的720P),假设一秒钟30帧(即1秒钟传输30张图片),那么一秒钟的数据为 1280 * 720(位像素)*30(张) / 8(1字节8位)(结果B)
,也就是一秒钟的数据量为3.456M数据量,一分钟就是207.36M,那么我们平常看一部电影就是大约18G的流量,试想下如果是这样对于存储即网络传输是件多么恐怖的事情。
正是因为以上原因,我们需要对视频数据进行编码,以最小程序减小清晰度与最大程序降低数据量,而H264正是目前广泛使用的一种编码格式,下面我们将主要介绍下H264的码流结构。
码流结构
刷新图像概念
在我们的印象中,一张图片就是一张图像,而在H264中图像是个集合的概念。
帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码。逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好。
H264原始码流
- 结构:由一个接一个的 NALU 组成的,而它的功能分为两层,VCL(视频编码层)和 NAL(网络提取层).
- VCL:包括核心压缩引擎和块,宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码。
- NAL:负责将VCL产生的比特字符串适配到各种各样的网络和多元环境中,覆盖了所有片级以上的语法级别。
- 组成:NALU (Nal Unit) = NALU头 + RBSP
在 VCL 数据传输或存储之前,这些编码的 VCL 数据,先被映射或封装进 NAL 单元(以下简称 NALU,Nal Unit) 中。每个 NALU 包括一个原始字节序列负荷(RBSP, Raw Byte Sequence Payload)、一组 对应于视频编码的 NALU 头部信息。RBSP 的基本结构是:在原始编码数据的后面填加了结尾 比特。一个 bit“1”若干比特“0”,以便字节对齐。
一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成
StartCode : Start Code 用于标示这是一个NALU 单元的开始,必须是”00 00 00 01” 或”00 00 01”
NALU Header
下表为 NAL Header Type
例如:
00 00 00 01 06: SEI信息
00 00 00 01 07: SPS
00 00 00 01 08: PPS
00 00 00 01 05: IDR Slice
- RBSP :NAL包将其负载数据存储在 RBSP(Raw Byte Sequence Payload) 中,RBSP 是一系列的 SODB(String Of Data Bits)。
- 一帧图片跟NALU的关联:
一帧图片经过 H.264 编码器之后,就被编码为一个或多个片(slice),而装载着这些片(slice)的载体,就是 NALU 了。
注意:片(slice)的概念不同与帧(frame),帧(frame)是用作描述一张图片的,一帧(frame)对应一张图片,而片(slice),是 H.264 中提出的新概念,是通过编码图片后切分通过高效的方式整合出来的概念,一张图片至少有一个或多个片(slice)。片(slice)都是又 NALU 装载并进行网络传输的,但是这并不代表 NALU 内就一定是切片,这是充分不必要条件,因为 NALU 还有可能装载着其他用作描述视频的信息。
什么是切片(slice)?
片的主要作用是用作宏块(Macroblock)的载体(ps:下面会介绍到宏块的概念)。片之所以被创造出来,主要目的是为限制误码的扩散和传输。
那么片(slice)的具体结构,我们用一张图来直观说明吧:
上图结构中,我们不难看出,每个分片也包含着头和数据两部分:
- 分片头中包含着分片类型、分片中的宏块类型、分片帧的数量、分片属于那个图像以及对应的帧的设置和参数等信息。
- 分片数据中则是宏块,这里就是我们要找的存储像素数据的地方。
什么是宏块?
宏块是视频信息的主要承载者,它包含着每一个像素的亮度和色度信息。视频解码的主要工作则是提供高效的方式从码流中获得宏块中的像素阵列。
宏块的组成:一个宏块由一个16*16亮度像素和附加的一个8 * 8Cb和一个8 * 8Cr彩色像素块组成。每个图像中,若干宏块被排列成片的形式。
下面是宏块的结构图:
切片(slice)类型跟宏块类型的关系
切片(slice)来讲,分为以下几种类型:
P-slice. Consists of P-macroblocks (each macro block is predicted using one reference frame) and / or I-macroblocks.
B-slice. Consists of B-macroblocks (each macroblock is predicted using one or two reference frames) and / or I-macroblocks.
I-slice. Contains only I-macroblocks. Each macroblock is predicted from previously coded blocks of the same slice.
SP-slice. Consists of P and / or I-macroblocks and lets you switch between encoded streams.
SI-slice. It consists of a special type of SI-macroblocks and lets you switch between encoded streams.
I片:只包 I宏块,I 宏块利用从当前片中已解码的像素作为参考进行帧内预测(不能取其它片中的已解码像素作为参考进行帧内预测)。
P片:可包 P和I宏块,P 宏块利用前面已编码图象作为参考图象进行帧内预测,一个帧内编码的宏块可进一步作宏块的分割:即 16×16、16×8、8×16 或 8×8 亮度像素块(以及附带的彩色像素);如果选了 8×8 的子宏块,则可再分成各种子宏块的分割,其尺寸为 8×8、8×4、4×8 或 4×4 亮度像素块(以及附带的彩色像素)。
B片:可包 B和I宏块,B 宏块则利用双向的参考图象(当前和 来的已编码图象帧)进行帧内预测。
SP片(切换P):用于不同编码流之间的切换,包含 P 和/或 I 宏块
SI片:扩展档次中必须具有的切换,它包含了一种特殊类型的编码宏块,叫做 SI 宏块,SI 也是扩展档次中的必备功能。
整体结构
H.264的码流结构并没有那么复杂,编码后视频的每一组图像(GOP,图片组)都给予了传输的序列(PPS)和本身这个帧的图像参数(SPS),所以,整体给够如下
GOP 图像组主要形容一个I帧到下一个I帧之间间隔了多少帧,增大图片组能有效的减少编码后视频的体积,但是也会降低视频质量,至于怎么取舍,得看需求。
NALU头部的类型
enum nal_unit_type_e
{
NAL_UNKNOWN = 0, // 未使用
NAL_SLICE = 1, // 不分区、非 IDR 图像的片(片的头信息和数据)
NAL_SLICE_DPA = 2, // 片分区 A
NAL_SLICE_DPB = 3, // 片分区 B
NAL_SLICE_DPC = 4, // 片分区 C
NAL_SLICE_IDR = 5, / ref_idc != 0 / // IDR 图像中的片
NAL_SEI = 6, / ref_idc == 0 / // 补充增强信息单元
-
参数集是 H.264 标准的一个新概念,是一种通过改进视频码流结构增强错误恢复能力的方法。
NAL_SPS = 7, // 序列参数集 (包括一个图像序列的所有信息,即两个 IDR 图像间的所有图像信息,如图像尺寸、视频格式等)
NAL_PPS = 8, // 图像参数集 (包括一个图像的所有分片的所有相关信息, 包括图像类型、序列号等,解码时某些序列号的丢失可用来检验信息包的丢失与否)
-
NAL_AUD = 9, // 分界符
NAL_FILLER = 12, // 填充(哑元数据,用于填充字节)
/ ref_idc == 0 for 6,9, 10 (表明下一图像为 IDR 图像),11(表明该流中已没有图像),12 /
};
ps: 以上括号()中的为类型描述
补充说明
- I,P,B 帧 与 pts / dts
I帧 | P帧 | B帧 |
---|---|---|
帧内编码帧 | 前向预测编码帧 | 双向预测编码帧 |
I帧通常是每个GOP的第一帧,经过适度压缩,作为随机访问的参考点,可看成一个图片经过压缩后的产物 | 通过充分低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据编码图像,也叫预测帧 | 既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧 |
I,P,B帧
- I frame : 自身可以通过视频解压算法解压成一张单独完整的图片
- P frame : 需要参考其前面的一个I frame 或者B frame来生成一张完整图片
- B frame : 既要参考其前一个I frame 或者 P frame以及其后一个P frame来生成一张完整的图片。
DTS,PTS
- PTS :PTS主要用于度量解码后的视频什么时候被显示
- DTS :DTS主要是标识内存中的Bit流什么时候开始送入解码器进行解码
GOP
GOP是画面组,一个GOP是一组连续的画面。
GOP一般有两个数字,如M=3,N=12.M制定I帧与P帧之间的距离,N指定两个I帧之间的距离。那么现在的GOP结构是
I BBP BBP BBP BB I
IDR
一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。
I和IDR帧都使用帧内预测。I帧不用参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。IDR就不允许这样。
- 核心作用 : H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。
帧内预测和帧间预测
- 帧内预测(也叫帧内压缩)
我们可以通过第 1、2、3、4、5 块的编码来推测和计算第 6 块的编码,因此就不需要对第 6 块进行编码了,从而压缩了第 6 块,节省了空间
可以看到前后两帧的差异其实是很小的,这时候用帧间压缩就很有意义。
这里涉及到几个重要的概念:块匹配,残差,运动搜索(运动估计),运动补偿.
帧间压缩最常用的方式就是块匹配(Block Matching)。找找看前面已经编码的几帧里面,和我当前这个块最类似的一个块,这样我不用编码当前块的内容了,只需要编码当前块和我找到的快的差异(残差)即可。找最想的块的过程叫运动搜索(Motion Search),又叫运动估计。用残差和原来的块就能推算出当前块的过程叫运动补偿(Motion Compensation).