2. H264编码架构:编码器整体结构与码流组织

好,咱们进入正题。H264 编码器到底长什么样?说白了,它就是一个“视频压缩流水线”。我当年第一次看完整框图时,说实话有点懵——模块太多了。但拆开来看,核心就五个环节:预测、变换、量化、熵编码,再加一个环路滤波。今天咱们把这五个环节讲透,再把码流结构(NAL、SPS、PPS、IDR)理清楚。

2.1 编码器整体架构:五步压缩流水线

先看这张整体框架图,我建议你把它存下来,后面每个模块都会反复提到。

H264 编码器整体架构 原始帧 Fn 预测(帧内/帧间) 变换(DCT) 量化(Q) 熵编码 H264 码流 环路滤波(去块效应) 参考帧存储 核心流程说明: 1. 原始帧 Fn 进入预测模块,减去预测值得到残差 2. 残差经过 DCT 变换和量化,去除空间冗余 3. 量化后的系数送入熵编码,进一步去除统计冗余 4. 量化后的系数同时反量化+反变换,重建残差 5. 重建残差 + 预测值 = 重建帧,经环路滤波后存入参考帧列表

2.2 五大模块逐个拆解

2.2.1 预测模块:去掉空间和时间冗余

预测是整个编码器里最“聪明”的部分。它分两种:

  • 帧内预测:用当前帧已经编码的像素,推测当前块的内容。说白了就是“看图猜画”——左边和上边的像素都知道了,那当前块大概率跟它们差不多。
  • 帧间预测:用前面或后面的帧,通过运动估计找到最匹配的块。这个更狠,直接“抄作业”——从参考帧里找个最像的块,只记录位置偏移(运动矢量)和差异(残差)。

我个人的经验:帧间预测的搜索范围别设太大。有一次我在项目中把搜索范围从 32 改成 128,码率没降多少,编码时间翻了 3 倍。后来我学乖了——对实时编码,32 就够用;对离线编码,64 是性价比最高的。

2.2.2 变换模块:把像素能量集中

预测完剩下的残差,还是有很多冗余。H264 用的是整数 DCT(离散余弦变换),把像素域的残差转到频域。低频分量集中在左上角,高频分量在右下角。

为什么要这么做?因为人眼对高频细节不敏感。后面量化的时候,就可以把高频系数往死里压,低频系数尽量保留。

关键点:H264 的 DCT 是 4x4 块的整数变换,不是浮点运算。这样做的好处是——没有舍入误差,解码端和编码端能完美对齐。我见过一些新手用浮点 DCT 做实验,结果编解码不一致,画面出现漂移,排查了半天才发现是精度问题。

2.2.3 量化模块:真正的“有损”环节

量化是整个编码器里唯一不可逆的步骤。说白了就是“四舍五入”——把 DCT 系数除以一个量化步长(QP),再取整。QP 越大,丢的信息越多,码率越低,画质越差。

QP 值 量化步长 码率 画质 典型场景
0 0.625 极高 无损 存档/医疗
18 5.0 高 视觉无损 广播级
28 16.0 中等 良好 流媒体
38 64.0 低 可接受 监控/会议
51 224.0 极低 差 缩略图

避坑指南:我曾经在移动端项目里把 QP 设成 40 想省带宽,结果画面全是块效应,用户直接投诉。后来我改用自适应 QP——平坦区域用大 QP,纹理复杂区域用小 QP,码率降了 30%,画质反而能接受。

2.2.4 熵编码:把符号变成比特

量化后的系数还是整数,熵编码的任务就是把这些整数变成更紧凑的二进制串。H264 支持两种:

  • CAVLC(上下文自适应变长编码):简单、快、硬件友好。适合实时编码。
  • CABAC(上下文自适应二进制算术编码):压缩率比 CAVLC 高 10%-15%,但计算量大。适合离线编码或高码率场景。

嗯,这里要注意:CABAC 虽然压缩率高,但解码端也需要更强的算力。我在做机顶盒项目时,老芯片不支持 CABAC 硬件解码,只能硬上 CAVLC。

2.2.5 环路滤波:消除块效应

