H.264 码流结构
从码流功能的角度可以分为两层:
- 视频编码层 (Video Coding Layer, VCL):
    - 职责: 核心压缩数据
- 输出: 经过VCL处理后,输出的是一系列Slice数据。Slice是H.264编码的基本单元,包含了一组宏块的压缩信息。
 
- 网络抽象层 (Network Abstraction Layer, NAL):
    - 职责: 负责将VCL产生的Slice数据以及其他附加信息(如参数集)打包成一个个独立的、适合在网络上传输或在文件中存储的单元。
- 输出: NAL单元 (NALU)。整个H.264码流就是由NALU组成的序列。
 
+------------------------------------+
|           视频内容 (像素)            |
+-----------------v------------------+
|      视频编码层 (VCL) Process        |
| (预测, 变换, 量化, 滤波...)           |
+-----------------v------------------+
|          Slice Data (切片)          |
+-----------------v------------------+
|     网络抽象层 (NAL) Packaging       |
| (添加NAL头, 打包成NALU)              |
+-----------------v------------------+
| H.264 Bitstream ([NALU][NALU]...)  |
+------------------------------------+
NALU
每个NALU由一个NAL头 (NAL Header) 和一个原始字节序列载荷 (RBSP) 组成。
NAL头 (NAL Header) 只有一个字节,包含了三个关键字段:
- forbidden_zero_bit (1 bit): 必须为0。
- nal_ref_idc (2 bits): 指示该NALU的重要性。- 00: 表示这个NALU不被用作参考,解码器即使丢失它也能继续解码后续帧。
- 非00: 表示这个NALU是参考帧的一部分,必须被正确解码,否则会影响后续帧。值越大,重要性越高。
 
- nal_unit_type (5 bits): 最重要的字段,定义了RBSP中存放的是什么类型的数据。
常见的 nal_unit_type 值:
| Type | 含义 | 属于VCL/Non-VCL | 
|---|---|---|
| 7 | SPS | Non-VCL | 
| 8 | PPS | Non-VCL | 
| 5 | IDR图像的切片 | VCL | 
| 1 | 非IDR图像的切片 | VCL | 
| 6 | SEI (补充增强信息) | Non-VCL | 
SPS与PPS
H.264使用参数集来传递解码所需的全局配置信息,以避免在每个Slice中重复发送。
SPS
(Sequence Parameter Set) - 序列参数集
- 作用域: 作用于一个连续的视频序列。当分辨率、帧率等核心参数变化时,就需要一个新的SPS。
- 包含的关键信息:
    - Profile, Level: 码流的档次和级别,决定了解码器需要具备的能力。
- 图像分辨率: pic_width_in_mbs_minus1,pic_height_in_map_units_minus1。
- 帧率信息。
- 宏块自适应帧场编码 (MBAFF) 开关。
- 最大参考帧数量 (max_num_ref_frames)。
 
PPS
(Picture Parameter Set) - 图像参数集
- 作用域: 作用于序列中的一张或多张图像。一个SPS可以被多个PPS引用。
- 包含的关键信息:
    - 熵编码模式选择: entropy_coding_mode_flag(0: CAVLC, 1: CABAC)。
- 初始QP值: pic_init_qp_minus26。
- 去块滤波相关参数。
- 依赖的SPS ID: seq_parameter_set_id,指明当前PPS属于哪个SPS。
 
- 熵编码模式选择: 
关系
- 层级关系: SPS -> PPS -> Slice Header -> Slice Data。解码器首先解析 SPS,然后根据 Slice Header 中指定的 PPS ID 去解析对应的 PPS,最后利用这两个参数集的信息来解码 Slice Data。
- 传输时机: SPS 和 PPS 通常在视频流的开头,与 IDR 帧(即时解码刷新帧,一种特殊的I帧) 绑定在一起传输。因为 IDR 帧是一个序列的绝对刷新点,解码器可以从任何一个 IDR 帧开始独立解码,所以必须确保解码器在解码 IDR 帧之前已经拥有了正确的 SPS 和 PPS。
Slice
- 将一帧划分为多个Slice是提高鲁棒性(一个Slice损坏不影响其他)和进行并行处理的基础。
- 一个Slice则由一系列宏块(Macroblock)组成。宏块是H.264进行运动补偿和变换的基本单元,固定为16x16像素。
- 每个Slice的开头都有一个Slice头(Slice Header)。
Slice Header的关键信息:
- slice_type: 定义了Slice的类型(I, P, B, SP, SI),决定了其编码方式。
- frame_num: 帧号,用于参考帧管理。
- pic_parameter_set_id: 指明当前Slice使用了哪个PPS。
- 参考帧列表重排序 (Reference Picture List Reordering) 语法。
- 加权预测 (Weighted Prediction) 参数。
NALU Payload
SODB
String Of Data Bits,称原始数据比特流,就是最原始的编码/压缩得到的VCL数据。
RBSP
Raw Byte Sequence Payload,又称原始字节序列载荷。在SODB的后面填加了结尾比特:一个bit“1”若干比特“0”,以便网络传输或存储时进行字节对齐。
RBSP = SODB + RBSP Trailing Bits(RBSP尾部补齐字节)
引入 RBSP Trailing Bits 做 8 位字节补齐。
EBSP
Encapsulated Byte Sequence Payload,称为扩展字节序列载荷。和 RBSP 关系如下:
EBSP = RBSP插入防竞争字节(0x03)
防止竞争字节(0x03):H264会在NALU前插入StartCode的字节串(三字节或四字节,0x000001或0x00000001)来分割NALU。于是问题来了,如果RBSP中也包括了StartCode,就无法找到NALU的起始和结束,于是就引入了防止竞争字节(0x03):
编码时,扫描 RBSP,如果遇到连续两个0x00字节,就在后面添加防止竞争字节(0x03);解码时,同样扫描 EBSP,进行逆向操作即可。
这样RBSP中就不会存在StartCode了,解码器就能正确找到NALU的边界。
如果RBSP包含了0x000003,同样会插入0x03,变成0x00000303。解码时也能还原出正确的数据。

封装格式
解码器在解析码流时,首先要解决“一个NALU在哪里结束,下一个在哪里开始”的问题。H.264主要有两种封装格式来解决这个问题。
- Annex B 格式 (字节流格式)
    - 特征: 在每个NALU的前面插入起始码(Start Code):0x000001或0x00000001。
- 应用: 广泛用于实时传输(如RTP流)。
 
- 特征: 在每个NALU的前面插入起始码(Start Code):
- AVCC 格式 (也称MP4格式)
    - 特征: 在每个NALU的前面加上一个N字节(通常是4字节)的长度字段,指明了这个NALU的长度。
- 应用: 广泛用于文件容器(如MP4, MKV, FLV)。