一、视频压缩基础

1.1 视频压缩必要性

未经压缩的视频数据量极其庞大。以常见的 1080P 60帧视频为例,画面分辨率为 1920x1080,如果用一张一张的RGB图片来存储,每个像素占 3 个字节:

单秒数据量 = 1920 x 1080 x 3 x 60 ≈ 373 MB。

这意味着一部两小时的视频就要吃掉大约 2.6 TB 存储空间。无论硬盘还是网速都无法承受,因此视频在传输和存储前必须进行压缩。

视频之所以能压缩到原大小的几十分之一甚至上百分之一,是因为连续画面中存在大量重复和不必要的信息(数据冗余):

  • 时间冗余:连续播放的前后两帧画面,绝大部分背景和人物都是静止或微动的,内容高度相似。
  • 视觉冗余:人类眼球对亮度变化非常敏感,但对细微的色度变化和超高频细节相对迟钝,适当剔除这些细节人眼根本察觉不出。
  • 空间冗余:单张图片内部,相邻像素之间的颜色和亮度往往非常接近,比如大片蓝天或白墙。
  • 编码冗余:数据中不同数值出现的概率不同,出现频率极高的数值用短码表示,极少出现的用长码表示(如哈夫曼编码),可以缩减总码长。

市面上常见的视频编码标准主要分为几大阵营:

编码标准 主导阵营 当前行业现状与生态
H.264 (AVC) MPEG / ITU-T 绝对的兼容性兜底底线,几乎所有设备、浏览器与硬件芯片均支持 100% 硬解
H.265 (HEVC) MPEG / ITU-T 4K 高清与安防监控主力;主流浏览器(Chrome、Safari、Edge)已普遍支持硬解,但受商业专利池收费困扰
AV1 AOMedia(谷歌、苹果、微软等) 开源免版税新主力,B站、YouTube 及长视频平台大规模使用,近两年的手机与 PC 显卡基本全标配硬解
VP9 Google 阵营 谷歌早期免版税过渡标准,广泛用于 WebRTC 和油管,当前逐步向 AV1 转移
AVS3 中国标准工作组 我国自主研发的新一代超高清标准,主打 8K 广电直播(央视春晚、冬奥等)与自主可控信创领域
H.266 (VVC) MPEG / ITU-T 压缩率比 H265 再翻倍,但计算开销巨大、硬件解码尚未大规模普及,处于各大平台小范围点播试水阶段

视频编码的最终目的,就是在人眼察觉不到明显失真的前提下,把上述冗余信息全部剔除,将庞大的原始画面压缩为紧凑的数据流。

H264编解码基本闭环流程
H264编解码基本闭环流程

1.2 封装格式与编码

初学者常把 mp4、mkv 和 H264 混为一谈:

  • 视频编码(如 H264、H265):负责将原始图像数据压缩成紧凑的数据流,决定了压缩比和画质。
  • 封装格式(如 mp4、mov、avi、mkv):相当于一个打包盒子(容器)。它的作用是把压缩好的视频轨、音频轨以及字幕按固定规则装进同一个文件,并记录时长、索引等元数据。

二、画面组织与分类

在深入微观算法前,我们先从宏观视角看整部视频是如何由不同的画面角色组织起来的。

2.1 对画面分类 I/P/B帧

根据预测依赖关系的不同,H264 将视频帧分为三类:

  • I 帧(Intra-coded picture):关键帧。完全采用帧内预测压缩,不依赖前后任何外部帧即可独立解码,相当于一张完整的全家福照片。它的压缩率最低,体积最大,是所有预测的基准。
  • P 帧(Predicted picture):前向预测帧。只能参考前面的 I 帧或 P 帧进行单向差值计算,体积约为 I 帧的一半。
  • B 帧(Bi-directional predicted picture):双向参考帧。既参考前面的帧,又参考后面的帧。由于能双向借力,它的压缩率最高,体积最小,通常只有 I 帧的五分之一甚至十分之一。

IPB帧参考关系与编解码顺序对比
IPB帧参考关系与编解码顺序对比

引入 B 帧带来了一个重要问题:解码顺序与播放顺序不同。
DTS(解码时间戳)告诉解码器什么时候拆包还原画面,PTS(显示时间戳)告诉播放器什么时候把画面亮在屏幕上。因为 B 帧要参考后面的画面,解码器必须先把后面的帧提前解出来放在缓冲区等着,所以解的和播的顺序会有先后倒挂。

2.2 阻断错误扩散 GOP与IDR刷新机制

