写在前面,在学习一个新的事物的时候,可以先大致的了解一下它的历史,这样你可以有一个薄薄的基础认识,和该事物相关联的东西也会顺带了解,而且还很有趣何乐而不为呢。
计算机中很多的东西都是经过一代一代的迭代发展而来,h264 也并不例外。从计算机的视频发展开始就面临存储和传输的问题。未经压缩的视频数据量非常的大。在颜色模型这篇文章里我们已经计算一次未压缩图像传输的大小。视频编解码器 H.261 诞生在 1990(技术上是 1988),被设计为以 64 kbit/s 的数据速率工作。它已经使用如色度子采样、宏块,等等理念。在 1995 年,H.263 视频编解码器标准被发布,并继续延续到 2001 年。
image.png

封装

说到编码之前我们需要先和封装格式区分一下。平时用户接触到的文件都是.mp4、.mov 等等视频文件。它们并不是编码的格式。这些文件格式都是视频编码,音频编码二进制的容器。所谓容器那一定是有容量和内部布局和一个收纳的盒子是一样的,盒子左边放的是音频,右边放的是视频。容器还有一个作用是将音频和视频以一定的格式打包在一起。容器内部的布局也不同,有的编码格式支持,有的则不支持,比如早年的 rmvb 格式里存放的是 RV40视频编码和 cook音频编码,mp4文件里通常放的是 H264的编码视频和 AAC 编码的音频。

H264 与 VP8

H264是由国际电信联盟通电信标准部 (ITU-T) 和国际标准化组织/国际电工委员会动态图像专家组 (ISO/IEC MPEG) 共同开发的一种视频压缩技术。所以它的另一个名字是 MPEG-4 AVC。所以在H264的参数中可以看到 AVC == H264,而 HECV == H265。参考 WiKi

目前争论 WebM 和 H.264 孰优孰劣的重点在于 WebM 虽然免费,但可能有潜在的专利问题,且由于没有硬件解码器无法在便携设备上使用;H.264 专利问题明朗,技术相对先进,有硬件解码器,且很多便携设备已经在广泛使用了,但需要付费。

H.264 涉及到很多专利技术,这些专利又由不同的组织和个人分别持有。如果有厂商要使用 H.264,显然他不可能一个一个去和这些专利持有人谈判。为了方便 H.264 的推广,持有 H.264 相关专利的组织和个人将其专利授权给一个名为 MPEG LA 的组织,然后由该组织统一收取 H.264 相关专利的授权费。

MPEG LA 将 H.264 专利许可分为两种:H.264 编码器和解码器(不论软硬件)厂商需要购买一种 H.264 的专利许可协议,H.264 编码的视频的分发商(如电视台等)需要购买另外一种 H.264 的专利许可协议。也就是说所有生产支持录制 H.264 视频的摄像设备、手机等的厂商都要向 MPEG LA 支付专利使用费。这个费用相对低廉:少于 10 万个设备免费;10 万个以上最多每个解码器 20 美分,且支付上限为每年 650 万美元。另外,MPEG LA 有规定,虽然每五年专利授权期过后它可以根据市场状况调整授权费用,但每次涨价不会超过 10%。因此厂商使用 H.264 制造编码器、解码器在成本上是有可靠保证的。所有支持 H.264 视频播放的硬件、软件厂商也要向 MPEG LA 支付专利费。大部分企业如苹果、微软、索尼等都购买了相应的 H.264 专利许可协议,因此最终用户在使用 H.264 技术制作和播放视频的时候不用担心费用问题。

VP8 是谷歌旗下的一个视频标准,由于 H264 授权费的问题,谷歌认为此项不利于互联网长期的发展。Google 在2010年初收购了 On2 及其旗下全部的视频压缩技术,在年中的时候宣布VP8永远免费,在 WebRTC 中默认的视频编码就是 VP8。WebM 支持 VP8 视频编码和音频 Vorbis 编码(另一个免费媲美 AAC 的音频编码)封装。
本节的扩展阅读在这里

H264 编码结构

H264 的文件是也是二进制文件一种。我摘一段看一下:

  1. 0000 0001 2764 0033 ad00 ce80 7802 27e5
  2. 9a80 8080 f800 0003 0008 0000 0300 f1a8
  3. 0053 4000 3e70 6fff e050 0000 0001 28ee
  4. 3cb0 0000 0001 25b8 4001 db80 2b1b 38f8
  5. 4a70 6a11 2ba9 317c f030 48e9 1d1d cb4a
  6. d95c 6544 12c2 9c51 7a49 6735 d20a 3b11
  7. ...

