第十三章|基础专题五:视频编码共性
从 I/P/B 图像、GOP、帧内预测、帧间预测、运动估计、残差、变换、量化、熵编码、参考帧和 DPB 理解块式视频编码共同框架。
第十三章|基础专题五:视频编码共性
原稿学习节奏:第 5 周。
本周目标不是记住某个 H.264 字段,而是建立一套能够迁移到 H.264、H.265、VP9、AV1 等块式视频编码标准的统一模型。
核心主线只有一句话:编码器不直接描述每一帧,而是尽量描述“怎样预测这一帧,以及预测错了多少”。
本周学习安排(8~10 小时)
| 学习单元 | 建议时长 | 产出 |
|---|---|---|
| 1. 图像类型、GOP 与参考关系 | 1 小时 | 能画出 I/P/B 图像及参考箭头 |
| 2. 帧内预测与帧间预测 | 1.5 小时 | 能解释预测块从哪里来 |
| 3. 运动估计、运动补偿与亚像素插值 | 2 小时 | 手算 SAD、SSE 和简单运动搜索 |
| 4. 残差、变换、量化与熵编码 | 1.5 小时 | 手算一个微型变换和量化例子 |
| 5. 编码器重建闭环、环路滤波与 DPB | 1.5 小时 | 能解释为什么编码器内部必须“自带解码器” |
| 6. Web 场景、性能估算与面试复盘 | 1.5~2.5 小时 | 完成练习与系统设计题 |
一、学习目标
完成本周后,你应当能够:
- 画出传统块式视频编码器和解码器的完整数据流。
- 解释视频压缩如何同时利用空间冗余和时间冗余。
- 严格区分帧内预测、帧间预测、运动估计和运动补偿。
- 解释 I、P、B picture、参考帧、关键帧、IDR 和 GOP 不是同一维度的概念。
- 使用 SAD、SSE、SATD 和率失真代价比较候选预测模式。
- 解释运动矢量为什么不等于物体真实运动,也不等于光流。
- 解释残差为何还要经过变换、量化和熵编码。
- 解释为什么编码器必须使用“重建参考帧”,而不能直接使用原始参考帧。
- 说明环路滤波与普通显示后处理的根本区别。
- 估算参考帧缓冲、运动搜索和原始帧队列的资源消耗。
- 解释编码器为什么通常远比解码器复杂。
- 将 GOP、关键帧、参考帧和 lookahead 应用于 AI 视频预览、在线编辑、Seek 与低延迟设计。
二、概念地图
2.1 本周位于完整媒体系统的哪一层
原始像素帧:RGB / Y'CbCr / YUV420P / NV12
↓
视频编码共性:本周内容
预测 → 残差 → 变换 → 量化 → 熵编码
↘ 重建 → 环路滤波 → 参考帧 ↗
↓
具体视频标准:H.264 / H.265 / VP9 / AV1
↓
基础码流:NALU / OBU / frame unit
↓
容器:MP4 / WebM / MOV
↓
浏览器:<video> / MSE / WebCodecs
本周讨论的是视频编码算法的共同骨架。H.264、H.265 等标准会规定具体块划分、预测模式、插值滤波器、变换、量化、熵编码和参考图像管理语法,但它们共享“预测 + 残差 + 重建闭环”的基本思想。ITU-T H.264 的规范目录明确包含帧间预测、分数采样插值、变换系数解码、参考图像管理和去块滤波等解码过程;H.265 也规定相应的预测、变换、参考图像和环路处理机制。12
2.2 标准规定什么,编码器实现决定什么
| 范畴 | 通常由标准规定 | 通常由编码器实现决定 |
|---|---|---|
| 合法码流 | 语法元素、取值范围、解析顺序 | 如何组织程序和数据结构 |
| 解码结果 | 给定合法码流时的规范性重建过程 | SIMD、线程、缓存和硬件调度 |
| 预测工具 | 可用的预测模式和插值规则 | 搜索哪些模式、搜索多深 |
| 变换与量化 | 允许的变换、缩放和反量化规则 | 如何选择块大小、QP 和量化优化 |
| 参考管理 | 解码器如何建立和维护参考关系 | lookahead、场景切换检测、参考帧选择 |
| 码率控制 | 标准通常只约束输出码流 | CRF、ABR、VBV 分配、AQ 等策略 |
必须记住:视频编码标准主要保证“码流可被一致解码”,通常不会规定编码器必须使用哪一种搜索算法。 FFmpeg 文档中的 g、bf、refs、me_method、subq、crf、preset 等是框架或具体编码器暴露的实现选项,不是所有视频标准共同定义的码流字段。3
三、直觉解释
3.1 为什么连续视频比 JPEG 序列更容易压缩
假设摄像机固定拍摄一个人在白墙前走动:
- 每一帧的大部分墙面几乎不变。
- 人的纹理在相邻帧中也没有完全消失,只是移动了位置。
- 真正需要重新描述的,往往只是运动边缘、遮挡区域、光照变化和预测误差。
若把每一帧都独立压缩,编码器只能利用单帧内部的空间相关性。视频编码进一步利用相邻帧之间的时间相关性:
上一重建帧中的某个区域
↓ 平移、插值或加权组合
当前帧的预测块
↓
当前原始块 - 预测块
↓
只编码较小的残差
3.2 “预测 + 修正”模型
把当前块记为 X,预测块记为 P,残差记为 R:
$$ R = X - P $$
解码器得到预测块和重建残差后:
$$ \hat X = P + \hat R $$
其中:
X:编码器看到的原始块。P:帧内或帧间预测块。R:原始残差。\hat R:量化后再反量化、反变换得到的重建残差。\hat X:编码器和解码器都能得到的重建块。
若预测准确,残差中的大量值会接近 0,经过变换和量化后会产生许多零系数,因而更容易熵编码。
3.3 编码器内部为什么必须有一条“解码路径”
┌────────────码流────────────→
原始块 X → 预测 P → 残差 R → 变换 → 量化 → 熵编码
↑ │
│ ↓
参考帧缓冲 ← 环路滤波 ← 重建块 ← 反变换 ← 反量化
关键原因是:
解码器只能得到有量化误差的重建帧,编码器后续预测也必须使用同一版本的重建帧。
若编码器用无损的原始上一帧做参考,而解码器只能用有损的重建上一帧做参考,两边从下一帧开始就会产生不同预测;误差会沿参考链持续传播,这叫 drift(漂移)。
3.4 编码器与解码器的不对称
编码器面对的是一个搜索问题:
块要不要拆分?
使用帧内还是帧间?
参考哪一帧?
运动矢量是多少?
使用哪种预测模式?
变换块多大?
QP 取多少?
解码器面对的是一个执行问题:
码流已经告诉我模式、参考索引、运动矢量差和量化系数;
我按标准规定恢复预测和残差即可。
这就是“编码复杂、解码相对简单”的根本原因。
四、数学原理
4.1 残差
对一个 N×M 块:
$$ r(i,j)=x(i,j)-p(i,j) $$
若当前块与预测块分别为:
$$ X= \begin{bmatrix} 52&55\ 54&58 \end{bmatrix},\qquad P= \begin{bmatrix} 50&54\ 53&60 \end{bmatrix} $$
则:
$$ R=X-P= \begin{bmatrix} 2&1\ 1&-2 \end{bmatrix} $$
与直接编码 50~60 附近的像素相比,编码集中在 -2~2 的残差通常更有利。
4.2 SAD:绝对误差和
Sum of Absolute Differences,绝对差之和:
$$ SAD(X,P)=\sum_{i,j}|x(i,j)-p(i,j)| $$
上述例子:
$$ SAD=|2|+|1|+|1|+|-2|=6 $$
特点:
- 只需减法、绝对值和加法。
- 速度快,适合大规模整数像素运动搜索。
- 没有直接考虑编码该运动矢量和残差所需的比特数。
4.3 SSE:平方误差和
Sum of Squared Errors,平方误差和:
$$ SSE(X,P)=\sum_{i,j}(x(i,j)-p(i,j))^2 $$
上述例子:
$$ SSE=2^2+1^2+1^2+(-2)^2=10 $$
SSE 会更强烈地惩罚较大的单点误差。
考虑另一个候选预测:
$$ P_2= \begin{bmatrix} 51&52\ 55&57 \end{bmatrix} $$
其差值为 [1,3;-1,1]:
SAD(P_2)=6,与第一个候选打平。SSE(P_2)=12,因此 SSE 更偏好第一个候选。
4.4 SATD:变换域绝对误差和
Sum of Absolute Transformed Differences,变换后绝对差之和通常先对差值块做小型 Hadamard 变换,再累加系数绝对值:
$$ SATD(X,P)=\sum_{u,v}|H(X-P)H^T| $$
其中 H 是 Hadamard 变换矩阵。SATD 比 SAD 多一些计算,但能更好地反映残差进入变换编码后的能量分布,因此常用于较精细的模式决策。具体归一化、块大小和实现方式由编码器决定。
4.5 运动估计
设当前块为 B_t,参考帧为 F_r,候选运动矢量为 v=(v_x,v_y):
$$ v^*=\arg\min_v D\left(B_t, F_r(v)\right) $$
F_r(v):从参考帧按位移v取得的候选块。D:SAD、SSE、SATD 或其他代价。v^*:失真最小的候选运动矢量。
但只看失真仍不够。一个非常精确却很大的运动矢量,可能需要更多比特描述。因此实际编码决策通常使用率失真代价。
4.6 率失真优化
$$ J = D + \lambda R $$
J:总决策代价。D:重建失真或其代理指标。R:预计需要的比特数,包括模式、参考索引、运动矢量差和残差系数。\lambda:失真与码率之间的权重。
例子:
| 候选 | 失真 D | 预计比特 R | λ=2 时的 J |
|---|---|---|---|
| A | 100 | 12 | 124 |
| B | 75 | 25 | 125 |
虽然 B 的预测更准,但 A 少用 13 bit;在 λ=2 时编码器会选择 A。
若 λ=0.5:
J_A=106J_B=87.5
此时编码器更重视质量,会选择 B。
直觉: 低码率或高 QP 时,编码器通常更不愿为小幅质量改善支付大量比特;高质量编码时则相反。
4.7 亚像素运动补偿
物体在两帧之间可能移动 0.5 或 0.25 个像素。参考帧没有直接存储“半像素”,解码器通过规范规定的插值滤波器生成这些位置的样本。
仅用于理解的线性示例:若相邻样本是 100 和 120,则中间半像素可近似为:
$$ p_{1/2}=\frac{100+120}{2}=110 $$
真实标准通常使用固定的多抽头滤波器、舍入和裁剪规则,而不是随意取平均。编码器搜索亚像素位置,解码器根据码流中的运动矢量执行同样的插值。
4.8 变换:把残差能量集中起来
以一个简化的二维正交变换说明直觉:
$$ H=\frac{1}{\sqrt2} \begin{bmatrix} 1&1\ 1&-1 \end{bmatrix},\qquad C=HRH^T $$
若残差块是:
$$ R= \begin{bmatrix} 4&4\ 4&4 \end{bmatrix} $$
则:
$$ C= \begin{bmatrix} 8&0\ 0&0 \end{bmatrix} $$
四个相同残差被集中到一个低频系数中,其余三个系数为 0。实际标准常使用整数变换或 DCT 类变换;不能笼统地说所有视频编码器都直接使用浮点 DCT。
4.9 量化
对变换系数 C(u,v):
$$ Q_C(u,v)=\operatorname{round}\left(\frac{C(u,v)}{q(u,v)}\right) $$
反量化:
$$ \hat C(u,v)=Q_C(u,v)\cdot q(u,v) $$
例子:
- 原系数:
37 - 量化步长:
8 - 量化结果:
round(37/8)=5 - 反量化结果:
5×8=40 - 系数误差:
3
量化步长越大,零系数通常越多、码率越低,但重建误差也越大。QP(Quantization Parameter,量化参数)通常用于控制量化强度,但 QP 到量化步长的精确映射由具体标准规定。
4.10 总比特预算
一个图像或帧的码率可粗略拆成:
$$ R_{total}=R_{header}+R_{mode}+R_{motion}+R_{residual}+R_{filter} $$
这解释了两个重要现象:
- 预测越复杂不一定越划算,因为模式和运动信息也要占比特。
- 残差为零不等于该块完全不花比特,仍可能需要模式或跳过标志。
五、底层数据结构
5.1 从图像到块
传统块式编码器通常可抽象为:
Sequence
└─ Picture
├─ Picture / slice / tile level syntax
└─ Block partition tree
├─ Prediction unit
│ ├─ intra mode
│ └─ 或 reference index + motion information
├─ Transform unit
│ └─ quantized coefficients
├─ quantization information
└─ loop-filter related information
上图是概念示意,不是某个标准的真实字段表。H.264 使用 macroblock 等术语;H.265 使用 CTU、CU、PU、TU 等层次。第 6~8 周再进入具体语法。
5.2 I、P、B picture
| 类型 | 核心预测来源 | 典型特征 | 不能错误推断 |
|---|---|---|---|
| I picture | 同一图像内已重建邻域 | 随机访问候选、体积通常较大 | I 不一定就是 IDR |
| P picture | 参考图像列表中的一个方向或单列表预测为主 | 常用于前向时间预测 | P 不等于“只参考显示顺序前一帧” |
| B picture | 可从两个参考列表选择、组合预测 | 压缩效率通常较高,可能引入重排 | B 不一定永远是非参考帧 |
“帧类型”“是否是参考图像”“是否构成随机访问点”是不同维度。
5.3 关键帧、I picture 与 IDR
| 概念 | 所属层次 | 更准确的理解 |
|---|---|---|
| I picture | 编码图像类型 | 图像内的编码单元使用帧内预测 |
| keyframe | 工程和播放器常用术语 | 通常指可作为 Seek 起点的样本,但语义可能依上下文而异 |
| IDR | H.264/H.265 等标准中的随机访问语义 | 阻止后续图像引用 IDR 之前的参考图像,并重置相应参考关系 |
| reference picture | 解码参考关系 | 被后续图像用于预测的重建图像 |
因此:
- I picture 可能不是可靠的独立随机访问点。
- B picture 在现代编码结构中也可能成为参考图像。
- 容器中的“同步样本”标记与基础码流语义需要正确对应。
5.4 GOP
Group of Pictures,图像组是编码结构和工程配置中的常用概念。
简化显示顺序示例:
I B B P B B P B B I
0 1 2 3 4 5 6 7 8 9
可能的参考关系:
I0 ─────────→ P3 ─────────→ P6 ─────────→ I9
↘ B1, B2 ↘ B4, B5 ↘ B7, B8
因为 B1、B2 需要 P3 作为未来显示参考,编码或解码输入顺序可能先处理 P3,再处理 B1、B2。显示顺序与解码顺序因此可能不同。
Closed GOP 与 Open GOP
- Closed GOP: GOP 内图像不依赖前一个 GOP 的参考图像,随机访问边界更清晰。
- Open GOP: 边界附近图像可能跨 GOP 引用,通常能提高一点压缩效率,但剪切、拼接和随机访问更复杂。
这些术语的精确行为取决于具体标准和编码器实现,不能只凭“看到 I 帧”判断是否真正闭合。
5.5 参考帧与 DPB
**Decoded Picture Buffer,解码图像缓冲区(DPB)**用于保存:
- 仍可能被后续图像引用的重建图像。
- 已解码但尚未到显示时刻的图像。
- 参考标记、图像顺序和输出状态等信息。
DPB 不是简单的“最近 N 帧数组”。某些较旧图像可能继续作为长期参考,某些刚解码图像可能不作为参考,但仍需等待输出。
六、编码器流程
6.1 总流程
输入原始帧
↓
像素格式、位深和颜色信息归一化
↓
lookahead:复杂度、场景切换、未来参考分析
↓
决定图像类型和参考结构
↓
块划分与候选模式生成
↓
帧内预测 / 帧间预测
↓
率失真选择
↓
残差变换与量化
↓
熵编码语法和量化系数
↓
输出基础码流
同时执行本地重建:
反量化 → 反变换 → 加预测 → 环路滤波 → DPB
6.2 第一步:输入准备
编码器通常需要统一:
- 像素格式,例如 YUV420P、NV12。
- 位深,例如 8-bit 或 10-bit。
- 色度采样位置。
- 宽高、stride 和 plane。
- 颜色 primaries、transfer、matrix 与 range 元数据。
- 时间戳、帧率或帧持续时间。
这些不直接决定运动搜索,但错误的像素格式或时间信息会让整个编码输出失真或时间轴错误。
6.3 第二步:lookahead 与图像类型决策
lookahead 会暂存未来若干帧,用于估计:
- 是否发生场景切换。
- 哪些帧适合作为参考帧。
- B picture 放置位置。
- GOP 长度和关键帧位置。
- 各帧复杂度与码率分配。
lookahead 越深,编码器通常有更多全局信息,但会增加:
- 首帧输出延迟。
- 原始帧内存占用。
- 编码决策复杂度。
低延迟场景通常缩短或关闭 lookahead。
6.4 第三步:帧内预测
帧内预测使用同一图像中已经重建并允许引用的邻域样本,构造当前块预测。
典型思路包括:
- DC:使用邻域平均值。
- Vertical:沿垂直方向延伸上方样本。
- Horizontal:沿水平方向延伸左侧样本。
- Directional:沿特定角度外推边缘纹理。
- Planar:平滑地结合上方和左侧边界。
为什么必须使用“已重建邻域”而不是原始邻域?因为解码器没有原始图像,必须保证两边预测输入一致。
6.5 第四步:帧间预测
运动估计
运动估计只发生在编码器:
当前块
↓
在一个或多个参考帧的搜索窗中找候选位置
↓
计算 SAD / SATD / RDO 代价
↓
选择参考索引、运动矢量和预测方式
运动补偿
运动补偿发生在编码器和解码器:
参考帧 + 参考索引 + 运动矢量 + 插值规则
↓
预测块
双向或双列表预测
预测块可以由两个参考预测加权组合:
$$ P=\operatorname{clip}\left(\frac{w_0P_0+w_1P_1+o}{s}\right) $$
P_0、P_1:两个参考预测。w_0、w_1:权重。o:偏置或舍入项。s:缩放因子。clip:裁剪到合法像素范围。
具体权重和舍入由标准或码流语法规定。
6.6 运动搜索伪代码
struct MotionCandidate {
int refIndex;
int mvX;
int mvY;
double distortion;
double estimatedBits;
double rdCost;
};
MotionCandidate searchMotion(
const Block& current,
const std::vector<Frame>& references,
int searchRange,
double lambda
) {
MotionCandidate best{};
best.rdCost = std::numeric_limits<double>::infinity();
for (int ref = 0; ref < references.size(); ++ref) {
for (int dy = -searchRange; dy <= searchRange; ++dy) {
for (int dx = -searchRange; dx <= searchRange; ++dx) {
Block prediction = interpolateBlock(references[ref], dx, dy);
double d = satd(current, prediction);
double r = estimateMotionBits(ref, dx, dy);
double j = d + lambda * r;
if (j < best.rdCost) {
best = {ref, dx, dy, d, r, j};
}
}
}
}
return best;
}
这段代码只表达思想。真实编码器会使用:
- 运动矢量预测。
- 分层搜索。
- diamond、hexagon、uneven multi-hexagon 等快速搜索。
- 整数像素、半像素、四分之一像素逐级细化。
- 提前终止。
- 多种块划分。
- SIMD 和多线程。
6.7 第五步:残差、变换与量化
原始块 X - 预测块 P
↓
残差 R
↓
变换系数 C
↓
量化系数 Q_C
↓
扫描、零游程、上下文建模或其他熵编码
编码器还可能比较:
- 不编码残差,直接使用预测。
- 使用不同变换块大小。
- 使用 transform skip 或 bypass(若标准支持)。
- 调整块级 QP 或自适应量化。
6.8 第六步:熵编码
熵编码负责压缩:
- 图像和块类型。
- 划分结构。
- 帧内模式。
- 参考索引。
- 运动矢量差。
- QP 差。
- 非零系数位置、幅值和符号。
- 环路滤波参数。
熵编码本身通常是无损的;主要有损误差来自量化和某些有损预处理。H.264 可使用 CAVLC 或 CABAC,H.265 主要使用 CABAC,但这属于后续具体标准内容。12
6.9 第七步:本地重建与参考入库
量化系数
↓ 反量化
重建变换系数
↓ 反变换
重建残差
↓ + 预测块
未滤波重建块
↓ 环路滤波
重建图像
↓
DPB / 参考帧缓冲
环路滤波为什么在“环”内
若滤波后的重建图像会被后续图像作为参考,滤波就是编码闭环的一部分。编码器和解码器必须一致执行。
显示端额外做的锐化、降噪或超分辨率,若不参与后续参考预测,则属于环外后处理。
6.10 场景切换
硬切场景中,上一帧内容与当前帧几乎无关:
- 帧间预测残差突然增大。
- 运动搜索可能找到“数值接近但语义错误”的候选块。
- 继续强行使用长参考链可能既费码率又降低质量。
编码器常通过直方图、帧差、帧内/帧间代价比或 lookahead RDO 检测场景切换,并在合适位置插入 I picture 或随机访问图像。FFmpeg 和 libx264 暴露的 scene-cut、GOP、B-frame、参考帧和运动搜索参数属于编码器策略。3
七、解码器流程
7.1 总流程
基础码流
↓
解析图像级和块级语法
↓
熵解码
↓
得到预测模式、参考索引、运动信息、量化系数
↓
构造帧内或帧间预测块
↓
反量化和反变换
↓
预测 + 重建残差
↓
环路滤波
↓
DPB 管理
↓
按显示时序输出
7.2 解码器不做什么
解码器通常不需要:
- 搜索最佳运动矢量。
- 尝试所有块划分。
- 比较多个帧内模式。
- 运行率失真优化。
- 做场景切换决策。
- 运行编码码率控制。
解码器只需按码流恢复编码器已经选定的方案。
7.3 概念伪代码
for (PictureSyntax picture : bitstream) {
Frame reconstruction(picture.width, picture.height);
for (BlockSyntax block : picture.blocksInDecodeOrder()) {
Block prediction;
if (block.isIntra()) {
prediction = intraPredict(reconstruction, block.intraMode);
} else {
prediction = motionCompensate(
dpb,
block.referenceIndices,
block.motionVectors,
block.weightingParameters
);
}
Coefficients qcoeff = entropyDecode(block.coefficientSyntax);
Coefficients coeff = inverseQuantize(qcoeff, block.qp);
Block residual = inverseTransform(coeff, block.transformType);
reconstruction.write(block.position, clip(prediction + residual));
}
applyInLoopFilters(reconstruction, picture.filterParameters);
dpb.markAndStore(reconstruction, picture.referenceCommands);
outputWhenReady(dpb, picture.presentationOrder);
}
这仍然是跨标准抽象。真实解码顺序、边界可用性、参考列表和滤波流程必须遵循具体标准。
八、复杂度与性能
8.1 运动搜索的数量级
以 1920×1080 图像、16×16 块、整数像素搜索范围 [-64,+64] 为极端全搜索估算:
- 每个方向候选数:
129 - 每块候选数:
129×129=16,641 - 每个候选比较样本数:
256 - 每块绝对差运算约:
16,641×256≈4.26×10^6 - 图像块数约:
120×68=8,160 - 每帧约:
3.48×10^10次样本差比较 - 30 FPS 时约:
1.04×10^12次/秒
这是用于建立数量级直觉的上界估算,真实编码器会利用边界裁剪、快速搜索、层级金字塔、预测起点、提前终止和硬件并行大幅减少工作量。
8.2 参考帧内存
8-bit YUV420P 单帧理论数据量:
$$ Bytes = width\times height\times1.5 $$
| 分辨率 | 单帧约占用 | 4 帧 | 8 帧 | 16 帧 |
|---|---|---|---|---|
| 1920×1080 | 3.11 MB | 12.44 MB | 24.88 MB | 49.77 MB |
| 3840×2160 | 12.44 MB | 49.77 MB | 99.53 MB | 199.07 MB |
实际硬件表面还可能包含:
- stride 对齐。
- 额外 padding。
- 10-bit 或更高位深。
- 压缩表面或厂商私有布局。
- 重排帧、输入队列和滤镜中间缓冲。
因此不能只按 width×height×1.5×参考帧数 估算整个解码器或编辑器内存。
8.3 主要性能瓶颈
| 阶段 | 常见瓶颈 |
|---|---|
| 运动估计 | 候选数、参考帧数、内存带宽、亚像素插值 |
| 模式决策 | 块划分组合爆炸、重复变换与熵估算 |
| 变换量化 | 小块密集计算、缓存局部性 |
| 熵编码 | 分支、上下文依赖、串行性 |
| 环路滤波 | 邻块依赖、跨行和跨 tile 边界 |
| DPB | 大分辨率表面、参考读带宽、重排 |
| Web 编辑 | JS 与媒体线程调度、GPU 上传、帧生命周期、复制 |
8.4 CPU、GPU 与专用硬件
- CPU 软件编码:搜索灵活、可控性高,但功耗和延迟可能较高。
- GPU compute:适合滤镜、缩放、合成,也可用于部分编码前处理。
- 固定功能硬件编解码器:吞吐量和功耗好,但可用参数、格式和并发资源受设备限制。
- 浏览器中的 WebCodecs 通常只暴露标准化接口,不保证使用何种底层实现,也不向页面暴露完整运动矢量、分块树或 RDO 过程。WebCodecs 不要求用户代理支持任何特定 codec,必须运行时探测。4
8.5 并行化
可利用的并行层次包括:
- 多帧并行。
- slice、tile 或行级并行。
- 块级候选并行。
- SIMD 向量化 SAD、插值、变换和滤波。
- CPU 与 GPU 流水线重叠。
但预测依赖和熵编码上下文会限制完全并行。更多 tile 或 slice 可能提高并行度,却可能损失跨边界预测效率或增加头部开销。
8.6 低延迟取舍
为了降低首帧和端到端延迟,常见做法是:
- 关闭或减少 B picture。
- 缩短 GOP。
- 减少参考帧。
- 缩短或关闭 lookahead。
- 限制重排。
- 使用低延迟码率控制和较小缓冲。
- 选择更快的编码 preset。
代价通常是同等质量下码率上升,或同等码率下质量下降。
九、实际场景
A. AI 生成视频网页
9.1 模型时序一致性直接影响可压缩性
AI 生成视频即使主观上“看起来相似”,若相邻帧中存在:
- 纹理随机抖动。
- 人脸细节持续变化。
- 背景颗粒闪烁。
- 物体轮廓轻微形变。
- 光照和颜色无规则跳动。
运动补偿也难以找到稳定预测,残差会变大。此时高码率并不一定是编码器问题,而可能是生成内容缺乏时间一致性。
9.2 边生成边预览
推荐思路:
生成帧
↓
排队与时间戳分配
↓
低延迟编码:短 GOP、少重排、受控 lookahead
↓
生成连续媒体分片
↓
浏览器 MSE 或其他播放管线
关键点:
- 每个分片边界最好与可随机访问点协调。
- 不能让生成速度无限领先于编码、网络或播放速度。
- 场景切换或提示词阶段切换处可考虑新的随机访问点。
- 预览编码与最终高质量编码可以分离。
9.3 预览与最终导出分层
| 产品阶段 | 优先目标 | 编码倾向 |
|---|---|---|
| 生成中预览 | 低首帧、低延迟 | 较快 preset、短 GOP、少 B picture |
| 用户挑选片段 | 可 Seek、稳定播放 | 代理视频、规律关键帧 |
| 最终导出 | 质量与体积 | 更深搜索、更多 lookahead、离线编码 |
B. 在线 Web 视频编辑器
9.4 Seek 与 GOP
拖动到时间 t 时:
查找 t 之前最近的随机访问样本
↓
从该点开始送入解码器
↓
维护参考关系并连续解码
↓
丢弃显示时间早于 t 的输出帧
↓
显示目标帧
GOP 越长,平均压缩效率可能越高,但最坏 Seek 解码距离通常也越长。
9.5 为什么需要代理视频
代理视频可以使用:
- 较低分辨率。
- 较低码率。
- 更短、规律的 GOP。
- 易于硬件解码的 profile 和像素格式。
它降低 Seek、预览合成、帧缓存和 GPU 上传压力。最终导出仍基于原始高质量资产。
9.6 帧精确剪切
- 切点位于可靠随机访问点:可能直接复制完整 GOP 或样本范围。
- 切点位于 GOP 中间:通常要从前置关键帧解码到切点。
- 要让输出从非关键帧位置独立开始,边界附近通常需重新编码。
- 转场、滤镜、字幕、缩放和画中画会改变像素,通常需要解码、合成和重新编码。
9.7 快速拖动时间线
必须设计取消与代际编号:
用户请求 seek #41
↓
用户立即请求 seek #42
↓
标记 #41 结果为过期
↓
重置或重建解码任务
↓
只显示 #42 对应输出
否则旧请求晚到的帧可能覆盖新位置,造成画面回跳。
十、Web 实现视角
10.1 WebCodecs 所在位置
MP4 / WebM 文件
↓ demux(应用自行完成或使用库)
EncodedVideoChunk
↓ VideoDecoder
VideoFrame
↓ Canvas / WebGL / WebGPU / 分析
VideoFrame
↓ VideoEncoder
EncodedVideoChunk
↓ mux(应用自行完成或使用库)
MP4 / WebM
WebCodecs 提供编解码和原始媒体接口,但不负责自动完成 MP4/WebM demux、mux、时间线编辑或文件索引。规范也不要求浏览器实现任何特定 codec。4
10.2 能力探测
const decoderSupport = await VideoDecoder.isConfigSupported({
codec: "avc1.640028",
codedWidth: 1920,
codedHeight: 1080,
});
if (!decoderSupport.supported) {
// 选择其他 profile、codec、分辨率或服务端降级路径
}
不能因为 codec string 有定义,就假设当前浏览器、操作系统和硬件一定支持该配置。
10.3 Backpressure
VideoDecoder.decodeQueueSize 和 VideoEncoder.encodeQueueSize 可用于观察待处理请求数量;规范还定义了队列减少时的 dequeue 事件。4
概念代码:
function once(target: EventTarget, type: string): Promise<Event> {
return new Promise(resolve => {
target.addEventListener(type, resolve, { once: true });
});
}
async function submitChunk(
decoder: VideoDecoder,
chunk: EncodedVideoChunk,
maxQueue = 8,
): Promise<void> {
while (decoder.decodeQueueSize >= maxQueue) {
await once(decoder, "dequeue");
}
decoder.decode(chunk);
}
队列阈值不是统一标准答案,应根据:
- 解码延迟。
- 分辨率。
- 帧率。
- 设备能力。
- 用户是否正在播放或拖动。
- 允许的内存上限。
动态调整。
10.4 VideoFrame.close()
解码媒体可能占用大量系统或 GPU 资源。WebCodecs 的资源模型要求应用通过 close() 释放不再需要的 VideoFrame 引用,并明确建议尽快关闭。4
const decoder = new VideoDecoder({
output(frame) {
try {
// drawImage、复制到目标缓冲,或把所有权明确传给下游
renderSynchronously(frame);
} finally {
frame.close();
}
},
error(error) {
console.error("decode failed", error);
},
});
若下游异步使用该帧,应先完成转移、克隆或复制,再关闭原对象;不能在消费者仍依赖资源时过早关闭。
10.5 浏览器通常看不到编码器内部决策
WebCodecs 页面层通常可以控制:
- codec、分辨率、码率、帧率等配置。
- 某帧是否请求 key frame。
- 输入时间戳。
- 编码和解码队列。
但通常不能直接读取或指定:
- 每个块的运动矢量。
- 完整分块树。
- 每个候选的 RDO 代价。
- 精确运动搜索算法。
- 所有参考帧选择细节。
若产品需要这些分析数据,通常要使用自有 WASM 编码器、服务端编码器、研究解码器或专门的码流分析工具。
10.6 Worker 与线程划分
推荐将以下任务移出主线程:
- demux 和索引解析。
- 编解码调度。
- 时间线帧请求。
- 缩略图生成。
- 部分 Canvas 或 WebGL/WebGPU 渲染。
主线程重点处理交互与轻量状态更新。即使底层编解码器在独立媒体线程工作,页面代码仍可能因频繁复制、回调风暴或大对象生命周期管理不当而卡顿。
十一、易错点
- 运动估计不等于运动补偿。 前者是编码器搜索;后者是根据已知运动信息生成预测。
- 运动矢量不等于物体真实速度。 它是使编码代价较小的块对应关系。
- I picture 不等于 IDR。 帧内编码不自动保证完整随机访问语义。
- 关键帧不是严格统一的基础码流术语。 容器、播放器和编码器语境可能不同。
- B picture 不一定是非参考图像。 现代编码结构可以让 B picture 被后续图像引用。
- P picture 不等于只参考紧邻上一显示帧。 参考列表和重排可能更复杂。
- GOP 长度不等于固定的 I 帧间隔。 场景切换、open GOP 和编码器策略都会影响结构。
- 编码器不能用原始参考帧替代重建参考帧。 否则编码器与解码器会漂移。
- 残差小不代表码率一定小。 模式、运动信息和系数分布也影响比特数。
- SAD 最小不代表最终码率质量最优。 应考虑
D+λR。 - QP 不是“质量百分比”。 它是控制量化强度的参数,映射由标准和编码器实现共同决定。
- 变换不产生主要有损误差。 正交或整数可逆近似本身不是主要损失源,量化通常才是核心有损环节。
- 熵编码不是有损压缩。 它无损地压缩已经选定的语法符号和量化系数。
- 去块滤波不一定只是播放器美化。 作为 in-loop filter 时,它影响后续参考预测。
- 参考帧数增加不总能提高质量。 还会增加搜索、信令、DPB 和内存带宽成本。
- lookahead 不是解码器需求。 它是编码器为了未来决策暂存原始帧的策略。
- 短 GOP 不一定画质更高。 它改善随机访问,却可能增加帧内编码开销。
- 长 GOP 不一定适合编辑。 Seek 和边界剪切成本会提高。
- WebCodecs 编码输出不是完整 MP4。 还需组织时间戳并 mux。
- 有 WebCodecs 接口不等于配置一定可用。 必须运行时探测。
decodeQueueSize不是内存占用字节数。 它只是待处理请求数量的一个信号。- 调用
flush()不是日常背压机制。 频繁 flush 可能破坏流水线效率。 VideoFrame.close()不能随意延迟到 GC。 大量媒体资源会造成显著内存压力。- AI 视频高码率不一定是编码器差。 时序闪烁和形变会显著破坏帧间预测。
十二、面试题
下面每题给出“标准回答、追问、常见错误、高级回答”。回答以本周通用框架为主,不提前陷入某个标准的全部语法细节。
12.1 基础题(10 题)
1. 视频编码为什么需要预测?
- 标准回答: 相邻像素和相邻帧高度相关。预测把原始块转换为更接近零的残差,便于变换、量化和熵编码。
- 追问: 预测越准是否一定越好?
- 常见错误: “预测后就不用编码残差了。”
- 高级回答: 应比较率失真代价;更准的预测可能需要更多模式或运动比特。
2. 帧内预测和帧间预测有什么区别?
- 标准回答: 帧内预测使用同一图像已重建邻域;帧间预测使用 DPB 中的重建参考图像。
- 追问: 为什么都强调“重建”?
- 常见错误: “帧内就是 I 帧,帧间就是 P 帧。”
- 高级回答: 图像类型和块级模式并非总是一一对应;关键是预测样本的来源和标准允许的模式。
3. 运动估计和运动补偿有什么区别?
- 标准回答: 运动估计在编码器中搜索运动信息;运动补偿在编码器和解码器中根据运动信息生成预测块。
- 追问: 解码器需要做全搜索吗?
- 常见错误: 把两者都说成“寻找运动矢量”。
- 高级回答: 编码器会把运动矢量与预测值的差进行信令,解码器只需恢复并执行插值。
4. I、P、B picture 分别是什么?
- 标准回答: I 主要使用图像内预测;P 主要使用一个参考列表方向的帧间预测;B 可使用两个参考列表和组合预测。
- 追问: B picture 能否作为参考?
- 常见错误: “B 一定不被参考。”
- 高级回答: picture type、reference status 和 random-access status 应分开讨论。
5. GOP 是什么?
- 标准回答: GOP 是一组具有特定预测和随机访问结构的连续编码图像,常用于描述关键帧间隔和参考结构。
- 追问: 长 GOP 与短 GOP 如何取舍?
- 常见错误: “GOP 就是两个 I 帧之间的帧数,所有编码器都严格固定。”
- 高级回答: 还要区分 open/closed GOP、场景切换和 IDR 周期。
6. 什么是参考帧?
- 标准回答: 被后续图像用于帧间预测的重建图像。
- 追问: 所有已解码帧都会成为参考帧吗?
- 常见错误: “参考帧就是上一帧。”
- 高级回答: DPB 需要同时处理参考标记与输出重排,两者生命周期不同。
7. 什么是残差?
- 标准回答: 原始块与预测块之差,编码器通常编码预测信息和量化残差,而非直接编码原像素。
- 追问: 残差为什么还要变换?
- 常见错误: “残差就是编码误差。”
- 高级回答: 原始残差和量化后的重建误差不是同一个概念。
8. QP 越大通常意味着什么?
- 标准回答: 通常意味着更粗的量化、更低码率和更大失真。
- 追问: QP 每增加 1,质量是否线性下降?
- 常见错误: “QP 是 0~100 的质量百分比。”
- 高级回答: 需区分标准定义的 QP、量化步长映射、编码器 AQ 和感知优化。
9. 为什么需要去块滤波?
- 标准回答: 块独立预测和量化可能导致边界不连续;去块滤波降低块效应,并在 in-loop 场景中改善后续参考质量。
- 追问: 能否只在显示时滤波?
- 常见错误: “只是播放器锐化。”
- 高级回答: 环路内滤波会改变参考图像,必须由编码器和解码器一致执行。
10. 为什么编码器比解码器复杂?
- 标准回答: 编码器要搜索块划分、预测模式、参考帧、运动矢量和 QP;解码器只执行码流已确定的选择。
- 追问: 慢 preset 会增加解码复杂度吗?
- 常见错误: “因为编码器做 DCT,解码器不做。”
- 高级回答: 慢 preset 主要增加编码搜索,但某些选择也可能影响解码负载和并行性,不能绝对化。
12.2 底层实现题(10 题)
11. 为什么编码器必须本地重建?
- 标准回答: 保证后续预测使用与解码器完全一致的参考图像,避免量化误差造成漂移。
- 追问: 漂移如何传播?
- 常见错误: “只是为了预览编码结果。”
- 高级回答: 漂移会沿参考依赖图扩散,直到随机访问或参考刷新边界切断依赖。
12. 为什么运动矢量常编码成差值?
- 标准回答: 邻近块的运动通常相关,使用预测运动矢量后只编码 MVD,分布更集中。
- 追问: 预测运动矢量从哪里来?
- 常见错误: “直接把绝对坐标写进码流。”
- 高级回答: 候选可来自空间邻块、时间同位块或 merge/AMVP 类机制,具体由标准规定。
13. 亚像素运动补偿为什么有用?
- 标准回答: 实际运动不一定是整数像素,亚像素插值能降低预测残差。
- 追问: 半像素样本存在哪里?
- 常见错误: “参考帧预先保存了所有半像素。”
- 高级回答: 通常按规范滤波按需生成或缓存,工程上需权衡计算与内存。
14. 块为什么要有多种大小?
- 标准回答: 大块适合平坦或一致运动区域,小块适合边缘、复杂运动和遮挡边界。
- 追问: 为什么不全部用最小块?
- 常见错误: “块越小质量一定越高。”
- 高级回答: 小块会增加划分、模式和运动信令,并放大搜索复杂度。
15. skip 或 merge 类模式解决什么问题?
- 标准回答: 当邻域已有合适运动信息且残差很小,可减少重复信令。
- 追问: skip 是否代表像素完全不变?
- 常见错误: “skip 就是复制上一帧同位置。”
- 高级回答: 预测候选、参考索引和是否编码残差取决于具体标准模式。
16. 为什么环路滤波必须在参考帧入 DPB 前完成?
- 标准回答: 后续运动补偿应引用规范规定的滤波后重建图像。
- 追问: 编码器与解码器滤波结果差一位会怎样?
- 常见错误: “滤波只影响当前显示。”
- 高级回答: 任何不一致都可能沿参考链扩散为持续漂移。
17. DPB 为什么不等于简单 FIFO?
- 标准回答: 图像是否仍作参考、是否已输出、显示顺序和长期参考状态可能不同。
- 追问: B picture 带来什么额外状态?
- 常见错误: “解码完就按输入顺序显示。”
- 高级回答: 需要同时管理 reference lifecycle 与 output reordering。
18. 场景切换时编码器为什么常插入帧内图像?
- 标准回答: 跨场景帧间相关性弱,继续预测会产生大量残差和无效运动搜索。
- 追问: 是否每个硬切都必须 IDR?
- 常见错误: “检测到色差就固定插 IDR。”
- 高级回答: 应结合随机访问需求、VBV、GOP 约束和 open-GOP 策略决定。
19. 为什么 SATD 常比 SAD 更适合精细模式决策?
- 标准回答: SATD 对差值做变换,更接近后续变换编码的能量与可编码性。
- 追问: 为什么不对所有候选做完整熵编码?
- 常见错误: “SATD 一定等于最终码流大小。”
- 高级回答: 编码器采用分层筛选:快速指标淘汰,再对少量候选做更精确 RDO。
20. 为什么参考帧越多不一定越好?
- 标准回答: 更多参考可能找到更好预测,但增加搜索时间、参考索引比特、DPB 和带宽。
- 追问: 哪类内容可能更受益?
- 常见错误: “把 refs 拉满就能免费提升质量。”
- 高级回答: 周期运动、遮挡后重现和镜头静止可能受益,但需看码率和复杂度预算。
12.3 数学题(10 题)
21. 手算 SAD 与 SSE
- 题目: 差值块为
[2,1;1,-2]。 - 标准回答:
SAD=6,SSE=10。 - 追问: 哪个更惩罚离群误差?
- 常见错误: 忘记绝对值或平方。
- 高级回答: 说明二者只是失真代理,不含码率项。
22. 用 RDO 选择候选
- 题目: A 的
D=100,R=12,B 的D=75,R=25,λ=2。 - 标准回答:
J_A=124,J_B=125,选 A。 - 追问:
λ变小时会怎样? - 常见错误: 只选 D 最小的 B。
- 高级回答:
λ与量化强度、slice type 和编码器模型通常相关。
23. 半像素线性插值
- 题目: 相邻样本 100 和 120,简化半像素值是多少?
- 标准回答: 110。
- 追问: 标准是否一定用简单平均?
- 常见错误: 认为半像素不存在所以只能四舍五入到整数位置。
- 高级回答: 真实规范采用固定滤波器、舍入与裁剪,确保跨实现一致。
24. 量化误差
- 题目: 系数 37、步长 8。
- 标准回答: 量化为 5,反量化为 40,误差 3。
- 追问: 为什么大量小系数会变成 0?
- 常见错误: 反量化后还能精确恢复 37。
- 高级回答: 零系数提高扫描和熵编码效率,但会造成纹理丢失和振铃。
25. 1080p 参考帧内存
- 题目: 1920×1080、8-bit YUV420P、8 个表面。
- 标准回答: 理论像素数据约
1920×1080×1.5×8≈24.88 MB。 - 追问: 为什么实际更高?
- 常见错误: 用 RGB24 的 3 bytes/pixel。
- 高级回答: 加上 stride、对齐、重排、输入和 GPU 私有表面。
26. 全搜索候选数
- 题目: 搜索范围
[-16,+16]。 - 标准回答: 每方向 33 个位置,共
33²=1089个整数像素候选。 - 追问: 加四分之一像素后能否简单乘 16?
- 常见错误: 写成
32²。 - 高级回答: 实际通常先整数搜索,再对最优附近做局部亚像素细化。
27. 残差全为常数时变换有什么特点?
- 标准回答: 能量集中在 DC 或最低频系数,其他系数接近 0。
- 追问: 这为何利于压缩?
- 常见错误: “变换直接删除了高频。”
- 高级回答: 变换重分布能量,真正丢弃通常发生在量化。
28. λ 增大对模式选择有什么影响?
- 标准回答: 更重视比特成本,倾向选择信令更省、残差更简单的模式。
- 追问: 是否必然选择更大块?
- 常见错误: “λ 越大质量越高。”
- 高级回答: 只是概率趋势,最终仍由每个候选的 D 和 R 决定。
29. 预测完全准确时码率是否为 0?
- 标准回答: 不一定;仍需编码块模式、参考索引、运动信息或 skip 标志等语法。
- 追问: 什么机制可进一步减少这些比特?
- 常见错误: “残差为 0 就完全无数据。”
- 高级回答: 利用 merge、skip、上下文概率和运动预测压缩语法。
30. 30 FPS、GOP=60 的名义关键帧间隔是多少?
- 标准回答: 若严格每 60 帧一个关键帧,则约 2 秒。
- 追问: 为什么实际可能不是精确 2 秒?
- 常见错误: 把 GOP 当作毫秒。
- 高级回答: 可变帧率、场景切换、最小关键帧间隔和强制关键帧都会改变结果。
12.4 Web 实战题(10 题)
31. WebCodecs 解码为什么要做 backpressure?
- 标准回答: 页面若持续送入 chunk,解码请求、输出帧和 GPU 资源会堆积;应根据队列和消费速度限流。
- 追问: 只看
decodeQueueSize足够吗? - 常见错误: 每次都
flush()控制队列。 - 高级回答: 同时观察输出帧缓存、渲染延迟、内存和代际取消状态。
32. VideoFrame.close() 为什么重要?
- 标准回答:
VideoFrame可能引用大块系统或 GPU 媒体资源,应在下游完成使用后立即释放。 - 追问: 能否依赖 GC?
- 常见错误: draw 后永不 close。
- 高级回答: 设计明确的所有权模型,跨 Worker 使用 transfer 或 clone 时也要处理原对象生命周期。
33. 如何实现时间线拖动到任意时间?
- 标准回答: 查找前置随机访问样本,从该处解码并丢弃目标前输出。
- 追问: 为什么不能只送目标 sample?
- 常见错误: 按文件字节比例 seek。
- 高级回答: 建立 sample 时间索引、同步样本索引、解码代际和最近窗口缓存。
34. 为什么 Web 编辑器需要代理视频?
- 标准回答: 降低解码、Seek、帧缓存、GPU 合成和网络压力。
- 追问: 代理应如何设计 GOP?
- 常见错误: 代理只降低码率,不降低分辨率和 GOP。
- 高级回答: 代理需保持与源素材可靠的时间映射、旋转和颜色元数据。
35. WebCodecs 能否直接导出 MP4?
- 标准回答:
VideoEncoder输出编码 chunk,不自动生成完整 MP4;仍需 mux、sample 表和时间戳组织。 - 追问: 配置数据放在哪里?
- 常见错误: 把 chunk 按顺序拼接后命名为
.mp4。 - 高级回答: 正确处理 decoder configuration、关键帧标记、DTS/PTS、duration 和 fragmented MP4 组织。
36. 快速连续 Seek 如何避免旧帧覆盖新帧?
- 标准回答: 使用请求 generation id,取消或 reset 旧解码任务,只接受当前代际输出。
- 追问: reset 后第一块应满足什么条件?
- 常见错误: 只在 UI 层忽略旧 Promise,不处理解码器队列。
- 高级回答: 将 demux、decode、render 都绑定到同一代际并建立可中断调度器。
37. 如何探测浏览器编码能力?
- 标准回答: 使用
VideoEncoder.isConfigSupported()测试具体 codec、profile、分辨率和参数。 - 追问: 返回 supported 是否保证持续实时?
- 常见错误: 根据浏览器品牌硬编码支持表。
- 高级回答: 还要做小样本运行测试、监测输出速度,并准备服务端降级。
38. AI 视频边生成边播放如何选择 GOP?
- 标准回答: 预览偏向短 GOP、少重排和可控分片边界,以换取低延迟和错误恢复。
- 追问: 最终导出是否沿用同一设置?
- 常见错误: 生成每一帧都设为关键帧。
- 高级回答: 采用快速预览编码和离线高质量重编码两条管线。
39. 页面内存持续增长如何排查?
- 标准回答: 检查
VideoFrame生命周期、解码输出缓存、Canvas/GPU 纹理、未取消任务和 demux 缓冲。 - 追问:
decodeQueueSize=0是否说明没有泄漏? - 常见错误: 只看 JS heap。
- 高级回答: 还要观察 GPU/process memory、帧引用图、transfer 与 clone 数量。
40. 为什么 WebCodecs 页面通常拿不到运动矢量?
- 标准回答: WebCodecs 抽象的是编解码输入输出,不是标准码流分析和编码器决策调试接口。
- 追问: 产品确实需要怎么办?
- 常见错误: 从
EncodedVideoChunk属性直接读取 MV。 - 高级回答: 使用服务端分析、WASM 解析器、研究解码器或编码时旁路生成元数据。
12.5 系统设计题(5 题)
41. 设计 AI 视频边生成边预览系统
- 标准回答: 生成帧 → 时间戳队列 → 低延迟编码 → 连续分片 → MSE 播放;全链路有 backpressure。
- 追问: 生成速度快于播放速度怎么办?
- 常见错误: 把每帧 PNG 直接无限推给浏览器。
- 高级回答: 设计预览质量档位、分片确认、缓冲上限、断点恢复和最终离线重编码。
42. 设计支持流畅拖动的在线编辑器预览管线
- 标准回答: 导入时建索引和代理;Seek 从前置同步样本解码;窗口化帧缓存;GPU 合成;旧请求可取消。
- 追问: 多轨同时可见时如何调度?
- 常见错误: 每次鼠标移动都从源文件头开始解码。
- 高级回答: 按可见贡献和层级优先级分配解码预算,复用共享资产解码结果。
43. 设计帧精确剪切与快速导出
- 标准回答: 中间完整 GOP 尽量码流复制,边界 GOP 解码并重新编码,再正确 mux 和时间戳重写。
- 追问: open GOP 如何处理?
- 常见错误: 任意 sample 边界都直接 copy。
- 高级回答: 建依赖分析,必要时扩大重编码边界,并验证参数集和参考完整性。
44. 设计 4K 多轨 Web 编辑器的内存控制
- 标准回答: 使用代理、窗口缓存、帧对象所有权、及时 close、GPU 纹理池和全局内存预算。
- 追问: 何时淘汰缓存?
- 常见错误: 每条轨道缓存整段解码帧。
- 高级回答: 结合播放方向、时间距离、重解码成本和资产共享度做加权淘汰。
45. 设计客户端预览、服务端最终导出的分工
- 标准回答: 客户端维护非破坏性时间线并做低分辨率交互预览;服务端按同一时间模型做高质量解码、合成、编码和 mux。
- 追问: 如何保证两端画面一致?
- 常见错误: 只把最终 Canvas 录屏上传。
- 高级回答: 版本化效果参数、字体与色彩配置,建立确定性渲染契约和抽帧对比测试。
十三、练习
练习 1:手算运动候选
当前块:
$$ X=\begin{bmatrix}52&55\54&58\end{bmatrix} $$
候选 A:
$$ P_A=\begin{bmatrix}50&54\53&60\end{bmatrix} $$
候选 B:
$$ P_B=\begin{bmatrix}51&52\55&57\end{bmatrix} $$
任务:
- 计算两个候选的 SAD 和 SSE。
- 若 A 的运动信息为 8 bit,B 为 3 bit,设
λ=1,分别用 SAD 和 SSE 作为 D 做一次选择。 - 解释为何不同失真指标可能给出不同排序。
答案要点: A:SAD 6、SSE 10;B:SAD 6、SSE 12。加入运动比特后 B 可能因信令更省而胜出。
练习 2:证明原始参考会漂移
构造一维例子:
- 原始参考样本
X0=100。 - 量化重建后
X̂0=98。 - 下一帧真实样本
X1=105。
分别计算:
- 编码器错误使用原始参考时的残差。
- 解码器使用重建参考恢复出的结果。
- 编码器正确使用重建参考时的残差。
答案要点: 错误路径编码残差 5,解码器得到 98+5=103;正确路径残差应为 7,解码器得到 105。
练习 3:运动搜索伪代码
实现一个只支持灰度 8-bit、整数像素、固定 8×8 块、搜索范围 ±8 的 SAD 全搜索函数。要求:
- 处理参考帧边界。
- 返回
mvX、mvY和 SAD。 - 对当前块越界、stride 异常做检查。
- 统计实际比较的候选数。
进阶:增加运动矢量比特估算和 J=D+λR。
练习 4:复杂度估算
计算 1280×720、16×16 块、搜索范围 ±32 时的:
- 每块整数候选数。
- 每帧块数。
- 全搜索样本差运算上界。
- 60 FPS 的每秒数量级。
然后说明 diamond search 为什么可能快很多,但为何不保证全局最优。
练习 5:DPB 内存预算
为以下配置估算理论像素内存:
- 3840×2160。
- 10-bit 4:2:0,以 16-bit 容器存放每个样本。
- 8 个参考/重排表面。
- 额外 4 个输入与滤镜表面。
提示:按存储容器计算时,每个 4:2:0 样本平均占 3 bytes/pixel,而不是 8-bit 时的 1.5 bytes/pixel。
练习 6:GOP 与显示顺序
给定显示顺序:
I0 B1 B2 P3 B4 B5 P6
画出一种可能的:
- 参考关系。
- 解码输入顺序。
- 显示输出顺序。
- DPB 中等待输出的帧。
说明为什么只保存一个“上一帧”无法完成该结构的解码。
练习 7:在线编辑器 Seek 设计
设计一个函数:
seek(assetId: string, targetUs: number, generation: number): Promise<VideoFrame>
至少说明:
- 如何查前置同步样本。
- 如何重建 decoder config。
- 如何送入 chunk。
- 如何丢弃目标时间前帧。
- 如何处理新的 generation 抢占旧请求。
- 谁负责关闭未返回的
VideoFrame。
练习 8:AI 视频压缩 Debug
同样 1080p、30 FPS、同一编码参数,两段 AI 视频码率差一倍。设计排查顺序:
- 检查实际帧率、分辨率和像素格式。
- 对比场景切换和镜头运动。
- 对比帧间差分与时序闪烁。
- 对比关键帧频率和 GOP。
- 对比 grain/noise、文字和细纹理。
- 最后再检查编码 preset、参考帧、B picture 和码率控制。
十四、本章速查表
14.1 名词
| 名词 | 一句话解释 |
|---|---|
| Intra prediction | 从同一图像已重建邻域预测当前块 |
| Inter prediction | 从重建参考图像预测当前块 |
| Motion estimation | 编码器搜索参考索引和运动矢量 |
| Motion compensation | 按运动信息和插值规则生成预测块 |
| Residual | 原始块减预测块 |
| Reference picture | 被后续图像用于预测的重建图像 |
| DPB | 管理参考图像和输出重排的解码图像缓冲区 |
| GOP | 一组具有特定参考和随机访问结构的图像 |
| QP | 控制量化强度的参数 |
| RDO | 使用 D+λR 在质量和码率之间做决策 |
| Lookahead | 编码器查看未来原始帧以优化结构和码率 |
| In-loop filter | 结果进入参考闭环的滤波处理 |
| Scene cut | 相邻图像内容关系突然中断的镜头切换 |
14.2 公式
$$ R=X-P $$
$$ SAD=\sum|X-P| $$
$$ SSE=\sum(X-P)^2 $$
$$ v^*=\arg\min_v D(B_t,F_r(v)) $$
$$ J=D+\lambda R $$
$$ Q_C=\operatorname{round}(C/q) $$
14.3 编码流程
输入帧
→ lookahead / 图像类型
→ 块划分
→ 帧内或帧间预测
→ RDO
→ 残差
→ 变换
→ 量化
→ 熵编码
→ 码流
本地闭环:
反量化 → 反变换 → 加预测 → 环路滤波 → DPB
14.4 解码流程
解析与熵解码
→ 预测模式、参考和运动信息
→ 反量化与反变换
→ 预测 + 残差
→ 环路滤波
→ DPB
→ 按显示顺序输出
14.5 面试结论
- 编码器搜索方案,解码器执行方案。
- 运动估计仅在编码器;运动补偿两端都有。
- 编码器必须引用重建帧,否则产生漂移。
- I picture、IDR、keyframe、reference picture 不能混为一谈。
- B picture 可能造成解码顺序和显示顺序不同,也可能作为参考。
- 最小 SAD 不一定是最优,应考虑
D+λR。 - 量化通常是主要有损环节,熵编码通常无损。
- in-loop filter 的结果会进入参考预测闭环。
- 长 GOP 偏压缩效率,短 GOP 偏 Seek、编辑和恢复。
- AI 视频时序不一致会直接降低帧间压缩效率。
- WebCodecs 不是容器封装器,也不保证特定 codec 可用。
- Web 视频管线必须同时控制队列、帧生命周期、取消和内存。
十五、参考资料
本周完成标准
在不看资料的情况下,完成以下四项即可进入第 6 周:
- 在白板上画出编码器主路径和本地重建闭环。
- 用一个 2×2 块手算残差、SAD、SSE、量化和简单 RDO。
- 用三分钟解释 I/P/B、参考帧、GOP、关键帧和 IDR 的区别。
- 设计一个支持取消、backpressure、帧释放和前置关键帧 Seek 的 Web 解码流程。
Footnotes
-
ITU-T Recommendation H.264, Advanced video coding for generic audiovisual services. https://www.itu.int/rec/t-rec-h.264 ↩ ↩2
-
ITU-T Recommendation H.265, High efficiency video coding. https://www.itu.int/rec/t-rec-h.265 ↩ ↩2
-
FFmpeg, Codecs Documentation. 文档列出 GOP、B-frames、reference frames、motion estimation、scene-cut、CRF、preset 等框架或编码器选项;这些选项不应误认为标准统一语法。https://ffmpeg.org/ffmpeg-codecs.html ↩ ↩2
-
W3C, WebCodecs. 规范定义
VideoDecoder、VideoEncoder、队列属性、配置探测和VideoFrame资源生命周期,同时不要求实现任何特定 codec。https://www.w3.org/TR/webcodecs/ ↩ ↩2 ↩3 ↩4