连续的 P 帧和 B 帧都在环环相扣地向前参考。如果网络丢包导致其中一个参考帧损坏,后续所有依赖它的帧就会全部推导失败,导致整个画面撕裂或出现严重色块,这种现象称为错误扩散。

为了阻止错误无限扩散,编码器必须周期性地插入完全独立的刷新帧。

GOP图像组内帧序列结构
GOP图像组内帧序列结构

  • IDR 帧(Instantaneous Decoding Refresh,立即刷新帧):这是一种特殊的 I 帧。当解码器遇到 IDR 帧时,会立刻清空现有的参考帧队列缓存,重新开始一个新的序列。从 IDR 帧之后的所有画面,绝不允许跨过它去参考它前面的帧。
  • GOP(Group of Pictures,图像组):相邻两个 IDR 帧之间的画面序列称为一个 GOP。一个 GOP 内通常只有开头一个 IDR 帧,后面全是由它派生出的 P 帧和 B 帧。

IDR帧界定GOP序列示意
IDR帧界定GOP序列示意

常见播放异常的本质:

  • 花屏:GOP 中途发生丢包,P 帧或 B 帧残缺,解码器拿残缺的参考数据强行计算,导致画面破损。
  • 卡顿:为了避免花屏,解码端在检测到丢包后主动把当前 GOP 剩余的所有数据全部扔掉,停在最后一幅画面静止等待下一个 IDR 帧重新刷屏。在等待新 IDR 帧到来的这段时间,画面定格形成卡顿。
  • 业务权衡:微信视频通话等实时通信场景,通常选择宁可偶尔卡顿或者快速丢弃请求重传,也要优先保证实时性;而在本地视频播放或广播录制时,则更倾向于丢弃坏帧等待刷新。

2.3 画质级别

为了适应从低性能手机、安防摄像头到蓝光光盘的不同硬件能力,H264 定义了不同的 Profile(特性档次开关):

  1. Baseline Profile:基础档次。只支持 I/P 帧,不支持 B 帧,仅支持 CAVLC。运算开销最小,延迟最低,广泛应用于实时音视频通信和低端硬件。
  2. Main Profile:主流档次。增加了对 B 帧、隔行扫描和 CABAC 的支持,适用于绝大多数流媒体和日常视频播放。
  3. Extended Profile:进阶档次。主要针对网络流媒体抗丢包场景设计,实际工业界极少使用。
  4. High Profile:高级档次。在 Main 的基础上增加了 8x8 频域变换和自定义量化矩阵,同等画质下比 Main 再省约 10% 码率,广泛应用于广电广播和高清蓝光存储。

H264各Profile特性支持对比表
H264各Profile特性支持对比表

三、图像色彩基础

宏观上理解了整部视频的帧分工后,再往下一层看单张画面本身是如何存储的——为什么视频不使用电脑屏幕通用的 RGB,而选用 YUV?

3.1 消除视觉冗余 YUV颜色模型

计算机显示器普遍使用 RGB(红、绿、蓝)三原色渲染画面,但视频压缩领域几乎全部采用 YUV 格式(也称 YCbCr)。

YUV 格式详解

YUV 将色彩信息拆分为两部分:

  • Y 分量:表示明亮度(Luminance,以前工程师喜欢用y代表亮度轴),即黑白灰度画面。
  • U 和 V 分量:分别表示色度(Chrominance)和浓度(Chroma),即色彩信息。

YUV全分量彩色渲染效果
YUV全分量彩色渲染效果

这种分离设计的最大好处是契合人眼生理特性:人眼视网膜上的视杆细胞(感知明暗)远多于视锥细胞(感知色彩)。哪怕画面完全没有 UV 分量,单纯保留 Y 分量,人眼依然能看清物体的清晰轮廓:

仅保留 Y 亮度分量 仅保留 U 色度分量 仅保留 V 色度分量
仅保留Y亮度分量的灰度图效果 仅保留U色度分量的渲染效果 仅保留V色度分量的渲染效果

3.2 YUV采样格式

利用人眼对色彩迟钝的特性,工程师在采集图像时就对 UV 分量进行下采样(色彩打折):

  • YUV 4:4:4:每个 Y 像素都对应完整的 U 和 V 分量,完全不压缩色彩,专业调色和制作用。
  • YUV 4:2:2:每两个横向相邻的 Y 像素共用一组 UV 分量,色彩数据减半。
  • YUV 4:2:0:最常用的通用格式。每 4 个相邻像素(2x2 方阵)只采样一组 UV 分量。相比 RGB 格式,原始图像数据量直接砍掉一半。

