返回文章列表

第十三章|基础专题六: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


一、学习目标

学完本周后,你应当能够:

  1. 从一段十六进制数据中识别 Annex B 起始码和 NAL Unit Header。
  2. 解释 forbidden_zero_bitnal_ref_idcnal_unit_type 的作用。
  3. 严格区分 NALU、Slice、Picture、Frame、Access Unit 和 MP4 Sample。
  4. 解释 RBSP、工程中常说的 EBSP、rbsp_trailing_bits 与防竞争字节 0x03
  5. 解释 Annex B 与 MP4/AVC length-prefixed 两种封装,并完成相互转换的伪代码。
  6. 说明 SPS、PPS、SEI 和 Slice Header 分别控制什么,以及 Slice 如何通过 PPS 间接选择 SPS。
  7. 手工解出无符号和有符号 Exp-Golomb 码。
  8. 从 SPS 推导宏块尺寸、编码尺寸与裁剪后的显示尺寸。
  9. 按顺序描述 H.264 解码器从字节流到 YCbCr 帧的完整过程。
  10. 解释 CAVLC 与 CABAC 在解码器中的位置及性能差异。
  11. 解释帧内预测、帧间预测、反量化、反变换、重建和环路去块滤波如何衔接。
  12. 解释参考图像列表、参考图像标记、DPB、POC 与显示重排。
  13. 解释为什么 I Picture 不一定是 IDR Picture,为什么 NALU 不等于一帧。
  14. 使用 FFmpeg/ffprobe 观察 SPS/PPS、NALU、关键帧、PTS/DTS 和像素格式。
  15. 设计基于 WebCodecs 的 H.264 解码、Seek、Backpressure 和资源释放流程。

本周安排

学习日主题产出
Day 1NAL、Annex B、length-prefixed写一个 NALU 拆分器
Day 2RBSP、SPS、PPS、Slice Header手工解析 NAL Header 与 Exp-Golomb
Day 3宏块、CAVLC/CABAC、反量化与反变换画出 Slice 解码流程
Day 4帧内/帧间预测、去块滤波解释一块像素如何重建
Day 5参考帧、DPB、POC、显示重排画出含 B Picture 的缓冲过程
Day 6FFmpeg、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、RGBANALU、MP4
编码标准H.264/AVCx264、FFmpeg
编码器实现x264、OpenH264、硬件编码器H.264 标准本身
基础码流SPS、PPS、SEI、Slice NALUMP4 文件
容器MP4、MOV、MPEG-TSH.264 编码算法
网络封装RTP、HLS、DASH 等Slice 或宏块
浏览器 API<video>、MSE、WebCodecscodec 或容器

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 ]

码字由以下两部分组成:

  1. k 个前导零;
  2. codeNum + 1 的二进制表示,共 k+1 位。
数值codeNum+1码字
011
110010
211011
310000100
410100101
511000110

**手算例子:**码字 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=2CropUnitY=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=240r=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=100q0=104p1=99q1=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
  • Fforbidden_zero_bit,标准合规码流中必须为 0。
  • NRInal_ref_idc。0 表示该 NALU 的内容不用于重建参考图像;非 0 表示丢失它可能破坏参考图像完整性。不要把 1、2、3 简化为严格的“质量等级”。
  • Typenal_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作用
1Coded slice of a non-IDR picture普通非 IDR Slice
2~4Data partition A/B/C数据分区模式,现代常见文件中较少见
5Coded slice of an IDR pictureIDR Picture 的 Slice
6SEI补充增强信息,如 picture timing、recovery point 等
7SPSSequence Parameter Set,序列级参数
8PPSPicture Parameter Set,图像级参数
9AUDAccess Unit Delimiter,可选边界提示
10End of Sequence序列结束
11End of Stream码流结束
12Filler 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_bytestart_code_prefix_one_3bytes、前导零和尾随零语义解析,而不是简单假设所有 NALU 都固定以四字节起始码开头。H264

边界解析原则:

  1. 找到当前起始码;
  2. NALU 从起始码后的第一个字节开始;
  3. 一直读取到下一个起始码之前;
  4. 起始码本身不属于 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_numpic_parameter_set_id、场标志、IDR 标志、idr_pic_id、POC 相关字段等,按照标准定义判断是否开始了新 Picture。只看 first_mb_in_slice == 0 不足以覆盖所有合法码流。

5.7 SPS:序列级状态

SPS 常见关键字段:

字段作用
profile_idcProfile
constraint flags约束集合,影响 codec string 与功能子集
level_idcLevel,限制分辨率、处理量、DPB 等
seq_parameter_set_idSPS 标识符
chroma_format_idc4:2:0、4:2:2、4:4:4 等,高 Profile 语法中出现
bit depth fields亮度、色度位深
scaling matrices可选量化缩放矩阵
log2_max_frame_num_minus4frame_num 位宽
pic_order_cnt_typePOC 推导方式
max_num_ref_frames参考图像相关上限
picture width/height fields宏块级编码尺寸
frame_mbs_only_flag是否仅逐行帧宏块
cropping fields从编码尺寸裁剪到显示尺寸
VUISAR、色彩、时序、HRD、限制等可选信息

