4. NAL单元类型详解:VCL与非VCL的“分工”
上一章我们把H.264码流拆成了一个个NAL单元。但光拆开还不够,你得知道每个单元是干嘛的。这就像你拿到一叠文件,得先分清哪些是合同正文,哪些是封面、目录和备注。
NAL单元分两大类:VCL NAL 和 非VCL NAL。VCL存的是真正的图像数据,非VCL存的是辅助信息。我刚开始学的时候,总觉得非VCL是“废话”,后来踩了坑才明白——没有这些“废话”,解码器根本不知道该怎么干活。
一句话总结:VCL是“肉”,非VCL是“骨架”。解码器先搭骨架,再填肉。
4.1 VCL NAL:图像数据的“搬运工”
VCL,全称Video Coding Layer。说白了,就是存像素压缩后的比特流。它又分两种:IDR图像 和 非IDR图像。
4.1.1 IDR图像(nal_unit_type = 5)
IDR,Instantaneous Decoding Refresh,瞬时解码刷新。这个名字很直白——它告诉解码器:“之前的数据全作废,从我开始重新来。”
我举个例子。你正在看视频,突然网络丢包,画面花了。这时候如果来了一个IDR帧,解码器会立刻清空参考帧缓冲区,重新开始解码。画面瞬间恢复清晰。
我的习惯:在直播推流中,我会强制每2秒插入一个IDR帧。这样即使网络抖动,客户端最多花2秒就能恢复画面。太频繁会浪费带宽,太稀疏则恢复慢。
IDR帧有几个关键特征:
- 封闭GOP起点:它后面的帧不能参考它前面的帧。
- 刷新参考帧列表:解码器会把所有参考帧标记为“不可用”。
- 通常对应I帧:但注意,I帧不一定是IDR,IDR一定是I帧。
4.1.2 非IDR图像(nal_unit_type = 1)
非IDR图像,就是除了IDR之外的所有图像帧。包括P帧、B帧,以及非IDR的I帧。
它们的特点是:可以依赖之前的帧做参考。解码器会维护一个参考帧列表,非IDR帧从这个列表里找参考数据。
这里有个容易混淆的点:nal_unit_type = 1 不代表一定是P帧或B帧。它只是说“这个NAL单元存的是非IDR图像数据”。至于具体是I、P还是B,要看slice header里的slice_type字段。
我曾经踩过的坑:有一次做视频拼接,我把两个视频的IDR帧直接连在一起,没清空解码器状态。结果第二个视频的画面全是马赛克。后来才意识到,IDR虽然能刷新,但解码器内部的状态机需要正确复位。所以我现在做拼接时,会在两个IDR之间插入一个SPS/PPS,确保解码器“认识”新的参数。
4.2 非VCL NAL:解码器的“说明书”
非VCL NAL不存图像数据,但解码器离了它们寸步难行。常见的三种:SPS、PPS、SEI。
4.2.1 SPS(nal_unit_type = 7)
SPS,Sequence Parameter Set,序列参数集。它描述的是整个视频序列的全局参数。比如分辨率、帧率、编码档次、参考帧数量等。
你可以把它想象成视频的“身份证”。解码器拿到SPS,才知道这个视频是1080p还是720p,是Baseline还是High Profile。
| SPS关键字段 | 含义 | 我的经验 |
|---|---|---|
| profile_idc | 编码档次(66=Baseline, 77=Main, 100=High) | 做兼容性测试时,我常检查这个值 |
| level_idc | 编码级别(如30对应Level 3.0) | 决定了最大分辨率和解码复杂度 |
| pic_width_in_mbs_minus1 | 图像宽度(以宏块为单位) | 实际宽度 = (这个值+1)*16 |
| pic_height_in_map_units_minus1 | 图像高度 | 注意帧编码和场编码的区别 |
| num_ref_frames | 参考帧最大数量 | 设太大浪费内存,设太小影响压缩率 |
4.2.2 PPS(nal_unit_type = 8)
PPS,Picture Parameter Set,图像参数集。它描述的是单个图像(或一组图像)的编码参数。比如熵编码模式、初始量化参数、去块滤波开关等。
SPS和PPS的关系,有点像“全局配置”和“局部配置”。SPS定大框架,PPS做微调。
我建议:在封装MP4或TS时,SPS和PPS一定要放在文件头。解码器打开文件的第一件事就是找它们。如果找不到,画面直接黑屏。我曾经见过一个封装器把PPS放在了文件末尾,结果播放器前5秒全是绿屏。
4.2.3 SEI(nal_unit_type = 6)
SEI,Supplemental Enhancement Information,补充增强信息。它不参与解码过程,但可以携带一些“额外信息”。比如:
- 时间戳(用于音视频同步)
- 字幕信息
- HDR元数据
- 用户自定义数据
SEI是可选字段。解码器可以忽略它,但不会影响画面质量。不过,有些播放器会依赖SEI里的时间戳做同步,所以别乱删。
注意:SEI虽然不参与解码,但它的payload长度有限制。最大不能超过2^32-1字节。我见过有人往SEI里塞整张图片,结果码流直接崩了。SEI不是用来存大数据的。
4.3 VCL与非VCL的协作关系
现在我们把所有类型串起来,看看它们怎么配合工作。
你看这个流程:码流进来,先解析NAL类型。VCL负责“干活”——存图像数据;非VCL负责“指挥”——告诉解码器怎么干。两者配合,才能输出正确的画面。
4.4 常见NAL类型速查表
最后,我整理了一张速查表。你调试码流时,对着这个表查就行。
| nal_unit_type | 名称 | 类别 | 是否必须 | 作用 |
|---|---|---|---|---|
| 1 | 非IDR图像 | VCL | 是 | 存储非IDR帧的编码数据 |
| 5 | IDR图像 | VCL | 是 | 存储IDR帧,刷新解码器状态 |
| 6 | SEI | 非VCL | 否 | 携带补充信息(时间戳等) |
| 7 | SPS | 非VCL | 是 | 序列级参数(分辨率、档次等) |
| 8 | PPS | 非VCL | 是 | 图像级参数(量化参数等) |
| 9 | 访问单元分隔符 | 非VCL | 否 | 标记图像边界 |
| 10 | 序列结束 | 非VCL | 否 | 标记序列结束 |
| 11 | 码流结束 | 非VCL | 否 | 标记整个码流结束 |
我的调试技巧:当你拿到一个陌生的H.264码流,第一步就是遍历所有NAL单元,统计类型分布。如果发现没有SPS或PPS,那这个码流大概率是坏的。如果IDR帧间隔超过5秒,说明编码器配置可能有问题。