2. H264编码架构:编码器整体结构与码流组织
好,咱们进入正题。H264 编码器到底长什么样?说白了,它就是一个“视频压缩流水线”。我当年第一次看完整框图时,说实话有点懵——模块太多了。但拆开来看,核心就五个环节:预测、变换、量化、熵编码,再加一个环路滤波。今天咱们把这五个环节讲透,再把码流结构(NAL、SPS、PPS、IDR)理清楚。
2.1 编码器整体架构:五步压缩流水线
先看这张整体框架图,我建议你把它存下来,后面每个模块都会反复提到。
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 里的关键帧。它有两个特点:
- IDR 帧本身是帧内编码(I 帧),不依赖任何其他帧。
- 解码器收到 IDR 帧后,会清空参考帧列表,后续帧不能再引用 IDR 之前的帧。
说白了,IDR 帧就是一个“重新开始”的信号。你想想看,如果直播画面卡了,播放器只能从下一个 IDR 帧开始恢复。IDR 帧间隔越长,随机访问的延迟就越大。
避坑指南:我曾经把 IDR 帧间隔设成 10 秒,想着省码率。结果用户拖动进度条时,要等 10 秒才能看到画面。后来我改成 2 秒一个 IDR 帧,虽然码率多了 5%,但用户体验好了不止一个档次。
2.4 码流结构全景图
最后,咱们用一张图把整个码流结构串起来: