14、H.264/H.265解码:编码标准差异、NAL单元解析、SPS/PPS处理、参考帧管理

各位同学,今天我们来聊聊硬解码里最核心的一块——H.264和H.265的解码差异。说实话,这两个标准虽然都是视频压缩的扛把子,但底层逻辑差别挺大的。我当年从H.264转向H.265时,也踩了不少坑,今天把这些经验掰开揉碎了讲给你听。

14.1 编码标准的核心差异

先说说宏观区别。H.264,也就是AVC,发布于2003年,到现在依然是兼容性最好的标准。H.265,也就是HEVC,2013年才定稿,压缩率能提升50%左右。但代价是什么?计算复杂度翻了好几倍。

我个人习惯用一个比喻:H.264像是手动挡汽车,你熟悉了之后开起来很顺手;H.265像是自动挡加混动,省油但电子系统复杂得多。在解码器层面,H.265引入了更多自适应的工具,比如更灵活的块划分结构。

对比维度 H.264/AVC H.265/HEVC
宏块/编码树单元 固定16x16宏块 64x64 CTU,可递归划分到8x8
帧内预测模式 9种(4x4亮度块) 35种(含Planar和DC)
运动补偿精度 1/4像素 1/4像素(亮度),1/8像素(色度)
去块滤波 基于宏块边界 基于CU/TU/PU边界,更精细
参考帧数量 最多16帧 最多16帧(但DPB管理更复杂)

你想想看,H.265的CTU最大能到64x64,这意味着对于平坦区域,一个CTU就能搞定原本H.264里16个宏块的工作量。但解码器要处理递归划分,逻辑复杂度上了一个台阶。

关键点:H.265的块划分是四叉树结构,解码器必须递归解析。我在项目中遇到过某款芯片对深度为4的划分支持不完善,导致花屏。后来加了层校验,才稳定下来。

14.2 NAL单元解析

NAL,全称Network Abstraction Layer,说白了就是视频数据的包装盒。H.264和H.265的NAL单元头部结构不同,这是解码器首先要面对的问题。

H.264的NAL头只有1个字节:forbidden_bit(1) | nal_ref_idc(2) | nal_unit_type(5)。而H.265的NAL头是2个字节:forbidden_bit(1) | nal_unit_type(6) | nuh_layer_id(6) | nuh_temporal_id_plus1(3)

嗯,这里要注意:H.265的nal_unit_type值定义和H.264完全不同。比如H.264的IDR帧类型是5,而H.265的IDR_W_RADL是19,IDR_N_LP是20。如果你用H.264的逻辑去解析H.265的码流,肯定出问题。

// H.264 NAL头解析示例
uint8_t nal_header = bitstream[0];
uint8_t nal_type = nal_header & 0x1F;
uint8_t nal_ref_idc = (nal_header >> 5) & 0x03;

// H.265 NAL头解析示例
uint16_t nal_header = (bitstream[0] << 8) | bitstream[1];
uint8_t nal_type = (nal_header >> 9) & 0x3F;
uint8_t temporal_id = (nal_header >> 3) & 0x07;

我曾经在移植解码器时,因为没注意H.265的nuh_temporal_id_plus1字段,导致时间层识别错误,B帧参考关系全乱了。避坑指南:解析NAL头时,先判断码流是H.264还是H.265,再选择对应的解析逻辑。

实战技巧:很多码流分析工具(如Elecard、CodecVisa)可以帮你快速查看NAL单元类型。调试阶段先用工具确认码流结构,再写解析代码,能省不少时间。

14.3 SPS/PPS处理

SPS(序列参数集)和PPS(图像参数集)是解码器的"说明书"。没有它们,解码器根本不知道视频的分辨率、帧率、编码工具集等信息。

H.264的SPS里有个关键字段pic_order_cnt_type,它决定了POC(图像顺序计数)的计算方式。我见过不少开发者直接写死POC计算逻辑,结果换了码流就崩。H.265的SPS更复杂,多了sps_max_sub_layers_minus1log2_min_luma_coding_block_size_minus3等字段。

PPS方面,H.265引入了pps_num_tile_columns_minus1pps_num_tile_rows_minus1,支持Tile划分。Tile是H.265特有的并行解码工具,把一帧图像分成多个矩形区域,可以独立解码。

