3. NAL单元头部解析:码流里的第一道门
上一章我们把 H.264 码流拆成了一个个 NAL 单元。但光拆开还不够——你得知道每个 NAL 单元是干什么的。是图像数据?是参数集?还是别的什么?
答案就藏在每个 NAL 单元的第一个字节里。没错,就一个字节,8 个比特,决定了这个单元的「身份」。我当年第一次对着 hex dump 看码流时,就是靠这个字节一点点摸清了整段流的脉络。
3.1 NAL 单元头部的结构
先看这个字节长什么样。一个 NAL 单元头部固定为 1 字节,按位划分成三个字段:
| 位偏移 | 字段名 | 长度(bit) | 含义 |
|---|---|---|---|
| bit 7 | forbidden_bit | 1 | 禁止位,必须为 0 |
| bit 6-5 | nal_ref_idc | 2 | NAL 单元重要性指示 |
| bit 4-0 | nal_unit_type | 5 | NAL 单元类型 |
说白了,你拿到一个字节,比如 0x67,转成二进制是 0110 0111。按上面的位拆开:
- forbidden_bit = 0
- nal_ref_idc = 0b11 = 3
- nal_unit_type = 0b00111 = 7
嗯,类型 7 对应的是序列参数集(SPS)。这个我后面会细讲。
核心记忆点:NAL 头部就 1 个字节,三个字段。解析时先读一个字节,然后按位掩码提取。代码里常见写法:
uint8_t nal_header = bitstream_read_byte();
uint8_t forbidden_bit = (nal_header >> 7) & 0x01;
uint8_t nal_ref_idc = (nal_header >> 5) & 0x03;
uint8_t nal_unit_type = nal_header & 0x1F;
3.2 各字段含义详解
3.2.1 forbidden_bit(1 bit)
这个位在 H.264 标准里被规定为 必须为 0。如果解码器读到 1,标准建议「丢弃这个 NAL 单元」——但实际解码器怎么处理,各家不一样。
我记得有一次调试一个摄像头码流,画面时不时花一下。抓包一看,有个别 NAL 单元的 forbidden_bit 被设成了 1。虽然我们的解码器没丢它,但解码出来的宏块就是错的。后来发现是编码器固件的一个 bug,升级后就好了。
避坑指南:我曾经在对接第三方编码器时遇到过 forbidden_bit 被置 1 的情况。如果你在写解码器,建议严格按标准丢弃;如果你在写编码器,务必把这个位写 0。别给自己挖坑。
3.2.2 nal_ref_idc(2 bits)
这个字段表示当前 NAL 单元的「重要性」。取值范围 0~3,值越大越重要。
| nal_ref_idc 值 | 含义 | 典型场景 |
|---|---|---|
| 0 | 不用于参考 | 非参考图像(B 帧、SEI 等) |
| 1 | 低重要性参考 | 某些参考帧的片 |
| 2 | 中等重要性参考 | 一般参考帧 |
| 3 | 最高重要性 | SPS、PPS、IDR 图像 |
你想想看,这个字段有什么用?
在丢包严重的网络环境下,解码器可以根据 nal_ref_idc 做优先级丢弃。比如网络带宽不够了,先丢 nal_ref_idc=0 的 NAL 单元,因为它们不影响后续帧的参考。我做过一个无线图传项目,就是用这个策略——优先保证 I 帧和 P 帧的完整性,B 帧实在保不住就扔。
个人习惯:我在写解析器时,会先检查 nal_ref_idc。如果为 0,说明这个 NAL 单元不参与帧间参考,可以单独处理,不用操心它跟其他帧的依赖关系。
3.2.3 nal_unit_type(5 bits)
这是最关键的字段。5 个比特,能表示 0~31 共 32 种类型。标准里定义了 1~23,剩下的保留给未来扩展或厂商自定义。
我把常用的类型列出来,你记牢这几个就行:
| nal_unit_type | 名称 | 说明 |
|---|---|---|
| 1 | 非 IDR 图像的片 | 普通 P/B 帧的片数据 |
| 5 | IDR 图像的片 | 关键帧,解码器可重置参考帧列表 |
| 6 | SEI | 补充增强信息,可选 |
| 7 | SPS | 序列参数集,描述整个序列的编码参数 |
| 8 | PPS | 图像参数集,描述单帧或几帧的编码参数 |
| 9 | 访问单元分隔符 | 标记一个访问单元的边界 |
为什么类型 7 和 8 这么重要?因为解码器必须先拿到 SPS 和 PPS,才能正确解析后面的片数据。没有参数集,后面的数据就是一堆乱码。
我遇到过一种情况:某个视频文件开头丢了 SPS,播放器死活打不开。后来我在文件头手动补了一个 SPS,就能播了。所以做封装或传输时,一定要确保 SPS/PPS 在码流最前面,或者通过带外方式先传过去。
3.3 NAL 头部解析流程图
下面这张图把整个解析流程串起来了。从拿到一个字节开始,到判断出 NAL 单元类型,再到决定怎么处理——一步到位。
3.4 实战:手撕一个 NAL 头部
光说不练假把式。我们拿一段真实的码流来解析。
假设我从一个 H.264 文件里读到了以下字节序列(十六进制):
00 00 00 01 67 42 00 1E A6 80 80 80 ...
00 00 00 01 68 CE 3C 80 ...
00 00 00 01 65 88 84 00 2A ...
跳过起始码 00 00 00 01,第一个 NAL 头部是 0x67:
- 0x67 = 0b 0110 0111
- forbidden_bit = 0 ✅
- nal_ref_idc = 0b11 = 3(最高重要性)
- nal_unit_type = 0b00111 = 7(SPS)
第二个 NAL 头部 0x68:
- 0x68 = 0b 0110 1000
- forbidden_bit = 0 ✅
- nal_ref_idc = 0b11 = 3
- nal_unit_type = 0b01000 = 8(PPS)
第三个 NAL 头部 0x65:
- 0x65 = 0b 0110 0101
- forbidden_bit = 0 ✅
- nal_ref_idc = 0b11 = 3
- nal_unit_type = 0b00101 = 5(IDR 片)
你看,一个典型的码流开头就是 SPS → PPS → IDR。这个顺序是解码器能正常工作的前提。
小技巧:我习惯在解析器里加一个 nal_unit_type 的日志输出。调试时一眼就能看出码流结构对不对。比如看到类型 7 后面紧跟着类型 8,再跟类型 5,基本就是正常的。如果类型 5 前面没有 7 和 8,那肯定有问题。
3.5 常见问题与避坑
- nal_ref_idc 为 0 的 IDR 片?标准不允许。IDR 片必须 nal_ref_idc > 0。如果遇到,要么码流损坏,要么编码器有 bug。
- forbidden_bit 不为 0 怎么办?我建议直接丢弃。虽然有些解码器能容忍,但标准明确说「解码器应忽略该 NAL 单元」。别赌。
- nal_unit_type 值大于 23?标准保留,可能是厂商自定义。如果你在做通用解码器,建议跳过;如果是定制系统,按约定处理。
嗯,NAL 头部解析就这些内容。一个字节,三个字段,搞清楚它们,你就能读懂码流里每个 NAL 单元的「身份证」了。下一层,我们要钻进这个字节后面的 RBSP 数据里,看看参数集和片数据到底长什么样。