第十三章|基础专题六:H.264 码流与解码器
从 Annex B、NAL Unit、RBSP、SPS、PPS、Slice Header、Exp-Golomb、宏块、CAVLC/CABAC、DPB 和 WebCodecs 解码流程理解 H.264。
第十三章|基础专题六:H.264 码流与解码器
原稿学习节奏:第 6 周。
建议学习时间:8~10 小时 本周主线:从字节流中拆出 NAL Unit,解析参数集和 Slice,重建图像,并通过 DPB 按正确顺序输出。 范围说明:本章聚焦单层 AVC/H.264 的常见 8-bit、4:2:0、逐行视频,同时指出隔行、FMO、SVC/MVC 等会增加实现复杂度的扩展点。
截至 2026 年 6 月,ITU-T 当前在用版本为 H.264 (08/24)。H.264/AVC 规定的是合法码流语法及解码语义;x264、OpenH264、FFmpeg、浏览器硬件解码器等属于具体实现。H264
一、学习目标
学完本周后,你应当能够:
- 从一段十六进制数据中识别 Annex B 起始码和 NAL Unit Header。
- 解释
forbidden_zero_bit、nal_ref_idc和nal_unit_type的作用。 - 严格区分 NALU、Slice、Picture、Frame、Access Unit 和 MP4 Sample。
- 解释 RBSP、工程中常说的 EBSP、
rbsp_trailing_bits与防竞争字节0x03。 - 解释 Annex B 与 MP4/AVC length-prefixed 两种封装,并完成相互转换的伪代码。
- 说明 SPS、PPS、SEI 和 Slice Header 分别控制什么,以及 Slice 如何通过 PPS 间接选择 SPS。
- 手工解出无符号和有符号 Exp-Golomb 码。
- 从 SPS 推导宏块尺寸、编码尺寸与裁剪后的显示尺寸。
- 按顺序描述 H.264 解码器从字节流到 YCbCr 帧的完整过程。
- 解释 CAVLC 与 CABAC 在解码器中的位置及性能差异。
- 解释帧内预测、帧间预测、反量化、反变换、重建和环路去块滤波如何衔接。
- 解释参考图像列表、参考图像标记、DPB、POC 与显示重排。
- 解释为什么 I Picture 不一定是 IDR Picture,为什么 NALU 不等于一帧。
- 使用 FFmpeg/ffprobe 观察 SPS/PPS、NALU、关键帧、PTS/DTS 和像素格式。
- 设计基于 WebCodecs 的 H.264 解码、Seek、Backpressure 和资源释放流程。
本周安排
| 学习日 | 主题 | 产出 |
|---|---|---|
| Day 1 | NAL、Annex B、length-prefixed | 写一个 NALU 拆分器 |
| Day 2 | RBSP、SPS、PPS、Slice Header | 手工解析 NAL Header 与 Exp-Golomb |
| Day 3 | 宏块、CAVLC/CABAC、反量化与反变换 | 画出 Slice 解码流程 |
| Day 4 | 帧内/帧间预测、去块滤波 | 解释一块像素如何重建 |
| Day 5 | 参考帧、DPB、POC、显示重排 | 画出含 B Picture 的缓冲过程 |
| Day 6 | FFmpeg、ffprobe、WebCodecs | 完成码流观察与浏览器伪代码 |
| Day 7 | 面试题、练习、复盘 | 在白板上讲完完整解码器 |
二、概念地图
2.1 H.264 位于媒体系统的哪一层
MP4 / fMP4 / MPEG-TS / RTP
│
│ Demux 或去包
▼
编码样本 / Access Unit
│
▼
NAL Unit 序列
SPS / PPS / SEI / Slice ...
│
▼
Slice Header + Slice Data
│
▼
宏块语法、预测信息、运动矢量、量化系数
│
▼
熵解码 → 反量化 → 反变换 → 预测重建
│
▼
去块滤波 → DPB → 按输出顺序交付 YCbCr 帧
│
▼
颜色转换 / GPU 纹理 / Canvas / <video>
2.2 必须严格区分的层次
| 层次 | H.264 场景中的对象 | 不应混淆为 |
|---|---|---|
| 原始像素 | I420、NV12、P010、RGBA | NALU、MP4 |
| 编码标准 | H.264/AVC | x264、FFmpeg |
| 编码器实现 | x264、OpenH264、硬件编码器 | H.264 标准本身 |
| 基础码流 | SPS、PPS、SEI、Slice NALU | MP4 文件 |
| 容器 | MP4、MOV、MPEG-TS | H.264 编码算法 |
| 网络封装 | RTP、HLS、DASH 等 | Slice 或宏块 |
| 浏览器 API | <video>、MSE、WebCodecs | codec 或容器 |
2.3 最重要的对象关系
一个 Access Unit
├─ 可选 AUD
├─ 可选 SPS / PPS / SEI
└─ 一个或多个 VCL NALU
└─ 每个 VCL NALU 通常承载一个 Slice
├─ Slice Header
└─ Slice Data
└─ 多个 Macroblock
一个 Access Unit 包含恰好一个 primary coded picture;但一张 Picture 可以由多个 Slice 组成,因此也可能对应多个 VCL NALU。W3C-AVC
三、直觉解释
3.1 为什么码流不是“每帧一个压缩包”
可以把解码器想成一条需要持续维护状态的装配线:
- SPS 像整条生产线的规格书:分辨率、Profile、Level、参考帧上限、POC 类型等。
- PPS 像某批产品的工艺卡:熵编码模式、初始 QP、参考列表默认值、去块滤波参数是否出现等。
- Slice Header 像本次任务单:这是 I/P/B Slice、使用哪份 PPS、当前帧号和 POC 信息是什么。
- Slice Data 才是实际压缩数据:宏块模式、运动矢量、残差系数等。
- DPB 像仓库:既保存未来预测要引用的重建帧,也暂存尚未到显示时刻的帧。
因此,解码一帧并不是“拿到一个 NALU 就输出一张图”。解码器需要先有正确参数集,还可能要汇集多个 Slice,并维护参考图像与显示重排状态。
3.2 为什么要有 NAL 层
Video Coding Layer(VCL,视频编码层)负责表示压缩后的图像内容;Network Abstraction Layer(NAL,网络抽象层)把编码信息组织为可供文件、传输或网络系统处理的单元。
NAL 解决的是“如何把不同用途的数据分成边界清晰的单元”,但它不直接规定 MP4 Box、RTP 包或 HTTP 分片。
3.3 为什么解码器必须重建与编码器相同的参考帧
帧间预测引用的不是编码前原图,而是编码器内部已经量化、反量化、反变换并滤波后的重建图像。若编码器引用原图而解码器引用重建图,两端预测值会逐帧分叉,产生漂移。
所以编码器内部也必须带一条“局部解码环路”:
量化系数
↓
反量化 + 反变换
↓
加预测值
↓
去块滤波
↓
重建参考帧
3.4 为什么 B Picture 会引入显示重排
假设显示顺序是:
I0 B1 B2 P3
B1、B2 可能同时参考 I0 和 P3。解码器必须先得到 P3,才能重建 B1、B2,因此一种典型解码顺序是:
I0 P3 B1 B2
H.264 基础码流主要描述解码语义;MP4 等系统层负责给样本提供 PTS/DTS。原始 Annex B 文件如果没有额外时间信息,并不能仅凭起始码得到可靠的真实展示时间。
四、数学原理
4.1 无符号 Exp-Golomb:ue(v)
许多 SPS、PPS 和 Slice Header 字段使用指数哥伦布编码。
对非负整数 codeNum:
[ k=\left\lfloor \log_2(codeNum+1)\right\rfloor ]
码字由以下两部分组成:
k个前导零;codeNum + 1的二进制表示,共k+1位。
| 数值 | codeNum+1 | 码字 |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 10 | 010 |
| 2 | 11 | 011 |
| 3 | 100 | 00100 |
| 4 | 101 | 00101 |
| 5 | 110 | 00110 |
**手算例子:**码字 00110。
- 前导零数量
k=2; - 后续读取
k=2位后缀10₂=2; codeNum=(2^k-1)+suffix=3+2=5。
**解码位置:**解析 SPS/PPS/Slice Header 和许多宏块语法时,由 BitReader 调用。
伪代码:
readUE():
leadingZeroBits = 0
while readBit() == 0:
leadingZeroBits += 1
suffix = readBits(leadingZeroBits)
return (1 << leadingZeroBits) - 1 + suffix
4.2 有符号 Exp-Golomb:se(v)
先得到无符号 codeNum,再映射为有符号值:
codeNum: 0 1 2 3 4 5 6 ...
value: 0 +1 -1 +2 -2 +3 -3 ...
可写成:
[ v=(-1)^{codeNum+1}\left\lceil\frac{codeNum}{2}\right\rceil ]
**例子:**上一节得到 codeNum=5,则 v=+3。
**解码位置:**例如 slice_qp_delta、运动矢量差和部分残差相关语法。
4.3 从 SPS 推导编码尺寸
定义:
[ PicWidthInMbs=pic_width_in_mbs_minus1+1 ]
[ PicHeightInMapUnits=pic_height_in_map_units_minus1+1 ]
[ FrameHeightInMbs=(2-frame_mbs_only_flag)\times PicHeightInMapUnits ]
[ CodedWidth=16\times PicWidthInMbs ]
[ CodedHeight=16\times FrameHeightInMbs ]
例子:1920×1080 的常见逐行 4:2:0 码流
pic_width_in_mbs_minus1=119,所以宽为120×16=1920;pic_height_in_map_units_minus1=67;frame_mbs_only_flag=1,所以编码高度为68×16=1088;- 通过帧裁剪去掉底部 8 行,显示高度得到 1080。
对常见逐行 4:2:0,CropUnitX=2、CropUnitY=2。若 frame_crop_bottom_offset=4,则裁掉 4×2=8 行。
**实际位置:**解析 SPS 后分配帧缓冲、配置硬件解码器和设置 codedWidth/codedHeight 时使用。显示尺寸不能简单等同于宏块对齐后的编码尺寸。
4.4 预测重建
每个像素或样本的核心重建关系是:
[ \hat{x}=Clip(P+r) ]
其中:
P:帧内或帧间预测样本;r:反量化、反变换后得到的残差;Clip:裁剪到当前位深允许的范围;\hat{x}:重建样本。
**例子:**8-bit 视频中,P=240、r=30,则 240+30=270,裁剪后为 255。
**实际位置:**宏块重建阶段,在预测完成和残差恢复之后、去块滤波之前。
4.5 QP 与量化步长
对常见 H.264 量化关系,可记住:
[ Qstep(QP+6)=2\times Qstep(QP) ]
这意味着 QP 每增加 6,量化步长约翻倍;残差系数更容易变成零,码率通常下降,但失真增加。
**例子:**从 QP 22 增至 QP 28,量化步长约变为原来的 2 倍。
**实际位置:**解码器从 Slice QP、宏块 QP 增量及参数集初值推导当前 QP,再据此进行反量化。码率控制如何选择 QP 是编码器策略,不是解码器决策。
4.6 运动矢量单位
H.264 亮度运动矢量通常以四分之一像素为单位。
若码流中的运动矢量为:
[ MV=(6,-3) ]
则对应亮度平面位移为:
[ (6/4,-3/4)=(1.5,-0.75)\text{ 像素} ]
半像素亮度样本使用规定的滤波过程生成,四分之一像素位置再由相邻整数或半像素样本插值得到。色度的采样网格还受 4:2:0 等下采样方式影响。
**实际位置:**帧间预测和运动补偿阶段。
4.7 去块滤波的直觉条件
对块边界两侧样本:
p2 p1 p0 | q0 q1 q2
滤波是否发生会考虑边界强度 bS、QP 推导出的阈值 α、β,以及类似条件:
[ |p_0-q_0|<\alpha ]
[ |p_1-p_0|<\beta,\quad |q_1-q_0|<\beta ]
**例子:**若 p0=100、q0=104、p1=99、q1=105、α=10、β=5,三个条件都成立,边界可能被滤波;最终强度还要看 bS 和其他条件。
**实际位置:**重建之后、图像进入参考帧缓冲之前。因为它是 in-loop filter,后续预测引用的是滤波后的图像。
4.8 DPB 内存估算
1920×1080、8-bit、4:2:0 一帧的理论像素数据量:
[ 1920\times1080\times1.5=3{,}110{,}400\text{ bytes} ]
若同时保留 6 张解码图像:
[ 3{,}110{,}400\times6\approx18.66\text{ MB} ]
实际内存还包括宏块对齐、stride、硬件 surface、显示队列、颜色转换目标和驱动开销,通常高于理论值。
**实际位置:**评估 DPB、帧缓存、WebCodecs 输出堆积和 4K 编辑器内存压力。
五、底层数据结构
5.1 NAL Unit Header
基础 AVC NALU 的首字节:
bit: 7 6 5 4 3 2 1 0
+---+-----+---------+
| F | NRI | Type |
+---+-----+---------+
1 2 5 bits
F:forbidden_zero_bit,标准合规码流中必须为 0。NRI:nal_ref_idc。0 表示该 NALU 的内容不用于重建参考图像;非 0 表示丢失它可能破坏参考图像完整性。不要把 1、2、3 简化为严格的“质量等级”。Type:nal_unit_type,决定载荷语法。
十六进制例子:
0x67 = 0110 0111
F=0, NRI=3, Type=7 → SPS
0x68 = 0110 1000
F=0, NRI=3, Type=8 → PPS
0x65 = 0110 0101
F=0, NRI=3, Type=5 → IDR Slice
5.2 常见 NAL Unit Type
| Type | 名称 | 是否 VCL | 作用 |
|---|---|---|---|
| 1 | Coded slice of a non-IDR picture | 是 | 普通非 IDR Slice |
| 2~4 | Data partition A/B/C | 是 | 数据分区模式,现代常见文件中较少见 |
| 5 | Coded slice of an IDR picture | 是 | IDR Picture 的 Slice |
| 6 | SEI | 否 | 补充增强信息,如 picture timing、recovery point 等 |
| 7 | SPS | 否 | Sequence Parameter Set,序列级参数 |
| 8 | PPS | 否 | Picture Parameter Set,图像级参数 |
| 9 | AUD | 否 | Access Unit Delimiter,可选边界提示 |
| 10 | End of Sequence | 否 | 序列结束 |
| 11 | End of Stream | 否 | 码流结束 |
| 12 | Filler Data | 否 | 填充数据 |
本表只覆盖学习与工程中最常见的基础类型。H.264 的扩展 Profile 还定义了其他类型和扩展头。H264
5.3 Annex B:起始码分隔
典型裸 H.264 字节流:
00 00 00 01 67 64 00 28 ... SPS
00 00 00 01 68 EE 3C 80 ... PPS
00 00 00 01 65 88 84 ... IDR Slice
00 00 01 41 9A ... non-IDR Slice
常见起始码表现为:
00 00 01
00 00 00 01
严格实现应按 Annex B 的 zero_byte、start_code_prefix_one_3bytes、前导零和尾随零语义解析,而不是简单假设所有 NALU 都固定以四字节起始码开头。H264
边界解析原则:
- 找到当前起始码;
- NALU 从起始码后的第一个字节开始;
- 一直读取到下一个起始码之前;
- 起始码本身不属于 NALU。
5.4 MP4/AVC:长度前缀
MP4 中的 H.264 样本通常不是用起始码分隔,而是:
[NAL length][NAL bytes][NAL length][NAL bytes]...
例如长度字段为 4 字节、大端序:
00 00 00 17 67 ...23 bytes...
00 00 00 08 68 ... 8 bytes...
00 00 04 A2 65 ...1186 bytes...
长度字段占几字节由 AVCDecoderConfigurationRecord 中的 lengthSizeMinusOne 决定,工程中常见为 4 字节,但不能硬编码假设。
avcC 配置记录通常还携带:
- Profile、compatibility、Level;
- NAL 长度字段大小;
- SPS 列表;
- PPS 列表;
- 高 Profile 的附加颜色格式和位深信息。
FFmpeg 的 h264_mp4toannexb bitstream filter 可以在不解码像素的情况下把 length-prefixed H.264 转成 Annex B。FFMPEG-BSF
5.5 RBSP、防竞争字节与工程中的 EBSP
压缩语法中可能自然出现:
00 00 01
若它出现在 NALU 内部,Annex B 扫描器可能误认为下一 NALU 开始。编码端因此在特定模式后插入 0x03:
逻辑 RBSP: 00 00 01
传输字节: 00 00 03 01
解码端解析 NAL 载荷时移除该 0x03,恢复原始字节。工程资料常把插入防竞争字节后的载荷称为 EBSP。
**关键规则:**不能删除所有 0x03。只有在符合“前面已有两个连续 0x00,并满足后续字节约束”的防竞争位置才移除。
RBSP 末尾还有:
rbsp_stop_one_bit = 1
rbsp_alignment_zero_bit = 0 ...直到字节对齐
这使 BitReader 能确认语法结束和字节对齐。
5.6 Access Unit、Picture 与 Slice
- Slice:一组宏块及其头部语法,通常由一个 VCL NALU 承载。
- Picture:一张编码图像,可由一个或多个 Slice 组成。
- Access Unit:按解码顺序排列、共同表示一个 primary coded picture 的 NALU 集合。
- Frame:逐行图像或由两个场组成的完整图像概念;不能在所有隔行场景中直接用 Picture 与 Frame 一一替换。
AUD 是可选的。没有 AUD 时,解析器需要比较相邻 VCL NALU 的 frame_num、pic_parameter_set_id、场标志、IDR 标志、idr_pic_id、POC 相关字段等,按照标准定义判断是否开始了新 Picture。只看 first_mb_in_slice == 0 不足以覆盖所有合法码流。
5.7 SPS:序列级状态
SPS 常见关键字段:
| 字段 | 作用 |
|---|---|
profile_idc | Profile |
| constraint flags | 约束集合,影响 codec string 与功能子集 |
level_idc | Level,限制分辨率、处理量、DPB 等 |
seq_parameter_set_id | SPS 标识符 |
chroma_format_idc | 4:2:0、4:2:2、4:4:4 等,高 Profile 语法中出现 |
| bit depth fields | 亮度、色度位深 |
| scaling matrices | 可选量化缩放矩阵 |
log2_max_frame_num_minus4 | frame_num 位宽 |
pic_order_cnt_type | POC 推导方式 |
max_num_ref_frames | 参考图像相关上限 |
| picture width/height fields | 宏块级编码尺寸 |
frame_mbs_only_flag | 是否仅逐行帧宏块 |
| cropping fields | 从编码尺寸裁剪到显示尺寸 |
| VUI | SAR、色彩、时序、HRD、限制等可选信息 |
注意:VUI/SEI 可以表达时序模型和显示相关信息,但 MP4 中逐样本的 PTS/DTS 仍属于系统层时间戳,不能把两者混为一谈。
5.8 PPS:图像级状态
PPS 常见关键字段:
| 字段 | 作用 |
|---|---|
pic_parameter_set_id | PPS 标识符 |
seq_parameter_set_id | 此 PPS 引用的 SPS |
entropy_coding_mode_flag | 0 常表示 CAVLC,1 表示 CABAC |
| slice group fields | FMO/多 Slice Group 配置 |
| default active reference counts | 默认参考图像数量 |
| weighted prediction fields | 加权预测配置 |
pic_init_qp_minus26 | 初始图像 QP 基值 |
| chroma QP offsets | 色度 QP 偏移 |
deblocking_filter_control_present_flag | Slice Header 是否携带去块滤波控制 |
transform_8x8_mode_flag | 高 Profile 下是否允许 8×8 变换 |
Slice Header 通过 pic_parameter_set_id 选择 PPS,PPS 再通过 seq_parameter_set_id 选择 SPS:
Slice → PPS → SPS
参数集可能在码流内出现,也可能作为 MP4 avcC 或 WebCodecs description 带外提供。
5.9 Slice Header
常见字段及解码作用:
| 字段 | 解码作用 |
|---|---|
first_mb_in_slice | 当前 Slice 从哪个宏块开始 |
slice_type | I、P、B、SP、SI;值 5~9 与 0~4 对应类别相同但含“全 Slice 同类”约束 |
pic_parameter_set_id | 激活 PPS/SPS |
frame_num | 图像编号与参考管理 |
| field flags | 场编码相关 |
idr_pic_id | 区分 IDR Picture |
| POC fields | 推导显示顺序 |
| reference list modification | 修改默认参考图像列表 |
| pred weight table | 加权预测 |
| decoded reference picture marking | DPB 标记、MMCO、IDR 标志 |
cabac_init_idc | CABAC 上下文初始化选择 |
slice_qp_delta | 当前 Slice QP |
| deblocking fields | 关闭或调整去块滤波 |
5.10 宏块层次
对常见 4:2:0:一个宏块覆盖:
16×16 Y
8×8 Cb
8×8 Cr
宏块可能描述:
- Intra 16×16、Intra 4×4;高 Profile 可有 Intra 8×8;
- P/B 帧间预测分区;
- 跳过宏块;
- 参考图像索引;
- 运动矢量差;
- coded block pattern;
- QP 增量;
- 变换系数;
I_PCM等特殊模式。
标准规定合法语法和确定性解码过程;编码器如何搜索模式、参考帧和运动矢量属于实现策略。
六、编码器流程
本周以解码器为主,但理解编码端如何产生这些字段,能帮助你读码流。
输入 YCbCr 帧
↓
决定 Picture 类型、GOP 与参考关系 ← 编码器策略
↓
划分 Slice 和 Macroblock ← 标准允许多种合法方式
↓
帧内/帧间预测,搜索参考帧和运动矢量 ← 编码器搜索
↓
原始块 - 预测块 = 残差
↓
整数变换
↓
量化,决定 QP ← 码率控制是实现策略
↓
扫描、CAVLC 或 CABAC 熵编码
↓
写 Slice Header 与 Slice Data
↓
写 SPS / PPS / SEI 等非 VCL NALU
↓
加入 NAL Header
↓
Annex B 起始码,或 MP4 length prefix
↓
输出基础码流或交给 Muxer
标准与实现的分界
| 内容 | H.264 标准是否规定 |
|---|---|
| Slice、宏块、运动矢量、系数的码流语法 | 是 |
| 解码器如何得到唯一重建结果 | 是 |
| 编码器必须使用哪种运动搜索算法 | 否 |
| CRF、preset、tune | 否,属于 x264 等实现 |
| 场景切换检测、lookahead | 否 |
| Annex B 字节流格式 | 是,见 Annex B |
MP4 Box 和 avcC | 不由 H.264 本体定义,属于 ISO Base Media File Format/AVC 文件格式体系 |
七、解码器流程
7.1 完整流水线
容器或网络输入
↓
Demux / RTP 去包
↓
得到按解码顺序排列的 Access Unit 或 NALU
↓
Annex B 扫描或 length-prefix 拆分
↓
解析 NAL Header
↓
移除 emulation prevention bytes,得到 RBSP
↓
SPS/PPS/SEI:更新解码状态
VCL NALU:解析 Slice Header
↓
判断新 Picture,激活 PPS/SPS
↓
构造参考图像列表
↓
CAVLC/CABAC 熵解码 Slice Data
↓
逐宏块解析模式、参考索引、运动矢量、系数
↓
反扫描、反量化、反变换
↓
帧内或帧间预测
↓
预测 + 残差,得到重建宏块
↓
环路去块滤波
↓
参考图像标记与 DPB 管理
↓
按 POC 和输出约束重排
↓
输出 YCbCr Frame / 硬件 Surface
7.2 字节流层
解码器首先要知道输入属于哪种包装:
- Annex B:扫描起始码;
- AVC/MP4:读取长度字段;
- RTP:先按 RFC 6184 处理单 NALU、聚合包或 FU 分片,再还原 NALU;
- WebCodecs:每个
EncodedVideoChunk的数据预期表示一个 Access Unit,具体是avc还是annexb由配置决定。RFC6184
7.3 参数集管理
解码器通常维护:
spsMap[sps_id]
ppsMap[pps_id]
收到新 SPS/PPS 后按 ID 更新。解析 Slice Header 时:
- 读取
pic_parameter_set_id; - 查找对应 PPS;
- 从 PPS 找 SPS;
- 检查 Profile、Level、尺寸、位深、色度格式是否受支持;
- 必要时重新配置缓冲区或硬件会话。
若 PPS/SPS 缺失,不能可靠解析后续 Slice,因为字段位宽和语法分支依赖参数集。
7.4 Picture 边界与多 Slice 汇集
一个 Picture 可包含多个 Slice。实现需要:
- 判断当前 VCL NALU 是否属于正在解码的 Picture;
- 为多个 Slice 共享同一目标帧缓冲;
- 处理 Slice 覆盖区域和宏块地址;
- 在 Picture 完成后进行完整参考标记和输出管理。
AUD 可简化边界识别,但不能假设它一定存在。
7.5 CAVLC 与 CABAC
CAVLC
Context-Adaptive Variable Length Coding,基于邻块非零系数数量等上下文选择可变长码表。
典型解码内容包括:
coeff_token;- trailing ones;
- 剩余系数 level;
- total zeros;
- run before。
优点是控制逻辑相对直接;压缩效率通常低于 CABAC。
CABAC
Context-Adaptive Binary Arithmetic Coding,流程可抽象为:
语法元素
↓ binarization
bin 串
↓ context model / bypass / terminate
二进制算术解码
↓
更新概率状态
CABAC 的上下文状态具有前后依赖,压缩效率高,但分支、状态更新和串行依赖更复杂。它不是“把 Huffman 换成算术编码”这么简单。
7.6 宏块重建
对每个宏块,解码器大致执行:
- 解析宏块类型和分区;
- 若为帧内块,解析预测模式并从已重建邻块生成预测;
- 若为帧间块,构造参考列表、解析参考索引和运动矢量差,推导最终运动矢量;
- 解析 coded block pattern 和 QP;
- 熵解码量化系数;
- 反扫描;
- 反量化;
- 4×4 或 8×8 反整数变换;
- 将残差加到预测样本;
- 写入当前重建图像。
7.7 帧内预测
预测样本来自当前 Picture 已重建的左、上、左上等邻居。不同模式适合不同纹理方向:
- DC:邻居平均;
- Vertical:沿竖直方向复制;
- Horizontal:沿水平方向复制;
- 多种对角线方向;
- Intra 16×16 还包括 Plane 等模式。
因为邻块必须先重建,宏块和块内处理存在数据依赖。
7.8 帧间预测
步骤:
解析参考索引
↓
构造/修改 RefPicList0、RefPicList1
↓
解析运动矢量差 MVD
↓
用邻块推导 MVP
↓
MV = MVP + MVD
↓
从参考图像取整数/分数像素样本
↓
可选加权预测
↓
得到预测块
P Slice 主要使用 List0;B Slice 可以使用 List0、List1 或双向预测。
7.9 反量化与反变换
熵解码得到的是量化后系数,不是像素残差。解码器根据:
- 当前 QP;
- 变换大小;
- scaling list;
- 亮度/色度分量;
执行规定的整数缩放和反变换。H.264 使用整数变换设计,避免不同平台浮点 DCT 带来的漂移。
7.10 去块滤波
边界强度通常受以下因素影响:
- 是否为 Intra 宏块;
- 边界两侧是否有非零残差;
- 参考图像或运动矢量是否显著不同;
- QP 与 Slice Header 中的偏移;
- 是否禁止跨 Slice 边界滤波。
去块滤波后的图像才进入参考流程,因此错误实现会影响当前视觉效果,也会污染后续预测。
7.11 DPB:参考与输出的双重职责
Decoded Picture Buffer 同时解决两件事:
- 保存短期或长期参考图像,供未来 Picture 预测;
- 暂存已解码但尚未到输出顺序的 Picture。
参考标记可能采用:
- 滑动窗口;
- MMCO(Memory Management Control Operation);
- IDR 的特殊刷新语义;
- long-term reference 标记。
重要:“不再作为参考”不一定等于“立即释放”,因为它可能仍需等待显示;反之,一张已经显示的图也可能继续作为参考。
7.12 POC 与输出重排
Picture Order Count 用于推导图像输出顺序。H.264 定义多种 POC 类型,解码器必须结合 SPS 和 Slice Header 计算。
容器层通常:
- 按 DTS/样本顺序把压缩数据送入解码器;
- 用 PTS 表示输出时间;
- 解码器输出的 Frame 继承对应呈现时间。
7.13 IDR 的实际语义
IDR Picture 是明确的解码刷新点。其后的图像不会再引用 IDR 之前的图像,解码器也会按 IDR 语义处理 DPB 参考状态。
但需要注意:
- I Picture 只说明使用帧内预测,不自动具有 IDR 的参考刷新语义;
- 一个 IDR Picture 可由多个 type 5 Slice NALU 构成;
- “关键帧”是容器、播放器和产品语境中的常用词,精确定义需结合随机访问规则;
- 恢复点 SEI、开放 GOP 等还会产生不同于简单 IDR 的随机访问行为。
7.14 解码器主循环伪代码
for packet_or_chunk in input_in_decode_order:
nalus = unpack_to_nalus(packet_or_chunk)
for nalu in nalus:
header = parse_nal_header(nalu)
rbsp = remove_emulation_prevention(nalu.payload)
switch header.type:
case SPS:
spsMap[id] = parse_sps(rbsp)
case PPS:
ppsMap[id] = parse_pps(rbsp)
case SEI:
parse_sei(rbsp)
case VCL_SLICE:
sliceHeader = parse_slice_header(rbsp, spsMap, ppsMap)
if starts_new_picture(sliceHeader, currentPicture):
finish_picture(currentPicture)
currentPicture = begin_picture(sliceHeader)
refs = build_reference_lists(sliceHeader, dpb)
decode_slice_data(rbsp, sliceHeader, refs, currentPicture)
finish_picture(currentPicture)
flush_output_pictures(dpb)
finish_picture() 通常包含剩余滤波、参考图像标记、DPB 插入、输出候选排序等步骤。
八、复杂度与性能
8.1 时间复杂度
| 阶段 | 近似复杂度 | 主要影响因素 |
|---|---|---|
| Annex B/length 解析 | O(B) | 码流字节数 B |
| 熵解码 | O(S) | 语法 bin/码字数量、CABAC 状态依赖 |
| 反变换与重建 | O(P) | 像素数 P、非零块数量 |
| 运动补偿 | O(P·T) | 插值滤波 tap、分区数量、缓存命中 |
| 去块滤波 | O(P) | 边界数量、边界强度 |
| DPB 管理 | 通常远小于像素处理 | 参考帧数量、重排深度 |
解码复杂度通常显著低于高质量编码复杂度,因为解码器不搜索模式和运动矢量,而是执行码流已经指定的选择。
8.2 内存占用
主要内存:
- 当前重建图像;
- DPB 参考图像;
- 输出重排队列;
- 硬件解码 surface;
- 颜色转换或渲染目标;
- Slice/CABAC 上下文和宏块状态;
- Web 应用额外持有的
VideoFrame。
4K 8-bit 4:2:0 单帧理论数据量约为:
[ 3840\times2160\times1.5\approx12.44\text{ MB} ]
若浏览器应用无节制保留 20 帧,仅像素理论值就约 249 MB,尚未计算对齐、GPU 和驱动开销。
8.3 CPU 与 GPU/硬件解码分工
典型路径:
CPU: 网络、Demux、时间戳、索引、调度
↓
硬件视频解码单元: CABAC/CAVLC、预测、反变换、滤波等
↓
GPU Surface: NV12/P010
↓
GPU: 缩放、色彩转换、滤镜、合成、显示
高性能的关键往往不是“解码核更快”,而是避免:
- 硬件 surface → CPU RGBA 的读回;
- CPU 内存中重复格式转换;
- 多次纹理上传;
- 主线程等待;
- 未关闭 Frame 导致 surface 池耗尽。
8.4 并行化
可使用:
- Slice 级并行;
- Frame 级流水并行;
- 宏块行或内部实现级并行;
- Demux、Decode、Render 分 Worker/线程流水线。
限制:
- 帧内预测依赖邻块;
- CABAC 上下文在 Slice 内有串行依赖;
- 参考帧必须先完成;
- B Picture 引入重排和额外缓冲;
- 增加 Slice 数量可能提升并行和错误隔离,但也会损失压缩效率并增加头部开销。
8.5 常见性能瓶颈
- 长 GOP Seek 时需要从很早的随机访问点向前解码。
decodeQueueSize持续增长,输入生产速度超过输出消费速度。VideoFrame未及时close()。- 每帧转 RGBA 后在 CPU 上处理。
- 参数集变化触发硬件解码器重配置。
- 频繁
reset()或创建销毁解码器造成启动成本。 - 同时为预览、缩略图、分析重复解码同一资产。
- 读取 MP4 Sample 时发生大量小对象和内存复制。
- B Picture 的重排缓冲增加延迟。
- 4K 多轨时间线超出硬件解码并发 surface 或会话数量。
九、实际场景
A. AI 生成视频网页
9.1 最终视频播放
推荐链路:
模型输出帧
↓
服务端编码 H.264
↓
封装 MP4 / fMP4
↓
CDN
↓
<video> 或 MSE
浏览器只需要播放时,优先让媒体栈完成 Demux、硬件解码、同步和渲染,不要无必要地把每帧拉到 JavaScript。
9.2 边生成边播放
若服务端持续输出 fMP4:
init segment: ftyp + moov(avcC)
media segment: moof + mdat
↓
MSE SourceBuffer
若服务端输出裸 H.264 给 WebCodecs:
Annex B bytes
↓
识别 Access Unit 边界
↓
补充 timestamp / duration
↓
EncodedVideoChunk
↓
VideoDecoder
必须解决:
- SPS/PPS 何时发送;
- 首个可解码点是否包含 IDR;
- 每个 Chunk 是否恰好包含一个 Access Unit;
- 时间戳如何生成;
- 断线后从哪里恢复;
- 分辨率或参数集改变时如何重新配置;
- 生产端如何感知浏览器 Backpressure。
9.3 缩略图与预览
- 时间线缩略图可以离线生成 JPEG/WebP,避免反复 Seek 解码;
- 动态预览可从最近 IDR 解码到目标时间;
- 不应把数千张 PNG 当作正式连续视频传输格式;
- AI 生成阶段若帧率尚不稳定,应先建立稳定媒体时间线,再封装为播放器可消费的样本。
B. 在线 Web 视频编辑器
9.4 导入资产
MP4
↓ Demux
avcC + H.264 Samples + stss/时间表
↓
建立 key sample、DTS、PTS、duration 索引
avcC 用于配置解码器;每个视频 Sample 中可能包含多个 length-prefixed NALU,通常组织为一个 Access Unit。
9.5 Seek
目标时间 T
↓
查找 T 之前最近可随机访问样本
↓
decoder.reset()
↓
重新 configure 或保持有效配置
↓
按 DTS 顺序送入 key chunk 和后续 delta chunk
↓
丢弃 timestamp < T 的输出
↓
保留第一张满足目标的 Frame
只把目标 P/B Sample 送给解码器通常会失败,因为它依赖前面的参考图像。
9.6 帧精确剪切
- 切点落在 IDR/可随机访问样本上时,才可能直接复制更大范围码流;
- 切点在 GOP 中间时,边界附近通常需要解码并重编码;
- 转场、滤镜、缩放、字幕、画中画都会改变像素,必须经过解码—合成—编码;
- 导出 H.264 后仍需 Muxer 生成 MP4 Box、Sample Table 和时间戳结构。
9.7 多轨预览
建议:
- 每个可见视频轨维护有限解码窗口;
- 使用代理视频降低分辨率和 GOP 长度;
- 在 GPU 合成 NV12/RGBA 纹理;
- 快速拖动时取消旧 Seek 世代,避免过期 Frame 污染当前画面;
- 对
VideoFrame使用明确所有权和释放规则。
十、Web 实现视角
10.1 WebCodecs 中的 AVC 格式
W3C AVC 注册规定:
EncodedVideoChunk数据预期是一个 Access Unit;description存在时,按AVCDecoderConfigurationRecord解释,数据按avclength-prefixed 格式处理;description不存在时,数据按 Annex B 处理;- Annex B 的 key chunk 应包含 IDR Picture 以及解码所需参数集;
avc格式的参数集通常由description提供。W3C-AVC
10.2 配置与能力探测
const config: VideoDecoderConfig = {
codec: "avc1.640028", // High Profile, compatibility 00, Level 4.0 示例
codedWidth: 1920,
codedHeight: 1080,
description: avcDecoderConfigurationRecord,
hardwareAcceleration: "no-preference",
};
const result = await VideoDecoder.isConfigSupported(config);
if (!result.supported) {
throw new Error("当前环境不支持该 H.264 Profile/Level 配置");
}
不能仅看到 avc1 字符串就假设所有 Profile、Level、位深和分辨率均受支持。hardwareAcceleration 也是提示,不是强制保证。WEBCODECS
10.3 解码主循环
const decoder = new VideoDecoder({
output(frame) {
try {
render(frame); // Canvas、WebGL 或 WebGPU
} finally {
frame.close();
}
},
error(error) {
console.error("H.264 decode failed", error);
},
});
decoder.configure(config);
for (const sample of samplesInDecodeOrder) {
while (decoder.decodeQueueSize > 8) {
await new Promise<void>((resolve) => {
decoder.addEventListener("dequeue", () => resolve(), { once: true });
});
}
decoder.decode(new EncodedVideoChunk({
type: sample.isRandomAccess ? "key" : "delta",
timestamp: Math.round(sample.pts * 1_000_000 / sample.timescale),
duration: Math.round(sample.duration * 1_000_000 / sample.timescale),
data: sample.accessUnitBytes,
}));
}
await decoder.flush();
decoder.close();
关键点:
- 输入按解码顺序送入;
- Chunk 的
timestamp表示呈现时间; - 不要按数组下标伪造时间戳;
- 输出回调可能与输入顺序不同;
- 消费完
VideoFrame后及时关闭; decodeQueueSize不是完整内存指标,但可用于输入节流。
10.4 Seek 与取消
let seekGeneration = 0;
async function seek(targetUs: number) {
const generation = ++seekGeneration;
const start = findPreviousRandomAccessSample(targetUs);
decoder.reset();
decoder.configure(config);
for (const sample of decodeRangeFrom(start)) {
if (generation !== seekGeneration) return; // 旧任务作废
decoder.decode(toChunk(sample));
if (sample.ptsUs > targetUs + PREFETCH_US) break;
await applyBackpressure(decoder);
}
}
输出回调也应检查 generation;否则用户快速拖动时,旧解码任务可能在新任务之后输出,造成画面回跳。
10.5 MSE
MSE 处理的是受支持的媒体分段,不是通用裸 NALU 管道。典型 H.264 Web 播放使用 fMP4 初始化分片和媒体分片;在创建 SourceBuffer 前使用:
const mime = 'video/mp4; codecs="avc1.640028"';
if (!MediaSource.isTypeSupported(mime)) {
// 选择其他 Profile、清晰度或服务端转码结果
}
返回 true 仅表示实现原则上可创建相应 SourceBuffer,实际资源不足时仍可能失败。MSE
10.6 MediaCapabilities
const info = await navigator.mediaCapabilities.decodingInfo({
type: "media-source",
video: {
contentType: 'video/mp4; codecs="avc1.640028"',
width: 1920,
height: 1080,
bitrate: 5_000_000,
framerate: 30,
},
});
console.log(info.supported, info.smooth, info.powerEfficient);
这比只判断 codec 名称更接近产品需要的“是否可能流畅、节能”。返回结果仍是当前环境下的能力估计。MEDIACAP
10.7 Canvas、WebGL 与 WebGPU
- Canvas 2D:易用,适合简单预览与叠字;
- WebGL/WebGPU:适合多轨缩放、颜色转换、滤镜和合成;
- 避免每帧
copyTo()到 CPU 再上传 GPU; - 颜色空间、limited/full range、矩阵系数和显示尺寸必须贯穿输入到导出;
- WebCodecs 不负责 MP4 Demux/Mux,仍需容器层代码或服务端管线。
10.8 浏览器不支持时的降级
WebCodecs 精细逐帧路径
├─ 支持 → 客户端预览/短片导出
└─ 不支持
↓
<video> + Canvas 抽帧,能力较弱
↓
或上传编辑描述到服务端 FFmpeg 管线
↓
服务端解码、合成、编码、Mux
十一、易错点
- **H.264 不是 MP4。**前者是视频编码标准,后者是容器。
- **H.264 不是 x264。**x264 是编码器实现。
- **NALU 不等于一帧。**一帧可有多个 Slice NALU,一个 Access Unit 还可含非 VCL NALU。
- **Slice 不等于 Picture。**多个 Slice 可组成一张 Picture。
- **I Picture 不等于 IDR Picture。**I 只表示帧内预测,IDR 还具有参考刷新语义。
- **IDR 不等于单个 type 5 NALU。**一张 IDR Picture 可以有多个 type 5 Slice。
- **SPS/PPS 不必每帧出现。**它们可复用,也可由容器带外提供。
- **AUD 不是必需。**不能依赖它作为唯一帧边界。
- **起始码不属于 NALU。**NALU 从 Header 字节开始。
- **MP4 中通常不是 Annex B。**常见为 length-prefixed NALU。
- **长度字段不一定固定 4 字节。**应读取
lengthSizeMinusOne。 - **不能删除所有
0x03。**只移除合法防竞争位置的字节。 - **
nal_ref_idc非零不等于数值越大画质越高。**核心语义是参考完整性。 - **SEI 通常不是基本像素重建所必需,但不能一概当作无用。**它可携带时序、恢复点、显示等信息。
- **H.264 码流不等于完整时间线。**PTS/DTS 多由容器或传输层提供。
- **解码顺序不等于显示顺序。**B Picture 会导致重排。
- **POC 不等于 PTS。**POC 是编码层图像顺序推导量,PTS 是系统层呈现时间。
- **宏块编码尺寸不一定等于显示尺寸。**1080 高度常编码为 1088 后裁剪。
- **去块滤波不是纯显示后处理。**它在环路内,影响后续参考预测。
- **参考帧不一定已显示,已显示帧也不一定能释放。**DPB 同时负责参考和输出。
- **CABAC 不是任意 Profile 都可使用。**Profile/约束决定合法工具集合。
- **硬件解码不保证零复制。**若把结果读回 CPU,仍可能很慢。
- **WebCodecs 的 Chunk 不是单个 NALU。**AVC 注册预期一个 Access Unit。
- **
type: "key"不能随意标。**它必须符合当前 AVC 格式的随机访问语义。 - **WebCodecs 输出不是完整 MP4。**编码后仍需 Mux。
- **MSE 通常不能直接 append 裸 Annex B 到 MP4 SourceBuffer。**需要受支持的媒体分段。
- **
isConfigSupported()成功不代表永远有资源。**硬件资源与运行环境会变化。 - **Seek 不能只解码目标 Sample。**通常从前置随机访问点向前解码。
- **快速拖动必须取消旧任务。**仅
reset()而不隔离旧输出可能仍产生竞态。 - **VideoFrame 必须明确释放。**否则可能耗尽硬件 surface 和 GPU 内存。
十二、面试题
以下回答以面试中的“先给结论,再解释机制,最后联系工程”方式组织。
A. 10 道基础题
1. H.264、x264 和 MP4 有什么区别?
- **标准回答:**H.264 是编码标准;x264 是实现 H.264 的编码器;MP4 是保存轨道、样本、时间戳和索引的容器。
- **可能追问:**FFmpeg 属于哪一层?
- 常见错误:“MP4 是 H.264 的另一种叫法。”
- **高级回答:**FFmpeg 的 libavcodec 处理编解码,libavformat 处理容器;同一 MP4 可封装不同视频 codec。
2. NALU 是否等于一帧?
- **标准回答:**不等于。一张 Picture 可由多个 Slice NALU 组成,Access Unit 还可含 SPS/PPS/SEI/AUD。
- **可能追问:**WebCodecs 一个 Chunk 应放什么?
- 常见错误:“每个起始码就是一帧。”
- **高级回答:**AVC WebCodecs 注册预期一个 Access Unit,而不是任意单 NALU。
3. SPS 和 PPS 分别是什么?
- **标准回答:**SPS 保存序列级参数;PPS 保存图像级编码配置。Slice 选 PPS,PPS 再选 SPS。
- **可能追问:**它们是否每帧重复?
- 常见错误:“SPS 是视频头,之后永远不会变化。”
- **高级回答:**可存在多组参数集并按 ID 切换;容器也可通过
avcC带外提供。
4. I Picture 与 IDR Picture 有何区别?
- **标准回答:**I Picture 使用帧内预测;IDR 还建立参考刷新边界,后续图像不再引用 IDR 之前的图像。
- **可能追问:**非 IDR I Picture 能否作为随机访问点?
- 常见错误:“所有 I 帧都是 IDR。”
- **高级回答:**随机访问还要结合 open GOP、recovery point、容器 sync sample 语义判断。
5. Annex B 与 MP4 内 H.264 有何区别?
- **标准回答:**Annex B 用起始码分隔 NALU;MP4/AVC 样本通常用长度前缀,并在
avcC中保存参数集和长度字段大小。 - **可能追问:**如何转换?
- 常见错误:“把
00 00 00 01直接替换成固定四字节长度即可。” - **高级回答:**需读取真实 NAL 边界、按
lengthSizeMinusOne写长度,并正确处理带外参数集。
6. RBSP 与防竞争字节是什么关系?
- **标准回答:**为防止 NAL 载荷内模拟起始码,编码端在特定
00 00模式后插入03;解码端移除后恢复 RBSP。 - **可能追问:**能否删除所有
03? - 常见错误:“任何
0x03都是填充。” - **高级回答:**严格解析还需验证前导零计数、后继字节范围和 RBSP trailing bits。
7. 什么是 Access Unit?
- **标准回答:**一组按解码顺序组织、共同表示一个 primary coded picture 的 NALU。
- **可能追问:**AUD 是否必需?
- 常见错误:“Access Unit 就是 MP4 文件中的任意 packet。”
- **高级回答:**多数视频 Sample 与一个 AU 对齐,但容器 packet、RTP packet 和 AU 是不同层概念。
8. 什么是 Slice?
- **标准回答:**Slice 是一组宏块及其头部语法,可独立开始熵解码并通常由一个 VCL NALU 承载。
- **可能追问:**为什么一帧要切多个 Slice?
- 常见错误:“Slice 是画面固定的一横条。”
- **高级回答:**Slice 可用于错误隔离和并行,但形状与宏块映射受 slice group 等语法影响,不必总是简单横条。
9. 为什么 PTS 与 DTS 会不同?
- **标准回答:**B Picture 可能依赖未来显示的参考图像,导致解码顺序与呈现顺序不同。
- **可能追问:**H.264 NALU 内是否直接带 PTS?
- 常见错误:“PTS 是帧号,DTS 是字节偏移。”
- **高级回答:**H.264 有 POC/VUI/HRD 语义,但逐样本 PTS/DTS 通常由容器或传输系统提供。
10. CAVLC 与 CABAC 的区别?
- **标准回答:**CAVLC 使用上下文自适应可变长码;CABAC 使用 binarization、上下文概率模型和二进制算术编码。
- **可能追问:**哪个更快?
- 常见错误:“CABAC 就是 Huffman 表更多。”
- **高级回答:**CABAC 通常压缩效率更高,但上下文状态和串行依赖使软解码与并行化更复杂。
B. 10 道底层实现题
11. 如何写 Annex B NALU 拆分器?
- **标准回答:**扫描三/四字节起始码,NALU 范围是当前起始码后到下一起始码前,并处理前导/尾随零。
- **可能追问:**跨网络分片出现半个起始码怎么办?
- **常见错误:**每个网络包独立扫描,丢失跨包边界状态。
- **高级回答:**维护最多三个尾部字节的流式状态,输出零复制 slice 或引用计数 buffer。
12. 如何从 NAL Header 判断类型?
- 标准回答:
type = header & 0x1F,nri = (header >> 5) & 0x03,f = header >> 7。 - 可能追问:
f=1怎么处理? - **常见错误:**把整个字节直接当类型。
- **高级回答:**基础 AVC 是一字节头;SVC/MVC 等扩展类型还可能有额外扩展头,解析器要按范围设计。
13. 没有 AUD 时如何判断新 Picture?
- **标准回答:**比较相邻 VCL Slice 的多个图像标识字段,按标准的新 Picture 条件判断。
- 可能追问:
first_mb_in_slice=0是否足够? - **常见错误:**只要它为 0 就切帧。
- **高级回答:**还需考虑
frame_num、PPS、field flags、IDR、idr_pic_id和 POC 字段等。
14. 如何从 SPS 得到 1920×1080?
- **标准回答:**先按宏块字段得到 1920×1088 编码尺寸,再按 crop unit 和裁剪 offset 得到 1080 显示高度。
- **可能追问:**为什么高度不是直接 1080?
- **常见错误:**把
pic_height_in_map_units_minus1+1直接当像素。 - **高级回答:**crop unit 取决于色度格式和场/帧编码,不能固定写死为 2。
15. 多 Slice Picture 如何重建?
- **标准回答:**多个 Slice 共享同一 Picture buffer,各自从 Slice Header 初始化熵状态并重建对应宏块。
- **可能追问:**Slice 能否并行?
- **常见错误:**每个 Slice 输出一张图。
- **高级回答:**可并行但存在邻块可用性、跨 Slice 去块、FMO 和输出完成检测等问题。
16. 参考图像列表如何得到?
- **标准回答:**先由 DPB 和当前 Slice 类型构建默认 List0/List1,再应用 Slice Header 的列表修改语法。
- **可能追问:**B Slice 为何有两张列表?
- **常见错误:**参考索引就是 DPB 数组下标。
- **高级回答:**短期/长期图像排序、POC、frame_num wrap 和场编码都会影响列表顺序。
17. DPB 中何时能释放一张图?
- **标准回答:**它既不再作为参考,也不再等待输出,且没有外部持有时才可释放。
- **可能追问:**已显示图能否仍作参考?
- **常见错误:**显示后立即释放。
- **高级回答:**硬件解码还受 surface 池和异步 GPU 使用生命周期约束。
18. 解码完成后为什么不一定立刻输出?
- **标准回答:**当前 Picture 可能在 POC 顺序上位于尚未解码图像之后,需要留在 DPB 等待重排。
- **可能追问:**低延迟如何减少等待?
- **常见错误:**解码顺序就是显示顺序。
- **高级回答:**编码侧减少 B Picture 和重排深度,系统层也要匹配缓冲模型。
19. 缺失 PPS/SPS 时如何处理?
- **标准回答:**不能可靠解析依赖它的 Slice,应等待参数集、请求关键刷新或报告错误。
- **可能追问:**能否沿用上一份?
- **常见错误:**随便选择最近 SPS/PPS。
- **高级回答:**参数集按 ID 激活;流式系统应缓存、周期重发并在切换时保持原子性。
20. H.264 解码后的颜色为何仍可能错误?
- **标准回答:**像素重建正确不代表颜色显示正确,还需处理 VUI/容器中的 primaries、transfer、matrix、range 和色度位置。
- **可能追问:**BT.709 limited range 转 RGB 出错会怎样?
- 常见错误:“H.264 解码器只要输出 YUV420P 就一定颜色正确。”
- **高级回答:**硬件 surface、Canvas 和导出编码器之间需要明确颜色元数据传递与转换责任。
C. 10 道数学题
21. 解码 00110 的 ue(v)。
- **标准回答:**两个前导零,后缀
10=2,codeNum=2²-1+2=5。 - **可能追问:**映射为
se(v)是多少? - **常见错误:**直接把
00110当二进制 6。 - **高级回答:**解释前缀长度决定后缀长度,因此是前缀码。
22. se(v) 的 codeNum=6 对应什么?
- 标准回答:
-3。 - 可能追问:
+3对应哪个codeNum? - **常见错误:**偶数映射为正数。
- **高级回答:**给出
0,+1,-1,+2,-2...映射序列。
23. QP 从 20 增加到 32,量化步长约变多少?
- **标准回答:**增加 12,即两个 6,约为 4 倍。
- **可能追问:**码率一定变为四分之一吗?
- **常见错误:**把 QP 当线性质量百分比。
- **高级回答:**Qstep 关系确定,但码率取决于内容、预测、熵和编码决策,不成简单反比。
24. 1080p 8-bit 4:2:0 六帧理论内存是多少?
- **标准回答:**约
1920×1080×1.5×6≈18.66 MB。 - **可能追问:**为何实际更高?
- **常见错误:**按 RGB24 每帧计算后声称是 DPB 必需值。
- **高级回答:**补充 stride、1088 对齐、硬件 surface 和显示队列。
25. 运动矢量 (−7, 10) 表示多少亮度像素位移?
- 标准回答:
(−1.75, 2.5)像素。 - **可能追问:**色度位移是否同样直接除 4?
- **常见错误:**认为单位是整像素。
- **高级回答:**色度插值坐标受色度下采样和标准规定影响。
26. SPS 给出宽宏块数 80、高 map unit 数 45、逐行,编码尺寸是多少?
- 标准回答:
1280×720。 - **可能追问:**隔行且
frame_mbs_only_flag=0呢? - **常见错误:**高始终乘 16,不考虑系数 2。
- **高级回答:**指出还需裁剪得到最终显示尺寸。
27. 8-bit 中预测 20、残差 −35,重建值是多少?
- 标准回答:
Clip(−15)=0。 - **可能追问:**10-bit 的裁剪范围?
- **常见错误:**允许负像素输出。
- **高级回答:**一般范围为
[0,2^bitDepth−1],但内部计算可能使用更宽整数。
28. 去块条件中 |p0−q0| 超过 α 会怎样?
- **标准回答:**通常不滤该边界,避免把真实强边缘错误平滑。
- **可能追问:**是否只看这一项?
- 常见错误:“只要是块边界就滤。”
- **高级回答:**还需
β条件、边界强度和强/弱滤波分支。
29. Annex B 扫描复杂度是多少?
- **标准回答:**单次线性扫描为
O(B)。 - **可能追问:**朴素反复查找为何可能退化?
- **常见错误:**认为有码流压缩所以是
O(log B)。 - **高级回答:**流式状态机可单遍处理并避免拷贝;错误的切片拼接可能带来额外
O(B²)复制。
30. 4K 8-bit 4:2:0 保留 12 帧理论像素内存?
- 标准回答:
3840×2160×1.5×12≈149.3 MB。 - **可能追问:**编辑器为何可能远超此值?
- **常见错误:**忽略多轨、代理、RGBA 合成目标和 GPU 双缓冲。
- **高级回答:**给出 surface 池、输出队列和应用持帧的分项预算。
D. 10 道 Web 实战题
31. WebCodecs 的一个 H.264 Chunk 放一个 NALU 吗?
- **标准回答:**AVC 注册预期一个 Access Unit,内部可以包含多个 NALU。
- **可能追问:**一个 Picture 有三个 Slice 怎么办?
- **常见错误:**把三个 Slice 分成三个带同一 timestamp 的 Chunk。
- **高级回答:**Demux/AU assembler 应先汇集完整 AU,再构造 Chunk。
32. VideoDecoderConfig.description 对 H.264 是什么?
- **标准回答:**存在时应为
AVCDecoderConfigurationRecord字节,即常说的 avcC record 内容。 - **可能追问:**是否包含 MP4 Box 的 8 字节 box header?
- **常见错误:**把整个
avcCBox 连 box header 一起传入而不核对 demuxer API。 - **高级回答:**明确区分 Box payload、record 与 codec extradata 的库接口约定。
33. EncodedVideoChunk.type="key" 如何确定?
- **标准回答:**依据容器随机访问标记和 AVC 注册语义;
avc格式应含 IDR primary picture,参数集由 description 提供。 - **可能追问:**检测到 type 5 NALU 就一定够吗?
- **常见错误:**只看是否有 SPS。
- **高级回答:**还需确认 AU 完整、参数集有效,以及容器 sync sample 是否可信。
34. B Picture 时 WebCodecs 输入和 timestamp 怎么设置?
- **标准回答:**按解码顺序调用
decode(),每个 Chunk 的 timestamp 使用其 PTS。 - **可能追问:**为何不能按 PTS 排序输入?
- **常见错误:**先把 Sample 按显示时间排序再送解码器。
- **高级回答:**Demuxer需同时保存 DTS、PTS、duration,并处理 edit list/timebase。
35. WebCodecs 如何 Seek?
- **标准回答:**找前置随机访问点,reset/configure,按解码顺序向前喂数据,丢弃目标前输出。
- **可能追问:**快速连续 Seek 如何取消?
- **常见错误:**只送目标 Sample。
- **高级回答:**使用 generation token、队列上限、预取窗口和共享关键帧索引。
36. 如何做 Backpressure?
- **标准回答:**监控
decodeQueueSize、输出消费速度和应用帧缓存,超过阈值暂停输入。 - **可能追问:**仅看 queue size 足够吗?
- **常见错误:**一次把整部电影所有 Chunk 送入 decoder。
- **高级回答:**结合内存预算、GPU fence、网络流控和 Seek 优先级形成端到端背压。
37. 为什么必须 VideoFrame.close()?
- **标准回答:**Frame 可能持有硬件 surface 或 GPU/系统媒体资源,不关闭会阻塞 surface 复用并增加内存。
- **可能追问:**绘制到 Canvas 后何时关闭?
- **常见错误:**等待 JavaScript GC 自动释放。
- **高级回答:**建立明确所有权;异步 GPU 使用完成前可 clone 或由渲染层负责最终关闭。
38. 能否把 Annex B 直接 append 到 MSE?
- **标准回答:**通常不能把裸 NALU 当作 MP4 SourceBuffer 的媒体分段;应封装为浏览器支持的字节流格式,如 fMP4。
- **可能追问:**为什么 WebCodecs 可以?
- **常见错误:**MSE 与 WebCodecs 都是“喂 H.264 字节”。
- **高级回答:**MSE 面向媒体分段和 HTMLMediaElement 状态机,WebCodecs 面向编码 Access Unit。
39. 如何判断浏览器支持某个 H.264 配置?
- **标准回答:**WebCodecs 用
VideoDecoder.isConfigSupported();MSE 用MediaSource.isTypeSupported();播放性能可查 MediaCapabilities。 - 可能追问:
avc1是否足够? - **常见错误:**通过 User-Agent 白名单判断。
- **高级回答:**用完整 Profile/constraint/Level codec string,并按实际分辨率、码率、帧率探测。
40. 分辨率中途变化怎么办?
- **标准回答:**检测新的 SPS/decoderConfig,刷新旧 Picture 边界并重新配置解码与渲染资源。
- **可能追问:**能否在同一 MP4 track 随意变化?
- **常见错误:**继续按旧 stride 和尺寸读 Frame。
- **高级回答:**容器 sample entry、MSE changeType/新初始化信息、硬件会话能力都需统一设计。
E. 5 道系统设计题
41. 设计 AI 视频边生成边播放。
- **标准回答:**优先生成连续时间戳的 fMP4 分片并用 MSE;逐帧处理时才走 Annex B/AU assembler/WebCodecs。
- **可能追问:**模型暂停或回滚怎么办?
- **常见错误:**每生成一张 PNG 就立即在
<img>中轮播。 - **高级回答:**设计 init segment、参数集稳定性、IDR 周期、服务端队列、客户端背压、断点恢复和降级转码。
42. 设计在线编辑器预览解码架构。
- **标准回答:**Demux/索引、按轨 Seek、有限解码窗口、代理视频、GPU 合成、Frame 生命周期和取消机制。
- **可能追问:**多轨同时 4K 怎么办?
- **常见错误:**每条轨从头持续解码并缓存全部帧。
- **高级回答:**按可见性和时间邻域调度,复用解码结果,限制硬件会话,并服务端生成代理。
43. 如何实现帧精确 Seek 与剪切?
- **标准回答:**索引随机访问点,Seek 后向前解码;非关键帧切点的边界区域重编码,其余可尝试复制。
- **可能追问:**音频怎么同步?
- **常见错误:**按
fps×秒直接算 Sample 下标。 - **高级回答:**使用整数 timebase、PTS/DTS、edit list、音频 priming 和导出时间线统一处理。
44. 设计 4K 长视频浏览器内存控制。
- **标准回答:**限制输入队列和 Frame 缓存,及时 close,使用代理、GPU surface、LRU 缓存和分段加载。
- **可能追问:**如何设预算?
- **常见错误:**只统计 JavaScript heap。
- **高级回答:**分别预算压缩缓存、DPB、硬件 surface、GPU 纹理、合成目标和多轨峰值,并监控淘汰。
45. 设计 H.264 兼容性降级。
- **标准回答:**运行时探测 Profile/Level/分辨率;失败时请求 Baseline/Main 较低级别或服务端转码;预览退回
<video>或服务端渲染。 - **可能追问:**为何不能只输出 High Profile?
- **常见错误:**维护一张静态浏览器版本表作为唯一判断。
- **高级回答:**建立编码梯度、能力上报、缓存键、失败遥测和按设备动态选择的媒体矩阵。
十三、练习
13.1 手算题
- 解码
0001010的ue(v)。 - 将
se(v)=-4映射为codeNum,再写出 Exp-Golomb 码字。 pic_width_in_mbs_minus1=79、pic_height_in_map_units_minus1=44、逐行,无裁剪,求尺寸。- 8-bit 4:2:0 的 1280×720 视频保留 10 帧,理论像素内存是多少?
- QP 从 18 增到 36,量化步长约放大多少倍?
- 运动矢量
(9,-14)对应多少亮度像素位移?
13.2 码流分析题
给定:
00 00 00 01 67 64 00 28 AA BB
00 00 00 01 68 EE 3C 80
00 00 01 06 05 FF
00 00 00 01 65 88 80 20
00 00 01 41 9A 10
完成:
- 标出每个 NALU 的起止范围。
- 解出每个 NAL Header 的 F、NRI、Type。
- 哪些是 VCL NALU?
- 能否仅凭这段文本断言有几张完整 Picture?为什么?
再分析:
00 00 03 01 12 00 00 03 03 45
移除合法防竞争字节后得到什么?说明为何不能全局删除 03。
13.3 伪代码题
- 写一个支持跨网络 chunk 的 Annex B 流式 NALU 拆分器。
- 写
readUE()与readSE()。 - 写 length-prefixed NALU 转 Annex B 的函数,长度字段大小由参数传入。
- 写一个基于 generation token 的 WebCodecs Seek 调度器。
13.4 架构设计题
- 设计“服务端持续生成 H.264,浏览器两秒内开始播放”的协议与客户端状态机。
- 设计在线编辑器的 Demux、Keyframe Index、Decoder Pool、Frame Cache 和 GPU Compositor。
- 设计参数集变化、分辨率变化和网络断线后的恢复流程。
13.5 性能估算题
- 4K 30 FPS,应用错误地把每帧转 RGBA 并复制一次,理论内存带宽至少是多少?
- 三条 1080p 轨道各缓存 12 帧 YUV420P,理论像素内存多少?
- GOP 为 240 帧、30 FPS,最坏 Seek 需要向前解码多少秒?代理视频若 GOP 为 30,改善多少?
13.6 Debug 排查题
- 解码器报“missing PPS”,但文件可在播放器中播放。
- Annex B 转 MP4 后第一帧黑屏。
- 画面顺序为 I、P、B、B,而不是 I、B、B、P。
- 1920×1080 视频输出缓冲被当作 1920×1088 显示。
- WebCodecs 播放数秒后停止,
decodeQueueSize很高且内存持续增长。 - Seek 后偶尔闪回旧画面。
- 硬件解码成功,但 Canvas 显示偏灰或对比度不对。
- 同一个 H.264 文件软件解码可用,硬件解码失败。
13.7 参考答案要点
- 手算 1:
0001010有 3 个前导零,后缀010=2,值为7+2=9。 - 手算 2:
-4 → codeNum=8;codeNum+1=9=1001₂,码字0001001。 - 手算 3:
80×16=1280,45×16=720。 - 手算 4:
1280×720×1.5×10=13,824,000 bytes≈13.18 MiB。 - 手算 5:增加 18,即三个 6,约 8 倍。
- 手算 6:
(2.25,-3.5)像素。 - 码流中
67/68/06/65/41分别为 SPS/PPS/SEI/IDR Slice/non-IDR Slice;仅看 NAL 类型与短字节片段不能严谨确认完整 Picture 边界和数量。 - 防竞争示例恢复为
00 00 01 12 00 00 03 45;第二个传输03是防竞争字节,但恢复后的03是原始数据。 - 4K RGBA 单帧约 33.18 MB,30 FPS 单次完整复制约 995 MB/s,尚未计算读写双向和其他处理。
- 三条 1080p、各 12 帧:
3.1104 MB×36≈112.0 MB十进制理论值。 - GOP 240@30 最坏约 8 秒;GOP 30 最坏约 1 秒。
十四、本章速查表
14.1 名词
| 名词 | 一句话定义 |
|---|---|
| NALU | NAL Header 加某类编码载荷的基本单元 |
| VCL NALU | 承载 Slice 图像编码数据的 NALU |
| SPS | 序列级参数集 |
| PPS | 图像级参数集,由 Slice 选择 |
| Slice | 一组宏块及 Slice Header/Data |
| Picture | 一张编码图像,可含多个 Slice |
| Access Unit | 表示一个 primary coded picture 的 NALU 集合 |
| RBSP | 去除防竞争字节后、含尾随对齐语义的原始字节序列载荷 |
| Annex B | 起始码分隔的 H.264 字节流格式 |
| AVC format | length-prefixed NALU,参数集常在 avcC 中 |
| DPB | 保存参考图像和等待输出图像的解码图像缓冲 |
| POC | 推导 Picture 输出顺序的编码层计数 |
| IDR | 具有即时解码刷新语义的 Picture |
14.2 常见 NAL Type
1 non-IDR Slice
5 IDR Slice
6 SEI
7 SPS
8 PPS
9 AUD
14.3 NAL Header
F = (byte >> 7) & 1
NRI = (byte >> 5) & 3
Type = byte & 0x1F
14.4 Annex B 与 AVC
| Annex B | AVC/MP4 |
|---|---|
00 00 01 / 00 00 00 01 | 大端长度字段 |
| 参数集常周期性内嵌 | 参数集常在 avcC |
| 常见于裸流、部分直播/传输 | 常见于 MP4 Sample |
| WebCodecs 无 description 时按 annexb | 有 AVCDecoderConfigurationRecord 时按 avc |
14.5 核心公式
ue(v): codeNum = (1 << leadingZeros) - 1 + suffix
Qstep(QP + 6) ≈ 2 × Qstep(QP)
recon = Clip(prediction + residual)
YUV420 8-bit bytes ≈ width × height × 1.5
MV pixel displacement = MV quarter-unit / 4
14.6 解码流程
拆 NALU
→ 去防竞争字节
→ 解析 SPS/PPS/Slice Header
→ 熵解码
→ 反量化/反变换
→ 帧内或帧间预测
→ 残差相加
→ 去块滤波
→ DPB 参考标记
→ POC 重排输出
14.7 面试结论
- NALU、Frame、Access Unit 不是同义词。
- I Picture 不一定是 IDR Picture。
- Annex B 与 MP4 length-prefixed 必须按输入格式解析。
- SPS/PPS 是解码状态,不是每帧固定头部。
- POC 管编码层输出顺序,PTS/DTS 属于系统时间线。
- 去块滤波在环路内,影响后续参考。
- DPB 同时管理“参考”与“等待显示”。
- WebCodecs Chunk 对 AVC 应按 Access Unit 组织。
- Seek 要从前置随机访问点向前解码。
- Web 工程必须做能力探测、Backpressure 和
VideoFrame.close()。
十五、参考资料
- ITU-T Recommendation H.264 (08/24), Advanced video coding for generic audiovisual services https://www.itu.int/rec/T-REC-H.264-202408-I/en
- RFC 6184, RTP Payload Format for H.264 Video https://datatracker.ietf.org/doc/html/rfc6184
- W3C, AVC (H.264) WebCodecs Registration https://www.w3.org/TR/webcodecs-avc-codec-registration/
- W3C, WebCodecs https://www.w3.org/TR/webcodecs/
- W3C, Media Source Extensions https://www.w3.org/TR/media-source-2/
- W3C, Media Capabilities https://www.w3.org/TR/media-capabilities/
- FFmpeg Bitstream Filters Documentation https://ffmpeg.org/ffmpeg-bitstream-filters.html
- ffprobe Documentation https://ffmpeg.org/ffprobe.html
本周验收
不看资料,尝试在白板上讲完下面这句话:
“MP4 Demux 后得到按 DTS 排列的 H.264 Sample;从 avcC 获得 SPS/PPS 和 NAL 长度字段大小,把每个 Access Unit 交给解码器。解码器解析 Slice、熵解码宏块、反量化反变换、做帧内或帧间预测、重建并去块滤波,再通过参考标记和 DPB 管理按 POC/PTS 输出。Seek 时必须从目标之前的随机访问点开始,WebCodecs 中还要控制 decodeQueue、丢弃过期输出并及时关闭 VideoFrame。”
能够把其中每个名词继续向下解释一层,即达到第六周通过标准。