添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
阅读16分钟

前文我们分析了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?,它的全称是网络抽象层单元(Network Abstraction Layer Unit),听起来有点像TCP网络分层结构,但你还真别说,在功能是比较类似的。我们前面说到h.264设计的目的之一就是拥有优良的网络适配性,因此再H.264中是有一个网络抽象层的,用于帮助视频流数据适应不同的网络协议,此外还有一个VCL(视频编码层)用于处理H.264的正事儿:对视频进行编码(此处先按下不表)。

    我们先来看一下NALU的基本结构

    注意,在上图的语法中,只有粗体的变量是NALU的内部结构。 Descriptor栏目下表示字段的占位,具体含义解析见文档第42页

    我们逐个分析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_typeNAL单元和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()参考上文
    5IDR图像的编码片段 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_idIDR图像的标识。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_idc8bit编码视频序列符合的配置
    seq_parameter_set_id当前参数集的id
    max_num_ref_frames参考帧的大小
    pic_width_in_mbs_minus1用于计算图片宽度,
    pic_height_in_map_units_minus1用于计算图片高度
    frame_mbs_only_flag1bit说明宏块的编码方式, 0:宏块可能为帧编码或场编码,1:所有宏块都采用帧编码
    mb_adaptive_frame_field_flag1bit是否存在宏块级的帧场自适应编码,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_idsps的id
    entropy_coding_mode_flag熵编码模式标识
    num_slice_groups_minus1slice 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的编码过程的一些技术细节。

    其他参考资料

    zhuanlan.zhihu.com/p/478741699

    blog.csdn.net/qq_42139383…