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 单元类型,再到决定怎么处理——一步到位。

NAL 单元头部解析流程 NAL 单元头部解析流程 读取 1 字节 NAL 头部 按位掩码提取三个字段 forbidden_bit(1) | nal_ref_idc(2) | nal_unit_type(5) forbidden_bit == 0 ? 否 丢弃该 NAL 单元 是 nal_unit_type ? 7/8 参数集 5 IDR 片 1 非 IDR 片 其他 SEI/分隔符等 根据 nal_unit_type 决定后续解析方式 参数集 → 解析 RBSP;片数据 → 进入片层解析

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 数据里,看看参数集和片数据到底长什么样。