四、H264核心压缩步骤

弄清楚了画面的色彩格式与宏观角色后,接下来进入微观计算环节:前面提到的 I/P/B 帧,在底层究竟是怎么一步步算出来的?

宏观与微观的对应关系非常清晰:
P 帧和 B 帧在底层靠帧间预测借用前后的画面来推断自己的画面,I 帧靠帧内预测只利用自身画面的相邻像素来推算;两种预测推导出的差值,再统一送进 DCT、量化和熵编码完成终极瘦身。

我们按照“从整图切块、到多帧位移、再到单帧局部、最后到底层比特”的顺序逐层拆解:

H264编码原理全流程框架
H264编码原理全流程框架

4.1 对画面切块 宏块子块切分

压缩的第一步是切块。编码器不可能把一整张几百万像素的大图当成一个整体去算,而是将画面切成一个个小方块进行处理。

视频到宏块与子块的层级划分
视频到宏块与子块的层级划分

待编码的原始画面
待编码的原始画面

H264 默认将图像划分为 16x16 像素大小的单元,称为宏块(Macroblock):

画面局部16x16宏块网格切分
画面局部16x16宏块网格切分

每个宏块在物理上由具体的像素数值矩阵构成:

8x8像素块的数值立体高度模型
8x8像素块的数值立体高度模型

画面拆解为二维像素数值矩阵
画面拆解为二维像素数值矩阵

为了兼顾压缩效率与画面细节,H264 引入了灵活的子块划分机制:

  • 对于天空、草坪等颜色变化平缓的区域,使用完整的 16x16 大宏块,可以用极少的数据描述一大片区域。
  • 对于建筑边缘、物体轮廓等细节复杂区域,把宏块进一步切分成 16x8、8x16、8x8 甚至 4x4 等更小的子块分别精细编码。

宏块内部细分为多种灵活子块
宏块内部细分为多种灵活子块

4.2 消除时间冗余 帧间预测与运动补偿

这一步是前面讲到的 P 帧和 B 帧的底层核心实现。切好宏块后,首先处理的是跨越多张连续画面的时间冗余。

连续播放的视频帧之间大部分背景根本没动,往往只有某些物体发生了位置移动:

时间轴上的连续视频帧序列
时间轴上的连续视频帧序列

相邻两帧运动物体的宏块比对
相邻两帧运动物体的宏块比对

工程上判断连续帧是否相似的标准通常是:相邻两帧中有差异的像素在 10% 以内,亮度变化小于 2%,色度变化小于 1%。只要满足这些条件,两帧之间就具备极高的时间相关性。

帧间压缩不需要在后续帧里重复画出物体,而是做运动估计与补偿:

  1. 运动估计:在参考帧的局部搜索窗口内寻找与当前块长得最相似的宏块,计算出它移动了多少距离和方向,这个位移量称为运动矢量(Motion Vector)。
  2. 运动补偿:用参考块加上位移,拼出当前画面的推测图,再用实际画面减去推测图,得到微小的运动残差。

相邻帧之间的运动估计搜索
相邻帧之间的运动估计搜索

跨帧运动矢量轨迹计算
跨帧运动矢量轨迹计算

这样一来,后续画面只需要存储一个极小的运动矢量加上一点点残差数据,大幅降低了数据体积。

4.3 消除空间冗余 帧内预测

这一步是前面讲到的 I 帧(关键帧)的底层核心实现。处理完多帧之间的时间位移后,对于不能参考其他帧的独立画面(或帧内宏块),则深入到单张图片内部去除空间冗余。

单张图像相邻像素之间颜色往往非常接近。如果在每个宏块里都原原本本地保存全部像素值,会产生极大的浪费。H264 的思路是不存真实像素,只存推测规律和差值:

当前宏块编码时,编码器利用已经编码完成的左侧和上方边缘像素作为参考物,按照不同方向进行延伸推导,生成一个虚拟的“预测宏块”:

4x4亮度块的9种帧内预测模式
4x4亮度块的9种帧内预测模式

  • 4x4 亮度子块支持 9 种预测方向模式(垂直、水平、对角线平推等):
