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] ...
解码器的工作流程:
- 在字节流中搜索
0x00000001或0x000001 - 找到后,从下一个字节开始读取NAL单元
- 继续搜索下一个起始码
- 两个起始码之间的数据就是一个完整的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单元序列,每一步都清晰可见。
从图里可以看得很清楚:原始视频帧经过VCL压缩,变成压缩数据;然后NAL层给每个数据块加上头部和防竞争字节;最后用起始码把一个个NAL单元串起来,形成完整的H264码流。
嗯,这就是H264码流结构的基础。说白了就是三层包装:内容层(VCL)→ 封装层(NAL)→ 分隔层(起始码)。搞懂这个,后面解析NALU就顺了。