环路滤波是 H264 的一大亮点。它放在编码环路内部(所以叫“环路”滤波),对重建帧做平滑处理,消除块边界上的不连续感。

为什么会有块效应?因为 DCT 和量化都是基于块的,块与块之间的量化误差不连续,视觉上就像“马赛克”。环路滤波就是专门处理这个的。

我建议:环路滤波的强度不要一刀切。平坦区域(比如天空、墙面)块效应明显,滤波强度要大;纹理复杂区域(比如草地、头发)块效应被纹理掩盖,滤波强度可以小甚至关掉。H264 的滤波强度就是基于块边界两侧的像素差异自适应的。

2.3 码流结构:NAL、SPS、PPS、IDR

编码器输出的不是裸数据,而是有结构的码流。H264 的码流分两层:VCL(视频编码层)和 NAL(网络抽象层)。

VCL 负责存真正的视频数据(宏块、残差、运动矢量等)。NAL 负责给这些数据“打包”,加上头部信息,方便网络传输和存储。

2.3.1 NAL 单元:码流的基本单位

每个 NAL 单元包含一个字节的头部和若干字节的载荷。头部最重要的字段是 nal_unit_type,它告诉解码器“这个包里装的是什么”。

nal_unit_type 名称 说明
1 非 IDR 图像片 普通帧的片数据
5 IDR 图像片 关键帧的片数据,解码器从这里开始刷新
7 SPS 序列参数集,描述整个视频序列的属性
8 PPS 图像参数集,描述每帧的编码参数
6 SEI 补充增强信息,比如时间戳、字幕
9 分隔符 用于码流随机访问

2.3.2 SPS 和 PPS:解码器的“说明书”

SPS(序列参数集)和 PPS(图像参数集)是解码器必须最先收到的两个 NAL 单元。没有它们,解码器连画面尺寸都不知道。

  • SPS 包含:分辨率、帧率、档次(Profile)、级别(Level)、参考帧数量等全局信息。
  • PPS 包含:熵编码类型、量化参数初始值、环路滤波开关等逐帧参数。

实战经验:我在做流媒体推流时,SPS/PPS 必须放在每个 IDR 帧之前。为什么?因为播放器随时可能加入直播流,如果 SPS/PPS 只发一次,后加入的播放器就解码不了。所以推流端会在每个关键帧前面重复发送 SPS/PPS,虽然浪费一点带宽,但保证了兼容性。

2.3.3 IDR 帧:解码器的“复位按钮”

IDR(Instantaneous Decoding Refresh)帧是 H264 里的关键帧。它有两个特点:

  1. IDR 帧本身是帧内编码(I 帧),不依赖任何其他帧。
  2. 解码器收到 IDR 帧后,会清空参考帧列表,后续帧不能再引用 IDR 之前的帧。

说白了,IDR 帧就是一个“重新开始”的信号。你想想看,如果直播画面卡了,播放器只能从下一个 IDR 帧开始恢复。IDR 帧间隔越长,随机访问的延迟就越大。

避坑指南:我曾经把 IDR 帧间隔设成 10 秒,想着省码率。结果用户拖动进度条时,要等 10 秒才能看到画面。后来我改成 2 秒一个 IDR 帧,虽然码率多了 5%,但用户体验好了不止一个档次。

2.4 码流结构全景图

最后,咱们用一张图把整个码流结构串起来:

H264 码流结构 SPS (type=7) 序列参数集 PPS (type=8) 图像参数集 IDR 帧 (type=5) 关键帧数据 非IDR帧 (type=1) P/B 帧数据 码流组织规则: 1. 每个 NAL 单元以起始码 0x00000001 或 0x000001 开头 2. SPS 和 PPS 必须在第一个 IDR 帧之前发送 3. IDR 帧之后可以跟多个非 IDR 帧(P/B 帧) 4. 非 IDR 帧只能引用同一 GOP 内的帧,不能跨 IDR 引用 5. 每个帧可以分成多个片(Slice),每个片是一个 NAL 单元 6. SEI 和分隔符是可选的,但 SPS/PPS/IDR 是必须的