模式 描述
模式 0(垂直) 由 A、B、C、D 垂直推出相应像素值
模式 1(水平) 由 I、J、K、L 水平推出相应像素值
模式 2(DC) 由 AD 及 IL 平均值推出所有像素值
模式 3(下左对角线) 由 45° 方向像素内插得出相应像素值
模式 4(下右对角线) 由 45° 方向像素内插得出相应像素值
模式 5(右垂直) 由 26.6° 方向像素值内插得出相应像素值
模式 6(下水平) 由 26.6° 方向像素值内插得出相应像素值
模式 7(左垂直) 由 26.6° 方向像素值内插得出相应像素值
模式 8(上水平) 由 26.6° 方向像素值内插得出相应像素值
  • 16x16 亮度宏块支持 4 种预测模式(垂直、水平、全块均值 DC、线性平面平滑):
模式 描述
模式 0(垂直) 由上边像素推出相应像素值
模式 1(水平) 由左边像素推出相应像素值
模式 2(DC) 由上边和左边像素平均值推出相应像素值
模式 3(平面) 利用线性 plane 函数及左、上像素推出相应像素值,适用于亮度变化平缓区域

帧内预测模式对应的色块纹理示意
帧内预测模式对应的色块纹理示意

编码器会把每种模式都试一遍,挑选出一个与原图最像的预测模式:

宏块选择最佳预测模式编号的组合画面
宏块选择最佳预测模式编号的组合画面

预测画面与原始画面效果对比
预测画面与原始画面效果对比

选定最佳预测模式后,拿原始图像减去预测图像,得到两者之间的差值,这个差值被称为残差(Residual):

相减后得到的灰度残差数据层
相减后得到的灰度残差数据层

最终,编码器只需记录两项数据:所采用的预测模式编号(占几个比特)以及残差数据。解码端拿到这两项数据后,即可通过模式重新推算并加上残差,还原出真实图像。

预测模式信息与残差数据合并压缩
预测模式信息与残差数据合并压缩

4.4 消除视觉冗余 DCT变换与量化

不管是帧内预测还是帧间预测,算出来的差值(残差矩阵)依然包含不少细节数据。要进一步大幅瘦身,就需要针对人眼的“视觉冗余”下手。

人眼视觉有一个显著特点:对平缓的明暗轮廓(低频信息)极度敏感,但对剧烈跳动的微小噪点和细碎纹理(高频信息)非常迟钝,这些看不清的高频信息就是视觉冗余。

片、宏块与残差数据的组织结构
片、宏块与残差数据的组织结构

为了把这些视觉冗余挑出来剔除,需要分两步走:

  • 离散余弦变换(DCT):变换本身不会减少数据体积,而是把数据从空间坐标域转换到频率域,起到分拣分类的作用。变换后,代表整体轮廓基调的低频系数集中在矩阵左上角,而代表细微噪点的高频系数分布在右下角,将人眼敏感与不敏感的信息彻底区分开。
  • 量化(Quantization):这是整个编码过程中唯一发生信息丢弃的有损压缩步骤。量化利用人眼对高频不敏感的特性,把频域矩阵里的每个数值除以量化步长 QP 并四舍五入取整:

$$
FQ = \text{round}\left(\frac{y}{QP}\right)
$$

通过量化除法,那些数值微小、人眼又根本注意不到的高频系数,直接被除成了数字 0,从而安全剔除了视觉冗余,只保留核心的低频轮廓。QP 值越大除数越大,被置零的高频越多,体积越小但细节损失越大;QP 值越小保留细节越多但体积越大。

4.5 消除编码冗余 熵编码

经过量化后,残差矩阵变成了由极少数非零系数和大量 0 组成的稀疏矩阵。此时已经来到了比特级的最底层处理。

编码器先通过 ZigZag(之字形)扫描,把二维矩阵拉成一条一维数组。由于高频系数都在右下角且大多已被量化为 0,扫描后数组末尾会聚集大量连续的 0。

最后一步是对这条数组进行无损的熵编码:

  • CAVLC(基于上下文的自适应可变长编码):运算量小,所有设备均能支持。量化扫描后数组里绝大部分都是 0,CAVLC 的思路是不去硬存长串的 0,而是只抓几个关键特征查表压缩:
    • TotalCoeffs:一共有几个非零数字。
    • TrailingOnes:末尾连着几个 ±1。
    • TotalZeros:所有非零数字前面一共夹了几个 0。
    • RunBefore:每个非零数字紧前面挨着几个 0。
    • 自适应变量 nC:根据左边和上方邻居的情况动态换表,越常用的特征分配越短的码。
      更细节的内容请参考编码原理(五)–熵编码–CAVLC
  • CABAC(基于上下文的自适应二进制算术编码):压缩效率更高,通常能比 CAVLC 再多压出 10%~15% 的空间,但计算量更大,常见于主流和高清档次。更细节的内容可以查看:https://lazybing.github.io/blog/2017/09/12/video-coding/

