h264 码流格式简述(Annex-B 格式)
1 nal unit stream(Network Abstraction Layer Unit Stream)
h.264 编码器把原始的 yuv 图像文件编码成码流文件,生成的码流文件称为 NAL 单元流(NAL unit Stream),NALU stream 由一个个 NALU(nal 单元) 组成(https://www.cnblogs.com/TaigaCon/p/5215448.html):
2 nal 单元分割方式
多个 nalu 之间,通过分割字节,组成了 nalu stream,分隔符定义如下:
zero_byte(0x00),一个字节。如果当前的 NAL 单元为 sps、pps 或者一个访问单元(access unit)的第一个 NAL 单元,这个字节就会存在
start_code_prefix_one_3bytes(0x000001),三个字节。固定存在的 NAL 单元起始码,用来指示下面为一个 NAL 单元
所以我们常看到的 nalu 分隔符一般由 0x00000001 或则 0x000001 组成。
3 nal 单元结构
NALU 由 header + payload 组成(http://iphome.hhi.de/wiegand/assets/pdfs/DIC_H264_07.pdf):
4 nalu header 结构
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
F:1 bit,forbidden_zero_bit。固定为 0
NRI:2 bit,nal_ref_idc。用于指示该 nalu 的重要性,实际应用层代码一般不关心此值
Type:5 bit,nal_unit_type。指定 nalu 的类型,如下所示:
我们通过读取 nalu 的第一个字节,取得 nalu header,然后判断 type,就能知道此 nalu 所属的帧类型,常见的如 IDR/I 帧的 type=5,P/B 帧的 type=1,sps 的 type=7,pps 的 type=8 等。
在 rfc6184 中,又为 nal_unit_type 从类型值 24 开始进行了扩充:
Table 1. Summary of NAL unit types and the corresponding packet
types
NAL Unit Packet Packet Type Name Section
Type Type
-------------------------------------------------------------
0 reserved -
1-23 NAL unit Single NAL unit packet 5.6
24 STAP-A Single-time aggregation packet 5.7.1
25 STAP-B Single-time aggregation packet 5.7.1
26 MTAP16 Multi-time aggregation packet 5.7.2
27 MTAP24 Multi-time aggregation packet 5.7.2
28 FU-A Fragmentation unit 5.8
29 FU-B Fragmentation unit 5.8
30-31 reserved -
注意,扩充的 nal_unit_type 类型并不是编码器输出的类型,而是为了适应 rtp payload 打包而定义的打包类型。
5 nalu payload
nalu payload 由 rbsp 结构组成。
rbsp 可以分为 video-codec-layer rbsp(如 I/P/B 帧等) 和 non-video-codec-layer rbsp(如 sps/pps/sei 等)。
防竞争字节emulation_prevention_three_byte,1字节,固定0x03,在 rbsp 中出现连续的 0x0000 两字节结构时,在后面添加 0x03 作为防竞争字节,避免与 nal 单元分割字节冲突:
0x000000 => 0x00000300
0x000001 => 0x00000301
0x000002 => 0x00000302
0x000003 => 0x00000303
…
rbsp 尾部
rbsp_stop_one_bit,1位,固定为1
rbsp_alignment_zero_bit,用于字节对齐,可选
SPS/PPS
SPS/PPS 不同于 slice,虽然也是编码器输出的内容,但是不包含任何原始码流。他们中包含的是解码器需要的解码信息,例如图像的宽高、一些编码参数等。
在编码的时候,可以通过 ffmpeg 设置 AV_CODEC_FLAG_GLOBAL_HEADER 参数,那么 sps、pps 会作为 extra data 出现在 AVCodecContext::extradata 变量中,而不是出现在每个
IDR 帧的前面。用户发送数据的时候,最好每次发送 idr 帧时,都将 sps、pps 一起发送。
sps、pps 会有一个 id 值,如 sps 中的 seq_parameter_set_id,用于标识 sps 版本。pps 中的 pic_parameter_set_id,用于标识 pps 版本(且 pps 也有一个 seq_parameter_set_id,用于标识参考的 sps)。idr 也会有一个 pic_parameter_set_id,用于标识参考的 pps id(然后通过 pps 的 seq_parameter_set_id 跟踪参考的 sps)。当编码参数发生变化时,这些值也会发生变化,所以发送给接收端的数据,一定要及时更新 sps、pps,否则会发生解码错误。
6 slice(条带)
编码后原始码流被保存到了称为 slice 的结构中,编码后的一帧图像可以对应一个 slice。但是因为一些编码器设置,例如设置了输出 slice 的数量、进行了多线程并行编码等,编码后的一帧图像,也会分为多个 slice,每个 slice 各自负责了图像中某一块的编码。
slice 也分为 header 和 data 部分:
通过 header 部分,我们可以得到当前 slice 在一帧编码图像中的位置(第几个)、当前编码图像是 I/P/B 帧等中的哪一种、当前编码图像参考的 SPS/PPS 等重要信息。
6.1 access unit(访问单元)
访问单元代表一张编码图像,不包含 sps、pps 等外部数据。由于一帧编码后的图像可能会生成多个 slice,所以,access unit(访问单元)可以由属于一帧编码图像的多个 slice(nalu) 组成。
6.2 idr 帧
idr 帧是立即刷新帧,意味着接收端收到 idr 帧时,前面的参考帧缓存都可以丢弃了(注意,仅针对 close-gop),且 idr 后面的帧不会参考 idr 前面的任何帧。
idr 与 I 帧不同,I 帧不具有刷新参考缓冲区的功能,使用 ffmpeg 编码时,可以设置 AVCodecContext::gop_size 来指定多少帧产生一个 IDR 帧。