2. H264码流结构:NAL单元概念、NAL头解析、RBSP与EBSP
好,我们直接进入正题。H264的码流,说白了就是一串二进制数据。但这一大串0和1,总得有个组织方式吧?不然解码器拿到手,根本不知道从哪里开始读。
这个组织方式,就是 NAL单元(Network Abstraction Layer,网络抽象层)。我个人习惯把NAL单元想象成「数据包」——每个包都有自己的身份标识,包里装着压缩后的视频数据。解码器拿到码流后,第一步就是把这些包一个个拆出来,然后根据包头的指示去处理。
核心概念:H264码流 = NAL单元序列。每个NAL单元 = NAL头 + RBSP数据。
2.1 NAL单元结构
每个NAL单元由两部分组成:NAL头(1字节)和 RBSP(原始字节序列载荷)。
先看一个典型的NAL单元在码流中的样子:
+--------+-----------------------------+
| NAL头 | RBSP |
| (1字节)| (可变长度,包含视频数据) |
+--------+-----------------------------+
嗯,这里要注意:NAL单元之间用 起始码(Start Code)分隔。起始码有两种格式:
- 0x00000001(4字节):用于关键位置,比如SPS、PPS、IDR帧开头
- 0x000001(3字节):用于普通数据包之间
我曾经在解析一个直播流时,发现解码器总是丢帧。排查了半天,最后发现是推流端在某些关键帧前用了3字节起始码,而解码器只认4字节的。从那以后,我写解析器时都会同时兼容两种格式。
2.2 NAL头解析
NAL头只有1个字节,8个比特位。结构如下:
| 比特位 | 字段名 | 长度(bit) | 说明 |
|---|---|---|---|
| bit[7] | forbidden_zero_bit | 1 | 必须为0。如果为1,表示语法错误 |
| bit[6:5] | nal_ref_idc | 2 | 重要性指示。00=不重要,非00=重要(如SPS/PPS/IDR) |
| bit[4:0] | nal_unit_type | 5 | NAL单元类型,取值范围0~31 |
你想想看,解码器拿到一个字节,先看最高位是不是0——如果是1,直接丢包。然后看 nal_ref_idc,决定这个包要不要优先处理。最后看 nal_unit_type,才知道包里装的是什么。
常见的 nal_unit_type 值:
| 类型值 | 缩写 | 含义 | 重要性 |
|---|---|---|---|
| 1 | SLICE | 非IDR图像片 | 视频数据 |
| 5 | IDR | IDR图像片(关键帧) | 极高 |
| 6 | SEI | 补充增强信息 | 可选 |
| 7 | SPS | 序列参数集 | 极高 |
| 8 | PPS | 图像参数集 | 极高 |
| 9 | AU | 访问单元分隔符 | 辅助 |
个人经验:解析NAL头时,我习惯用位运算直接取。比如 nal_unit_type = byte & 0x1F,nal_ref_idc = (byte >> 5) & 0x03。别用结构体位域,不同编译器的内存对齐会让你怀疑人生。
2.3 RBSP与EBSP
好,现在我们知道NAL头怎么读了。但NAL头后面的数据,并不是直接就能用的。这里有两个概念:RBSP 和 EBSP。
先看它们的关系:
原始编码数据(SODB)
↓ 添加RBSP尾部填充位
RBSP(原始字节序列载荷)
↓ 防竞争字节插入(0x03插入)
EBSP(扩展字节序列载荷)—— 这才是码流中实际存储的数据
为什么会这样?
H264的起始码是 0x000001 或 0x00000001。如果视频数据里恰好出现了 0x000001 或 0x000000 这样的字节序列,解码器就会误判为起始码。所以编码器在生成码流时,会在数据中插入一个 0x03 字节来「转义」这些敏感序列。
具体规则:
- 如果数据中出现连续的
0x000000→ 在第三个0后面插入0x03,变成0x00000300 - 如果数据中出现
0x000001→ 在0x01前面插入0x03,变成0x00000301 - 如果数据中出现
0x000002→ 同样插入0x03,变成0x00000302
解码时,我们需要把 0x03 去掉,还原成RBSP。这个过程叫 EBSP到RBSP的转换。
避坑指南:我曾经在写解码器时,直接拿EBSP去解析SPS,结果解析出来的分辨率是错的。查了半天才发现,SPS里的某些字段值恰好包含 0x000003 序列,而我没有先做0x03去除。记住:所有语法解析必须在RBSP上进行,EBSP只是传输格式。
2.4 从EBSP到RBSP的转换算法
这个算法其实很简单,但细节容易出错。我直接给代码:
// 输入:EBSP数据(包含0x03插入)
// 输出:RBSP数据(去除0x03)
// 返回:RBSP长度
int ebsp_to_rbsp(uint8_t *ebsp, int ebsp_len, uint8_t *rbsp) {
int rbsp_len = 0;
int zero_count = 0;
for (int i = 0; i < ebsp_len; i++) {
if (zero_count == 2 && ebsp[i] == 0x03) {
// 遇到连续的 0x00 0x00 0x03,跳过这个0x03
zero_count = 0;
continue;
}
if (ebsp[i] == 0x00) {
zero_count++;
} else {
zero_count = 0;
}
rbsp[rbsp_len++] = ebsp[i];
}
return rbsp_len;
}
注意:这个算法只处理 0x000003 的情况。如果遇到 0x000002,那是保留的语法标记,不应该出现在普通数据中。如果遇到 0x000000 后面跟的不是 0x03,那说明码流有问题。
2.5 整体流程:从码流到NAL单元
把上面的知识点串起来,解析一个H264码流的完整流程是这样的:
- 查找起始码:在码流中搜索
0x000001或0x00000001 - 提取NAL单元:从起始码之后到下一个起始码之前,就是完整的NAL单元(包含NAL头+EBSP)
- 解析NAL头:读取第一个字节,提取
forbidden_zero_bit、nal_ref_idc、nal_unit_type - EBSP转RBSP:对NAL头后面的EBSP数据做0x03去除,得到RBSP
- 解析RBSP:根据
nal_unit_type调用对应的解析函数(SPS解析、PPS解析、Slice解析等)
下面这张图展示了整个流程:
实用技巧:实际开发中,我建议把NAL单元解析和RBSP解析分开成两个模块。NAL单元解析负责「切包」,RBSP解析负责「解包」。这样如果后续要支持H265,只需要换NAL单元解析部分,RBSP解析逻辑可以复用。
2.6 小结
这一章我们讲了三个核心概念:
- NAL单元:H264码流的基本组成单位,由NAL头+RBSP构成
- NAL头:1字节,包含类型和重要性信息,是解码器判断数据类型的依据
- RBSP与EBSP:EBSP是码流中实际存储的格式,通过0x03插入防止起始码误判;解码时需要还原成RBSP才能解析
嗯,这些概念看起来简单,但实际编码时坑不少。尤其是EBSP转RBSP这一步,很多人会忽略,导致解析出来的参数全是错的。记住我前面说的:所有语法解析都在RBSP上进行。