ffmpeg开发——初探H.264

前言

前文我们分析了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的帧来获得一些有效信息解码自身。

image.png

使用开放式的GOP后,GOP的结束帧可以是B帧(因为可以前后参考了)。而GOP的起始帧可以不是I帧,而是B帧:

image.png

Closed GOP

封闭式的GOP指GOP内的帧只能引用该GOP内的帧来获得信息。

image.png

H.264的编码方式

H.264编码方式大致分为三种,帧编码场编码帧场自适应编码

帧编码

指的是把一张图像分为多个16x16的小块(宏块),然后对这些小块进行编码。这是目前互联网上的视频最普遍的编码方式

场编码

解释场编码之前需要解释一下什么是场(field),话要说到很久以前,在显像管时代,电视的画面是通过粒子打到屏幕上来显示的(大家高中都做过磁场下粒子运动的物理题吧?)。

显像管显示画面有两种方式:

  • 逐行扫描,就是粒子先打到第一行,在到第二行...直到最后一行。
    • 这种扫描方式会产生画面割裂和闪烁的问题。
  • 隔行扫描,即先打到第一行,再到第三行...直到奇数行的末尾,再回到打第二行,第四行...。
    • 这种扫描方式极大缓解了第一种方式带来的问题。

在隔行扫描的背景下,所有的奇数行和所有的偶数行被划分为两个部分,分别为顶场(top filed)和底场(bottom filed)。一帧图像可以分为两场。

image.png

于是场编码就是值得根据场来拆分成不同的宏块进行编码。这种编码方式以及这种显示方式都非常古老,不是很常见了

帧场自适应编码

简单讲就是两者混着来。

H.264的基本结构

H.264的原始数据流是由一个个NALU,结构大概如下图:

image.png

在不同的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的基本结构

image.png

注意,在上图的语法中,只有粗体的变量是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结构细化就是这样

image.png

在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的类型:

image.png

我们对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)。

image.png

那么Slice自身的结构是怎样的呢?

image.png

Slice是Header+Data的结构。

其中Header结构如下:

image.png
image.png

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部分:

image.png
  • 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)之间的关系如下

宏块的语法定义如下:

image.png
  • 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的具体的图像编码方法了,本文暂时不涉及(我还没学会)。

image.png

SPS(Sequence Paramater Set)

前面我们说到,根据nal_unit_type的不同,NALU的Body部分的数据是不一样的,我们已经分析了body为Slice的情况,接下来看看nal_unit_type=7的情况,此时NALU Body的数据应该是SPS,即一个序列图像之间的参数集合,这个图像序列就是一个GOP。

我们看关于SPS的语法定义

image.png

主要的数据集合在seq_parameter_set_data中:

image.png
image.png

对其中的部分字段进行解释:

字段 占位 含义
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,图片参数序列。

image.png
image.png

对其中部分字段进行解释

字段 占位 含义
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的编码过程的一些技术细节。

其他参考资料

https://zhuanlan.zhihu.com/p/478741699

https://blog.csdn.net/qq_42139383/article/details/119422968

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342