参数集 H.264关键字段 H.265关键字段
SPS pic_order_cnt_type, frame_num, gaps_in_frame_num_value_allowed_flag sps_max_sub_layers_minus1, log2_min_luma_coding_block_size_minus3, sps_scaling_list_data_present_flag
PPS entropy_coding_mode_flag, num_slice_groups_minus1 pps_num_tile_columns_minus1, pps_num_tile_rows_minus1, pps_loop_filter_across_tiles_enabled_flag

我个人习惯的做法是:解析完SPS/PPS后,立即做一次完整性校验。比如检查分辨率是否超过芯片支持的最大值,参考帧数量是否合理。如果发现异常,直接拒绝解码,而不是等到解码过程中崩溃。

注意:有些编码器会在码流中间发送新的SPS/PPS,比如分辨率动态变化时。解码器必须支持这种"动态参数集更新"。我遇到过一款播放器,只在初始化时解析一次SPS,结果直播流切换分辨率后直接黑屏。

14.4 参考帧管理

参考帧管理是解码器最复杂的部分之一。H.264和H.265都使用DPB(解码图像缓冲区)来管理参考帧,但管理逻辑有差异。

H.264使用max_num_ref_frames来限制参考帧数量,通过MMCO(内存管理控制操作)命令来标记帧的用途。MMCO有6种类型,比如"标记为未使用"、"标记为长期参考"等。说实话,MMCO的逻辑挺绕的,我当年啃标准文档时看了好几遍才搞明白。

H.265改用RPL(参考图像列表)机制,分为RPL0和RPL1。每个P/B帧都携带自己的参考帧列表,解码器根据num_ref_idx_l0_active_minus1num_ref_idx_l1_active_minus1来构建列表。H.265还引入了reference_pic_set的概念,可以更灵活地管理短期和长期参考帧。

// H.264参考帧管理简化示例
// 假设当前帧是P帧,参考列表中有3帧
// 通过MMCO命令标记第2帧为"未使用"
// 解码器需要从DPB中移除该帧

// H.265参考帧管理简化示例
// 当前帧的RPL0指定了2个参考帧索引
// 解码器根据SPS中的RPS信息构建参考列表
// 短期参考帧和长期参考帧分开管理

为什么H.265要改这个?说白了,是为了更好的错误恢复和随机访问能力。H.264的MMCO是逐帧修改状态,一旦丢帧,参考关系就乱了。H.265的RPS是显式声明,每个帧都告诉解码器"我需要哪些参考帧",容错性更好。

避坑指南:我曾经在实现H.265解码器时,忽略了delta_poc_msb_present_flag的处理,导致POC计算错误,参考帧匹配全乱套。后来加上了MSB(最高有效位)的处理逻辑,才解决。建议你在实现参考帧管理时,先画一张状态机图,把帧的进出逻辑理清楚。

14.5 知识体系总览

下面这张图总结了本章的核心知识点,你可以对照着梳理自己的理解。

H.264/H.265解码核心知识体系 编码标准差异 • 块划分:16x16宏块 vs 64x64 CTU • 帧内预测:9种 vs 35种 • 运动补偿精度差异 • 去块滤波范围不同 NAL单元解析 • H.264:1字节头,nal_unit_type(5) • H.265:2字节头,nal_unit_type(6) • 类型值定义完全不同 • 时间层标识:temporal_id SPS/PPS处理 • SPS:分辨率、帧率、编码工具集 • PPS:Tile划分、熵编码模式 • 动态参数集更新支持 • 完整性校验:分辨率、参考帧数 参考帧管理 • H.264:MMCO命令,6种类型 • H.265:RPL0/RPL1,RPS机制 • 短期/长期参考帧管理 • POC计算与MSB处理 解码器实现 = 标准理解 + 容错处理 + 性能优化

这张图把四个模块串起来了。编码标准差异是基础,决定了NAL解析和SPS/PPS的字段定义。参考帧管理则依赖前三个模块的正确解析。你想想看,如果NAL类型都识别错了,后面的参考帧管理肯定全乱套。

好了,这一章的内容就到这里。H.264和H.265的解码差异,说白了就是"老标准稳,新标准强"。作为开发者,我们既要兼容老设备,也要拥抱新技术。下一章我们会深入解码器的实际实现,从码流读取到图像输出,一步步带你走通整个流程。