注意:VUI/SEI 可以表达时序模型和显示相关信息,但 MP4 中逐样本的 PTS/DTS 仍属于系统层时间戳,不能把两者混为一谈。

5.8 PPS:图像级状态

PPS 常见关键字段:

字段作用
pic_parameter_set_idPPS 标识符
seq_parameter_set_id此 PPS 引用的 SPS
entropy_coding_mode_flag0 常表示 CAVLC,1 表示 CABAC
slice group fieldsFMO/多 Slice Group 配置
default active reference counts默认参考图像数量
weighted prediction fields加权预测配置
pic_init_qp_minus26初始图像 QP 基值
chroma QP offsets色度 QP 偏移
deblocking_filter_control_present_flagSlice 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_typeI、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 markingDPB 标记、MMCO、IDR 标志
cabac_init_idcCABAC 上下文初始化选择
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 时:

  1. 读取 pic_parameter_set_id
  2. 查找对应 PPS;
  3. 从 PPS 找 SPS;
  4. 检查 Profile、Level、尺寸、位深、色度格式是否受支持;
  5. 必要时重新配置缓冲区或硬件会话。

若 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 宏块重建

对每个宏块,解码器大致执行:

  1. 解析宏块类型和分区;
  2. 若为帧内块,解析预测模式并从已重建邻块生成预测;
  3. 若为帧间块,构造参考列表、解析参考索引和运动矢量差,推导最终运动矢量;
  4. 解析 coded block pattern 和 QP;
  5. 熵解码量化系数;
  6. 反扫描;
  7. 反量化;
  8. 4×4 或 8×8 反整数变换;
  9. 将残差加到预测样本;
  10. 写入当前重建图像。

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 同时解决两件事:

  1. 保存短期或长期参考图像,供未来 Picture 预测;
  2. 暂存已解码但尚未到输出顺序的 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 常见性能瓶颈

  1. 长 GOP Seek 时需要从很早的随机访问点向前解码。
  2. decodeQueueSize 持续增长,输入生产速度超过输出消费速度。
  3. VideoFrame 未及时 close()
  4. 每帧转 RGBA 后在 CPU 上处理。
  5. 参数集变化触发硬件解码器重配置。
  6. 频繁 reset() 或创建销毁解码器造成启动成本。
  7. 同时为预览、缩略图、分析重复解码同一资产。
  8. 读取 MP4 Sample 时发生大量小对象和内存复制。
  9. B Picture 的重排缓冲增加延迟。
  10. 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 解释,数据按 avc length-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

