H264封装解析从码流到NALU单元 - 第2节

码流结构基础:H264的层次结构

好,咱们正式开始啃H264码流。说实话,我第一次接触H264裸流时,打开十六进制编辑器一看,满屏的字节串,心里直犯怵——这玩意儿怎么拆?后来摸清了门道才发现,H264的设计者其实把结构梳理得挺清楚。

H264码流分两大层:VCL(视频编码层)和NAL(网络抽象层)。你可以这么理解——VCL负责“干活”,NAL负责“打包”。

VCL层:真正干活的

VCL层处理的是视频内容本身。宏块划分、运动估计、DCT变换、量化、熵编码……所有这些压缩算法都在VCL里完成。VCL输出的是一堆压缩后的视频数据,但这些数据不能直接扔到网络或文件里——为什么?因为接收端不知道哪一段是图像头、哪一段是像素数据、哪一段是参数集。

我早年做播放器时踩过一个坑:直接从编码器拿到VCL数据就往解码器塞,结果解码器直接崩了。后来才明白,VCL数据必须“包装”一下才能传输。

NAL层:负责打包的

NAL层就是干这个包装活的。它把VCL输出的数据切成一个个NAL单元,每个单元前面加一个头部,告诉解码器“我是什么类型的数据”。

打个比方:VCL是工厂里生产出来的零件,NAL就是包装盒和标签。没有标签,工人不知道盒子里装的是螺丝还是螺母。

核心关系:

  • VCL → 视频压缩数据(内容)
  • NAL → 封装+标识(容器)
  • 码流 = NAL单元序列

NAL单元的概念

一个NAL单元长什么样?结构非常简单:

+---------------+-----------------------+
| NAL Header(1B) |   RBSP (原始字节序列载荷)  |
+---------------+-----------------------+

就两个部分:1字节的头部,后面跟着载荷数据。头部里最关键的是nal_unit_type字段,占5个bit,取值范围0~31,决定了这个NAL单元的类型。

nal_unit_type 类型名称 说明
1 非IDR图像片 普通帧的片数据
5 IDR图像片 关键帧的片数据(可随机访问)
7 SPS 序列参数集(分辨率、帧率等)
8 PPS 图像参数集(编码模式等)
6 SEI 补充增强信息(可选)
9 分隔符 访问单元分隔符

我个人习惯把NAL单元分成两大类:

  • VCL NAL单元:type 1~5,携带视频像素数据
  • 非VCL NAL单元:type 6~9及其他,携带参数、辅助信息

你想想看,解码器拿到码流后,第一步就是读头部里的type字段。如果是SPS(type=7),就更新序列参数;如果是IDR片(type=5),就知道可以开始解码了。

小技巧:我在分析码流时,经常用nal_unit_type来快速定位关键帧。type=5的NAL单元出现的位置,就是IDR帧的起始点。这在做视频剪辑、seek操作时非常有用。

码流的起始码:0x00000001

好了,现在我们知道NAL单元是什么了。但解码器怎么知道一个NAL单元从哪里开始、到哪里结束?

答案就是起始码(Start Code)。H264使用0x00000001(3字节)或0x000001(2字节)作为NAL单元的分隔符。

码流结构长这样:

[0x00 0x00 0x00 0x01] [NAL Header + RBSP] [0x00 0x00 0x00 0x01] [NAL Header + RBSP] ...

解码器的工作流程:

  1. 在字节流中搜索0x00000001或0x000001
  2. 找到后,从下一个字节开始读取NAL单元
  3. 继续搜索下一个起始码
  4. 两个起始码之间的数据就是一个完整的NAL单元

这里有个细节要注意:起始码本身不属于NAL单元的内容。它只是“分隔符”。

我曾经踩过的坑:早期写码流解析器时,我把起始码也算进了NAL单元长度里,结果解码器解析出的SPS参数全是错的。后来排查了半天才发现——起始码必须跳过,不能算在RBSP里。

为什么是0x00000001?

你可能会问:为什么选这个值?原因很简单——H264码流里几乎不会连续出现三个以上的0x00字节。用0x00000001作为起始码,误判概率极低。

但这也带来一个问题:如果VCL层压缩出来的数据里恰好出现了0x000001或0x00000001怎么办?解码器会误认为是起始码,导致解析错乱。

H264的解决方案是防竞争字节(Emulation Prevention Byte)。编码器在生成RBSP时,如果检测到连续两个0x00,就在后面插入一个0x03。解码器收到后,再把这个0x03去掉。

举个例子:

  • 原始数据:0x00 0x00 0x01
  • 编码后:0x00 0x00 0x03 0x01
  • 解码时:去掉0x03,还原为0x00 0x00 0x01

这个机制叫EBSP(扩展字节序列载荷),说白了就是在RBSP基础上加了防竞争字节。你解析码流时,必须处理这个逻辑,否则拿到的数据是错的。

实战建议:写解析器时,先搜索起始码定位NAL单元边界,然后对RBSP做防竞争字节移除,最后再解析NAL头部。顺序别搞反了——我见过有人先解析头部再做防竞争处理,结果头部字段全乱了。

整体结构图

下面这张图把H264码流的层次结构串起来了。从原始视频到最终的NAL单元序列,每一步都清晰可见。

H264码流层次结构 原始视频帧 VCL 编码层(压缩) NAL 封装(添加头部 + 防竞争字节) 添加起始码 0x00000001 分隔每个 NAL 单元 H264 码流(NAL 单元序列) [起始码] [NAL1] [起始码] [NAL2] [起始码] [NAL3] ... 关键点: VCL 负责压缩视频内容 → NAL 负责打包加头部 → 起始码负责分隔 → 最终形成可传输/存储的码流

从图里可以看得很清楚:原始视频帧经过VCL压缩,变成压缩数据;然后NAL层给每个数据块加上头部和防竞争字节;最后用起始码把一个个NAL单元串起来,形成完整的H264码流。

嗯,这就是H264码流结构的基础。说白了就是三层包装:内容层(VCL)→ 封装层(NAL)→ 分隔层(起始码)。搞懂这个,后面解析NALU就顺了。