五、本地存储与网络传输

在压缩算法完成后,得到的是纯粹的数学数据。如何将这些数据标准化地包装起来,用于网络推流或写入文件?

5.1 解耦算法与网络 VCL与NAL分层

H264 在架构上清晰解耦为两层:

  • VCL(Video Coding Layer,视频编码层):负责前文讲述的所有核心压缩运算,只关注数学算法,输出未经封装的原始二进制数据。
  • NAL(Network Abstraction Layer,网络抽象层):负责把 VCL 算出来的数据打包封装成统一格式的包裹(NALU),屏蔽底层网络介质的差异,适配不同的网络协议或文件容器。

为了抗丢包,一帧图像通常不会打成一个死板的大包,而是被切分成一个或多个片(Slice)。每个 Slice 独立包含片头和片数据,各自封装成一个 NALU 单元发送:

NAL层到宏块层的句法结构全景
NAL层到宏块层的句法结构全景

H264码流分层结构体系
H264码流分层结构体系

5.2 组装传输包裹 NALU打包结构

从底层计算数据到网络传输单元,需要经过一次流水线包装:

SODB到NALU及字节流转换关系
SODB到NALU及字节流转换关系

  1. SODB(String of Data Bits):算法算出来的原始比特流,像散装散沙,只记录有效数据,位数不一定能凑齐整字节。
  2. RBSP(Raw Byte Sequence Payload):在尾部加上补齐位强行凑成整字节,相当于把散沙装进标准规格的箱子里。
  3. NALU(NAL Unit):在箱子外面贴上一个 1 字节的标签头(NAL Header),标明这箱装的是什么,装箱完毕即可用于传输。

经过这三个步骤,即可讲视频编码层里的数据打包为可以方便网络传输的数据

1
2
3
RBSP = SODB + 结尾字节对齐比特
NALU = 1 字节 NAL Header + RBSP
H264 码流 = 分隔标识 + NALU + 分隔标识 + NALU + ...

码流中起始码与NALU排布关系
码流中起始码与NALU排布关系

NALU头部与RBSP负载排布示意
NALU头部与RBSP负载排布示意

NAL Header 包含 1 字节信息,其末尾 5 个 bit 决定了当前 NALU 的具体类型:

NALU头部类型定义表
NALU头部类型定义表

开发中最常见的 NALU 类型包括:

  • Type 1:非 IDR 图像的片(如普通的 P 帧、B 帧数据)。
  • Type 5:IDR 图像的片(关键帧数据)。
  • Type 7(SPS,序列参数集):整部视频的总说明书,记录分辨率、帧率、Profile 等全局关键参数。
  • Type 8(PPS,图像参数集):分章节说明书,记录局部画面所用的熵编码类型、初始量化参数等。

解码端要正常播放视频,必须先拿到 SPS 和 PPS 说明书。解码器只有读懂了这些参数,才知道该开辟多大的内存、用什么规则去解后面的图像片,否则拿到了画面数据也无法还原。

5.3 适配网络与本地存储 码流封包格式

NALU 只是一个个独立的数据包裹,怎么把它们连续组合成一段完整的数据流?工程上有两种主流方案:

  • Annex B 格式NALU 本身没有记录自己的长度。为了让接收端知道前一个包裹何时结束、下一个包裹何时开始,Annex B 在每个 NALU 前面硬塞一个起始码(0x0000010x00000001)作为分界线。起始码就像每个包裹之间的分隔标志。但万一包裹里的正文数据碰巧也算出了 00 00 01,解码器就会看走眼,以为包裹提前结束了。为了防止误判,编码器在打包时凡是遇到 00 00 0000 00 03 这类特征序列,就会在中间强行塞入一个 0x03 字节破坏特征(防竞争机制);解码器收到后再把塞进去的 03 拔掉扔掉,恢复原始数据。Annex B 格式支持从任意关键帧位置切入解码,非常适合 RTSP/RTMP 实时直播推流以及 .h264 裸流文件。
  • AVCC 格式
    不使用起始码和防竞争字节,而是在每个 NALU 前面直接固定用 4 个字节记录当前 NALU 的实际长度。SPS 和 PPS 参数集通常集中存放在外层文件头部。这种格式解析速度快且没有防竞争开销,主要用于 MP4、MKV、FLV 等本地封装容器。

参考引用