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值,解码器直接把它当非关键帧处理了。

注意:nal_unit_type是6比特,取值范围0~63。H.265标准里0~9是VCL单元,16~21是non-VCL单元,其他值保留或用于特定场景。不要拿H.264的NAL类型表来套,两者完全不同。

4.2 VCL与non-VCL NAL类型

说白了,VCL(Video Coding Layer)就是真正的视频压缩数据,non-VCL就是辅助信息——参数集、增强信息、分隔符这些。我画了一张图,帮你理清这个分类:

H.265 NAL单元分类 VCL NAL单元 类型0-9 视频编码数据 包含:IDR/BLA/CRA等帧类型 non-VCL NAL单元 类型16-21 辅助信息 包含:VPS/SPS/PPS/SEI/AUD等 VCL单元携带真正的像素数据 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)。

我的经验:在实际码流分析中,VCL类型0-9里最常用的是TRAIL_R(普通参考帧)和RADL_R(可解码参考帧)。如果你在做转码或切片,务必保留_R后缀帧的参考关系,否则解码器会报错。

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主控显示色彩体积
避坑指南:SEI的payloadSize可能为0,表示没有payload数据。另外,一个NAL单元里可以包含多个SEI消息,解析时不要只读一个就退出。

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引用。这种层级关系让参数管理更灵活。

实战技巧:在FFmpeg中,可以用ffprobe -show_streams input.hevc查看SPS和PPS的详细信息。如果想看原始二进制,用ffmpeg -i input.hevc -c copy -bsf hevc_mp4toannexb -f hevc -输出原始H.265码流,然后用十六进制工具分析。

4.8 总结一下NAL单元的解码顺序

解码器处理一个访问单元时,NAL单元的顺序通常是:

  1. AUD(如果有)——标记访问单元开始
  2. VPS——视频参数集(通常只在序列开始时出现)
  3. SPS——序列参数集(通常只在序列开始时出现)
  4. PPS——图像参数集(每帧或每几帧出现一次)
  5. SEI(如果有)——补充增强信息
  6. VCL NAL单元——真正的视频数据(一个或多个slice)

这个顺序不是绝对的,但大部分编码器都遵循这个约定。如果你自己写码流分析工具,建议按这个顺序解析,兼容性最好。