上面的文件格式是换成16进制后,每四个数字是两个字节。现在看起来很懵,但是等你学习完它的语法后就能看懂了,这和学英语没有区别。
学习音视频也是有知识层次的,就像你需要先学加减法,后学乘除法最后学方程式是一样的。
H264 编码结构 -> FLV 格式封装 -> RTMP 流格式分析。
H264 编码格式多参考标准,可以在参考文章集合中找到。H264内容庞大,本文无法面面俱到,主要讲解的是结构。在了解概念后详细的内容参数多看标准中的文档。参考文章可以看这里。
视频参考文章

H264 的结构

流的种类

H264 分成两种流格式,一种是 Annex-B 格式,一种是 RTP 包流的格式。

  • Annex-B 格式是默认的输出格式。数据单元(NALU 单元下面会讲到)的分割使用0x000001或0x00000001 作为起始码。
  • RTP 包的格式不用上面的起始码做分割,而是使用长度+单元+长度+单元的方式,观看 RTMP 协议里的 H264 流就采用这种格式。

这两种格式使用 MediaInfo 是可以看到区别的,只注意标记的字段,其他的可以先忽略。
Annex-B
Screen Shot 2021-02-07 at 11.37.55 AM.png
RTP 包格式
Screen Shot 2021-02-07 at 11.41.14 AM.png
二者的区分很小,就是视频数据单元分割不同。注意一下区分。

视频单元结构

结构主要单元

了解结构前我们需要先了解都有那些主要单元。H264中的数据按大类分分成视频数据和视频参数数据,再细分分成 序列参数数据,图像参数数据和视频数据。之后你可以看到 VCL 数据和 NAL 数据的名词,这个就是按大类分的。

结构引用

我放了一张 H264 结构引用图
image.png

引用图

引用在视频编码中的表现是
Untitled Diagram.svg
可以看到片引用了图像参数集的索引,它们的编码参数由图像参数定义,这些h264 的单元在流中以上面的结构循环构成。在上面的引用关系中我们也可以看到引用的方在时间上必须要先被发送,打开一个 h264码流时可以看到序列参数集和图像参数集处于码流的最前面。
注意的是,相对视频标准H264 取消了图像层,片成为携带图像像素数据,所以每个片都必须携带所属的图像的编号,大小等信息,这些片组成同一个图像。

一个视频图像可编码成一个或更多个片,每片包含整数个宏块(MB),即每片至少一个 MB,最 多时每片包含整个图像的宏块。总之,一幅图像中每片的宏块数不一定固定。

视频的画面类型

类型划分

从 NAL 的类型上划分如下
image.png
序列的第一个图像是 I 帧,也是 IDR 帧(立即刷新图像)。H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列 清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果在前一 个序列的传输中发生重大错误,如严重的丢包,或其他原因引起数据错位,在这里可以获得重新同步。 IDR 图像之后的图像永远不会引用 IDR 图像之前的图像的数据来解码。 要注意 IDR 图像和 I 图像的区别,IDR 图像一定是 I 图像,但 I 图像不一定是 IDR 图像。一个序列中可以有很多的 I 图像,I 图像之后的图像可以引用 I 图像之间的图像做运动参考。
总结而言IDR 是一种特殊的 I 帧。在 H264 中 IDR帧除了提供原始 I 帧参考帧功能,IDR 帧还提供了编码参数刷新,错误阻断的重要作用。

视频的单元

上面大致的分了视频的结构后,我们知道一个 h264的Annex-B流是下面这个样子。下面进入比较复杂的 h264 核心结构部分。
nalus.svg
startcode 是0x000001。NALU 就是 h264的实际数据部分。NALU 自身由 NALUHeader+EBSP 组成。EBSP = 防止竞争码+RBSP 。

EBSP为扩展字节序列载荷(Encapsulated Byte Sequence Payload),RBSP为原始字节序列载荷(Raw Byte Sequence Payload) 一些字节序中用的 startcode 是0x00000001,这是因为二进制需要考虑字节对齐问题,h264规定长度不够时使用0补齐。

防止竞争码

由于 startcode 0x000001的存在,导致如果 NALU 中如果也存在这些个字节会导致 NALU 分割错误!(试想一下如果0x000001出现在一个片的编码中,这个片会被拆成两部分),对于这个问题,H264 提出如果在 NALU 的内部如果出现以上的字节在它的前面插入一个新的字节0x03。这样原来的0x000001将变成0x00000301。在 h264中还规定了0x000000可以作为 NALU 的结尾,0x000002 是保留字节,0x000003 可能在内部也有。