十一、易错点

  1. **H.264 不是 MP4。**前者是视频编码标准,后者是容器。
  2. **H.264 不是 x264。**x264 是编码器实现。
  3. **NALU 不等于一帧。**一帧可有多个 Slice NALU,一个 Access Unit 还可含非 VCL NALU。
  4. **Slice 不等于 Picture。**多个 Slice 可组成一张 Picture。
  5. **I Picture 不等于 IDR Picture。**I 只表示帧内预测,IDR 还具有参考刷新语义。
  6. **IDR 不等于单个 type 5 NALU。**一张 IDR Picture 可以有多个 type 5 Slice。
  7. **SPS/PPS 不必每帧出现。**它们可复用,也可由容器带外提供。
  8. **AUD 不是必需。**不能依赖它作为唯一帧边界。
  9. **起始码不属于 NALU。**NALU 从 Header 字节开始。
  10. **MP4 中通常不是 Annex B。**常见为 length-prefixed NALU。
  11. **长度字段不一定固定 4 字节。**应读取 lengthSizeMinusOne
  12. **不能删除所有 0x03。**只移除合法防竞争位置的字节。
  13. **nal_ref_idc 非零不等于数值越大画质越高。**核心语义是参考完整性。
  14. **SEI 通常不是基本像素重建所必需,但不能一概当作无用。**它可携带时序、恢复点、显示等信息。
  15. **H.264 码流不等于完整时间线。**PTS/DTS 多由容器或传输层提供。
  16. **解码顺序不等于显示顺序。**B Picture 会导致重排。
  17. **POC 不等于 PTS。**POC 是编码层图像顺序推导量,PTS 是系统层呈现时间。
  18. **宏块编码尺寸不一定等于显示尺寸。**1080 高度常编码为 1088 后裁剪。
  19. **去块滤波不是纯显示后处理。**它在环路内,影响后续参考预测。
  20. **参考帧不一定已显示,已显示帧也不一定能释放。**DPB 同时负责参考和输出。
  21. **CABAC 不是任意 Profile 都可使用。**Profile/约束决定合法工具集合。
  22. **硬件解码不保证零复制。**若把结果读回 CPU,仍可能很慢。
  23. **WebCodecs 的 Chunk 不是单个 NALU。**AVC 注册预期一个 Access Unit。
  24. **type: "key" 不能随意标。**它必须符合当前 AVC 格式的随机访问语义。
  25. **WebCodecs 输出不是完整 MP4。**编码后仍需 Mux。
  26. **MSE 通常不能直接 append 裸 Annex B 到 MP4 SourceBuffer。**需要受支持的媒体分段。
  27. **isConfigSupported() 成功不代表永远有资源。**硬件资源与运行环境会变化。
  28. **Seek 不能只解码目标 Sample。**通常从前置随机访问点向前解码。
  29. **快速拖动必须取消旧任务。**仅 reset() 而不隔离旧输出可能仍产生竞态。
  30. **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 & 0x1Fnri = (header >> 5) & 0x03f = 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. 解码 00110ue(v)

  • **标准回答:**两个前导零,后缀 10=2codeNum=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?
  • **常见错误:**把整个 avcC Box 连 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 手算题

  1. 解码 0001010ue(v)
  2. se(v)=-4 映射为 codeNum,再写出 Exp-Golomb 码字。
  3. pic_width_in_mbs_minus1=79pic_height_in_map_units_minus1=44、逐行,无裁剪,求尺寸。
  4. 8-bit 4:2:0 的 1280×720 视频保留 10 帧,理论像素内存是多少?
  5. QP 从 18 增到 36,量化步长约放大多少倍?
  6. 运动矢量 (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

完成:

  1. 标出每个 NALU 的起止范围。
  2. 解出每个 NAL Header 的 F、NRI、Type。
  3. 哪些是 VCL NALU?
  4. 能否仅凭这段文本断言有几张完整 Picture?为什么?

再分析:

00 00 03 01 12 00 00 03 03 45

移除合法防竞争字节后得到什么?说明为何不能全局删除 03

13.3 伪代码题

  1. 写一个支持跨网络 chunk 的 Annex B 流式 NALU 拆分器。
  2. readUE()readSE()
  3. 写 length-prefixed NALU 转 Annex B 的函数,长度字段大小由参数传入。
  4. 写一个基于 generation token 的 WebCodecs Seek 调度器。

13.4 架构设计题

  1. 设计“服务端持续生成 H.264,浏览器两秒内开始播放”的协议与客户端状态机。
  2. 设计在线编辑器的 Demux、Keyframe Index、Decoder Pool、Frame Cache 和 GPU Compositor。
  3. 设计参数集变化、分辨率变化和网络断线后的恢复流程。

13.5 性能估算题

  1. 4K 30 FPS,应用错误地把每帧转 RGBA 并复制一次,理论内存带宽至少是多少?
  2. 三条 1080p 轨道各缓存 12 帧 YUV420P,理论像素内存多少?
  3. GOP 为 240 帧、30 FPS,最坏 Seek 需要向前解码多少秒?代理视频若 GOP 为 30,改善多少?

13.6 Debug 排查题

  1. 解码器报“missing PPS”,但文件可在播放器中播放。
  2. Annex B 转 MP4 后第一帧黑屏。
  3. 画面顺序为 I、P、B、B,而不是 I、B、B、P。
  4. 1920×1080 视频输出缓冲被当作 1920×1088 显示。
  5. WebCodecs 播放数秒后停止,decodeQueueSize 很高且内存持续增长。
  6. Seek 后偶尔闪回旧画面。
  7. 硬件解码成功,但 Canvas 显示偏灰或对比度不对。
  8. 同一个 H.264 文件软件解码可用,硬件解码失败。

13.7 参考答案要点

  • 手算 1:0001010 有 3 个前导零,后缀 010=2,值为 7+2=9
  • 手算 2:-4 → codeNum=8codeNum+1=9=1001₂,码字 0001001
  • 手算 3:80×16=128045×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 名词

名词一句话定义
NALUNAL 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 formatlength-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 BAVC/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 面试结论

  1. NALU、Frame、Access Unit 不是同义词。
  2. I Picture 不一定是 IDR Picture。
  3. Annex B 与 MP4 length-prefixed 必须按输入格式解析。
  4. SPS/PPS 是解码状态,不是每帧固定头部。
  5. POC 管编码层输出顺序,PTS/DTS 属于系统时间线。
  6. 去块滤波在环路内,影响后续参考。
  7. DPB 同时管理“参考”与“等待显示”。
  8. WebCodecs Chunk 对 AVC 应按 Access Unit 组织。
  9. Seek 要从前置随机访问点向前解码。
  10. Web 工程必须做能力探测、Backpressure 和 VideoFrame.close()

十五、参考资料

  1. 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
  2. RFC 6184, RTP Payload Format for H.264 Video https://datatracker.ietf.org/doc/html/rfc6184
  3. W3C, AVC (H.264) WebCodecs Registration https://www.w3.org/TR/webcodecs-avc-codec-registration/
  4. W3C, WebCodecs https://www.w3.org/TR/webcodecs/
  5. W3C, Media Source Extensions https://www.w3.org/TR/media-source-2/
  6. W3C, Media Capabilities https://www.w3.org/TR/media-capabilities/
  7. FFmpeg Bitstream Filters Documentation https://ffmpeg.org/ffmpeg-bitstream-filters.html
  8. 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。”

能够把其中每个名词继续向下解释一层,即达到第六周通过标准。