前言
前文我们分析了MP4文件封装格式,以及MP4文件中的AAC音频的数据格式,接下来我们需要了解一下MP4文件中的视频数据经常采用的h.264编解码方法以及其中涉及到的一些数据格式。
老规矩还是以编码标准的文档为主要参考,有H.264英文版和H.264官方中文版,个人建议主要参考英文版本,因为中文版本是很老的版本,而且翻译有一些问题,不是很准确。
除此之外还有一些相关资料我觉得写得深入浅出,非常值得学习:
H.264的历史背景
聊到H.264的历史背景,我们需要先理解一个问题:视频是否都需要进行编码?
答案是yes!
正如我们再之前的相关文章中的计算,原始的图像本来就大,合成视频就更大了,不进行编码压缩,那是谁都受不的。
因此早在1990年ITU-T(国际电信联盟电信标准化部门)就制定了第一个视频编码标准H.261。当时设计的目标就是在64kb/s的带宽下实现视频传输。
在1995年,ITU-T发布H.262标准,其内容和ISO/IEC(国际标准化组织)的发布MPEG-2的视频技术标准一致,或者说两个组织一起发布了新一代的视频编码标准,发布之后各自冠名。不过h262的使用范围并不广。
转过年ITU-T就发布H.263视频编码标准,主要用于视频会议的低码率图像传输。在过去发布的视频编码标准之上,H.263的性能有巨大提升,它的所有码率下的图像质量都好于H.261。
在2003年,ITU-T和ISO又联合发布了新一代视频编码技术标准H.264(也可称为MPEG-4-part10),设计目的主要是在更低的码率下也能实现较好的视频质量,同时能够适应各种网络环境。H.264是目前互联网是使用最广泛的编码标准。
2013年,ITU-T和ISO接着联合发布了更新一代的编码标准H.265,设计目的是在h264的基础上再次大幅提高视频压缩率(较高高分辨率的视频的压缩率是h264的两倍),从而支持4k/8k视频的传播和消费。随着4k视频的普及,H.265确实也逐渐的被大家所接受,可以说h265是未来的趋势,不过由于各种历史原因,H.264仍然是主流。
H.264作为H.26x家族中的其中一代视频编码标准,他继承了前代的优点,且有比前代更高的视频压缩率,虽然早有更新的标准等着替代它,但由于H.264是随着互联网的普及而发展的,导致它是使用最广泛,支持的设备最多的视频编码标准,因此实际上就是市场份额最大的编码标准。即使现在4k时代已经到来,H.264却还没有退场的意思。
了解一些基本概念
在真正进入H.264的学习之前,我们需要了解一些概念。
这些概念对于接下来学习理解H.264的结构有一些帮助,至少后面碰到了不至于懵逼。
关于帧的分类
我们在之前的文章中提到过一般的视频编码过程中,会把视频帧分为I帧,P帧,B帧。他们的特点如下:
帧类型 | 名称 | 含义 |
---|---|---|
I帧 | Intra-coded Frame(帧内编码帧/关键帧) | 通过自身的信息就可以解码获得一张完整的图片 |
P帧 | Predictive Frame(预测帧) | 需要参考前一帧(I-frame/P-Frame)以及自身才能解码获得一张完整图片 |
B帧 | Bidirectional predicted Frame(双向预测帧) | 需要参考前后两帧(I-frame/P-Frame)以及自身才能解码获得一张完整图片 |
压缩率:B>P>I
IDR帧(Instantaneous Decoding Refresh Frame)
在H.264编码标准中,定义了IDR帧,可以称作即时解码刷新帧
。它本质上是一个I帧,我们可以通过一个IDR帧来解码出一张图,不需要依赖其他帧。
但是它和I帧有一些细微的区别:解码器在遇到一个IDR帧之后,就立即清理参考图片缓冲区,不再参考过去已经解码的图像信息。而I帧并没有这样的要求。
这样的规定可以有效的阻止错误解码信息的蔓延
GOP(group of pictures)
既然I帧或IDR帧可以独立解析出一张图,而P帧,B帧不同程度的需要依赖前一帧或前后两帧,那么两个I帧(IDR帧)之间,天然形成一个组别。这个组别就叫GOP。
所以GOP就是指两个I帧之间的一组帧序列(严格来讲是从I帧开始,到下一个I帧之前)。
GOP的大小与视频质量
在其他条件不变的情况下,gop越大,表示帧序列中B,P帧数量越多,视频的压缩率就越高,同时视频的图像质量相对会降低。
GOP的分类
GOP可以分为开放式GOP和封闭式GOP
Open GOP
开放式GOP指GOP内的帧可以引用依赖其他GOP的帧来获得一些有效信息解码自身。
使用开放式的GOP后,GOP的结束帧可以是B帧(因为可以前后参考了)。而GOP的起始帧可以不是I帧,而是B帧:
Closed GOP
封闭式的GOP指GOP内的帧只能引用该GOP内的帧来获得信息。
H.264的编码方式
H.264编码方式大致分为三种,帧编码
,场编码
,帧场自适应编码
。
帧编码
指的是把一张图像分为多个16x16的小块(宏块),然后对这些小块进行编码。这是目前互联网上的视频最普遍的编码方式
场编码
解释场编码之前需要解释一下什么是场(field),话要说到很久以前,在显像管时代,电视的画面是通过粒子打到屏幕上来显示的(大家高中都做过磁场下粒子运动的物理题吧?)。
显像管显示画面有两种方式:
-
逐行扫描
,就是粒子先打到第一行,在到第二行...直到最后一行。- 这种扫描方式会产生画面割裂和闪烁的问题。
-
隔行扫描
,即先打到第一行,再到第三行...直到奇数行的末尾,再回到打第二行,第四行...。- 这种扫描方式极大缓解了第一种方式带来的问题。
在隔行扫描的背景下,所有的奇数行和所有的偶数行被划分为两个部分,分别为顶场(top filed)和底场(bottom filed)。一帧图像可以分为两场。
于是场编码就是值得根据场来拆分成不同的宏块进行编码。这种编码方式以及这种显示方式都非常古老,不是很常见了。
帧场自适应编码
简单讲就是两者混着来。
H.264的基本结构
H.264的原始数据流是由一个个NALU,结构大概如下图:
在不同的NALU之间通过startcode来分隔,startcode有两个取值0x00000001(4Byte)或0x000001(3Byte),前一个取值表示接下来的NALU中的Slice为一帧图像的开始,否则就用后一个取值来分隔NALU。
NALU
那么,我们就要问什么是NALU?,它的全称是网络抽象层单元(Network Abstraction Layer Unit),听起来有点像TCP网络分层结构,但你还真别说,在功能是比较类似的。我们前面说到h.264设计的目的之一就是拥有优良的网络适配性,因此再H.264中是有一个网络抽象层的,用于帮助视频流数据适应不同的网络协议,此外还有一个VCL(视频编码层)用于处理H.264的正事儿:对视频进行编码(此处先按下不表)。
我们先来看一下NALU的基本结构
注意,在上图的语法中,只有粗体的变量是NALU的内部结构。
我们逐个分析NALU的结构
字段 | 占位 | 含义 |
---|---|---|
forbidden_zero_bit | 1bit | 固定为0 |
nal_ref_idc | 2bit | 当前NAL单元的优先级,最低为0 |
nal_unit_type | 5bit | 表明当前NALU的类型 |
svc_extension_flag | 1bit | 略 |
avc_3d_extension_flag | 1bit | 略 |
rbsp_byte | 字节数组 | RBSP,原始字节序列载荷 |
emulation_prevention_three_byte | 8bit | 防止竞争,避免码流数据与特定bit数据发生重叠 |
-
emulation_prevention_three_byte
- 放防止竞争的字段,什么意思呢?我们再前文中说过NALU之间通过start_code来分隔,那么假如NALU内部出现了0x000001或者0x00000001这样的数据该怎么办呢?当输出出现连续两个0字节时(0x0000),在后面插入0x03。从而避免了这种错误识别,在解析时则在数据中把0x03去掉。
0x000000 => 0x00000300
0x000001 => 0x00000301
0x000002 => 0x00000302
0x000003 => 0x00000303
RBSP(raw byte sequence payload)
原始字节序列载荷,就是NALU中装载编码数据的结构,在语法定义中主要包含rbsp_byte+emulation_prevention_three_byte。如果我们按照header+body来理解的话,上文表中的前五个字段都可以归属于header,RBSP可以称作Body。
那么把NALU结构细化就是这样
在H264标准的文档中其实并没有划分NALU Header这种结构
SODB/RBSP/EBSP
这部分属于概念的差异,实际的差别并不大,他们的全称分别是
- SODB(String Of Data Bits),原始数据比特流
- RBSP(raw Byte Sequence Payload),原始字节序列载荷
- EBSP(Encapsulated Byte Sequence Payload) 拓展字节序列载荷
而这三者之间的关系如下:
RBSP = SODB + rbsp_trailing_bits(末尾的字节对齐)
EBSP = RBSP + 插入的防止竞争字段(0x03)
因此,严谨的讲,我们其实不能认为NALU Body部分就是RBSP,因为也可能是EBSP(如果插入了防竞争字段的话)。不过为了简单起见,我们下面还是统称为RBSP,具体情况大家清楚就好。
RBSP的类型
前面我们说到nal_unit_type表示NALU的类型,其实主要表示RBSP的类型:
我们对nal_unit_type的其中一些重要的类型进行分列解释:
nal_unit_type | NAL单元和RBSP语法结构的内容 | 含义解释 |
---|---|---|
1 | 非IDR图像的编码片段 slice_layer_without_partitioning_rbsp() | |
2 | 编码片段的分区A slice_data_partition_a_layer_rbsp() | 编码片段的ABC分区主要用于对数据的重要性排序,重要性排序A>B>C |
3 | 编码片段的分区B slice_data_partition_b_layer_rbsp() | 参考上文 |
4 | 编码片段的分区C slice_data_partition_c_layer_rbsp() | 参考上文 |
5 | IDR图像的编码片段 slice_layer_without_partitioning_rbsp() | |
6 | 补充改进信息 sei_rbsp() | |
7 | 序列参数集合 seq_parameter_set_rbsp() | 为整个图片序列(两个IDR图像之间的图片序列)提供信息,如图像尺寸,视频格式 |
8 | 图像参数集合 pic_parameter_set_rbsp() | 为一个图片或几张图的Slice提供信息 |
9 | 访问单元分隔符 access_unit_delimiter_rbsp() |
在RBSP这个层级之下,数据还能不能继续往下划分?可以。某些类型(nal_unit_type=1...5)的RBSP内部实际就是Slice。
Slice
什么是Slice?我们可以理解为编码图片中的一个片段(碎片),一帧视频图像可编码成一个或者多个 片段
(Slice)。
那么Slice自身的结构是怎样的呢?
Slice是Header+Data的结构。
其中Header结构如下:
Slice Header内部的字段太多,挑一些重要的解释一下
字段 | 含义 |
---|---|
first_mb_in_slice |
当前slice的第一个宏块在图像中的位置(什么是宏块?往下看) |
slice_type |
当前slice的类型(I,B,P,SI,SP) |
pic_parameter_set_id |
当前slice所使用的PPS的id(PPS看下文) |
frame_num |
当前 Slice 所属的帧的帧号 |
idr_pic_id |
IDR图像的标识。IDR图像中所有Slice的 idr_pic_id 值应保持不变 |
然后我们看一下data部分:
- cabac_alignment_one_bit 字节对齐相关
- mb_skip_run 挑过多少个宏块
- mb_field_decoding_flag 略
- end_of_slice_flag slice 结束标识
关于slice data部分字段较少,而逻辑很多,我们就明白真正的数据也不在Slice这一个层级,而在它的下一级,就是我们前面提到的宏块(macro block),也就是语法定义中的macroblock_layer方法中。
macroblock(宏块)
宏块是对Slice的细分,是视频图像信息的主要承载者,也是图像编码过程的主要被操作对象。一个编码图像由一个或多个Slice组成,一个Slice通常由为多个宏块承载.宏块包含着图像像素的亮度和色度信息(通常宏块包含16x16的YUV阵列)。
如果需要,宏块还可以拆分成更小的子块。如:16x8、8x16、8x8、.. 4x4。
视频解码最主要的工作则是从码流中获得宏块内的像素阵列。
关于视频帧,切片(Slice),宏块(macro block)之间的关系如下
宏块的语法定义如下:
- mb_type 当前宏块的类型,根据不同的slice类型(I,B,P)可以划分出不同的宏块类型
- pcm_alignment_zero_bit I_PCM模式下的字节对齐
- pcm_sample_luma I_PCM模式下的亮度信息,16x16
- pcm_sample_chroma I_PCM模式下的色度信息,根据YUV格式不同,阵列大小不同(YUV420,为两个8x8阵列)
- transform_size_8x8_flag 略
- coded_block_pattern 略
- mb_qp_delta 略
- mb_pred(mb_type) 宏块预测的语法定义
- residual( 0, 15 ) 残差编码的语法定义
我们可以看到在宏块的定义下,还有其他的方法,说明宏块之下还有更多的细节,但这就涉及到了H264的具体的图像编码方法了,本文暂时不涉及(我还没学会)。
SPS(Sequence Paramater Set)
前面我们说到,根据nal_unit_type的不同,NALU的Body部分的数据是不一样的,我们已经分析了body为Slice的情况,接下来看看nal_unit_type=7的情况,此时NALU Body的数据应该是SPS,即一个序列图像之间的参数集合,这个图像序列就是一个GOP。
我们看关于SPS的语法定义
主要的数据集合在seq_parameter_set_data中:
对其中的部分字段进行解释:
字段 | 占位 | 含义 |
---|---|---|
profile_idc | 8bit | 编码视频序列符合的配置 |
seq_parameter_set_id | 当前参数集的id | |
max_num_ref_frames | 参考帧的大小 | |
pic_width_in_mbs_minus1 | 用于计算图片宽度, | |
pic_height_in_map_units_minus1 | 用于计算图片高度 | |
frame_mbs_only_flag | 1bit | 说明宏块的编码方式, 0:宏块可能为帧编码或场编码,1:所有宏块都采用帧编码 |
mb_adaptive_frame_field_flag | 1bit | 是否存在宏块级的帧场自适应编码,0:不存在,1:可能存在 |
其中pic_width_in_mbs_minus1和pic_height_in_map_units_minus1分别和图片的宽高的计算有关。
对于图片的宽度计算如下:
// 图片宽度(用宏块数据来作为单位)
PicWidthInMbs = pic_width_in_mbs_minus1 + 1
// 色图图片宽度 (以色度分量导出为例) MbWidthC是色度块的宽度
PicWidthInSamplesC = PicWidthInMbs * MbWidthC
// 图像宽度(以亮度Y分量导出为例,亮度分量的宽度和图片的宽度一致)
PicWidthInSamplesL = PicWidthInMbs * 16
高度计算如下:
PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1
// 根据frame_mbs_only_flag的取值(一般就是1)来求取图片高度(以宏块为单位)
FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) * PicHeightInMapUnits
// 图片高度 (宏块数x宏块高度)
PicWidthInSamplesL = FrameHeightInMbs * 16
PPS(Picture Paramater Set)
PPS,图片参数序列。
对其中部分字段进行解释
字段 | 占位 | 含义 |
---|---|---|
pic_parameter_set_id | 当前PPS的ID,slice引用PPS的方式就是在Slice header中保存PPS的id值 | |
seq_parameter_set_id | sps的id | |
entropy_coding_mode_flag | 熵编码模式标识 | |
num_slice_groups_minus1 | slice group的个数(num_slice_groups_minus1+1) |
数据结构分层的必要性
上文我们从NALU一直逐层分析直到marcoblock:这个H.264编码操作的基本单元为止。
现在我们需要问自己一个问题,那就是为什么数据要分这么多的层级?不要行不行?直接把视频数据流分为无数个marcoblock行不行?
只有回答了这个问题,我们才能说对H.264有了一个基本的理解。
答案当然是no。我们可以回想TCP5层模型,为什么需要分5层?就是因为把TCP的功能拆分之后,让不同的层独立完成不同的功能,相互之间可以互不干扰。这就是良好的设计。
我们回到H.264编码标准,数据层级是从 视频图像序列 ——> NALU ——> Slice ——> macro block。那么每一层的作用是什么呢?
- NALU 这个层级对视频的字节序列进行分类:视频帧数据以及非视频帧数据(视频的相关信息)
- Slice 需要在编码数据内保存一些全局信息,给编解码过程提供支持。
- macro block 编解码过程中可以直接操作的数据层级。
严格来讲,SODB,RBSP,EBSP也可以算作三个层级,不过他们之间的差异太小了。
总结
虽然本文完全不涉及到H.264视频编码的技术细节,但仅仅对H.264编码标准的整体结构进行描述都非常耗费精力,里面的概念太多了,而且官方标准文档又非常晦涩。所幸到这里我认为算是对H.264有了一个整体的理解。
个人预计还会再整理一篇文章,关于H.264的编码过程的一些技术细节。