想一下C 语言中 Printf 中如果输出 % 怎么办?

因此下面是几个防止竞争码处理后的结果。
image.png

在解码的时候如果在内部遇到0x000003序列时,就可以将其抛弃即可以恢复原始数据。EBSP 去除防止竞争码后就可以得到 RBSP。

NALU Header

先看一下 NALU 的头部有那些
image.png
上面是截取的 NAL 层的语法头部部分。如果先不考虑语法,可以先如下理解,将第一个字节(1+2+5 正好是一个字节)按 bits 展开。

  • 第一位为 forbidden_zero_bit
  • 后两位为 nal_ref_idc
  • 最后五位为 nal_unit_type
  1. forbidden_zero_bit 正常为0,当网络传输过程发生错误的时候当前 NALU 可能存在错误,编码器可以丢弃
  2. nal_ref_idc 代表 NALU 的重要性。值越大说明约重要。取值范围0~3。当当前的 NAL 是参考帧,序列集参数集或图像集重要数据时必须大于0。
  3. nal_unit_type 指的是当前 NAL 的类型。前面我们已经看到了三种基本类型,具体的类型参考下表

image.png
先看序列参数集为 7,图像参数集为8,简单的使用掩码方式就可以求得 &1f。比如上面H264 编码结构中的二进制 0x27 & 0x1f = 7。也就是说明是序列参数集。1~5 为片的类型。关键的 IDR 片在上文有说明。6 为 SEI 补充增强信息帧,在这里可以自定义数据,参看这里。9代表 Access Unit,是一个完整帧的集合。10 是序列的结束。

  1. 问题 NALU 1~5 是否可以代表一个完整的帧?

答:这个问题待定。5 是一个完整的帧,但是1~4并不一定是一个完整的帧。参考这里

多个NAL-units组成一个access unit。多个access unit再组成一个Coded video sequence。

NALU Body

去掉防竞争码后,我们得到了 RBSP。下一步就是如何从 RBSP 中获取原始的编码数据SODP(String Of Data Bits)。
RBSP = SODB + RBSP 尾部。
对于 RBSP 尾部分成两种类型:

  • 非1到5的 NALU 类型 SODB+停止位1+若干个0字节对齐
  • 条带 NALU 类型 SODB+停止位1+若干个0字节对齐,如果是 CABAC 的编码方式且有更多的数据时,后面再补充一个或多个0x00。

总结一下视频单元的结构nalu-struct.drawio
sodb.svg
想理解H264是怎么解析二进制的需要了解h264 句法。H264 的语法采用了类 C 的语言,在标准的第52页开始。
例如下面是对 NALU 头和 body 部分的解析。本文不做详细讲解,另建一个新文章。
image.png

H264 单元类型

h264 的单元类型有很多,主要的类型有如下sps,pps,idr,slice,sei 类型。简单介绍一下这几个类型和他们的关键参数,具体各个参数的定义可以查看h264标准文档,在参考文章中。
视频参考文章
image.png
H264 的 RBSP 举例。
Screen Shot 2021-02-08 at 9.45.04 AM.png

SPS

全称 Sequence Paramater Set,nal_unit_type值为7,中文名字为”序列参数集”。SPS 中保存了一下编码序列的全局参数。

  • profile_idc、 level_idc 指明所用 profile、level。profile 一般分成三个等级如下:

    1) 基本档次:利用 I 片和 P 片支持帧内和帧间编码,支持利用基于上下文的自适应的变长编码进行的熵编码(CAVLC)。主要用于可视电话、会议电视、无线通信等实时视频通信; 2) 主要档次:支持隔行视频,采用 B 片的帧间编码和采用加权预测的帧内编码;支持利用基于上下文的自适应的算术编码(CABAC)。主要用于数字广播电视与数字视频存储; 3) 扩展档次:支持码流之间有效的切换(SP 和 SI 片)、改进误码性能(数据分割),但不支持隔行视频和 CABAC。主要用于网络的视频流,如视频点播;

各种 level 等级查看这里

  • seq_parameter_set_id 指明本序列参数集的 id 号,这个 id 号将被 picture 参数集引用
  • log2_max_frame_num_minus4 读取另一个句法元素 frame_num 服务的, frame_num 是最重要的句法元素之一,它标识所属图像的解码顺序。
  • pic_order_cnt_type 指明了 poc (picture order count) 的编码方法,poc 标识图像的播放顺序。

更多的信息查看标准的第55页和各自的定义第80页。
image.png

PPS

