4、H265NAL单元解析:NAL单元头、VCL与non-VCL NAL类型、AUD、SEI、VPS、SPS、PPS
好,我们正式开始啃H.265的码流结构。很多初学者拿到一个H.265文件,用二进制查看器打开,满眼都是0x00 0x00 0x01,然后后面跟着一堆看不懂的字节。这其实就是NAL单元——H.265码流的基本组成单位。
我个人习惯把NAL单元比作“集装箱”。视频数据被切成一块一块,装进不同的集装箱里,每个箱子外面贴个标签,告诉你里面装的是什么。这个标签,就是NAL单元头。
4.1 NAL单元头结构
H.265的NAL单元头是2个字节,固定长度。相比H.264的1字节头,它多了一个字节的信息。结构如下:
| 字段 | 比特数 | 说明 |
|---|---|---|
| forbidden_zero_bit | 1 | 必须为0,否则表示语法错误 |
| nal_unit_type | 6 | NAL单元类型,核心字段 |
| nuh_layer_id | 6 | 层次ID,用于HEVC分层编码 |
| nuh_temporal_id_plus1 | 3 | 时域层ID + 1,0表示关键帧 |
这里有个坑,我踩过。nuh_temporal_id_plus1这个字段,解码器用它来判断当前帧的时域层级。如果这个值算出来是0,意味着当前帧不能被其他帧参考——说白了就是关键帧。我曾经在解析一个流媒体切片时,发现画面一直花屏,排查了半天,最后发现是某个PPS的temporal_id被错误地设成了非0值,解码器直接把它当非关键帧处理了。
4.2 VCL与non-VCL NAL类型
说白了,VCL(Video Coding Layer)就是真正的视频压缩数据,non-VCL就是辅助信息——参数集、增强信息、分隔符这些。我画了一张图,帮你理清这个分类:
VCL类型(0-9)包括:TRAIL_N、TRAIL_R、TSA_N、TSA_R、STSA_N、STSA_R、RADL_N、RADL_R、RASL_N、RASL_R。带_R后缀的表示该帧可以被其他帧参考,_N的则不行。这个设计比H.264灵活很多。
non-VCL类型(16-21)包括:VPS、SPS、PPS、AUD、EOS、EOB。另外还有SEI(类型39)和FD(类型38)。
4.3 AUD——访问单元分隔符
AUD(Access Unit Delimiter),类型35。它的作用很简单——告诉解码器:一个访问单元(通常就是一帧图像)结束了,下一个要开始了。
你想想看,码流是一串连续的字节流,如果没有分隔符,解码器怎么知道哪里是一帧的边界?虽然可以通过SPS里的帧尺寸信息推算,但有了AUD就更明确。不过,AUD是可选的。很多编码器默认不生成AUD,因为会多占几个字节。
我曾经在做一个实时推流项目时,接收端解码器频繁丢帧。排查后发现,发送端编码器开启了AUD插入,但接收端解码器没处理AUD,直接把AUD当普通数据送了进去,导致帧边界错乱。解决方案很简单:要么两端都支持AUD,要么都关掉。
4.4 SEI——补充增强信息
SEI(Supplemental Enhancement Information),类型39。它不参与解码过程,但可以携带各种元数据——比如时间戳、字幕、HDR信息、全景视频参数等。
SEI的结构是:nalu头 + payloadType + payloadSize + payload数据。payloadType和payloadSize使用变长编码(类似UTF-8的编码方式),所以解析时要注意:
// SEI payloadType解析示例
uint8_t byte;
uint32_t sei_type = 0;
do {
byte = read_byte();
sei_type += byte;
} while (byte == 0xFF);
// 同理解析payloadSize
uint32_t sei_size = 0;
do {
byte = read_byte();
sei_size += byte;
} while (byte == 0xFF);
常见的SEI类型:
| 类型值 | 名称 | 用途 |
|---|---|---|
| 0 | buffering_period | 缓冲周期信息 |
| 1 | pic_timing | 图像时序信息 |
| 5 | user_data_unregistered | 用户自定义数据(如UUID+自定义内容) |
| 137 | mastering_display_colour_volume | HDR主控显示色彩体积 |
4.5 VPS——视频参数集
VPS(Video Parameter Set),类型32。这是H.265新增的概念,H.264没有。VPS描述的是整个视频序列的全局参数,比如:
- 视频层数(用于可分级编码)
- 每个层的最大时域子层数
- 每个层的档次和级别
在单层编码(最常见的场景)中,VPS内容很简单。但在多层编码(比如SVC可分级视频编码)中,VPS就变得很复杂。我建议初学者先掌握单层场景,VPS基本就是固定模板。
4.6 SPS——序列参数集
SPS(Sequence Parameter Set),类型33。它描述的是整个视频序列的编码参数,包括:
- 图像尺寸(宽、高)
- 编码工具开关(如SAO、AMP等)
- 档次和级别(Main、Main10等)
- 色度格式(4:2:0、4:2:2、4:4:4)
- 比特深度(8bit、10bit)
SPS是解码器初始化时必须解析的第一个参数集。没有SPS,解码器连图像尺寸都不知道,更别提解码了。
我记得有一次,一个客户发来的H.265文件,播放器一直黑屏。我用ffprobe一看,SPS里的宽度和高度都是0。后来发现是编码器在写入SPS时,把pic_width_in_luma_samples和pic_height_in_luma_samples字段写成了0。这种低级错误,排查起来真的很头疼。
4.7 PPS——图像参数集
PPS(Picture Parameter Set),类型34。它描述的是每帧图像的编码参数,包括:
- 初始量化参数(init_qp_minus26)
- 熵编码模式选择(cabac或cavlc)
- 去块滤波参数
- 片(slice)的分割方式
PPS和SPS的关系是:一个SPS可以被多个PPS引用,一个PPS可以被多个slice引用。这种层级关系让参数管理更灵活。
ffprobe -show_streams input.hevc查看SPS和PPS的详细信息。如果想看原始二进制,用ffmpeg -i input.hevc -c copy -bsf hevc_mp4toannexb -f hevc -输出原始H.265码流,然后用十六进制工具分析。
4.8 总结一下NAL单元的解码顺序
解码器处理一个访问单元时,NAL单元的顺序通常是:
- AUD(如果有)——标记访问单元开始
- VPS——视频参数集(通常只在序列开始时出现)
- SPS——序列参数集(通常只在序列开始时出现)
- PPS——图像参数集(每帧或每几帧出现一次)
- SEI(如果有)——补充增强信息
- VCL NAL单元——真正的视频数据(一个或多个slice)
这个顺序不是绝对的,但大部分编码器都遵循这个约定。如果你自己写码流分析工具,建议按这个顺序解析,兼容性最好。