全称为Picture Paramater Set,它的nal_unit_type值为8。包含图像的参数如熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等等。配合 SPS 实现编码器的配置。

  • pic_parameter_set_id pps 的 id。该 id 会被码流中的片进行引用。
  • seq_parameter_set_id pps 引用的激活的SPS
  • entropy_coding_mode_flag 指明熵编码的选择,为0时表示使用 CAVLC,为1 时代表使用 CABAC。
  • pic_order_present_flag 计算 poc 用。

其他的查看标准第57页 7.3.2.2和86页 7.4.2.2 节。

IDR

全称为(Instantaneous Decoding Refresh),它的nal_unit_type值为5上面已经提到,IDR 在 H264中的关键作用。在 H264中参考帧不是只在一个 GOP 的画面组内,但是这样错误误差可能会延续,IDR 帧可以阻断延续,IDR 出现后编码器会清空参数重新建立图像。

为了提高预测精度,H.264 编码器可从一组前面或后面已编码图像中选出一个或两个与当前最匹 配的图像作为帧间编码间的参数图像,这样一来,复杂度大为增加,但多次比较的结果,使匹配后 的预测精度显著改进。H.264 中最多可从 15 个参数图像中进行选择,选出最佳的匹配图像。

Slice

在标准也翻译成条带,主要分成 A、B、C 三种类型,其中 A 条带数据更敏感一些。按片分一共有五种类型,除了 I 片,B 片,P 片外还有 SP 片和 SI 片(在扩展档里存在)。SP(切换 P 片) 片用于不同编码流之间的切换。它包含了 P 和/或 I 宏块。SI(切换 I 片)和 SP 的原理大致相同。

SP 帧编码的基本原理同 P 帧类似,仍是基于帧间预测的运动补偿预测编码,两者之间的差异在 于 SP 帧能够参照不同参考帧重构出相同的图像帧。充分利用这一特性,SP 帧可取代了 I 帧,广泛 应用于流间切换(bitstream switching)、拼接(splicing)、随机接入(random access)、快进快退(fast forward, fast backward)以及错误恢复(error recovery)等应用中,同时大大降低了码率的开销。与 SP 帧相对应,SI 帧则是基于帧内预测编码技术,其重构图像和对 SP 的重构图像完全相同。

Slice 除了含有 NALU header 外在RBSP 部分还分成了 Slice header 和 Slice data 两个部分。片的语法结构如下。
image.png

  • slice_type 代表片的类型,IDR 的 slice_type 等于2,4,7,9

image.png

  • pic_parameter_set_id 图像参数索引
  • frame_num 简单可以认为是视频的解码顺序。frame_num 解释比较复杂。IDR 帧的时候为0。 //TODO 单独分析,与视频图像的重新排序有关系。
  • idr_pic_id IDR 图像的标识。不同的 IDR 图像有不同的 idr_pic_id 值。

更多的信息请参考标准第91页 7.4.3 条带的定义。

SEI

全称为 Supplemental Enhancement Information ,翻译为补充增强帧。可以增加图像参数,用户信息等。在标准的第89页 7.4.3 节。关于 SEI 实践使用可以参考这篇文章

一个 SEI NAL 单元包括一个或多个 SEI 消息。每个 SEI 消息由表示 SEI 载荷类型的 payloadType 和表示 SEI 载荷大小的 payloadSize 变量组成。

小结

  1. 在视频中如果出现绿屏或者马赛克怎么办?这是因为 sps 或者 pps 参数没有放在视频头之前进行发送。第一帧需要是 I 帧否则会出现马赛克的问题。但是有一些解码器可能兼容和纠错能力好一些,有一些会差一些。实际在测试中 pps 的长度偏差传错后,ijkplayer 是可以正常播放的,但是 iOS 的硬解就无法播放。
  2. 回顾一下文章开头的二进制 ```c 0000 0001 2764 0033 ad00 ce80 7802 27e5

9a80 8080 f800 0003 0008 0000 0300 f1a8 0053 4000 3e70 6fff e050 0000 0001 28ee

  1. --------- --

3cb0 0000 0001 25b8 4001 db80 2b1b 38f8

  1. --------- --

4a70 6a11 2ba9 317c f030 48e9 1d1d cb4a d95c 6544 12c2 9c51 7a49 6735 d20a 3b11 … ``` 可以看到分成三个部分,第一个部分是0x27 & 0x1f = 7 代表是 sps 。剩下的按 h264 的语法结构就能分析。第二部分是0x28 & 0x1f = 8 代表是 pps 。第三部分是 0x25 & 0x1f = 5 代表的是 IDR 类型。