H.264 视频编码与码流参考手册
H.264 视频编码与码流参考手册
H.264/AVC 是视频编码标准,不是网络协议。它定义如何将图像压缩成宏块、切片和 NAL 单元;文件封装和网络传输还需要 Annex-B、AVCC、MP4、RTP 等其他格式或协议。
本文依据 ITU-T H.264 与 ISO/IEC 14496-10 的公开语法体系整理。Annex-B 属于字节流封装约定,AVCC 属于 AVC 配置记录/长度前缀表达,RTP 只在专门的传输章节中作为上层承载示例出现。
第一章 标准定位与编码层级
编码层级
1 | 图像帧 → slice(切片)→ NAL unit → 码流封装/传输 |
一个图像可以由一个或多个 slice 组成,一个 NAL unit 通常承载一个 slice 或参数集。编码器还会产生 SPS、PPS、SEI 等非图像 NAL。
NAL 单元结构
NAL 的 RBSP 前面有一个 NAL header。H.264 NAL header 为 1 字节:
| 位域 | 长度 | 含义 |
|---|---|---|
| forbidden_zero_bit | 1 | 必须为 0 |
| nal_ref_idc | 2 | 参考重要性 |
| nal_unit_type | 5 | NAL 类型 |
常见类型:
| type | 名称 | 作用 |
|---|---|---|
| 1 | 非 IDR slice | 普通 P/B 图像切片 |
| 5 | IDR slice | 可作为随机接入点的关键图像 |
| 6 | SEI | 补充增强信息 |
| 7 | SPS | 序列参数集 |
| 8 | PPS | 图像参数集 |
| 9 | AUD | 访问单元分隔符 |
| 10/11 | EOS/EOB | 序列/码流结束 |
| 24/28 | STAP/FU | RTP 聚合/分片指示,非裸 H.264 NAL 类型 |
第二章 参数集与解码初始化
SPS、PPS 与解码初始化
SPS 描述分辨率、Profile、Level、参考帧、帧号和解码窗口等序列级参数;PPS 描述熵编码、切片组和量化等图像级参数。解码器通常必须先获得有效 SPS/PPS,才能正确解释后续 slice。
profile_idc、约束标志和 level_idc 常组成 avc1/avcoti 等能力描述。不要只根据文件扩展名判断 H.264 能力。
第三章 码流封装与传输
Annex-B 格式
Annex-B 在 NAL 前使用起始码:
1 | 00 00 01 或 00 00 00 01 |
典型码流:
1 | 00 00 00 01 67 ...SPS... |
解析时必须识别 3 字节和 4 字节起始码,并计算 NAL 长度;不能把起始码本身传给解码器作为 NAL payload。
防竞争字节与 RBSP
为了避免 payload 中出现类似起始码的序列,编码器会插入 03,例如:
1 | 00 00 03 01 → 00 00 01 |
这称为 emulation prevention。解析 RBSP 时需要移除符合规则的 03;但传输和存储完整 NAL 时通常保留它。错误地全部删除或重复插入都会破坏码流。
AVCC 格式
AVCC 通常用于 MP4 等容器。它不使用起始码,而是在每个 NAL 前放置固定长度的 NAL size,长度通常为 4 字节:
1 | [4-byte NAL length][NAL header + RBSP] |
avcC 配置记录保存 SPS/PPS 和 lengthSizeMinusOne。Annex-B 与 AVCC 转换时必须使用正确的 NAL 长度字段,不能只做字符串替换。
第四章 图像类型、访问单元与时间
图像类型与随机接入
- I slice:只依赖当前图像内部信息。
- P slice:可参考前面的图像。
- B slice:可参考前后图像。
- IDR:解码器在其后可以清空旧参考图像,常作为随机接入点。
“I 帧”等于“IDR”并不总是严格成立;某些编码配置存在非 IDR 的 I slice。
访问单元与时间戳
一个 access unit 通常对应一个输出图像,由一个或多个 slice NAL 及其附属 NAL 构成。RTP、MP4 等上层需要把多个 NAL 组织成访问单元,并为其分配 PTS/DTS。B 帧可能导致显示顺序和解码顺序不同。
RTP 传输要点
单个 NAL 小于 MTU 时可直接发送;较大 NAL 通常使用 FU-A 分片。FU-A 由 FU indicator 和 FU header 指示原始 NAL 类型、Start、End。接收端需按 RTP sequence number 重组,并在 End 到达时恢复原 NAL header。
解码链路
1 | 读取/接收 → 去除封装 → NAL 检查 → SPS/PPS 更新 → 解码 → 帧缓冲 → 显示 |
切换分辨率或编码器重启时,SPS/PPS 可能变化;播放器应在关键点更新配置,而不能永久缓存第一次参数。
第五章 解析实现与错误处理
解析检查清单
- 确认是 Annex-B 还是 AVCC。
- 验证 NAL 长度不越界。
- 检查 NAL header 的 forbidden_zero_bit。
- 检查 SPS/PPS 是否在关键图像前可用。
- 区分 packet、NAL、access unit 和 decoded frame。
- 记录 RTP 序号、时间戳和 marker bit。
常见错误
- 将 AVCC 当 Annex-B 输入硬解码器。
- 把起始码当作 NAL 数据传入。
- 只发送 IDR,不发送 SPS/PPS,导致新客户端黑屏。
- FU-A 丢包后仍拼接出损坏 NAL。
- 用 packet 数量代替帧数量统计。
- 忽略 DTS/PTS,导致 B 帧顺序错误。
NAL 类型与封装关系
| 层 | 示例 | 是否包含在 H.264 标准码流中 |
|---|---|---|
| NAL header | 0x67、0x68、0x65 |
是 |
| Annex-B start code | 00 00 01 |
码流封装约定 |
| AVCC length prefix | 00 00 00 19 |
容器/传输封装约定 |
| RTP FU-A header | S/E/type | RTP payload 格式 |
不要把 RTP 的 FU-A header 当成 H.264 NAL header;FU-A 只在网络封装层存在。
SPS/PPS 的 RBSP 字段
SPS 中常见语法字段包括:
| 字段 | 作用 |
|---|---|
profile_idc |
Profile 编号 |
constraint_set_flags |
编码约束 |
level_idc |
Level 能力等级 |
seq_parameter_set_id |
SPS 标识 |
chroma_format_idc |
色度采样格式 |
log2_max_frame_num_minus4 |
frame_num 范围 |
pic_order_cnt_type |
图像顺序计数方式 |
max_num_ref_frames |
最大参考帧数 |
pic_width_in_mbs_minus1 |
宏块宽度减 1 |
pic_height_in_map_units_minus1 |
map unit 高度减 1 |
frame_mbs_only_flag |
是否仅逐帧编码 |
frame_cropping_* |
裁剪边界 |
vui_parameters_present_flag |
是否存在 VUI |
分辨率不能简单等于宏块数乘 16,还要考虑 frame cropping 和色度格式对应的 crop unit。
Exp-Golomb 编码
H.264 大量语法使用无符号 ue(v) 和有符号 se(v) Exp-Golomb。解析 ue(v) 的步骤是:统计前导零 N,读取后续 N 位,结果为 (1 << N) - 1 + suffix。读取前导零时必须设置最大位数限制,防止恶意码流造成无限循环。
Annex-B 扫描算法
1 | 从 offset 开始寻找 00 00 01 或 00 00 00 01 |
连续零字节、文件尾没有下一个起始码、空 NAL 都必须单独处理。解析结果应记录 NAL 起始偏移、长度、type 和所属 access unit。
AVCC 配置记录
avcC 常见字段:
| 字段 | 长度 | 说明 |
|---|---|---|
| configurationVersion | 1 | 通常为 1 |
| AVCProfileIndication | 1 | SPS profile_idc |
| profile_compatibility | 1 | 约束标志 |
| AVCLevelIndication | 1 | level_idc |
| lengthSizeMinusOne | 2 bits | NAL 长度字段字节数减 1 |
| numOfSequenceParameterSets | 5 bits | SPS 数量 |
| sequenceParameterSetLength | 16 bits | SPS 长度 |
| numOfPictureParameterSets | 8 bits | PPS 数量 |
| pictureParameterSetLength | 16 bits | PPS 长度 |
长度字段和保留位必须校验,不能直接信任外部文件。
关键图像判定
对 Annex-B,可根据 NAL type 5 识别 IDR;对 MP4,应结合 stss 和 sample entry;对 RTP,应使用 marker 和 payload 解析结果。三者的“关键帧”标志不是同一个字段。
调试命令
1 | ffprobe -show_streams -show_frames -select_streams v:0 input.h264 |
裸 H.264 如果没有 SPS/PPS 或格式判断错误,ffprobe 的失败信息通常比播放器黑屏更容易定位。
H.264 码流构成图
1 | Access Unit(一个输出图像) |
NAL Header 位图
1 | bit 7 bit 6..5 bit 4..0 |
例如 0x67 = 0110 0111b:
| 位域 | 值 | 解释 |
|---|---|---|
| forbidden_zero_bit | 0 | 合法 |
| nal_ref_idc | 3 | 该 NAL 对参考图像重要 |
| nal_unit_type | 7 | SPS |
0x68 是 PPS,0x65 通常是 IDR slice,0x41 通常是非 IDR 参考 slice。
Annex-B 与 AVCC 字节实例
Annex-B:
1 | 00 00 00 01 67 [SPS bytes] |
AVCC:
1 | 00 00 00 04 67 [SPS bytes] |
AVCC 中的 4 字节数值是后续 NAL 的长度,不是起始码。长度字段的字节数由 avcC.lengthSizeMinusOne + 1 决定。
字段之间的关系
1 | SPS → 解码分辨率、Profile、Level、参考帧 |
标准语法与本文的边界
H.264 语法中常见的记号有 f(8)、u(n)、ue(v)、se(v) 和 me(v)。f(n) 表示固定长度字段,u(n) 表示无符号 n 位字段,ue(v) 表示无符号 Exp-Golomb,se(v) 表示有符号 Exp-Golomb,me(v) 表示通过映射表解析的编码值。标准语法还会使用条件分支,例如某个标志为 1 时才出现后续字段。解析器必须按照条件顺序读 bit,不能把语法表简单当成按字节排列的 C 结构体。
本文把 H.264 分成三个边界明确的层次。编码层描述图像、宏块、预测、变换、量化和熵编码;网络抽象层描述 NAL Header、RBSP 和 NAL 单元;文件或传输层描述 Annex-B、AVCC、RTP payload 等外部组织。一个 NAL 的字节可以被容器重新加长度前缀,但容器转换不应该改变 NAL 内部的 RBSP 语义。
NAL 单元的完整字节模型
在不考虑 Annex-B 起始码时,一个 NAL 单元由一个字节的 NAL Header 和 EBSP payload 构成:
1 | NAL unit = nal_header(8 bits) + EBSP bytes |
NAL Header 的三个字段不是独立的字节。forbidden_zero_bit 必须为 0;接收器看到 1 时应把 NAL 标记为非法或按实现策略丢弃。nal_ref_idc 为 0 通常表示该 NAL 不用于参考图像,非 0 表示具有参考意义;但是否为关键图像仍要结合 slice type、IDR 语义和图像顺序判断。nal_unit_type 只占低 5 位,取值 0 和保留范围不能任意解释成视频帧。
RBSP、EBSP 与 trailing bits
RBSP 是去掉 NAL Header 后、按语法解释的原始字节序列。为了避免 RBSP 内出现 00 00 00、00 00 01 或 00 00 02 等可能被误识别为起始码的序列,封装成 EBSP 时会在两个连续零字节后插入 03。解码器先去除符合规则的插入字节,再在 RBSP 上运行 bit reader。参数集通常以 rbsp_trailing_bits() 结束,它至少包含一个 1,随后补足若干个 0 到字节边界。解析器到达 trailing bits 后必须停止读取,不能把 padding 当成新的语法字段。
SPS 的字段依赖关系
SPS 不是一个固定长度结构。读取 profile_idc 后,只有高 Profile 集合才会出现 chroma_format_idc、位深、缩放矩阵等字段;读取 pic_order_cnt_type 后,可能出现 log2_max_pic_order_cnt_lsb_minus4,也可能出现参考帧中 POC 周期的完整参数;读取 frame_cropping_flag 后,才会出现四个裁剪偏移。一个正确的 SPS 解析器应使用局部变量保存这些标志,并在每个分支结束后检查剩余 bit 数量。
图像尺寸的计算也不是简单的宏块数乘 16。先计算 PicWidthInMbs 和 FrameHeightInMbs,再根据 chroma_format_idc、separate_colour_plane_flag、frame_mbs_only_flag 计算 crop unit,最后扣除左右和上下裁剪偏移。若 frame_mbs_only_flag 为 0,图像以场或 MBAFF 形式编码,宏块高度和显示高度的换算会不同。VUI 中的 aspect_ratio_idc 还可能说明显示宽高比与编码像素宽高不同,播放器不能只看 SPS 得到的 coded width/height 就决定最终显示比例。
PPS 与 Slice Header 的引用链
PPS 通过 seq_parameter_set_id 引用一个 SPS;Slice Header 通过 pic_parameter_set_id 引用 PPS。解码某个 slice 的最小状态集合是:当前有效 SPS、当前有效 PPS、slice header 中的图像序号和参考图像管理状态。参数集 ID 并不等于数组下标的安全索引,外部码流可以使用稀疏 ID,解析器应检查 ID 是否在实现支持的范围内,并确认引用对象确实已收到。
Slice Header 的前半部分通常包括 first_mb_in_slice、slice_type、pic_parameter_set_id 和 frame_num。如果当前图像是 IDR,还会出现 idr_pic_id;如果使用场编码或特定 POC 类型,还会出现场标志和 POC 相关字段。后半部分可能包含参考列表重排、加权预测、解码参考图像标记、CABAC 初始化、量化参数增量和去块滤波参数。不同 slice type 和 PPS 标志会改变字段是否存在,因此不能用一个固定长度读取所有 slice。
图像、访问单元和解码顺序
H.264 的“帧”在工程代码中可能指压缩 access unit、解码图像或显示图像。一个 access unit 通常包含一个图像时刻的所有 slice,还可能包含 AUD、SEI、参数集等附属 NAL。解码顺序由参考关系决定,显示顺序由 POC 决定;存在 B slice 时,DTS 与 PTS 不必相同。容器中的 sample、RTP 的 timestamp 和解码器输出的 frame 不是同一个概念。文档和接口必须明确单位,否则很容易把一个 RTP packet 当成一帧或把一个 NAL 当成一个 sample。
参考图像与随机访问
IDR 图像会使解码器按照规范清理旧参考图像,从其后开始可以建立新的解码链。非 IDR 的 I slice 只说明当前 slice 的预测类型,不保证可以脱离之前的参考图像独立解码。发送端在新客户端接入时,应发送能够初始化解码器的 SPS/PPS,并从合适的随机接入点开始;只发送一段 IDR payload 而遗漏参数集,播放器可能无法得到分辨率、熵编码或参考帧配置。
Annex-B 解析的边界处理
Annex-B 的起始码可以是 00 00 01 或 00 00 00 01。扫描器通常跳过任意数量的连续零后检查 01,但必须保留“零字节属于前一个 NAL 的 trailing_zero_8bits 还是下一个起始码”的边界语义。文件头可能先出现零填充,文件尾可能没有下一个起始码;扫描结束时应把最后一个起始码后的所有合法 payload 作为最后一个 NAL。空 NAL、只有零的区间和超出最大长度的 NAL 应被记录为错误,而不是交给解码器。
AVCC 与 Annex-B 的实际转换
AVCC 转 Annex-B 时,先从 avcC 读取 lengthSizeMinusOne,再读取 SPS/PPS 数组;对于每个参数集和每个媒体 sample,使用固定长度的大端整数读取 NAL size,检查 size 不超过 sample 剩余字节,输出起始码和 NAL payload。Annex-B 转 AVCC 时,先扫描 NAL、提取 SPS/PPS 生成配置记录,再为每个 NAL 写入长度前缀。转换器不能把一个访问单元的所有 NAL 粗暴拼成一个 NAL,也不能把起始码四个字节当作 NAL 内容。
码流校验和失败处理
RBSP 位流的读取方法
从 EBSP 还原 RBSP
NAL 单元头之后的字节通常是 EBSP。编码器为了避免 payload 中出现类似起始码的序列,会在 00 00 后的特定位置插入 03 防竞争字节。解析 SPS、PPS 和 slice header 前,必须按顺序删除合法的 03,但不能把所有值为 03 的字节都删除。只有在前两个已输出字节都是零且当前字节为 03,并且后续字节属于规范允许的范围时,才可以执行去竞争。
bit read、bit skip 和 trailing bits
RBSP 语法以 bit 为单位。读取器至少需要 read_bit(n)、skip_bits(n)、无符号整数读取和有符号映射四类操作。语法元素结束后通常存在 rbsp_stop_one_bit 和若干 rbsp_alignment_zero_bit,解析器应检查 stop bit 为 1、后续对齐位为 0。把 trailing bits 当作下一个字段会导致字段整体错位。
无符号 Exp-Golomb
ue(v) 由前导零、一个值为 1 的停止位和信息位组成。设前导零数量为 leadingZeroBits,则 codeNum = 2^leadingZeroBits - 1 + infoBits。例如比特串 00101 有两个前导零,停止位后的信息位为 01,因此 codeNum = 4。读取器必须限制前导零数量,防止恶意码流造成无限扫描或整数溢出。
有符号 Exp-Golomb
se(v) 先读取 codeNum,再映射为有符号值:
1 | value = (codeNum + 1) / 2, codeNum 为奇数 |
因此 codeNum 0、1、2、3、4 对应 0、1、-1、2、-2。量化参数增量、去块滤波参数等字段常使用 se(v);如果误用 ue(v),码流可能仍能读完,但图像会出现持续的量化或边缘错误。
SPS 图像参数的计算
宏块尺寸与编码尺寸
对常见的逐帧序列,编码宽度可由 PicWidthInMbs = pic_width_in_mbs_minus1 + 1 得到,编码像素宽度为 PicWidthInMbs × 16。宏块行数还由 pic_height_in_map_units_minus1 和 frame_mbs_only_flag 共同决定。显示尺寸必须在获得 crop unit 后再扣除裁剪偏移,不能直接从宏块数决定播放器窗口大小。
crop unit 的条件分支
crop unit 由 chroma_format_idc 和隔行标志决定。4:2:0、4:2:2、4:4:4 的水平和垂直采样关系不同;separate_colour_plane_flag 为 1 时又改变了亮度/色度平面的解释。解析器应先建立色度格式状态,再计算 crop unit,最后检查裁剪后宽高是否为正数且不超过编码尺寸。
VUI 与显示时间
VUI 可携带宽高比、视频格式、色度位置、时序信息和 HRD 参数。timing_info_present_flag 为 1 时,num_units_in_tick 与 time_scale 可用于推导标称帧率,但 fixed_frame_rate_flag 决定该关系能否直接解释为固定帧率。容器或传输层提供的时间戳仍需按照媒体管线的时钟策略处理,不能重复叠加帧率。
Slice Header 与解码状态
slice_type 的语义
slice type 的取值可能出现模 5 的别名。例如 0 和 5 都表示 P slice 类别,但后者还携带该图像中没有其他 slice 类型的语义。解析器通常将其归一化为 slice_type % 5 供内部判断,同时保留原始值用于诊断。I、P、B 只描述预测工具集合,不等同于容器层面的关键帧标志。
frame_num、POC 与参考列表
frame_num 用于检测缺失和推导短期参考关系;POC 用于决定输出顺序。不同 pic_order_cnt_type 选择不同字段集合,不能用固定偏移读取 POC。发生 MMCO、IDR 或序列参数变更时,解码器应重置相应参考图像状态,否则旧序列的 frame_num 会污染新序列。
熵编码模式
PPS 的 entropy_coding_mode_flag 决定 slice 使用 CAVLC 还是 CABAC。CABAC 还受到 cabac_init_idc、slice type 和量化参数影响。只做 NAL 边界解析的工具可以不实现熵解码;声称能够验证 slice 内容时,必须根据该标志选择相应的语法解析路径。
访问单元与封装实例
Annex-B 访问单元示例
一个常见的初始化和关键图像序列可以表示为:
1 | 00 00 00 01 67 ... // SPS |
67 和 68 是 NAL header 的第一个字节,不是起始码的一部分;NAL type 分别来自 67 & 0x1f = 7 和 68 & 0x1f = 8。一个 access unit 可以包含多个 slice、AUD、SEI 和参数集。
AVCC sample 的长度前缀
AVCC sample 不使用 Annex-B 起始码。若 lengthSizeMinusOne 为 3,则每个 NAL 前使用四字节大端长度:
1 | 00 00 00 04 65 88 84 21 |
前四字节表示后续 NAL 总长度为 4,65 才是 NAL header。长度为零的 NAL 通常没有解码意义,转换器应报告或跳过。
参数集更新和错误恢复
活动参数集版本
同一个 SPS ID 在码流中可以被新的内容覆盖。解码器应把 SPS/PPS 与其生效位置绑定,而不是永久缓存第一次出现的版本。分辨率、参考帧数或熵编码配置发生变化时,通常需要重新初始化下游解码器和输出缓冲区。
错误恢复边界
检测到 NAL 截断、非法 RBSP 或缺失引用时,最安全的策略是丢弃当前 NAL;若无法确定 access unit 边界,则丢弃直到下一个 AUD、IDR 或明确随机接入点。错误恢复不应伪造 SPS/PPS 或把任意字节补成 slice,因为这样会让错误扩散到多个参考图像。
最低限度的校验包括:起始码或长度前缀不越界、NAL Header 保留位合法、EBSP 去竞争后 RBSP 仍然可以按 bit 语法结束、SPS/PPS 引用存在、slice type 合法、frame_num 和 POC 不违反当前序列约束。发生 FU-A 丢包时,应丢弃当前不完整 NAL 及其所属 access unit,直到收到新的可用随机接入点;把缺失分片填零会制造一个看似完整但解码语义错误的 NAL。
第六章 Profile、Level 与约束标志
Profile 描述的是工具集合
profile_idc 与约束标志共同说明码流使用的编码工具集合。Baseline、Main、High 等名称不是分辨率等级,也不是质量等级。Baseline 常用于不需要 B slice、CABAC 等工具的实现;Main 引入更多预测和熵编码能力;High 系列扩展了色度格式、位深和变换工具。解码器声明支持某个 Profile,意味着它实现该 Profile 允许出现的语法分支,并不意味着它可以解码任意尺寸和任意码率。
Level 描述的是资源上限
level_idc 约束最大宏块处理率、图像宏块数、解码图像缓冲、码率和 CPB 等资源。判断一条码流是否属于某 Level,不能只看宽度和高度;还要考虑帧率、参考帧数量和码率。编码器写入 level_idc 后,应保证所有 SPS/HRD 和实际输出均不超过对应上限。
constraint_set 标志
SPS 中 profile 后的约束位可将某些高 Profile 码流限制为较小工具子集,并影响兼容性判断。解析器应把完整 profile/constraint/level 三元组保存下来,不能只输出一个 profile 名称。保留位必须按规范为零,非零时应作为码流合法性问题报告。
NAL 单元类型参考
VCL 与非 VCL
VCL NAL 携带 slice data,非 VCL NAL 携带参数、补充信息或边界信息。常见类型如下:
nal_unit_type |
名称 | 作用 |
|---|---|---|
| 1 | 非 IDR slice | 普通编码图像的 slice |
| 5 | IDR slice | 即时解码刷新图像的 slice |
| 6 | SEI | 补充增强信息 |
| 7 | SPS | 序列参数集 |
| 8 | PPS | 图像参数集 |
| 9 | AUD | 访问单元分隔符 |
| 10 | end of sequence | 序列结束 |
| 11 | end of stream | 码流结束 |
| 12 | filler data | 填充数据 |
表中只列出工程常见类型,不表示其他类型无效。扩展 Profile 可能使用额外 NAL header 和扩展类型;不支持的解析器应依据 type 安全跳过或拒绝,不能把它们统一当作普通 type 1 slice。
nal_ref_idc
nal_ref_idc 表示该 NAL 是否可能被参考图像解码过程使用。值为零通常意味着不作为参考,非零值具有相对重要性语义,但不应被简单解释为网络 QoS 优先级。某些 NAL 类型对该字段有强制要求,验证器需要结合 type 检查合法组合。
SPS 条件语法详解
高 Profile 扩展字段
当 profile 属于规范定义的高 Profile 集合时,SPS 会出现 chroma_format_idc。值为 3 时还有 separate_colour_plane_flag;随后是亮度和色度位深减 8、变换旁路标志和 scaling matrix 标志。若 scaling list 存在,应按 4×4 或 8×8 列表语法解析 delta scale,不能用固定字节数跳过。
frame_num 语法
log2_max_frame_num_minus4 决定 slice header 中 frame_num 的位数:
1 | MaxFrameNum = 2^(log2_max_frame_num_minus4 + 4) |
该字段应落在规范范围内。读取 slice header 时必须使用当前引用 SPS 的位数;参数集切换后继续使用旧位数,会使后续所有字段错位。
POC 类型零
pic_order_cnt_type == 0 时,SPS 给出 log2_max_pic_order_cnt_lsb_minus4,slice header 再读取固定宽度 pic_order_cnt_lsb,某些 PPS 条件下还会出现 delta bottom。POC LSB 会环绕,解码器要结合前一参考图像推导 MSB,不能把 LSB 直接作为全局显示序号。
POC 类型一
类型一使用 delta_pic_order_always_zero_flag、non-ref offset、top-to-bottom offset 和一个 POC 周期数组。数组长度由 num_ref_frames_in_pic_order_cnt_cycle 决定,每项使用 se(v)。解析器必须限制数组数量并检查累加溢出;类型二则不出现这些字段,使用 frame_num 和参考状态推导顺序。
参考帧与间隙
max_num_ref_frames 影响解码图像缓冲需求,gaps_in_frame_num_value_allowed_flag 决定 frame_num 间隙是否允许按规范补推。它不能被理解为允许任意网络丢帧继续无损解码;网络丢失仍可能破坏参考关系。
尺寸字段和场编码
pic_width_in_mbs_minus1 和 pic_height_in_map_units_minus1 给出编码网格。frame_mbs_only_flag 为零时,SPS 还携带 mb_adaptive_frame_field_flag,slice header 可能出现 field 标志。实现只支持逐帧码流时,应在这里明确拒绝场编码,而不是继续按逐帧高度计算。
裁剪和 VUI
frame_cropping_flag 控制四个 ue(v) 裁剪偏移。crop unit 由色度采样和 frame/field 模式决定。VUI 在其后通过 vui_parameters_present_flag 引入;VUI 不是固定长度扩展,内部的 aspect ratio、overscan、video signal、chroma location、timing、HRD 和 bitstream restriction 各自都有条件字段。
PPS 条件语法详解
参数集引用与熵编码
PPS 开头是 pic_parameter_set_id 和 seq_parameter_set_id,随后是 entropy_coding_mode_flag 与 bottom-field POC 标志。ID 必须在规范范围内,引用的 SPS 必须存在。参数集可以晚于其他无关 NAL 到达,但在解析引用它的 slice 前必须完成绑定。
Slice Group
num_slice_groups_minus1 非零时会引入 FMO 的多种 map type,每种 map type 的后续字段不同,包括 run length、top-left/bottom-right、change direction/rate 或显式 slice group id。许多硬件解码器不支持 FMO,能力协商应明确这一点。解析器即使不解码,也必须按 map type 正确跳过条件字段。
参考索引与加权预测
PPS 给出 P/B slice 的默认参考索引数量、weighted prediction 标志和 weighted bipred 模式。Slice Header 可以通过 override 改变活动参考索引数量;不能把 PPS 默认值当成每个 slice 的固定实际值。
量化、去块与冗余图像
PPS 的 pic_init_qp_minus26、pic_init_qs_minus26 和 chroma_qp_index_offset 与 slice header 的增量共同决定量化参数。deblocking_filter_control_present_flag 决定 slice header 是否携带去块控制。redundant_pic_cnt_present_flag 决定冗余图像计数字段是否出现。PPS 扩展还可能提供 transform 8×8、scaling matrix 和第二个色度偏移。
Slice Header 字段流程
固定起始字段
所有 slice header 首先读取 first_mb_in_slice、slice_type 和 pic_parameter_set_id。随后根据 PPS 找 SPS,才能知道 frame_num 的位宽、色度平面、场编码和 POC 语法。解析器不能在解析开始前根据最近一次 SPS 猜位宽,因为同一访问单元理论上必须按明确引用解析。
IDR 和场字段
若当前 NAL 是 IDR,读取 idr_pic_id;若序列允许场编码,则读取 field_pic_flag,为真时再读取 bottom_field_flag。这些条件会改变 POC 和参考图像标记的后续路径。
参考列表重排
P、SP 和 B 类 slice 可以出现参考图像列表重排。语法以标志开始,循环读取 modification idc 及其参数,直到终止值。解析器必须限制循环项数量;遇到无终止值的截断码流应失败,不能读到 RBSP 外部。
加权预测表
当 PPS 和 slice type 条件满足时,slice header 包含 pred_weight_table。它给出亮度/色度权重分母,并按活动参考索引逐项携带权重和偏移。位深影响偏移的有效范围,验证器应检查 se(v) 映射后的值符合约束。
解码参考图像标记
参考 slice 会出现 dec_ref_pic_marking。IDR 使用 no-output 和 long-term-reference 标志;非 IDR 可使用 adaptive memory control operation 循环。MMCO 参数影响短期/长期参考图像集合,是解码状态而不是普通元数据。丢失包含 MMCO 的 slice 可能让后续多帧参考状态失配。
CABAC、QP 与去块
CABAC 模式和非 I/SI slice 条件满足时读取 cabac_init_idc。slice_qp_delta 总是参与最终 QP 计算;SP/SI 还可能有额外字段。去块控制存在时,disable idc 决定是否继续读取 alpha/beta offset。字段存在性完全由 PPS 和前序值决定。
访问单元边界判定
AUD 存在时
AUD 明确指出新的 access unit,并携带 primary_pic_type。它简化边界扫描,但很多合法码流省略 AUD,所以解析器不能把 AUD 当作必需 NAL。参数集和 SEI 可能出现在 AUD 前后,归属需遵循访问单元排列规则。
没有 AUD 时
没有 AUD 时,需要比较连续 VCL NAL 的 slice header。frame_num、PPS ID、field 标志、reference idc、POC 字段、IDR 状态和 idr_pic_id 等关键属性变化可能表示新图像。只有 first_mb_in_slice == 0 并不足以覆盖所有情况,多 slice 图像和 ASO/FMO 需要更完整的判定。
一个 sample 与一个访问单元
MP4 AVC 轨道通常将一个 sample 对应一个访问单元,但 sample 内可包含多个 NAL。Annex-B 文件没有 sample table,只能按码流语法寻找边界。RTP 中同一 timestamp 通常对应同一采样时刻,但丢包和聚合包仍需先还原 NAL,再判断访问单元是否完整。
SEI 和补充信息
payloadType 与 payloadSize
SEI RBSP 由一个或多个消息组成。payloadType 和 payloadSize 使用连续 0xFF 字节累加再加最后一个非 FF 字节,因此不是固定一字节字段。解析器必须检查累计溢出和剩余 RBSP 长度,跳过未知 payload 时也要准确移动到下一消息。
常见 SEI
常见消息包括 buffering period、picture timing、user data、recovery point 和 mastering/display 相关信息。它们的字段依赖 SPS VUI/HRD;例如 picture timing 的时间字段宽度来自 HRD 参数。只解析 SEI payload 而没有对应 SPS 状态,无法可靠解释所有 bit。
SEI 的解码地位
很多 SEI 不影响基础像素重建,但会影响时间、色彩、恢复和显示。中间设备删除 SEI 前应明确业务允许丢失哪些语义。用户数据 SEI 也不能被当作可信字符串直接输出,长度和编码必须校验。
HRD 与缓冲模型
CPB 参数
HRD 描述 coded picture buffer 的码率、缓冲大小、延迟字段宽度和 CBR 标志。SPS VUI 可以分别携带 NAL HRD 和 VCL HRD。数组长度由 cpb_cnt_minus1 决定,码率和大小由 scale 与 value 共同计算,计算时应使用足够宽的整数。
timing 与容器时间戳
HRD/picture timing 可描述码流级移除和输出时序,容器 DTS/PTS 则描述 sample 时间。封装器需要让两者一致,但播放器不应把两套时间独立累加。没有 HRD 不代表码流没有时间,外部容器或 RTP timestamp 仍可提供调度依据。
H.264 解析器安全设计
BitReader 边界
每个 read 操作都应先确认剩余 bit 数,Exp-Golomb 应限制前导零,循环字段应限制规范最大数量。长度来自 AVCC 时先验证 NAL length 不超过 sample;来自 Annex-B 时把下一个起始码或输入末尾作为硬边界。不能让 RBSP 读取器跨到下一个 NAL。
资源上限
SPS 声明的宽高、参考图像数、bit depth 和 chroma format 决定分配规模。分配前应验证 Level 和实现上限,并检查宽×高×帧数乘法溢出。即使语法值在标准范围内,也可以超过某个嵌入式设备的能力,此时应返回 unsupported,而不是 memory corruption。
状态提交
参数集只有完整解析且 trailing bits 合法后才提交到活动表。解析失败不应覆盖同 ID 的上一有效版本。Slice Header 也应在所有依赖字段验证后才改变参考图像或访问单元状态,避免半截 NAL 污染后续恢复。
十六进制实例分析方法
分层记录偏移
分析一个 SPS 时,应同时记录文件偏移、NAL 内字节偏移、去竞争后的 RBSP bit offset 和语法字段值。插入的 03 会让文件字节偏移与 RBSP bit offset 不再线性对应,只写“第几个字节”不足以复现解析过程。
最小实例报告
一个合格实例报告至少列出:起始码或长度前缀、NAL header 三个字段、原始 EBSP、去竞争后的 RBSP、profile/level、SPS/PPS ID、frame_num/POC 位宽、编码尺寸、裁剪尺寸、VUI 宽高比和 timing。对于 slice,还要列出引用链、slice type、IDR 状态和访问单元归属。
转换后的等价性
Annex-B 与 AVCC 转换后,逐个 NAL 的 header 和 EBSP payload 应完全相同,改变的只有边界表示及参数集放置方式。验证器可以对每个 NAL payload 计算哈希并比较序列;如果哈希变化,说明转换器错误删除了防竞争字节、包含了起始码或截断了长度。
第七章 图像、Slice 与宏块层级
编码图像的组成
一幅编码图像由一个或多个 slice 组成,每个 slice 由 Slice Header 和 Slice Data 组成。slice 是独立熵解码边界之一,但预测仍可引用图像内其他区域或参考图像,具体受 constrained intra prediction、slice boundary 和错误恢复工具限制。一个 NAL 通常承载一个 slice,但一幅图像可以有很多 VCL NAL。
宏块地址
经典 H.264 图像以 16×16 亮度宏块为主要处理单位。色度块尺寸由 chroma_format_idc 决定。first_mb_in_slice 给出 slice 的首宏块地址,随后宏块遍历顺序可能受 FMO map 和 MBAFF 影响,不能永远简单加一。
宏块不是输出像素块
宏块是语法和预测单位。边缘宏块可能超出显示裁剪尺寸,解码后要按 SPS crop 裁掉。变换块还可能为 4×4 或 8×8,色度分辨率也可能低于亮度,因此“一个宏块等于一个固定 384 字节 YUV420 数据块”只适用于解码后特定像素格式,不是压缩码流结构。
Slice Data 入口
CABAC 对齐
当 PPS 选择 CABAC 时,Slice Header 结束后要处理 cabac_alignment_one_bit,直到字节边界。对齐位具有规定值,不是任意 padding。CAVLC 模式则按对应 macroblock layer 语法继续,没有相同 CABAC 初始化过程。
mb_skip_run
CAVLC 编码的非 I slice 可使用 mb_skip_run 表示连续跳过宏块。跳过宏块不会携带完整 macroblock layer,但解码器仍要根据 slice type、参考列表和邻居推导预测与运动。mb_skip_run 采用 ue(v),恶意大值不得使宏块地址越过图像。
CABAC skip flag
CABAC 模式逐宏块编码 mb_skip_flag,上下文取决于邻居状态。skip 宏块仍推进地址并更新必要的解码状态。解析器若只统计显式 mb_type,会漏掉 skip 宏块并错误判断 slice 覆盖范围。
Macroblock Layer
mb_type
宏块类型的码值解释取决于 slice type。相同 ue(v) 数值在 I、P、B slice 中可能代表不同的分区、预测模式或 PCM 宏块。实现应先归一化 slice type,再使用对应表映射,不要建立一张跨所有 slice 的固定 mb_type 表。
I_PCM 宏块
I_PCM 允许在宏块中直接携带未预测、未变换的样本值。进入 PCM sample 前需要对齐,样本位宽由 SPS bit depth 决定,亮度和色度样本数量由色度格式决定。完成后熵解码状态按规范重新初始化。I_PCM 数据不是外部文件的普通 RGB/PCM 字节,仍按 H.264 分量与位深排列。
coded_block_pattern
该字段表示哪些亮度和色度块存在残差。其码值通过宏块类型相关映射得到实际 bit pattern,不是直接按位读取。transform_size_8x8_flag 的存在还受 PPS、宏块分区和 coded block pattern 条件影响。
QP 增量
存在残差或特定条件满足时读取 mb_qp_delta。当前宏块 QP 由上一 QP、delta 和位深相关范围按模运算推导。不能把 delta 直接当绝对 QP。亮度 QP 再通过 PPS 色度偏移和标准映射得到色度 QP。
第八章 帧内预测
Intra 4×4
Intra 4×4 为每个亮度 4×4 块选择预测模式。码流可通过 prev_intra4x4_pred_mode_flag 使用邻居推导的最可能模式,否则读取 rem mode。可用模式受左/上参考样本是否存在影响;slice 边界和 constrained_intra_pred_flag 会使某些邻居不可用。
Intra 8×8
High Profile 可在条件满足时使用 Intra 8×8,预测模式编码与 4×4 类似,但参考样本滤波和块尺寸不同。PPS 必须允许 8×8 transform,宏块类型和 transform flag 共同决定是否出现该路径。
Intra 16×16
Intra 16×16 的亮度预测模式、色度 coded block pattern 和亮度 AC 状态部分编码在 mb_type 中。残差分为亮度 DC 和各 4×4 AC。解码器不能把它当作十六个独立 Intra 4×4 块处理。
Chroma Prediction
色度帧内模式由 intra_chroma_pred_mode 给出,可包含 DC、水平、垂直和平面等模式。可用性依赖相邻色度样本和色度格式。4:4:4 separate colour plane 情况会改变普通色度路径。
帧间预测与宏块分区
P Slice 分区
P 宏块可使用 16×16、16×8、8×16 或 8×8 分区。8×8 分区还可细分为 8×8、8×4、4×8、4×4 子宏块。每个预测分区需要参考索引和运动向量差,实际运动向量通过邻居预测加差值获得。
B Slice 分区
B slice 可以使用 List 0、List 1、双向预测或 direct 模式。宏块/子宏块类型决定每个分区使用哪条列表。Direct 模式的运动信息通过空间或时间方法推导,受 direct_spatial_mv_pred_flag 和参考图像状态影响。
Reference Index
当活动参考数量大于一时,码流携带 ref_idx_l0/ref_idx_l1。字段的熵编码方式依 CAVLC/CABAC 和活动数量而定。索引必须落在 Slice Header 建立的参考列表范围;不能直接当成 DPB 数组下标,因为列表可能已重排。
Motion Vector Difference
mvd_l0/mvd_l1 是相对于预测运动向量的差值,水平和垂直分量分别编码。重建后还要应用 quarter-sample 精度、场/帧缩放和边界扩展。运动向量可以指向图像边界外的扩展参考区域,不应仅因坐标为负就判非法。
参考图像列表
默认列表构造
P slice 的 List 0 通常按短期参考图像顺序再接长期参考图像;B slice 的两条列表按 POC 相对当前图像构造不同顺序。构造依赖当前 DPB 状态,不是按 NAL 到达顺序简单排列。
列表重排
Slice Header 可通过 ref_pic_list_modification 改写默认列表。短期重排用 picNum 差值,长期重排用 long-term pic num。每次修改都要更新预测变量并防止重复/越界。错误列表会导致运动补偿引用完全错误但语法仍可能继续解码。
活动参考数量
PPS 提供默认活动数量,Slice Header 可 override。活动数量限制 ref_idx 的有效范围,不等于 DPB 中全部参考图像数。编码器可以保留更多 DPB 图像但只在当前 slice 暴露一部分。
运动补偿插值
亮度 Quarter-sample
H.264 亮度运动补偿支持四分之一像素精度。半像素通过规定的六抽头滤波器生成,四分之一位置由整数/半像素值组合插值。实现需要按标准规定的舍入和限幅顺序,不能使用任意双线性缩放器替代。
色度插值
色度运动精度与色度采样格式和亮度运动向量换算相关。4:2:0 下色度网格更稀疏,使用规定的双线性权重。场编码还要考虑垂直坐标和参考场关系。
Weighted Prediction
显式或隐式加权预测对参考样本应用权重、偏移和分母。P slice 是否使用由 PPS flag 决定,B slice 由 weighted_bipred_idc 决定。运算顺序、舍入偏置和 bit-depth 限幅属于规范语义。
第九章 变换与反量化
4×4 整数变换
H.264 使用与 DCT 性质相近的整数变换,规范可用加减和移位精确实现,避免传统浮点 IDCT 的实现漂移。残差系数经反扫描、反量化和逆变换得到残差样本,再与预测值相加并限幅。
8×8 变换
High Profile 可启用 8×8 变换。是否使用由 PPS、宏块模式和 transform_size_8x8_flag 决定。8×8 有独立扫描顺序和 scaling list,不能把四个 4×4 系数表直接拼接。
DC 专用变换
Intra 16×16 亮度 DC 和色度 DC 使用额外 Hadamard 类变换,具体尺寸取决于色度格式。应先逆 DC 变换和反量化,再与各块 AC 组合。错误顺序会在平坦区域产生明显方格。
Scaling Matrix
SPS/PPS 可提供 4×4 和 8×8 scaling list,缺失时按推导或默认矩阵处理。PPS scaling list 可覆盖 SPS。解析 delta scale 时存在 useDefaultScalingMatrixFlag 语义,不能把得到零值的列表直接用于除乘。
第十章 CAVLC 熵编码
残差块语法
CAVLC residual block 先编码 coeff_token,得到 TotalCoeff 和 TrailingOnes;然后读取 trailing ones 的符号、其余 level、total_zeros 和 run_before,最后逆序放入扫描位置。选择 coeff_token VLC 表依赖相邻块非零系数估计 nC。
邻居上下文 nC
nC 通常由左/上块的 TotalCoeff 推导,不可用邻居按规范处理。色度 DC 使用专用表。slice/FMO 边界影响邻居可用性;若使用简单 raster 数组而忽略 map,VLC 表选择会错。
Trailing Ones
最多若干个绝对值为 1 的尾随非零系数可紧凑编码,仅写符号 bit。随后 level 解码的 suffixLength 会动态变化,并包含特殊首 level 调整。实现应严格按状态机处理,不能使用通用 Exp-Golomb 代替。
Total Zeros 与 Run Before
若 TotalCoeff 未占满块,读取 total_zeros;再为除最后一个非零系数外的系数读取 run_before,直到零数耗尽。任何累计位置超出 block 最大系数数都表示损坏。
CAVLC 残差块的完整解码模型
从语法元素到变换系数数组
CAVLC 不直接逐个写出一个 4×4 块的十六个有符号系数。它先编码非零系数数量和末尾连续 ±1 的数量,再按反向扫描顺序编码 Level,最后编码非零系数之间分布了多少个零。解析器最终要把这些符号恢复到扫描序数组,再由当前块类型对应的扫描表映射到二维变换系数位置。
1 | coeff_token |
这里的“末尾”指正向扫描中最高频端的非零系数,而码流首先给出这些 trailing ones。Level 解码与零游程恢复都带有逆序关系。若把方向反过来,非零系数数量可能仍正确,但频率位置和重建像素会完全错误。
块容量与条件字段
residual_block_cavlc(coeffLevel, startIdx, endIdx, maxNumCoeff) 的容量不总是 16。普通亮度 4×4 常见上限为 16;排除 DC 的 AC 路径可能只处理 15 个位置;色度 DC 上限随色度格式变化;8×8 变换在 CAVLC 下还要按规定的子块语法处理。容量必须由宏块类型、分量、DC/AC 路径和扫描范围派生,不能硬编码成同一个数。
目标数组先清零。若 TotalCoeff=0,当前块立即结束,不读取 sign、level、total_zeros 或 run_before。若 TotalCoeff>maxNumCoeff、TrailingOnes>TotalCoeff 或 TrailingOnes>3,码流非法。每次 VLC 查找前都要确认剩余 bit 足够,不能让补零读取碰巧命中一个表项。
nC 上下文与 coeff_token
邻块非零数量
普通残差块的 coeff_token 表族由邻接块非零系数预测值 nC 选择。左邻块为 A,上邻块为 B;只有当前 Slice、Slice Group、宏块和分量规则判定邻居可用时,其 TotalCoeff 才能参与:
1 | if A and B are available: |
色度 DC 等特殊块使用专门表,不采用普通左/上预测。FMO、MBAFF、场图像和 Slice 边界会改变邻居定位或可用性;几何上相邻不代表语法上可引用。若邻块尚未成功提交,就不能读取未初始化的 TotalCoeff。
coeff_token 一次解出 (TotalCoeff, TrailingOnes),不是两个独立 VLC。表选择错误会从残差块第一个符号开始失步,后续 level_prefix 的长零串可能把错误扩散到整个 Slice。当前块全部校验成功后再把 TotalCoeff 写入上下文;失败块不能提交部分状态。
Trailing Ones 与 Level
符号位和临时数组
Trailing Ones 是反向扫描最先遇到的连续绝对值为 1 的系数,最多三个。每个仅编码一个 sign flag:0 表示 +1,1 表示 -1。例如 TotalCoeff=5, TrailingOnes=2,sign bits 为 1,0,则反向 level 数组开头是:
1 | -1, +1 |
其余三个非零系数通过 level_prefix/level_suffix 解码。第一个非 trailing level 存在由 TrailingOnes<3 控制的特殊调整,用来保持码字映射一致;不能对所有 level 统一加减。解出的 Level 总数必须恰好为 TotalCoeff,然后才能进入零游程语法。
Prefix、Suffix 和 suffixLength
Level 使用一元形式的 prefix 和条件宽度 suffix。解析器逐 bit 寻找 prefix 终止 1,同时限制最大前导零;suffix 位数由当前 suffixLength 及特殊 prefix 分支决定。组合出 levelCode 后映射为正负 Level,随后按 Level 幅值更新 suffixLength,使大幅值后的编码不必继续使用过长 prefix。该状态只在一个残差块内延续,新块重新初始化。
防御性实现必须处理:输入全零而找不到终止位、左移过大导致整数溢出、escape 分支截断、有符号最小整数取绝对值溢出,以及 suffixLength 超出合法范围。位读取器应返回显式失败,不允许越界后自动返回零。
Total Zeros 与 Run Before
Total Zeros 的边界
当 TotalCoeff<maxNumCoeff 时读取 total_zeros;块已全非零时该字段不出现且零数为零。它表示扫描起点到最高扫描位置非零系数之间的零总数,不包括最高非零之后直到块末尾的高频尾零。
若 TotalCoeff=5、total_zeros=6,最高非零扫描索引为:
1 | TotalCoeff + total_zeros - 1 = 5 + 6 - 1 = 10 |
索引 11~15 是最高非零之后的尾零,不计入 total_zeros。若 TotalCoeff+total_zeros>maxNumCoeff,当前块无法容纳这些系数,应立即失败。
递减的 zerosLeft
令 zerosLeft=total_zeros。按反向非零系数顺序,为除最后一个低频系数外的系数读取 run_before,所用 VLC 表由 zerosLeft 选择:
1 | require 0 <= run_before <= zerosLeft |
zerosLeft 变为零后,后续 run 自动为零,不再消费码流。最低频最后一个非零系数无需单独编码 run,它前面的剩余零数就是最终 zerosLeft。所有 run 之和必须等于 total_zeros。
五个非零系数的重建实例
已解出的符号
本例从 VLC 表查找完成后的语法符号开始,演示容易出错的逆序恢复。设块容量为 16:
1 | TotalCoeff = 5 |
run 总和 1+0+3+2+0=6。最高非零扫描索引为 10,从该位置向低频恢复:
| 反向次序 | Level | 放置位置 | 当前 run | 下一位置 |
|---|---|---|---|---|
| 0 | -1 | 10 | 1 | 8 |
| 1 | +1 | 8 | 0 | 7 |
| 2 | +2 | 7 | 3 | 3 |
| 3 | -2 | 3 | 2 | 0 |
| 4 | +3 | 0 | 0 | 完成 |
移动公式是 next_position = current_position - run_before - 1。最终正向扫描数组:
1 | scan index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
非零数为 5;最高非零索引之前有六个零,位于 1、2、4、5、6、9;最高非零之后有五个尾零;反向最先的两个系数正是 -1 和 +1,与 TrailingOnes 一致。
从扫描序映射到二维块
扫描数组还不是二维自然顺序。解析器按当前块适用的扫描表映射:
1 | for scanIdx in 0 .. maxNumCoeff-1: |
帧扫描与场扫描不同,色度 DC、亮度 DC 和 8×8 路径也有各自顺序。不能用 JPEG Zigzag 表代替 H.264 扫描表,不能把 run_before 当成二维行内零数;MBAFF 下还要依据当前块的场编码状态选择扫描方式。
CAVLC 不变量与验证矩阵
块级一致性
每个成功块应满足:
1 | 0 <= TrailingOnes <= min(3, TotalCoeff) |
若 residual 只覆盖子区间,位置计算还要加 startIdx,并保证不越过 endIdx。系数数组和 TotalCoeff 宜先保存在局部对象,成功后一次提交;VLC 截断、表项非法或任何不变量失败都丢弃局部结果。
正常与错误向量
黄金向量应覆盖 TotalCoeff=0、单个 DC、三个 trailing ones、全非零块、total_zeros=0、较大 zerosLeft、Level escape、正负大 Level、帧/场扫描、色度 DC 和宏块边界 nC。错误向量覆盖 coeff_token 不存在、TrailingOnes 大于 TotalCoeff、TotalCoeff 超容量、prefix 无终止、suffix 截断、total_zeros 过大、run 大于 zerosLeft和最终位置为负数。
邻居上下文不能只测单块。应构造至少 2×2 宏块区域,控制左/上 TotalCoeff,使表选择跨越不同 nC 范围;并在 Slice 边界、FMO group 边界和 MBAFF 场/帧组合处验证邻居可用性。解析器可记录宏块地址、分量、块号、nA/nB/nC、coeff_token bit offset、Levels、zeros/runs 和最终数组,与独立参考解码器的变换系数对比。系数级对比比只观察花屏更容易区分熵解码、预测、反量化和参考图像错误。
CABAC 熵编码
Context Model
CABAC 将二值化后的语法 bin 与上下文概率状态结合,使用算术区间编码。上下文初值由 slice type、cabac_init_idc 和 Slice QP 推导。每个 bin 解码后更新状态和 MPS/LPS,不能跨 slice 延用未经规范初始化的上下文。
Regular、Bypass 与 Terminate
Regular bin 使用上下文概率;bypass bin 用于近似等概率的符号/后缀;terminate bin 指示 slice 结束等状态。三种路径有不同 range/offset 更新规则。读取器必须处理 renormalization 和字节输入边界。
Arithmetic State
CABAC 解码维护 codIRange、codIOffset 和 bitstream reader。初始化时读取规定数量 bit,后续在 range 过小时左移并补 bit。任何补 bit 越过 RBSP 或 terminate 未按期出现都应报告 slice 截断。
Context Index
每个语法元素根据邻居、宏块类型、block position 等推导 context index。CABAC 的困难不仅是算术解码,还在上下文选择。为了验证而跳过未知 bin 数量不可行,因为码长是概率编码结果,没有独立字段边界。
第十一章 解码图像缓冲 DPB
DPB 的职责
DPB 保存已解码但可能尚未输出或仍被参考的图像。图像可同时处于等待输出和参考状态;输出后也可能继续作为参考,反之亦然。容量由 Level、图像大小和 SPS bitstream restriction 等约束。
Short-term 与 Long-term
短期参考通过 frame_num/PicNum 标识并参与滑动窗口;长期参考通过 LongTermFrameIdx/LongTermPicNum 管理。MMCO 可把图像转长期、释放参考或重置状态。IDR 还可指定当前图像是否作为长期参考。
Sliding Window
未启用 adaptive marking 时,当参考数量达到上限,解码器按滑动窗口释放最旧短期参考。释放选择基于 PicNum 语义,不是简单释放内存数组第一个元素。
Output Reordering
DPB 按 POC 选择可输出图像,同时受 buffering 和 no-output 标志影响。低延迟码流可能解码/显示顺序相同,含 B 图像则通常需要重排。容器 PTS 可以调度显示,但解码器仍需按参考与 POC 保证正确输出状态。
Picture Order Count 推导
POC Type 0
从当前 pic_order_cnt_lsb、上一参考图像的 LSB/MSB 和最大 LSB 推导当前 MSB,处理环绕。top field order count 等于 MSB+LSB,bottom field 还可能加 delta。MMCO5 会改变下一图像的 previous POC 状态。
POC Type 1
先由 frame_num offset 和 absFrameNum 推导 POC 周期,再累加 SPS 周期 offsets,并加入 non-ref、top-to-bottom 和 slice delta。该算法包含多个 signed 累加,必须用宽整数并按 IDR/MMCO5 重置。
POC Type 2
类型二主要从 frame_num offset 推导临时 POC,参考与非参考图像的奇偶关系不同。它不读取 type 0/1 的 slice POC 字段。解析器必须依据当前 SPS 精确选择一种路径。
场图像 POC
场编码分别产生 top/bottom field order count。互补场对可以组成一帧输出,但字段到达、参考和显示仍按规范状态处理。只保存一个 frame POC 的简化实现无法正确覆盖所有场码流。
第十二章 去块滤波
边界强度
去块滤波对宏块内部和宏块边界的 4×4/8×8 边缘计算 boundary strength。强度取决于是否为帧内块、是否有非零残差、参考图像和运动向量差异。不是对每条边使用固定模糊强度。
Alpha、Beta 与 tc0
阈值由相邻块 QP、slice alpha/beta offset 和标准表得到。先检查像素差是否满足滤波条件,再按 boundary strength 选择强/弱滤波并限幅修改量。bit depth 会缩放阈值。
Slice 边界控制
Slice Header 的 disable_deblocking_filter_idc 决定关闭滤波或是否跨 slice 边界。丢失 slice 时,错误隐藏输出也需避免读取不存在邻居。去块发生在参考图像进入 DPB 前,错误滤波会影响后续预测。
错误隐藏与参考污染
Slice 丢失
解码器可以用邻近像素、运动复制或上一图像隐藏丢失区域,但这是实现策略,不是恢复原始数据。若损坏图像仍作为参考,隐藏误差会传播。接收端可根据 nal_ref_idc、slice 状态和随机接入策略决定是否丢弃整图。
参数集丢失
缺失 SPS/PPS 时无法正确读取 Slice Header 的字段宽度和条件语法,不应盲猜分辨率或 CAVLC/CABAC。等待有效参数集与随机接入点是安全策略。会话层可以重复发送参数集降低加入延迟。
参考图像丢失
检测 frame_num gap 不等同于知道丢了哪张网络图像,特别是非参考图像可能不改变参考状态。解码器依据规范 gap 处理和错误策略维护 placeholder;网络层序号和容器 sample count可提供额外证据。
第十三章 RTP H.264 Payload 格式
Single NAL Unit Packet
当一个完整 NAL 不超过允许 RTP payload 时,可直接放入 RTP payload。NAL header 保持原样,不包含 Annex-B 起始码或 AVCC 长度。RTP sequence number 逐包递增,timestamp 对同一访问单元的包通常相同,marker 常标记访问单元的最后一个 RTP 包。
STAP-A
Single-Time Aggregation Packet A 用一个聚合 header 后跟若干“16 位大端 NAL size + NAL payload”。所有聚合 NAL 共享 RTP timestamp。解析器循环到 payload 结束,每个 size 必须非零且不越界。STAP 自身的 type 不是要交给 H.264 decoder 的普通 NAL;接收器需要拆出内部 NAL。
1 | STAP-A indicator |
FU-A
Fragmentation Unit A 使用 FU indicator 与 FU header。FU indicator 保留原 NAL 的 F 和 NRI,并把 type 设为 FU-A;FU header 包含 Start、End、Reserved 和原始 NAL type。首分片重建的 NAL header 为 indicator 的 F/NRI 加 header 的 type,随后拼接每片 fragment payload。
FU-A 状态机
Start 建立上下文,要求此前没有同 timestamp/type 的未完成 NAL;中间片要求 RTP sequence 连续;End 完成并提交。Start 与 End 不应同时为普通多片语义的非法组合(具体按 payload 规范校验),Reserved 位必须为零。丢一片后丢弃整个 FU,不能把后续 End 当成完整 NAL。
STAP/FU 与访问单元
一个 STAP 可以聚合 SPS/PPS/SEI 或小 slice,一个大 slice 可拆成许多 FU。RTP packet 数、NAL 数和 access unit 数没有一一对应关系。接收器先解聚合/重组 NAL,再按 timestamp、marker 和 slice header 判断访问单元完整性。
Packetization Mode
SDP 等信令中的 packetization-mode 限制允许的单 NAL、非交错或交错模式结构。发送端和接收端必须协商一致。看到 FU-A 并不能证明对端支持所有聚合类型;实现应按协商模式验证 payload type。
RTP 固定头与 H.264 Payload 的职责边界
RTP Header 不属于 H.264 NAL
RTP/H.264 映射由 RTP 固定头和 H.264 payload format 共同组成。最小 RTP Header 为 12 字节;存在 CSRC、Header Extension 或 Padding 时会更长。接收器必须先按 RTP Header 的 CC、X、P 等字段计算 payload 起点,随后才解释 H.264 payload 的第一个字节。把固定偏移 12 永远当作 payload 起点,会在扩展头存在时把扩展数据误认为 NAL Header。
RTP 固定头的前十二字节布局是:
1 | 0 1 2 3 |
| 字段 | 位数 | H.264 传输中的作用 |
|---|---|---|
| V | 2 | RTP 版本,当前为 2 |
| P | 1 | 包尾是否带 RTP padding;最后一字节给出 padding 长度 |
| X | 1 | 固定头后是否有扩展头 |
| CC | 4 | 固定头后 CSRC 标识数量,每项四字节 |
| M | 1 | H.264 映射中通常标记一个访问单元最后一个 RTP 包 |
| PT | 7 | 动态 payload type,由 SDP 等信令映射到 H.264 |
| sequence number | 16 | 每发送一个 RTP 包递增,用于乱序、重复和丢包检测 |
| timestamp | 32 | 采样时刻;H.264 视频通常使用 90 kHz 时钟 |
| SSRC | 32 | 标识同步源,变化可能意味着新的接收状态 |
Marker 不是“关键帧标志”,也不是“FU 最后一片”的同义词。一个访问单元的最后一个包应设置 Marker;如果该包刚好是 FU-A 末片,那么 FU Header 的 E 和 RTP M 通常同时为 1,但 E 结束的是一个 NAL,M 结束的是一个访问单元。访问单元最后还有另一个 NAL 时,前一个 FU 的 E 可以为 1 而 M 仍为 0。
sequence number 按 RTP 包递增,不按 NAL 或帧递增。一个 STAP-A 虽含多个 NAL,也只消耗一个序号;一个 FU-A NAL 分成十包则消耗十个序号。timestamp 对同一采样时刻的包保持一致,帧率 25 frame/s 且 90 kHz 时钟时,恒定帧率相邻时间戳通常增加 90000/25=3600,但网络包到达间隔不能替代 timestamp。
一个最小 RTP Header 实例
假设版本 2、无 padding、无 extension、无 CSRC,Marker=1,动态 PT=96,sequence=0x1234,timestamp=0x01020304,SSRC=0x11223344:
1 | 80 E0 12 34 01 02 03 04 11 22 33 44 |
前两字节解析为:
1 | 0x80 = 10 0 0 0000 -> V=2, P=0, X=0, CC=0 |
若后面直接跟 NAL 65 88 84 FC ...,则为 Single NAL Unit Packet。完整包前部是:
1 | 80 E0 12 34 01 02 03 04 11 22 33 44 65 88 84 FC ... |
0x65 才是 H.264 NAL Header。RTP Header、UDP Header、IP Header、以太网 Header 都不能传入 H.264 解码器;Annex-B 解码 API 若需要起始码,应在提取 NAL 后另行添加 00 00 00 01。
Single NAL Unit Packet
发送条件与大小计算
Single NAL 模式把完整 NAL 的 Header 与 EBSP payload 原样放进一个 RTP payload:
1 | RTP payload = nal_unit_header + nal_ebsp_payload |
不包含 Annex-B start code,不包含 AVCC length prefix,也不包含额外的 H.264 payload-format header。可用 payload 上限取决于路径 MTU 和所有下层头:
1 | max_rtp_payload = path_mtu |
以 MTU 1500、IPv4 无选项 20 字节、UDP 8 字节、RTP 最小头 12 字节且无加密为例,理论上限是 1500-20-8-12=1460 字节。若使用 IPv6 基础头,则为 1500-40-8-12=1440。SRTP 认证标签、RTP extension、VPN、TURN ChannelData 或其他隧道都会继续降低上限。工程中常采用更保守的 1200 字节 RTP 包目标以适应互联网路径,但具体值应由传输环境决定。
若 NAL size 小于等于最大 payload,可直接发送;否则使用协商模式允许的 FU。不能依赖 IP 分片来承载任意大 NAL:IP 分片任一片丢失会使整个数据报不可用,中间设备也可能丢弃分片。发送器应在 RTP/H.264 层按 MTU 分片。
接收验证
Single NAL payload 至少一字节。首字节 type 在 1~23 的单 NAL 范围时可作为完整 NAL 提交;type 24 以上应按聚合或分片结构解释,而不是作为普通 NAL 交给解码器。forbidden_zero_bit 必须为 0,payload 总长要受实现上限约束。对于 VCL NAL,只有在参数集已就绪且访问单元边界明确时才能组成解码输入。
STAP-A 聚合包的完整实例
字节结构
STAP-A 的 payload 第一个字节是 STAP-A indicator,其 F 与 NRI 按聚合规则设置,type 固定为 24。之后重复两字节无符号大端 NALU-size 和对应的完整 NAL:
1 | +----------------------+ 1 byte |
使用前述 22 字节 SPS 与 5 字节 PPS,选择 F=0、NRI=3,STAP-A indicator 为:
1 | 0 | 11 | 11000 = 0x78 |
完整 RTP payload 为 32 字节:
1 | 78 |
长度闭合为:
1 | 1 + (2 + 22) + (2 + 5) = 32 |
STAP-A indicator 0x78 只是聚合外壳,不是输出 NAL。拆包后应得到原始 22 字节 SPS 和 5 字节 PPS:
1 | NAL 0 = 67 ... 92 |
若目标解码器接收 Annex-B,可以分别输出:
1 | 00 00 00 01 + SPS |
不能输出 00 00 00 01 78 ...,因为 type 24 是 RTP payload format 的聚合结构,不是应进入普通 H.264 Annex-B 解码流的 NAL 类型。
聚合解析算法
1 | parse_stap_a(payload): |
解析器必须先验证剩余至少两字节再读长度,使用 n <= remaining 防止越界,拒绝零长度项,并限制聚合 NAL 数量。内层 NAL 不带 start code 和 AVCC length。STAP-A 中的所有 NAL 共享外层 RTP timestamp,但仍可能包含一个访问单元的多个非 VCL/VCL NAL;不能因为在一个包中就把它们合并为单个 NAL。
NRI、顺序与 Marker
STAP-A indicator 的 NRI 应反映聚合内容中最高重要性,使网络单元不会把包含高重要性参数集的包当成可随意丢弃。内层 NAL Header 仍保留各自 NRI,拆包时不得用外层 NRI 覆盖。聚合顺序就是原 NAL 顺序,SPS 应在引用它的 PPS 前,PPS 应在引用它的 Slice 前。
如果 STAP-A 是访问单元最后一个 RTP 包,Marker 可为 1;如果后面还有 Slice 的 Single NAL 或 FU-A,Marker 为 0。聚合 SPS/PPS 的包不天然具有 Marker=1,也不天然是独立一帧。
FU-A 分片的逐包实例
Header 位布局
FU-A payload 至少由两个字节 Header 和一个 fragment data 字节组成:
1 | FU indicator: |
F 与 NRI 从原始 NAL Header 复制;FU indicator 的 Type 改为 28。FU Header 的 S 表示首片,E 表示末片,R 必须为零,低五位保存原始 NAL type。原始 NAL Header 不出现在任何 fragment data 中,接收器在首片到达时由两字节 FU Header 重建一次。
三片 IDR 实例
取一个简化 IDR NAL:
1 | 65 88 84 FC 42 22 A0 0C 08 43 F0 10 A3 |
原始 Header 0x65 表示 F=0、NRI=3、type=5。去掉 Header 后有 12 字节 fragmentable payload,平均分成三片,每片四字节。FU indicator:
1 | F=0, NRI=3, Type=28 -> 0 11 11100 -> 0x7C |
三个 FU Header 分别是:
1 | 首片: S=1 E=0 R=0 Type=5 -> 10000101 -> 0x85 |
三个 RTP payload 为:
1 | sequence 0x2000, timestamp 0x55667788, M=0 |
接收器从首片重建 Header:
1 | reconstructed_header = (FU_indicator & 0xE0) | (FU_header & 0x1F) |
随后按序拼接 fragment data:
1 | 65 |
结果与原 NAL 逐字节相同。若输出 Annex-B,再在前面加一个起始码;若交给单 NAL 硬件接口,则提交从 65 开始的 13 字节,不加 RTP FU Header。
Fragment 大小计算
FU-A 每个 RTP payload 比普通 NAL 多两个 FU 字节,但不携带原 NAL Header。因此每片 fragment data 上限是:
1 | max_fragment_data = max_rtp_payload - 2 |
原 NAL 可分片数据量为 nal_size-1。需要的包数:
1 | packet_count = ceil((nal_size - 1) / max_fragment_data) |
例如最大 RTP payload 为 1200,FU Header 占 2 字节,每片最多 1198 字节;一个 5000 字节 NAL 除 Header 后有 4999 字节,需要 ceil(4999/1198)=5 个 FU-A。发送器应避免生成只有 FU Header、没有 fragment data 的空片;最后一片可以短于其他片。
FU-A 重组状态机
状态键与首片
接收状态至少关联 SSRC、RTP timestamp 和当前 FU NAL 身份。在常见非交错模式中,同一 SSRC 的 sequence 顺序提供片序。收到 S=1 时:
- 验证 E=0、R=0、fragment data 非空;
- 验证 FU type=28,原 type 合法且不是另一个分片/聚合外壳;
- 重建原 NAL Header;
- 记录首片 sequence、期望下一 sequence、timestamp、F/NRI/type;
- 初始化有最大长度约束的重组缓冲区。
若同一状态键已有未完成 FU,又收到新的 Start,旧上下文应标记丢失并丢弃,不能把两个 NAL 拼接。若 SSRC 重启、timestamp 跳变或超时,也应结束旧上下文。sequence number 是 16 位循环计数,连续判断应使用模 2^16 运算,使 0xFFFF 后的 0x0000 被视为正常相邻。
中片与末片
中片要求 S=0、E=0;末片要求 S=0、E=1。两者应满足:
- timestamp 与首片一致;
- F、NRI 和原 type 与首片一致;
- sequence 等于期望值;
- fragment data 非空;
- 累积大小不超过配置和 H.264 Level/应用限制。
末片通过后提交完整 NAL,并销毁上下文。RTP Marker 用于访问单元层校验:如果末片是该 AU 最后一个包,M 应为 1;否则可为 0。不要仅因 M=1 就把一个缺少 E 的 FU 强行完成,也不要因 E=1、M=0 就拒绝合法的“同一 AU 后面还有 NAL”。
丢包、乱序与重复
若三片实例缺少 sequence 0x2001,收到 0x2002 时已知存在序号间隙。即使末片有 E=1,也必须丢弃整个 NAL:
1 | 错误拼接结果会是: |
缺失的 22 A0 0C 08 位于熵编码数据中,后续字节无法靠 NAL 长度恢复语法同步。把损坏 NAL提交解码器可能造成花屏和参考污染。接收端应报告该 NAL 丢失,并由访问单元策略决定丢弃整帧或交给具备明确错误隐藏能力的解码器。
网络可能乱序,因此实际实现通常先在有限抖动缓冲中按 sequence 排序,再驱动 FU 状态机,而不是到包即拼。等待窗口受时延预算约束;超时后确认缺包并释放缓存。重复包应按 SSRC+sequence 去重,重复首片不能重置一个已经正常推进的上下文,重复中片也不能追加两次。
只靠 sequence gap 不能断言丢失的一定是当前 FU:间隙中可能包含其他 payload type、FEC/RTX 流或复用策略,具体取决于会话。RTX 应先按重传格式还原原 sequence 和 payload,再进入 H.264 重组;不同 SSRC 的序号空间独立。
交错模式结构与 DON
STAP-B、MTAP 与 FU-B
RFC 6184 还定义带 Decoding Order Number 的结构,用于交错包化:STAP-B、MTAP16、MTAP24 和 FU-B。它们不能在未协商相应 packetization-mode 时随意发送。DON 用于恢复 NAL 解码顺序,RTP sequence 仍只是包发送顺序,timestamp/offset 描述采样时间关系。
| Type | 名称 | 关键附加字段 |
|---|---|---|
| 25 | STAP-B | 聚合头后有 16-bit DON,随后长度+NAL 数组 |
| 26 | MTAP16 | DON base;每个 NAL 有 size、DOND 和 16-bit timestamp offset |
| 27 | MTAP24 | 与 MTAP16 类似,但 timestamp offset 为 24 bit |
| 29 | FU-B | FU indicator、FU header 后带 16-bit DON |
MTAP 中多个 NAL 可以具有不同的采样时刻,不能像 STAP-A 那样全部直接采用 RTP timestamp。接收器以包的 RTP timestamp 加每项 timestamp offset(按 32 位时间戳模运算)得到各 NAL 时间,并结合 DON/DOND 恢复解码顺序。若实现只支持现代常见的非交错模式,应在协商阶段明确 packetization-mode=1 并拒绝 type 25/26/27/29,而不是用 STAP-A/FU-A 解析器误读。
SDP 参数与参数集传递
packetization-mode 与 profile-level-id
动态 RTP payload type 通过 SDP rtpmap/fmtp 关联 H.264,例如:
1 | a=rtpmap:96 H264/90000 |
H264/90000 声明 90 kHz RTP 时钟。profile-level-id=42C01E 的三个字节分别对应本例 profile_idc=0x42、profile compatibility=0xC0、level=0x1E。它应与实际发送 SPS 能力一致,但接收器仍要解析收到的参数集进行验证,不能只信 SDP 文本。
packetization-mode 常见值:
| 值 | 模式 | 典型允许结构 |
|---|---|---|
| 0 | Single NAL mode | 单 NAL,不使用 STAP-A/FU-A |
| 1 | Non-interleaved mode | Single NAL、STAP-A、FU-A |
| 2 | Interleaved mode | 包含带 DON 的交错结构,规则更复杂 |
省略该参数时的默认语义应按规范处理,不能由实现随意假定为 mode 1。发送器选择的每个 payload packet type 都必须落在协商模式允许集合中。
sprop-parameter-sets
sprop-parameter-sets 以逗号分隔 Base64 编码参数集 NAL。编码对象从 NAL Header 开始,例如 SPS 的首字节 0x67 和 PPS 的首字节 0x68 都在 Base64 数据中;不包含 Annex-B 起始码、AVCC 长度或 STAP 长度字段。解码 Base64 后应验证 NAL type、解析 ID 引用并装入参数集缓存。
带外参数集不保证永远足够。会话中是否允许带内更新、参数集是否必须在随机访问点重复,以及新接收者如何取得当前配置,都要结合 SDP offer/answer、payload format 规则和应用协议处理。收到带内 SPS/PPS 时,不能仅因已有 sprop-parameter-sets 就丢弃;应按参数集 ID/版本与协商约束验证。
RTP/H.264 接收验证矩阵
包级不变量
每个 RTP 包先验证版本、Header 实际长度、padding 长度、扩展头长度、payload 非空和 PT 映射。随后根据 H.264 payload type 分派:1~23 为 Single NAL,24 为 STAP-A,28 为 FU-A;其他类型按模式与实现能力处理。禁止把未知聚合类型当 Single NAL。
包级测试至少包括:
| 类别 | 正常向量 | 错误向量 |
|---|---|---|
| RTP Header | CC/X/P 各种合法组合 | 扩展长度越界、padding 为零或超过包长 |
| Single NAL | type 1、5、6、7、8、9 | 空 payload、F=1、type 与模式不符 |
| STAP-A | 一条、多条、SPS+PPS | size=0、长度截断、内层 F=1、尾部一字节垃圾 |
| FU-A | 两片、多片、序号回绕 | S/E 冲突、R=1、空 fragment、type 改变、缺首/中/末片 |
| 时序 | 同 AU 同 timestamp、末包 M=1 | FU 中途 timestamp 改变、过早 Marker |
NAL 与访问单元级不变量
解包后的 NAL 序列应与发送前序列在允许聚合/分片变换下逐 payload 相同。可对每个重建 NAL 计算 SHA-256,并记录来源包序号范围。访问单元层检查参数集引用、VCL 新图像判定、timestamp 分组和 Marker;Marker 缺失时可用 timestamp 变化及 Slice 语法补充判断,但应记录传输格式告警。
对前述三片 FU-A,验证记录可表示为:
1 | SSRC = 0x11223344 |
这种记录比只打印“收到 IDR”更适合联调:一旦哈希不同,可以追溯是哪一片的相对范围、sequence、声明边界或重组 Header 出错。发送端也用同样方式记录原 NAL hash,即可对跨网络转换做端到端一致性比较。
Annex-B 扫描器实现
Start Code 搜索
扫描器寻找至少两个连续零后跟 01。三字节和四字节起始码都要识别,前导额外零属于 zero_byte/trailing_zero 语义而不是 NAL payload。找到第一个起始码前的非零垃圾应报告。
NAL 结束
当前 NAL 到下一个 start code 前结束;归属于边界的连续零不应全部放入 EBSP。文件末尾没有后续起始码时,以 EOF 为 NAL 结束,并根据 trailing zero 规则处理。空 payload 或只有 NAL header 的特定类型需要语法层进一步验证。
流式分块
网络/file read buffer 可能把 00 00 01 拆在两个块之间。扫描状态至少保留尾部连续零数量和当前 NAL 累积范围。不能独立扫描每个 read buffer,否则会漏掉跨边界起始码。
AVCC Sample Parser 实现
固定长度前缀
从 avcC 取得 1 到 4 字节长度宽度后,在每个 sample 内循环:读取大端长度、检查不超过剩余、取出相应 NAL。循环必须恰好消费 sample;尾部不足一个 length field 或剩余垃圾都表示 sample 损坏。
Length 为零
标准/生态对零长度单元的容忍不同。严格模式应报告无效或空 NAL,兼容模式可跳过但保证游标前进。不要将零长度输出为只有起始码的 Annex-B 数据。
Sample 边界
长度前缀只在当前 sample 内有效,不能用一个 NAL length 跨到下一个 MP4 sample。Sample table 已经给出 access unit 外层边界,越界说明容器大小或 payload 其中之一错误。
H.264 验证测试矩阵
参数集
覆盖 Baseline/Main/High、不同 chroma format/bit depth、scaling matrix、POC 三类型、frame/field、VUI/HRD、裁剪和非法引用。每份 SPS/PPS 需验证 RBSP trailing bits、派生尺寸和 Level 资源。
Slice 与熵编码
覆盖 I/P/B、multi-slice、CAVLC、CABAC、skip、I_PCM、8×8 transform、weighted prediction、reference reorder、MMCO、FMO(若支持)和截断残差。验证宏块覆盖、QP、参考列表、POC 与解码输出。
封装与传输
覆盖 Annex-B 三/四字节起始码、跨 buffer 起始码、AVCC 1/2/4 字节 length、多个 NAL/sample、STAP-A、FU-A、丢包、乱序、重复、参数集更新和 marker 错误。转换前后比较逐 NAL payload 哈希和访问单元时间。
Level 约束计算
PicSizeInMbs
编码图像的宏块数量由宏块宽和 frame高度得到。对逐帧图像:
1 | PicWidthInMbs = pic_width_in_mbs_minus1 + 1 |
乘法使用宽整数并检查上限。显示裁剪不会减少 Level 资源计算所用编码宏块数。
宏块处理率
宏块处理率近似为 PicSizeInMbs × frame rate,精确评估还需考虑场/图像时序。Level 表为每级给出 MaxMBPS。只看“1920×1080”不能判 Level,因为 30 fps 与 120 fps 处理率不同。
DPB 容量
MaxDpbMbs 限制 DPB 可容纳的宏块总数。最大参考/缓冲 frame 数近似受 floor(MaxDpbMbs/PicSizeInMbs) 和标准额外上限限制。SPS max_num_ref_frames、VUI max_dec_frame_buffering 与实际 reorder 需求都不得超过可用容量。
码率和 CPB
Level 的 MaxBR/MaxCPB 还受 Profile 缩放因子影响。VUI HRD 中声明的 bit_rate_value/cpb_size_value 和实际码流必须相容。瞬时 NAL 大小、平均码率和 CPB 模型不是一个数值,验证器应区分。
分辨率常用名
720p、1080p 等产品名称不是标准 Level 字段。编码宽高可能因宏块对齐为 1920×1088再裁到1080,也可能有非方形像素。报告以 SPS派生编码/显示尺寸和实际fps计算,不用名称猜等级。
Profile 能力检查
Baseline 家族限制
Baseline工具集不允许某些 B slice、CABAC、8×8 transform等特性,约束标志还可表示更受限子集。验证器不仅读取profile_idc,还扫描 PPS/Slice是否实际使用被禁止工具。
Main
Main支持CABAC、B slice和interlaced等工具,但不包含High的全部8×8/高位深扩展。一个标为Main的码流若PPS启用transform_8x8,应报告profile不一致。
High
High增加8×8 transform、自定义量化矩阵等;更高专业Profile允许4:2:2、4:4:4和更高bit depth。chroma_format_idc和bit_depth字段只在相应profile语法出现,能力检查需结合profile集合。
Constraint Flags
某些constraint_set标志让高profile标识的码流保持与较低工具子集兼容。解码器协商时比较完整profile-level-id而非只比较profile数字。保留constraint位非零属于语法问题。
第十四章 VUI Aspect Ratio
aspect_ratio_idc
VUI可给出预定义样本宽高比索引。特殊Extended_SAR时再读取16位sar_width/sar_height,二者应非零。SAR描述单个编码像素的显示形状,不是图像宽高本身。
DAR
1 | display_aspect_ratio = display_width × sar_width |
display width/height先应用SPS crop。容器pasp或显示矩阵可能另有信息,播放器需定义优先级并报告冲突。
Overscan
overscan_info_present_flag后可说明是否适合overscan显示。现代数字显示可能忽略,但解析器仍按条件读取。漏读会使后续video_signal_type错位。
VUI Video Signal Type
Video Format
video_format是来源类别提示,不等于像素格式。video_full_range_flag说明样本范围解释;colour_description_present_flag控制原色、传递特性和矩阵系数三个8位字段。
Full 与 Limited Range
Range影响解码YUV映射到RGB。8-bit limited常用视频范围而full使用完整码值,但具体换算按矩阵/标准。错误range会造成黑位抬升或压黑、白位错误。它不改变H.264熵解码系数本身。
Colour Primaries
原色标识描述RGB色度坐标体系,transfer描述光电曲线,matrix描述Y’CbCr变换。三者是独立字段,不能把“BT.709”一个字符串无条件填给全部。未知/unspecified值应保留。
Chroma Location
chroma_loc_info_present_flag后给top/bottom field色度样本位置类型。它影响4:2:0上采样相位,场编码可上下场不同。软件缩放器需要把位置元数据传入或采用明确默认。
VUI Timing Info
VUI Timing Info 基础字段
Timing info包含32位 num_units_in_tick、32位 time_scale 和 fixed_frame_rate_flag。二者为零无法构成有效时钟。H.264常用一个frame对应两个tick的关系,但场、pic_struct和编码方式会影响解释,不能统一写 fps=time_scale/num_units_in_tick。
Frame Rate 推导
对常见逐帧固定帧率码流,标称frame rate常为 time_scale/(2×num_units_in_tick)。验证器应注明这是特定条件下推导,并与容器/RTP时间戳比较。VFR码流不能由一个固定值描述每帧PTS。
fixed_frame_rate_flag
该标志与每个时间间隔内图像输出约束相关,不等同于“容器每sample duration完全相同”的万能保证。SEI pic timing和field结构仍可能影响输出单位。
HRD Parameters 逐字段
cpb_cnt_minus1
CPB schedule可有多个entry,数量为字段+1,并受标准上限约束。为每个entry读取bit_rate_value_minus1、cpb_size_value_minus1和cbr_flag。数组分配前检查count。
Scale
bit_rate_scale和cpb_size_scale与value共同形成实际单位。计算使用宽整数移位,先检查shift与乘法溢出。字段名minus1意味着先加一再缩放。
Delay Length
initial_cpb_removal_delay_length_minus1、cpb_removal_delay_length_minus1、dpb_output_delay_length_minus1和time_offset_length决定SEI字段bit宽。它们不是delay值本身。解析pic timing前必须保存当前SPS HRD。
NAL 与 VCL HRD
VUI可分别包含NAL HRD与VCL HRD;任一存在后还有low_delay_hrd_flag,并影响pic_struct_present_flag位置。漏读其中一套会使整个VUI尾部错位。
Bitstream Restriction
Motion Vector Boundary
motion_vectors_over_pic_boundaries_flag描述是否允许运动向量指向图像边界外的扩展区域。即使允许,解码器仍按标准边界扩展访问,不得实际读取未分配内存。
Reorder 与 Buffer
max_num_reorder_frames给出输出重排能力提示,max_dec_frame_buffering约束解码缓冲。二者应与max_num_ref_frames、Level和实际POC顺序相容。低延迟应用常要求reorder为零,但编码B slice可能违反。
MV Length
log2_max_mv_length_horizontal/vertical限制运动向量范围。解析器可用它做能力/资源检查,但实际每个MV仍需按语法和边界扩展安全处理。
Bytes/Bits per Pic Denom
max_bytes_per_pic_denom和max_bits_per_mb_denom属于限制参数,不是直接的最大字节/宏块值。验证需要按规范公式结合Level上限,不能把denom原值当byte数。
第十五章 Field Picture 与 MBAFF
frame_mbs_only_flag
值为1表示序列只含frame macroblock coding;为0时允许field picture和MBAFF,并出现mb_adaptive_frame_field_flag。解码器只支持progressive时可在SPS阶段拒绝后者。
Field Picture
Slice Header的field_pic_flag为1表示当前coded picture为一个field,bottom_field_flag区分上下场。Frame_num/POC/参考列表和宏块高度按field规则解释。两个field可形成互补场对,但也可不同时间/参考状态。
MBAFF
MBAFF以宏块对为单位选择frame或field编码。宏块地址、邻居、运动向量垂直分量、参考索引和去块都受当前pair模式影响。不能只在输出阶段把两行交织,语法层就已不同。
PAFF
Picture-adaptive frame/field在图像层选择整frame或field picture;MBAFF在宏块对层自适应。两者都属于interlaced工具,但状态层级不同。报告应区分。
PicStruct
SEI picture timing可通过pic_struct描述frame、top field、bottom field、重复field等显示结构。它结合VUI时序影响输出持续时间。容器frame/sample数不一定直接等于显示field数。
SPS Scaling List 解析
Delta Scale
Scaling list逐项读取delta_scale,维护lastScale和nextScale,按模256更新。nextScale为零时后续使用lastScale,并可能触发default matrix语义。每项用se(v),不是8-bit原始值。
Scan 顺序
4×4和8×8 scaling list按规定zig-zag/扫描顺序关联变换系数。存储到自然矩阵时需映射。直接行序打印会误解量化权重。
继承规则
某list未present时,从前一list、SPS默认或PPS对应list推导,规则依索引和参数集层级。解码器应在激活PPS时生成最终完整矩阵,避免slice中反复判断。
合法范围
最终scale值必须在规定范围,列表长度固定。Exp-Golomb截断或delta累计异常时整份参数集无效,不提交半张矩阵。
FMO Slice Group Map
Map Type 0
Interleaved map用每组run_length_minus1循环分配宏块。生成PicSizeInMapUnits长度map,并检查计数推进,不得无限循环。
Map Type 1
Dispersed map按宏块坐标和组数使用规定公式分散分配,无额外每组run数组。验证生成group id范围。
Map Type 2
Foreground with leftover通过top_left/bottom_right定义矩形区域,逆序覆盖后剩余归最后组。坐标由map unit地址转换,矩形必须在图像内且top-left不晚于bottom-right。
Dynamic Types
Box-out、raster scan和wipe类型使用slice_group_change_direction_flag、rate和Slice Header中的cycle动态生成map。cycle字段bit宽由图像大小/rate推导,不是Exp-Golomb固定字段。
Explicit Type 6
显式map读取pic_size_in_map_units_minus1+1个group id,每个id位宽为ceil(log2(num_groups))。声明pic size应与SPS图像map一致或按约束验证,巨大值不得分配失控。
ASO 与 Redundant Slice
Arbitrary Slice Order
FMO可使slice按非raster顺序到达/覆盖map unit。Access unit重组不能仅按first_mb排序假设连续范围。解码状态需依据slice group map判断覆盖。
Redundant Picture Count
PPS允许时Slice Header带redundant_pic_cnt,可提供冗余编码用于错误恢复。主/冗余slice可能覆盖同一区域,解码器按规范优先级处理。统计宏块覆盖不能把两者简单相加。
能力限制
很多硬件不支持FMO/ASO/redundant slice。协商与离线验证应明确unsupported feature;不能把解码失败误判为网络分片错误。
SEI Buffering Period
SPS 引用
Buffering Period消息首先引用SPS ID,随后依据该SPS的NAL/VCL HRD读取每个CPB的initial delay与offset。字段宽度来自HRD,不可固定为24/32bit。
新缓冲周期
消息可标记HRD缓冲调度新周期,常与随机接入关联。播放器不实现HRD也可按payload size跳过,但时间分析器需要更新CPB状态。
Buffering Period 的参数集版本
引用SPS必须是消息生效位置的版本。同ID被更新后,旧SEI不能用新HRD字段宽度重解析。解析报告保存SPS snapshot。
SEI Picture Timing
CPB/DPB Delay
若HRD存在,读取cpb_removal_delay和dpb_output_delay,bit宽来自SPS。它们在HRD时钟模型中描述移除/输出关系,不是直接毫秒。
Pic Struct 与 Clock Timestamp
VUI pic_struct_present时读取pic_struct,并按其类型决定clock timestamp项数。每项有ct_type、nuit_field_based、counting_type、full_timestamp、discontinuity、cnt_dropped和n_frames等条件字段。
Full/Partial Timestamp
full_timestamp为真时直接读取seconds/minutes/hours;否则各级由flag逐层出现。time_offset使用HRD规定bit宽并可能为signed。固定偏移读取会错。
SEI Recovery Point
recovery_frame_cnt
Recovery Point用ue(v)表示从当前访问单元到恢复输出点的图像计数,并有exact_match、broken_link和changing_slice_group等标志。它可提供非IDR随机恢复语义。
与 IDR 区别
IDR在编码图像/参考管理层清理旧参考;Recovery Point是SEI信令,可能允许在若干图像后完全恢复。容器关键帧映射应理解差异,不能看到SEI就把当前sample当即时独立帧。
Broken Link
broken_link提示此前参考链不可用等情况,拼接/切流时影响隐藏策略。中间设备删除该SEI可能使接收端错误显示恢复前图像。
SEI User Data
Registered
Registered user data以国家代码及可能扩展代码开始,后续格式由注册机构定义。解析器按payload size限制,不把任意字节当null-terminated字符串。
Unregistered UUID
Unregistered user data通常先16字节UUID,再是用户payload。UUID用于分派私有格式,不保证内容可信。日志限制长度并十六进制/转义输出。
生命周期
用户SEI可逐访问单元出现,不一定持续有效。应用自行定义状态。转封装是否保留应按业务要求,但不得改动NAL边界或RBSP防竞争规则。
SEI Mastering 与显示元数据
Mastering Display
相关SEI可描述显示原色、白点和亮度范围,字段用定点/整数缩放。它描述母版显示环境,不等于每像素最大值。解析时保留原始单位并转换为可读值。
Content Light
内容亮度信息可给最大内容光级和最大帧平均光级。它们是元数据,可能缺失或与实际内容不一致。播放器色调映射可使用但应处理未知/零值。
容器冲突
MP4 colr等容器元数据和SEI都可描述颜色/HDR。Muxer应保持一致;验证器并列显示并指出冲突。不能只改容器brand声称HDR。
Access Unit NAL 排列
Leading Non-VCL
AUD、SPS、PPS、SEI等可位于首VCL前,归属由标准访问单元规则决定。连续参数集更新可能在新图像前出现。Assembler不要一看到SPS就立即提交上一AU而忽略允许的SEI顺序。
VCL 集合
同一primary coded picture的所有slice属于同一访问单元,可能有冗余coded picture等额外VCL。边界比较需使用首VCL与后续slice header关键字段。
Trailing NAL
某些end/filler NAL可在VCL后出现。RTP marker常在最后承载包上,但乱序/丢包时仍需timestamp和语法判断。容器sample边界通常更可靠。
Parameter Set Carry
Annex-B直播常在IDR前重复SPS/PPS;MP4 avc1通常放avcC。转换时可以按目标规范插入/提取,但不要在每个非随机AU无条件重复,避免码率和语义混乱。
Bitstream 拼接
参数兼容
拼接两段H.264前比较profile/level、尺寸、chroma/bit depth、frame_num/POC参数、VUI和PPS工具。即使SPS ID相同内容也可不同,直接字节拼接会让缓存参数覆盖。
ID 重映射
如需同时保留冲突参数集,可解析并重编码SPS/PPS ID及所有slice引用。这不是简单替换某个字节,因为ID是Exp-Golomb且改变RBSP长度,还需重新做防竞争。
Random Access
第二段应从合适IDR/recovery入口开始,并处理DPB/POC/frame_num重置。仅在文件字节边界追加一个SPS/PPS不能消除前段参考状态。
时间线
裸Annex-B没有统一PTS。拼接文件的播放帧率来自VUI或外部约定,VFR信息可能丢失。容器拼接还要重建DTS/PTS而非只拼NAL。
H.264 Conformance 报告
Syntax
列出每个NAL offset/type/size、RBSP trailing状态,参数集字段与引用,Slice Header条件字段和entropy模式。未知扩展NAL按profile能力分类。
Derived
输出coded/cropped尺寸、SAR/DAR、颜色、标称timing、PicSizeInMbs、DPB需求、参考/reorder上限、Level计算和HRD。派生值附公式/源字段。
Picture
按Access Unit输出frame_num、POC、IDR/recovery、slice数、宏块覆盖、reference marking、DTS/PTS(若有)和错误。检测缺slice、重复覆盖与参数更新。
Transport
Annex-B/AVCC/RTP分别输出边界、长度/sequence、timestamp、fragment重组、丢包和marker。Transport错误与H.264 syntax错误分层。
第十六章 BitReader 参考实现
状态
BitReader保存RBSP字节view、总bit数和当前位置。所有API先验证requested <= total_bits-position。不要让reader直接指向含防竞争字节EBSP,否则字段offset与语法不符。
1 | struct BitReader { |
读取固定 Bit
H.264语法按MSB-first。第p个bit位于rbsp[p/8]的7-(p%8)位。读取N bit循环或使用安全聚合,N为0返回0,N超过目标整数宽度拒绝。
1 | read_bits(n): |
u(n) 与 f(n)
两者都按固定N bit读取,f(n)表示值在语法中固定/应满足特定值。Parser读取后检查固定值,例如forbidden_zero_bit必须0。不要因“fixed”而不消费bit。
b(8)
字节序列语法仍从当前bit位置读取,若规范要求byte aligned先验证/对齐。不能直接取底层指针除非bit_pos%8=0且范围足够。
Exp-Golomb 参考实现
ue(v)
扫描前导0直到1,记leadingZeroBits。若超过实现/规范上限或到RBSP末尾无1则失败。然后读取同数量info bits:
1 | codeNum = (1 << leadingZeroBits) - 1 + info |
当leadingZeroBits等于整数宽度时移位溢出,必须提前拒绝。
se(v)
读取codeNum后映射:奇数为正(codeNum+1)/2,偶数为负-(codeNum/2)。CodeNum最大值映射前检查signed范围。不要先在unsigned中做负号。
te(v)
Truncated Exp-Golomb的语法依range。某些range为1时只读一个bit并取反/按定义映射,其他情况用ue(v)。它不是普通ue的无条件别名。
me(v)
Mapped Exp-Golomb先读codeNum,再根据当前语法表(如coded_block_pattern与chroma/slice类型)映射。表索引越界非法。Parser API接收map类型而非返回原codeNum冒充最终值。
EBSP 去竞争实现
删除条件
遍历NAL header后的EBSP,维护连续零计数。当已输出两个零且当前字节为03时,将其视为emulation_prevention_three_byte,并检查后续字节存在且属于允许范围;不输出03,零计数按规范更新。
非法序列
00 00 03位于payload末尾、后续值超允许范围或出现应被防止的00 00 00/01/02而无03,都可作为符合性错误。容错器可继续,但原始offset和warning保留。
Offset 映射
调试器可保存RBSP每字节对应EBSP/file offset。字段报告同时给RBSP bit offset与文件offset。删除03后简单file_offset=rbsp_offset+constant不成立。
重写 EBSP
生成NAL时从RBSP逐字节输出;当连续两个输出零且下一个RBSP字节在需要防止集合时,先插03。Header不参与RBSP去/加竞争。随后再加Annex-B start code或AVCC length。
RBSP Trailing Bits 验证
Stop Bit
语法主体结束后读取一个rbsp_stop_one_bit,必须1;之后读到字节对齐的rbsp_alignment_zero_bit,必须0。若主体刚好在字节最后一位,stop bit仍占下一语法位置。
More RBSP Data
判断more_rbsp_data()不能只看剩余bit>0:若剩余正好是1加若干0,就是trailing bits,没有更多语法。实现可检查从当前位置到末尾是否匹配该pattern。
CABAC Zero Word
CABAC slice后可能有cabac_zero_word等附加语义,不能用SPS/PPS普通trailing检查一概处理。NAL类型/entropy路径决定结束验证。
SPS 完整读取顺序
基础头
Sequence Parameter Set RBSP首先:
1 | profile_idc u(8) |
Reserved zero bits必须0。SPS ID在标准范围并作为后续PPS引用键。
High Profile 条件块
若profile属于扩展集合,读取:
1 | chroma_format_idc ue(v) |
Scaling list数量由chroma_format和profile语法决定,不固定永远8或12。Bit depth加8后检查profile/实现上限。
Frame Number 与 POC
1 | log2_max_frame_num_minus4 ue(v) |
Type只能0..2。Cycle count受标准限制,累加offset用signed宽整数。
Reference 与尺寸
1 | max_num_ref_frames ue(v) |
Direct 8×8 flag对B direct预测有语义。尺寸加一/乘法在读取后安全派生。
Crop 与 VUI
1 | frame_cropping_flag u(1) |
Crop offset乘crop unit后不能超过coded尺寸。VUI解析失败使整SPS无效,不提交不完整基础部分。
SPS 派生色度参数
ChromaArrayType
若separate_colour_plane_flag为1,ChromaArrayType按0处理;否则等于chroma_format_idc。它决定SubWidthC/SubHeightC和宏块色度尺寸。
| ChromaArrayType | 采样 | SubWidthC | SubHeightC |
|---|---|---|---|
| 0 | monochrome/separate plane | 不适用 | 不适用 |
| 1 | 4:2:0 | 2 | 2 |
| 2 | 4:2:2 | 2 | 1 |
| 3 | 4:4:4 | 1 | 1 |
Crop Unit
非色度array时crop unit按亮度与frame/field规则;有色度时水平为SubWidthC,垂直为SubHeightC×(2-frame_mbs_only_flag)。最终:
1 | display_width = coded_width |
Bit Depth
BitDepthY=8+bit_depth_luma_minus8,色度类似。QpBdOffsetY=6×(BitDepthY-8)影响QP范围。不能把高bit-depth系数/像素限幅在8-bit。
PPS 完整读取顺序
PPS 基础字段
1 | pic_parameter_set_id ue(v) |
ID范围、SPS存在性和slice group count先验证。Entropy flag 0=CAVLC、1=CABAC。
Slice Group 条件
若num_slice_groups_minus1>0,读取slice_group_map_type并按0..6分支:run lengths、矩形、direction/rate、显式map等。每个数组长度由group数或pic_size字段决定。Map type越界非法。
Reference 与 Prediction
1 | num_ref_idx_l0_default_active_minus1 ue(v) |
Weighted_bipred_idc保留值检查。QP与色度offset按bit depth/profile范围验证。
PPS Extension
若more_rbsp_data(),读取transform_8x8_mode_flag、pic_scaling_matrix_present_flag及条件scaling lists,再读second_chroma_qp_index_offset。Extension存在性不是由payload字节数简单判断,而是排除trailing pattern。
提交
完成trailing bits后生成最终PPS对象,绑定当时引用SPS版本或以ID在激活时解析的策略。更新同ID前保留旧对象供已排队access unit。
VUI 完整读取顺序
Aspect 与 Overscan
1 | aspect_ratio_info_present_flag |
Aspect idc查标准表,未知保留值不越界索引。
Video Signal 与 Chroma
1 | video_signal_type_present_flag |
Chroma location取值范围验证。
Timing 与 HRD
1 | timing_info_present_flag |
任何flag读取缺失都会导致后续restriction错位。HRD两个对象分别保存。
HRD 完整读取顺序
CPB Table
1 | cpb_cnt_minus1 ue(v) |
Delay Width
1 | initial_cpb_removal_delay_length_minus1 u(5) |
实际前三种delay width为字段+1,time_offset_length按字段值。SEI读取器从活动SPS获得。
Slice Header 完整读取框架
起始与参数集
1 | first_mb_in_slice ue(v) |
先解析PPS ID并解析到SPS,后续所有条件才可确定。Slice type原值0..9,类别取mod5,但原值保留。
Field 与 IDR
1 | if !frame_mbs_only_flag: |
Field标志影响POC与macroblock地址。
POC
Type0读取pic_order_cnt_lsb及条件delta_bottom;type1且非always_zero读取delta_pic_order_cnt[0]及条件[1]。字段存在还受field_pic和PPS bottom flag影响。
Redundant 与 Direct
PPS允许时读取redundant_pic_cnt。B slice读取direct_spatial_mv_pred_flag。随后P/SP/B可出现num_ref_idx_active_override_flag和活动数量。
List Modification
非I/SI slice按List0条件读取修改循环,B slice再读List1。循环每项读取idc,0/1带abs_diff_pic_num_minus1,2带long_term_pic_num,3结束。其他idc非法,循环项上限。
Pred Weight Table
P加权或B显式加权条件满足时调用pred_weight_table:读取luma/chroma denom,并按活动List参考逐项flags、weight、offset。ChromaArrayType=0时无色度项。
Reference Marking
nal_ref_idc非零时读取dec_ref_pic_marking。IDR两个flag;非IDR adaptive flag后循环MMCO,operation 0结束,不同operation带参数。Operation范围与循环上限验证。
CABAC/QP/SP
CABAC且非I/SI时读cabac_init_idc;总是读slice_qp_delta。SP/SI有sp_for_switch_flag条件与slice_qs_delta。
Deblocking
PPS允许时读disable_deblocking_filter_idc。若不等于1,读取slice_alpha_c0_offset_div2和slice_beta_offset_div2。Idc和offset范围验证。
Slice Group Change Cycle
动态FMO map type 3..5时读取固定宽slice_group_change_cycle,位宽由PicSizeInMapUnits与SliceGroupChangeRate推导ceil(log2(…+1))。不是ue(v)。
Pred Weight Table 细节
Denominator
luma_log2_weight_denom和有色度时chroma_log2_weight_denom用ue(v),有规范上限。默认weight为1<<denom,默认offset 0。
List Entries
对每个活动reference读取luma_weight_flag,若真读se weight/offset;色度flag若真为两个色度分量各读weight/offset。List1仅B slice条件。
Bit Depth Offset
Offset有效范围和解码缩放受bit depth。解析值先验证,再运动补偿阶段按标准公式应用/限幅。
Dec Ref Pic Marking 细节
IDR
no_output_of_prior_pics_flag控制此前未输出图像,long_term_reference_flag决定当前IDR是否长期参考。IDR还重置frame_num/POC相关状态。
MMCO 1/2
Operation1标记短期参考unused,参数difference_of_pic_nums_minus1;operation2标记指定长期pic unused。目标不存在时按错误/规范处理。
MMCO 3/4
Operation3把短期图像赋LongTermFrameIdx,可能先释放冲突长期图;operation4设置MaxLongTermFrameIdx。参数均ue(v)且需范围映射。
MMCO 5/6
Operation5清空全部参考并影响当前/后续frame_num POC状态;operation6把当前图标长期。一个循环以0结束,异常重复/冲突按规范处理。
SPS 十六进制解析模板
NAL 边界
假设Annex-B序列:
1 | 00 00 00 01 67 xx xx xx ... |
前4字节start code不属于NAL;67为header:F=0、NRI=3、type=7。后续字节为EBSP,先去03得到RBSP。
字段记录表
分析时填写:
| RBSP bit offset | 语法 | 编码类型 | 原始bit | 值 |
|---|---|---|---|---|
| 0 | profile_idc | u(8) | … | … |
| 8 | constraints/reserved | u(8) | … | … |
| 16 | level_idc | u(8) | … | … |
| 24 | sps_id | ue(v) | … | … |
后续offset因Exp-Golomb和条件分支动态变化,不能预填固定字节位置。
尺寸派生表
记录pic_width_in_mbs_minus1、pic_height_in_map_units_minus1、frame_mbs_only、chroma格式和crop四值,分别计算coded width/height、crop units和display尺寸。再记录SAR得到DAR。
结束验证
VUI完成后当前位置应匹配RBSP trailing pattern。EBSP剩余额外非零字节说明解析分支/参数集长度错误。原NAL后若有Annex-B trailing zeros按字节流层处理,不进RBSP。
PPS 十六进制解析模板
NAL Header
常见68给F=0、NRI=3、type=8。去除header/start code和防竞争后,从RBSP bit0读pps_id与sps_id。
引用
必须先找到对应SPS,才能验证slice group map与scaling lists。PPS本身不重复宽高/bit depth。报告写PPS id → SPS id/version。
Extension
基础字段后调用more_rbsp_data;若只有stop bit则无extension。不能用“PPS大于N字节”判断8×8 transform。解析extension后trailing闭合。
Slice Header 十六进制解析模板
Header 与 RBSP
NAL type1/5 header后去竞争。记录NAL F/NRI/type,载入活动PPS/SPS。从bit0读取first_mb、slice_type、pps_id。
动态位宽
Frame_num宽度来自SPS,POC LSB宽度也来自SPS;colour_plane/field/IDR条件影响offset。模板报告每个条件“present/absent及依据”,便于复查。
不解析 Slice Data 的边界
仅header analyzer完成最后一个条件字段后返回slice_data_bit_offset;CABAC还需alignment。它不能宣称整个NAL合法,因为残差/熵数据未验证。报告能力范围明确。
受控 Baseline Annex-B 测试流
编码参数
测试流使用 H.264 Constrained Baseline、Level 3.0、YUV 4:2:0、32×32 像素、2 frame/s、无 B 帧,GOP 长度为两帧。编码器为每个访问单元写入 AUD,并在 IDR 前重复 SPS/PPS。裸 Annex-B 文件总长 1811 字节。
前 45 字节展示了 AUD、SPS、PPS 和 SEI 的边界:
1 | 0000 00 00 00 01 09 10 |
起始码不属于 NAL 单元。NAL 长度统计从 NAL Header 开始,到下一起始码前结束。
11 个 NAL 的完整目录
| NAL | Start Code 偏移 | Start Code 长度 | NAL 偏移 | NAL 字节数 | Header | nal_ref_idc |
Type |
|---|---|---|---|---|---|---|---|
| 0 | 0x0000 |
4 | 0x0004 |
2 | 0x09 |
0 | AUD |
| 1 | 0x0006 |
4 | 0x000A |
22 | 0x67 |
3 | SPS |
| 2 | 0x0020 |
4 | 0x0024 |
5 | 0x68 |
3 | PPS |
| 3 | 0x0029 |
3 | 0x002C |
620 | 0x06 |
0 | SEI |
| 4 | 0x0298 |
3 | 0x029B |
511 | 0x65 |
3 | IDR Slice |
| 5 | 0x049A |
4 | 0x049E |
2 | 0x09 |
0 | AUD |
| 6 | 0x04A0 |
3 | 0x04A3 |
93 | 0x41 |
2 | Non-IDR Slice |
| 7 | 0x0500 |
4 | 0x0504 |
2 | 0x09 |
0 | AUD |
| 8 | 0x0506 |
4 | 0x050A |
22 | 0x67 |
3 | SPS |
| 9 | 0x0520 |
4 | 0x0524 |
5 | 0x68 |
3 | PPS |
| 10 | 0x0529 |
3 | 0x052C |
487 | 0x65 |
3 | IDR Slice |
最后一个 NAL 从 0x052C 到文件末尾 0x0713,长度 0x1E7=487。扫描器必须同时识别三字节和四字节起始码;若只搜索 00 00 00 01,会漏掉 SEI 和所有 Slice。
NAL Header 实算
以 0x67 为例:
1 | 0x67 = 0b0_11_00111 |
forbidden_zero_bit=0、nal_ref_idc=3、nal_unit_type=7。IDR Header 0x65=0b0_11_00101;Non-IDR Header 0x41=0b0_10_00001。SEI 0x06 和 AUD 0x09 的 NRI 为零。
三个访问单元
AUD 划分
本流每个访问单元都以 AUD 开始,所以可以直接分组:
1 | AU 0: AUD, SPS, PPS, SEI, IDR Slice |
AUD 能简化边界,但不是所有合法码流都必须包含。没有 AUD 时仍要比较相邻 VCL NAL 的 Slice Header 条件确定新图像。
AUD primary_pic_type
第一个和第三个 AUD NAL 为 09 10,第二个为 09 30。NAL Header 后读取三位 primary_pic_type:001b=1 与 011b=3 的具体允许 Slice 类集合由标准表解释。AUD 后的 rbsp_stop_one_bit 和补零必须闭合;不能把第二字节整体当作 picture type 枚举。
IDR 重复参数集
AU 2 在 IDR 前重复与 AU 0 相同的 SPS/PPS,使从该随机访问点开始的 Annex-B 接收端可重新初始化。参数集重复不等于“每帧配置变化”;缓存应比较 ID 与 RBSP 内容。完全相同可复用不可变对象,同时记录新出现位置。
SPS EBSP 与防竞争字节
SPS 原始 NAL
22 字节 SPS 为:
1 | 67 42 C0 1E D9 09 6C 04 40 00 00 03 00 40 00 00 03 01 03 C5 8B 92 |
去掉 NAL Header 67 后,EBSP 中有两处防竞争字节:
1 | ... 40 00 00 03 00 40 00 00 03 01 ... |
移除每个符合条件的 03,RBSP 变为 19 字节:
1 | 42 C0 1E D9 09 6C 04 40 00 00 00 40 00 00 01 03 C5 8B 92 |
若不移除,VUI 的 32 位时钟字段会被插入的 03 改写;若全局删除所有值为 03 的字节,又会破坏合法数据。删除条件必须基于前两个输出字节为零且下一个字节范围符合 EBSP 规则。
Offset Map
诊断器最好保存 RBSP bit 到原 EBSP byte 的映射。比如第二处删除使后续 RBSP 字节的 EBSP 偏移多二。报告语法错误时既给 RBSP bit offset,也给原文件绝对偏移,才能在十六进制编辑器定位。
SPS 逐 bit 解析结果
Profile、Constraint 与 Level
以下 bit offset 包含八位 NAL Header,因此 SPS RBSP 第一字段从 bit 8 开始:
| Bit Offset | 语法 | 原始位 | 值 |
|---|---|---|---|
| 8 | profile_idc |
01000010 |
66,Baseline |
| 16 | constraint_set0_flag |
1 |
1 |
| 17 | constraint_set1_flag |
1 |
1 |
| 18~21 | constraint 2~5 | 0000 |
全零 |
| 22 | reserved_zero_2bits |
00 |
0 |
| 24 | level_idc |
00011110 |
30,Level 3.0 |
Baseline profile 加 constraint_set0/1 得到常见 Constrained Baseline 能力表达。avcC 中的 compatibility 字节也应为 0xC0,与 SPS 第二字节一致。
SPS ID、FrameNum 与 POC
| Bit Offset | 语法 | 编码 | 值 |
|---|---|---|---|
| 32 | seq_parameter_set_id |
1 |
0 |
| 33 | log2_max_frame_num_minus4 |
1 |
0 |
| 34 | pic_order_cnt_type |
011 |
2 |
| 37 | max_num_ref_frames |
00100 |
3 |
| 42 | gaps_in_frame_num_allowed_flag |
0 |
0 |
log2_max_frame_num_minus4=0,所以 Slice Header 的 frame_num 固定读取四位。POC Type 2 不读取 log2_max_pic_order_cnt_lsb_minus4,Slice 中也没有 pic_order_cnt_lsb;若错误按 POC Type 0 读取,后续字段会立即错位。
图像尺寸
| Bit Offset | 语法 | 值 |
|---|---|---|
| 43 | pic_width_in_mbs_minus1 |
1 |
| 46 | pic_height_in_map_units_minus1 |
1 |
| 49 | frame_mbs_only_flag |
1 |
| 50 | direct_8x8_inference_flag |
1 |
| 51 | frame_cropping_flag |
0 |
派生:
1 | PicWidthInMbs = 1 + 1 = 2 |
没有裁剪,所以显示样本矩阵仍为 32×32。Baseline 未显式给 chroma_format_idc,按 profile 默认语义为 4:2:0 范围内的常见格式路径。
VUI Aspect 与时钟
vui_parameters_present_flag=1。Aspect Ratio 信息存在,aspect_ratio_idc=1,即 SAR 1:1。Overscan、Video Signal Type、Chroma Location 都未显式携带。
Timing Info:
1 | num_units_in_tick = 1 |
对逐帧常见解释,名义速率比例 time_scale/(2×num_units_in_tick)=2 frame/s,与编码输入一致。但 fixed_frame_rate_flag=0,不能仅凭这个比例断言每个访问单元时间间隔永远固定;容器时间戳仍是呈现时序的权威来源。
Bitstream Restriction
NAL/VCL HRD 均不存在,pic_struct_present_flag=0。Bitstream Restriction 存在:
| 字段 | 值 |
|---|---|
motion_vectors_over_pic_boundaries_flag |
1 |
max_bytes_per_pic_denom |
0 |
max_bits_per_mb_denom |
0 |
log2_max_mv_length_horizontal |
10 |
log2_max_mv_length_vertical |
10 |
max_num_reorder_frames |
0 |
max_dec_frame_buffering |
3 |
无 B 帧,因此 reorder frames 为零;解码缓冲最多三帧,与 max_num_ref_frames=3 相容。SPS 在 bit 158 写 rbsp_stop_one_bit=1,bit 159 为对齐零,恰好闭合 20 字节(含 NAL Header)的逻辑位结构;EBSP 因两枚防竞争字节达到 22 字节。
PPS 逐 bit 解析结果
原始 NAL 与字段
PPS NAL 为:
1 | 68 CB 83 CB 20 |
没有需要移除的防竞争字节。字段 offset 同样包含八位 NAL Header:
| Bit Offset | 语法 | 原始编码 | 值 |
|---|---|---|---|
| 8 | pic_parameter_set_id |
1 |
0 |
| 9 | seq_parameter_set_id |
1 |
0 |
| 10 | entropy_coding_mode_flag |
0 |
CAVLC |
| 11 | bottom_field_pic_order_in_frame_present_flag |
0 |
0 |
| 12 | num_slice_groups_minus1 |
1 |
0,无 FMO |
| 13 | num_ref_idx_l0_default_active_minus1 |
011 |
2,即默认 3 refs |
| 16 | num_ref_idx_l1_default_active_minus1 |
1 |
0 |
| 17 | weighted_pred_flag |
0 |
0 |
| 18 | weighted_bipred_idc |
00 |
0 |
| 20 | pic_init_qp_minus26 |
00111 |
-3 |
| 25 | pic_init_qs_minus26 |
1 |
0 |
| 26 | chroma_qp_index_offset |
00101 |
-2 |
| 31 | deblocking_filter_control_present_flag |
1 |
1 |
| 32 | constrained_intra_pred_flag |
0 |
0 |
| 33 | redundant_pic_cnt_present_flag |
0 |
0 |
bit 34 是 RBSP Stop Bit,bit 35~39 为对齐零。pic_init_qp_minus26=-3 给出初始亮度 QP 基值 23;每个 Slice 再加 slice_qp_delta。
三个 Slice Header 实例
第一帧 IDR
第一 IDR NAL 从 0x029B 开始,前 16 字节:
1 | 65 88 84 FC 42 22 A0 0C 08 43 F0 10 A3 21 00 00 ... |
Header 65 后的 Slice 字段:
| 语法 | 值 | 解释 |
|---|---|---|
first_mb_in_slice |
0 | Slice 从首宏块开始 |
slice_type |
7 | 7 % 5 = 2,I Slice,且为 all-slices 形式 |
pic_parameter_set_id |
0 | 引用 PPS 0 → SPS 0 |
frame_num |
0 | SPS 指定四位宽 |
idr_pic_id |
0 | 第一 IDR 标识 |
no_output_of_prior_pics_flag |
0 | 不请求丢弃先前待输出图像 |
long_term_reference_flag |
0 | 当前 IDR 不作长期参考 |
slice_qp_delta |
0 | 亮度 Slice QP 基值为 23 |
disable_deblocking_filter_idc |
0 | 启用标准去块过滤 |
| alpha/beta offset div2 | 0 / 0 | 使用默认偏移 |
由于 POC Type 2,不存在 POC LSB 字段;由于是 IDR,参考图像标记走 IDR 两标志分支,不进入 MMCO 循环。
第二帧 Non-IDR P Slice
Non-IDR NAL 从 0x04A3 开始:
1 | 41 9A 39 FE 36 2E 00 A4 C8 33 0E 65 6A 27 F0 E0 ... |
解析结果:
| 语法 | 值 |
|---|---|
first_mb_in_slice |
0 |
slice_type |
5,5 % 5 = 0,P Slice |
pic_parameter_set_id |
0 |
frame_num |
1 |
num_ref_idx_active_override_flag |
1 |
num_ref_idx_l0_active_minus1 |
0,即一个活动 L0 参考 |
ref_pic_list_modification_flag_l0 |
0 |
adaptive_ref_pic_marking_mode_flag |
0 |
slice_qp_delta |
0 |
disable_deblocking_filter_idc |
0 |
PPS 默认 L0 refs 为三,但当前 Slice 用 override 将活动数量改为一。解析器必须用 Slice 值建立参考列表,不能永远采用 PPS 默认值。
第三帧 IDR
第二个 IDR NAL 从 0x052C 开始:
1 | 65 88 82 17 C4 18 45 40 18 10 87 E0 21 46 CC 00 ... |
frame_num 重新为 0,idr_pic_id=1,slice_qp_delta=1,所以亮度 QP 基值为 23+1=24。IDR 使参考与 FrameNum/POC 状态按规范重置;idr_pic_id 与上一 IDR 不同也帮助区分随机访问图像。
Slice Type 规范化
0~9 的等价类别
Slice Type ue(v) 合法值 0~9。0~4 分别代表 P、B、I、SP、SI;5~9 表示“当前图像所有 Slice 都属于对应类别”的同类形式。业务分类应使用:
1 | slice_type_class = slice_type % 5 |
因此本例 7 是 I,不是未知 Type 7;5 是 P,不是“第五类 Slice”。同时仍保留原始值用于一致性验证。
QP 派生
基础公式:
1 | SliceQPY = 26 + pic_init_qp_minus26 + slice_qp_delta |
前两帧为 26-3+0=23,第三帧为 26-3+1=24。完整实现还要应用位深偏移、宏块级 QP delta 与色度 QP 映射,不能把 SliceQPY 当整帧所有块最终 QP。
AVCDecoderConfigurationRecord 完整解析
配置记录与媒体样本的职责边界
ISO Base Media File Format 中的 AVC 轨道通常通过 avc1 或 avc3 sample entry 表达。sample entry 内的 avcC box 保存 AVCDecoderConfigurationRecord;媒体 sample 则保存一个或多个带长度前缀的 NAL 单元。这里的配置记录不是 H.264 NAL,也不带 Annex-B 起始码。它是容器层向解码器提供初始化信息的数据结构,至少说明轨道所用的 Profile、兼容性标志、Level、NAL 长度字段宽度以及一组初始 SPS/PPS。
对 avc1,参数集通常作为 sample entry 配置的一部分在带外提供,媒体 sample 不应被假定一定重复 SPS/PPS;对 avc3,参数集允许出现在媒体 sample 中,从而支持流内更新。实际文件可能同时在 avcC 和 sample 中出现相同参数集,因此解析器要按 sample entry 语义和参数集 ID 管理状态,不能仅凭“看见 SPS/PPS”判断文件错误,也不能在转换时无条件删掉所有流内参数集。
本节使用前述 32×32、三帧测试序列封装得到的 MP4。avcC payload 共 38 字节:
1 | 01 42 C0 1E FF E1 00 16 |
按字节偏移解析如下。偏移以 avcC payload 第一个字节为零,不包含 box 的 size 和 type:
| 偏移 | 字节 | 字段 | 解析值与作用 |
|---|---|---|---|
0x00 |
01 |
configurationVersion |
版本 1;当前 AVC 配置记录使用该固定值 |
0x01 |
42 |
AVCProfileIndication |
0x42=66,Baseline Profile,与 SPS 的 profile_idc 一致 |
0x02 |
C0 |
profile_compatibility |
constraint_set0/1 为 1,其余约束位为 0,与 SPS 第二个 RBSP 字节一致 |
0x03 |
1E |
AVCLevelIndication |
0x1E=30,即 Level 3.0 |
0x04 |
FF |
保留位和 lengthSizeMinusOne |
高六位必须为 1;低两位为 3,所以每个媒体 NAL 使用四字节长度 |
0x05 |
E1 |
保留位和 numOfSequenceParameterSets |
高三位必须为 1;低五位为 1,后面有一条 SPS |
0x06 |
00 16 |
sequenceParameterSetLength |
大端 0x0016=22,只计算 SPS NAL,不含长度字段 |
0x08 |
67 ... 92 |
sequenceParameterSetNALUnit |
22 字节 SPS,首字节 0x67 的 NAL type 为 7 |
0x1E |
01 |
numOfPictureParameterSets |
后面有一条 PPS |
0x1F |
00 05 |
pictureParameterSetLength |
大端 0x0005=5 |
0x21 |
68 CB 83 CB 20 |
pictureParameterSetNALUnit |
5 字节 PPS,首字节 0x68 的 NAL type 为 8 |
长度计算必须闭合。固定前导为 6 字节,一条 SPS 使用 2+22=24 字节,PPS 计数字段使用 1 字节,一条 PPS 使用 2+5=7 字节,总计 6+24+1+7=38。如果解析后的游标不是配置记录末尾,应依据 Profile 判断是否存在高 Profile 扩展字段,而不能把任何尾随字节都当作垃圾。对具有 chroma/bit-depth 扩展的配置记录,还可能出现 chroma_format、bit_depth_luma_minus8、bit_depth_chroma_minus8 和 SPS extension 数组;是否读取这些字段由 Profile 条件决定。
保留位与长度宽度校验
0xFF 不能整体解释成长度 255。它的位布局是:
1 | bit: 7 6 5 4 3 2 | 1 0 |
NAL 长度字段宽度为 lengthSizeMinusOne + 1,理论取值为一、二或四字节;值 2 所表示的三字节长度在 AVC 配置记录语法中保留,不应作为正常输出格式生成。解析器应同时检查高六个保留位是否全 1。兼容模式可以记录告警后继续使用低两位,严格验证模式则应拒绝不符合保留位约束的记录。
同理,0xE1 的高三位是保留位,低五位才是 SPS 数量。把整个字节作为 225 个 SPS 会导致循环越界。每个数组元素都采用两字节大端长度,因此单条配置参数集的容器表示上限是 65535 字节;读取长度后还要确认记录剩余空间至少有这么多字节,并验证首字节确实是预期的 NAL type。
配置头三个能力字节通常从第一条 SPS 复制,但这不免除一致性检查。如果 AVCProfileIndication、compatibility 或 Level 与实际 SPS 冲突,播放器可能按错误能力建立硬件解码会话。验证器应解析所有 SPS,报告配置头与 SPS 的差异,并检查多 SPS 是否落在 sample entry 能表达的兼容集合中。
长度前缀 MP4 Sample 的逐字节解析
Sample 边界是最高层硬边界
MP4 sample table 给出每个 sample 的文件位置和字节大小。AVC 解析器必须先把输入限制在当前 sample 范围内,再在这个范围里读取长度前缀。NAL length 只能描述当前 sample 内的后续字节,不能跨越到下一个 sample;一个 sample 循环结束时,游标应当恰好等于 sample end。这样既能发现长度损坏,也能防止攻击者伪造一个巨大 NAL 长度读取邻接 box 或下一帧数据。
本例三个 sample 的范围是:
| Sample | 文件起点 | 大小 | 文件终点 | 图像意义 |
|---|---|---|---|---|
| 0 | 0x0363 |
1804 | 0x0A6F |
第一个 IDR 访问单元 |
| 1 | 0x0A6F |
103 | 0x0AD6 |
P 图像访问单元 |
| 2 | 0x0AD6 |
532 | 0x0CEA |
第二个 IDR 访问单元 |
终点按半开区间计算:sample_end = sample_offset + sample_size。例如 0x0363+1804=0x0A6F,所以 0x0A6F 属于 Sample 1,而不属于 Sample 0。所有边界判断都宜采用这种 [begin,end) 表达,避免最后一个字节的包含关系出错。
第一个 IDR Sample
Sample 0 共含六个长度前缀 NAL。下面的相对偏移以 Sample 0 起点为零:
| 相对偏移 | 长度字段 | NAL 长度 | 首字节/类型 | 下一位置 |
|---|---|---|---|---|
0x0000 |
00 00 02 6C |
620 | 06,SEI |
0x0270 |
0x0270 |
00 00 00 02 |
2 | 09,AUD |
0x0276 |
0x0276 |
00 00 00 16 |
22 | 67,SPS |
0x0290 |
0x0290 |
00 00 00 05 |
5 | 68,PPS |
0x0299 |
0x0299 |
00 00 02 6C |
620 | 06,SEI |
0x0509 |
0x0509 |
00 00 01 FF |
511 | 65,IDR Slice |
0x070C |
相对终点 0x070C 等于十进制 1804,证明六个单元恰好消费整个 sample。总长度也可独立求和:NAL payload 为 620+2+22+5+620+511=1780 字节,六个四字节前缀为 24 字节,合计 1804。
该 sample 开头的真实字节为:
1 | 00 00 02 6C 06 ... # 620-byte SEI |
在相对偏移 0x0270、文件偏移 0x05D3 处可看到:
1 | 00 00 00 02 09 10 |
前四字节 00 00 00 02 是长度,不是 Annex-B 起始码;紧接的 09 10 才是两字节 AUD NAL。下一项的 00 00 00 16 表示十进制 22,随后 22 字节从 67 到 92 完整构成 SPS。由于长度字段中经常含多个零,不能用搜索 00 00 01 的办法解析 AVCC sample,否则长度值、payload 内的合法字节和真正边界会被混淆。
第一个 sample 中出现两条 620 字节 SEI,说明“封装后的 sample NAL 序列”不一定与输入 Annex-B 文件的 NAL 目录逐项相同。编码器或复用链路可能写入额外 SEI,参数集也可能因全局头策略被保留或重复。容器转换程序必须以实际 sample 长度目录为准,不能根据预想的 AUD,SPS,PPS,SEI,IDR 模板硬编码跳过顺序。是否保留 SEI 应由明确策略决定;未知 SEI 默认应透明保留。
P 图像 Sample
Sample 1 有两个 NAL:
1 | 00 00 00 02 09 30 |
| 相对偏移 | 长度 | NAL 类型 | 结束位置 |
|---|---|---|---|
0x00 |
2 | AUD,type 9 | 0x06 |
0x06 |
93 | Non-IDR Slice,type 1 | 0x67 |
0x67=103,与 sample size 完全一致。AUD 第二字节的 primary_pic_type 与第一帧不同,但访问单元类型的最终判断仍应以 Slice Header 为准。第二个 NAL 首字节 0x41 表示 nal_ref_idc=2、type 1;它不是 IDR,却可作为参考图像。把“非 IDR”直接等同于“不可参考”是错误的。
第二个 IDR Sample
Sample 2 包含 AUD、SPS、PPS 和 IDR Slice:
1 | 00 00 00 02 09 10 |
| 相对偏移 | 长度 | 类型 | 结束位置 |
|---|---|---|---|
0x000 |
2 | AUD | 0x006 |
0x006 |
22 | SPS | 0x020 |
0x020 |
5 | PPS | 0x029 |
0x029 |
487 | IDR Slice | 0x214 |
0x214=532。末项的长度字段 00 00 01 E7 是十进制 487;其后首字节 65 属于长度范围,不属于前缀。解析器若错误地把 00 00 01 识别为 Annex-B 三字节起始码,就会把长度低字节 E7 当 NAL Header,并立即得到非法 forbidden_zero_bit=1。这类现象是自动格式探测误判 AVCC 的典型症状。
Annex-B 与长度前缀格式的无损转换
从 AVC 配置和 Sample 生成 Annex-B
转换分成“初始化参数集”和“媒体访问单元”两个层面。初始化时解析 avcC,取得长度宽度以及 SPS/PPS 数组;媒体阶段逐 sample 读取所有 NAL。输出裸 Annex-B 时,通常在可独立随机访问的第一个访问单元之前输出配置参数集,并在需要从中间起播的 IDR/恢复点之前保证对应版本参数集可用。是否在每个 IDR 前重复参数集是输出策略,不是简单字节替换的固有要求。
核心算法如下:
1 | parse_avcc(record): |
四字节起始码适合统一输出,也便于参数集和访问单元首 NAL 的边界清晰;标准字节流允许特定位置使用三字节前缀,但转换器没有必要为了节省一个字节而增加状态复杂度。起始码不参与 NAL 哈希,不进入 EBSP,也不能传给只接受单 NAL payload 的硬件 API。
参数集注入不能只取 avcC 中“第一条 SPS 和第一条 PPS”。应先解析当前访问单元 Slice 引用的 pic_parameter_set_id,再由 PPS 找到 seq_parameter_set_id,选择正确版本并按 SPS、PPS、Slice 的依赖顺序输出。如果 sample 本身已经在访问单元前带有字节完全相同的参数集,可以去重;如果同 ID 内容已更新,则必须保留更新并切换缓存版本。
从 Annex-B 生成配置记录和长度前缀 Sample
反向转换首先扫描 Annex-B NAL 边界,然后按访问单元划分 sample。扫描得到的 NAL payload 不包括 leading_zero_8bits、zero_byte、start_code_prefix_one_3bytes 和 trailing_zero_8bits。转换器收集 SPS/PPS 生成 avcC,并对每个应留在媒体 sample 的 NAL 写固定宽度大端长度。
1 | annexb_access_unit_to_sample(nals, length_width): |
若采用四字节长度,前述两字节 AUD 09 10 转换为:
1 | Annex-B: 00 00 00 01 09 10 |
22 字节 SPS 则由:
1 | 00 00 00 01 67 42 C0 1E ... C5 8B 92 |
变为:
1 | 00 00 00 16 67 42 C0 1E ... C5 8B 92 |
变化的只有外部边界表达,67 ... 92 的 22 个 NAL 字节必须逐字节不变。转换阶段绝不能顺便执行 EBSP 去竞争或重新生成 RBSP;否则即使解码语义偶尔相同,NAL payload 哈希、签名、加密边界和未知扩展数据都会改变。
生成 avcC 时,头部 Profile/compatibility/Level 应从选定 SPS 派生;长度宽度按目标生态选择;参数集数组按配置记录语法写入。若目标是 avc1,通常把初始化参数集放入 avcC 并依据封装策略从媒体 sample 移除冗余副本;若目标是 avc3,要保留允许的流内参数集更新。两者不只是 box 名称不同,还决定播放器对参数集出现位置和更新方式的预期。
参数集生命周期与随机访问
带外配置并非永久不变
解码器收到 avcC 后,可以在第一个媒体 sample 之前建立初始参数集缓存。但缓存键应是参数集 ID,值应包含版本,而不是只有一个“当前 SPS”和一个“当前 PPS”。Slice 通过 PPS ID 间接引用 SPS;同一轨道可以声明多组参数集,也可能在合法切换点更新同一 ID 的内容。
对于仅在初始化时提供参数集的 avc1 轨道,修改分辨率、色度格式、位深或其他不能由原 sample entry 覆盖的配置,通常需要新的 sample entry、初始化段或新的轨道配置,而不是悄悄在任意 sample 中替换 SPS。对于允许流内参数集的 avc3,解析器仍要保证更新在使用它的 Slice 之前到达,并处理硬件解码器重新配置。若把 avc3 机械当成 avc1,中途更新可能被忽略;反过来把所有 avc1 sample 中的偶然参数集当成正式配置更新,也可能破坏容器约束。
从 MP4 转成可独立分段的 Annex-B 时,每个分段不能依赖已丢失的上一个初始化上下文。常见策略是在段首随机访问单元前注入其依赖的 SPS/PPS。这里“随机访问”还需要结合容器 sample flags、stss/fragment flags 和 H.264 NAL 语义;仅看 NAL type 5 可以识别 IDR,但不能覆盖恢复点 SEI、开放 GOP 或容器声明的全部随机访问语义。
重复、更新与冲突
参数集处理可分为三种情况:
| 情况 | 判断依据 | 建议行为 |
|---|---|---|
| 完全重复 | ID 相同且规范化前的 NAL/RBSP 内容相同 | 复用解析对象;按目标格式决定是否保留重复字节 |
| 同 ID 更新 | ID 相同但 RBSP 内容不同 | 建立新版本;仅对后续访问单元生效;必要时重配解码器 |
| ID 不同 | 参数集 ID 不同 | 并存于缓存;由 Slice/PPS 引用选择 |
所谓“规范化”只能用于比较和诊断,输出仍应保留原始 NAL。不能因为两个 SPS 派生出的宽高相同就认定它们等价;VUI、参考帧限制、POC、裁剪、色度格式和位深等任一字段都可能影响解码或呈现。
长度字段的错误模型与防御性解析
一、二、四字节长度测试
转换器不能只在四字节模式下正确。应使用相同 NAL 序列分别构造一、二、四字节前缀:
1 | NAL = 09 10, length = 2 |
一字节模式只能表达 255 字节以内的 NAL,因此本例 511、620 字节单元必须拒绝,不能截断为低八位。二字节模式上限 65535 字节,足以容纳本例;四字节模式还要受到实现内存限制和 sample 剩余长度限制,不能因为字段理论上可达 0xFFFFFFFF 就尝试分配同等大小缓冲区。
读取大端长度应使用逐字节累积,并在累积前确认输入足够:
1 | value = 0 |
不要把未对齐地址强制转换成宿主 uint32_t* 后直接解引用;这样既可能违反对齐要求,也会在小端机器上得到反序值。若使用标准整数加载函数,仍需显式执行 big-endian 到 host-endian 转换。
截断、零长度和越界
需要明确区分以下错误:
| 错误 | 示例 | 检测时机 |
|---|---|---|
| 长度字段截断 | sample 只剩 3 字节,但 width 为 4 | 读取长度之前 |
| 零长度 NAL | 00 00 00 00 |
读出长度之后 |
| payload 截断 | 声明 100 字节,sample 只剩 80 | 移动 payload 游标之前 |
| 尾随垃圾 | 最后一个 NAL 后剩余不足 width 的字节 | 循环下一次读取时 |
| 非法 NAL Header | payload 首字节 forbidden_zero_bit=1 |
确认至少一字节 payload 后 |
| 配置宽度非法 | lengthSizeMinusOne 为保留组合 |
解析 avcC 时 |
严格模式下零长度 NAL 应报告格式错误。兼容模式可以跳过,但必须先消费长度字段并保证循环进展,且不能输出一个只有起始码的 Annex-B 空单元。任何错误都应附带 sample index、sample 文件范围、NAL ordinal、长度字段相对偏移、声明长度和剩余长度,便于定位损坏来源。
算术必须防溢出。不要先计算 cursor + nal_size 再与 sample_size 比较,因为无符号加法可能回绕;应使用 nal_size <= sample_size - cursor,前提是已经验证 cursor <= sample_size。sample 的文件偏移和大小来自容器表时也应用 64 位受检算术验证 offset + size 不越过 mdat 或文件末尾。
自动探测的限制
裸数据缺少容器上下文时,有些工具尝试在 Annex-B 与长度前缀间自动探测,但结果只能作为启发式。00 00 00 01 既可能是四字节起始码,也可能是长度为 1;00 00 01 xx 既可能是三字节起始码加 NAL Header,也可能是四字节长度的低三字节。可靠方法是由上层容器/sample entry 提供格式,并从 avcC 读取长度宽度。
如果必须探测,应验证整段闭合而不是只看头四字节:Annex-B 候选必须找到合法起始码序列和 NAL Header;AVCC 候选必须在某一宽度下逐项长度合法并恰好消费 sample;两者都要检查合理的 NAL type 和参数集依赖。当多个解释同时成立时,应返回“不确定”并要求调用方指定,不能随机选择。
转换结果的一致性验证
逐 NAL Payload 哈希
无损转换的核心不变量是:忽略 Annex-B 起始码和 AVCC 长度字段后,按保留策略对应的每个 NAL payload 完全一致。验证器应先分别解析两种外部格式,得到有序记录:
1 | AccessUnitIndex |
若转换策略把 avcC 参数集注入 Annex-B,应在比较时把这些“配置来源 NAL”标记为 injected,而不是误报媒体 sample 多出内容。若策略从 avc1 sample 中提取参数集进入 avcC,则建立 extracted 对应关系。除这些声明过的移动外,AUD、SEI、Slice 和未知 NAL 的顺序、长度与哈希都应相同。
只比较解码后图像不足以证明容器转换正确。解码器可能忽略未知 SEI、容忍尾随数据或对某些损坏进行隐藏,导致画面看似相同但元数据已丢失。逐 NAL 哈希能发现误删防竞争字节、把起始码包含进 payload、长度少算一字节、错误合并相邻 NAL 等问题。
访问单元与时间映射
字节一致之外,还要验证访问单元边界。每个 MP4 AVC sample 通常承载一个访问单元;转成 Annex-B 再解析后,应得到相同数量和顺序的 VCL 访问单元。AUD 可辅助边界判断,但不能作为唯一依据,因为标准不要求每个流都有 AUD。应使用 first_mb_in_slice、frame_num、PPS ID、field flags、IDR 状态、POC 相关字段等新图像判定规则。
MP4 的 DTS、CTS offset、duration 和 sync sample 标志属于容器时间轴,裸 Annex-B 不携带等价的逐 sample 时间表。若转换结果还要重新封装回 MP4,必须在旁路保存这些时间与随机访问元数据;单纯把 NAL 字节写入 .h264 再读回,无法可靠恢复可变帧率、B 帧合成时间和 edit list 语义。
本例的闭合验证
本测试可执行以下独立检查:
1 | avcC payload bytes = 38 |
随后把每个 sample 转成 Annex-B,再重新扫描 NAL,逐项比较 type、size 和 SHA-256。反向写回四字节长度前缀后,三个 sample 的媒体字节应与转换前完全相同;若同时改变了参数集放置策略,则只在声明的 SPS/PPS 项上允许结构差异。最后用 H.264 语法解析器验证 SPS/PPS/Slice 引用,用 MP4 解析器验证 sample size/table 边界,并用独立解码器完成三帧解码。只有“容器闭合、NAL 哈希、语法依赖、访问单元数量和解码”全部通过,才能说明转换链路正确。
SEI NAL 的消息层完整结构
NAL、RBSP 与 SEI Message 三层边界
补充增强信息 SEI 使用 NAL type 6。一个 SEI NAL 的 payload 是 sei_rbsp(),其中可连续放置多个 sei_message(),最后以 rbsp_trailing_bits() 结束。因此以下三种“长度”不能混为一谈:
| 层次 | 是否包含 NAL Header | 是否包含防竞争字节 | 长度来源 |
|---|---|---|---|
| NAL 单元 | 是 | 是,NAL payload 为 EBSP | Annex-B 边界或 AVCC length |
sei_rbsp() |
否 | 否 | 对 NAL payload 去除防竞争字节后得到 |
sei_payload() |
否 | 否 | 每条 message 的 payloadSize |
解析顺序应是:先由外层格式取得完整 NAL;验证首字节的 forbidden_zero_bit=0 和 nal_unit_type=6;仅对 NAL Header 后的 EBSP 去除 emulation_prevention_three_byte;再在 RBSP 中循环读取 message。不能在尚未确定 NAL 边界时根据 payloadSize 跳到下一个 NAL,也不能把 EBSP 中插入的 0x03 计入 SEI payload size。
语法可以概括为:
1 | sei_rbsp(): |
payloadType 和 payloadSize 都不是固定一字节,也不是 Exp-Golomb。每出现一个 0xFF 就累加 255,直到遇到非 0xFF 的终止字节,再加终止字节的数值。例如:
1 | 05 -> 5 |
若 RBSP 在一串 0xFF 后结束,则字段截断;若累加超出实现上限,则必须立即失败。应使用至少 64 位临时整数并设置业务上限,防止大量 0xFF 导致整数回绕或无限循环。
payloadSize 是硬边界
读出 payloadSize 后,应创建一个长度受限的子读取器:
1 | payload_end = cursor + payloadSize |
已知 payload 的字段解析不得跨过 payload_end。如果已知语法提前结束,剩余 payload 字节按该类型的保留/扩展规则处理或记录;如果语法试图越界,当前 SEI message 损坏。未知 payloadType 不应导致整个 NAL 无法继续解析:保留原始 payload 或按长度跳过,然后读取下一条 message。正是独立的长度边界使新版本 SEI 能与旧解析器共存。
SEI message payload 在概念上按字节计数,但某些具体 payload 内部包含位字段。此时子 BitReader 的容量是 payloadSize*8 位。解析完成后,类型专用的 payload bit equal to one、payload bit equal to zero 或保留位处理不能与整个 NAL 末尾的 rbsp_trailing_bits 混淆。NAL 最后的 0x80 通常是 RBSP stop bit;SEI payload 内也可能因对齐出现类似比特形态,必须依靠 payload 边界区分。
User Data Unregistered 的完整字节实例
构造一条可复现的 SEI
user_data_unregistered 的 payloadType 为 5。payload 由 16 字节 uuid_iso_iec_11578 加任意用户数据组成。本例选择 UUID:
1 | 12 34 56 78 9A BC DE F0 10 32 54 76 98 BA DC FE |
用户数据为 ASCII 字符串 TXW:
1 | 54 58 57 |
payload 总长度是 16+3=19=0x13。完整的 SEI RBSP 为:
1 | 05 13 |
逐字段目录如下:
| RBSP 偏移 | 长度 | 内容 | 含义 |
|---|---|---|---|
0x00 |
1 | 05 |
payloadType=5,user data unregistered |
0x01 |
1 | 13 |
payloadSize=19 |
0x02 |
16 | 12 ... FE |
UUID,按网络字节序原样保存 |
0x12 |
3 | 54 58 57 |
用户数据 TXW |
0x15 |
1 | 80 |
rbsp_stop_one_bit 加七个对齐零 |
给 RBSP 加上 NAL Header 0x06,且本例字节序列不触发防竞争插入,得到 NAL:
1 | 06 05 13 12 34 56 78 9A BC DE F0 10 32 54 76 98 BA DC FE 54 58 57 80 |
Annex-B 表达为:
1 | 00 00 00 01 |
使用四字节长度前缀的 AVCC 表达中,NAL 长度为 23 字节,即 0x17:NAL Header 1 字节、Type/Size 2 字节、UUID 16 字节、用户数据 3 字节、RBSP trailing byte 1 字节。
1 | 00 00 00 17 |
这个计算也说明 payloadSize=19 与 NAL length=23 并不矛盾。NAL length 还计算 Header、payloadType、payloadSize 和 trailing bits;payloadSize 只覆盖 UUID 与用户数据。
用户数据不是 C 字符串
SEI user data 没有内建字符串编码和 NUL 终止约定。UUID 后的字节可为 UTF-8、JSON、二进制结构或厂商私有数据,含 0x00 完全合法。解析器必须用 payloadSize-16 得到用户数据长度,不能调用依赖 NUL 终止的字符串函数,也不能在日志中不经转义直接输出任意控制字符。
若 payloadSize<16,连 UUID 都不完整,应报告该 message 损坏。若业务只识别特定 UUID,应以 16 字节精确比较后再解释后续结构;未知 UUID 的 payload 应保留或跳过。相同 UUID 的语义由发送方约定,H.264 标准并不定义其中版本号、端序、校验和或文本编码,所以厂商格式最好自行携带 version、length 与校验字段。
防竞争字节对用户数据的影响
假设用户二进制数据中出现:
1 | 00 00 01 |
RBSP 到 EBSP 转换会输出:
1 | 00 00 03 01 |
payloadSize 仍按 RBSP 中原始三个 payload 字节计数,不因 EBSP 多出的 0x03 增加。NAL 外层的 AVCC length 则必须按最终 EBSP NAL 字节计数,因此会包含这个插入字节。接收端先按 AVCC length 取得 NAL,再去除防竞争字节,最后才按 payloadSize 读取用户数据。若顺序颠倒,payload 结束位置会偏移,下一条 SEI 的 Type 或 trailing bits 将被错读。
多消息 SEI 与扩展长度实例
两条消息共用一个 NAL
一个 SEI NAL 可以连续保存多条 message。例如第一条是前述 type 5 用户数据,第二条使用一个未知类型 200、长度 3、payload 为 AA BB CC:
1 | 05 13 12 34 56 78 9A BC DE F0 10 32 54 76 98 BA DC FE 54 58 57 |
解析第一条后,游标恰好落在 C8;more_rbsp_data() 发现后续不只是 stop bit,因此继续读取第二条。0xC8=200 不是错误;旧实现可根据 payloadSize=3 跳过 AA BB CC。最后只出现一次 0x80,它属于整个 sei_rbsp(),不是第二条 message 的第四个 payload 字节。
more_rbsp_data() 不能简单实现为“还有剩余字节”。剩余最后一字节 0x80 时,已经没有新 message。位级实现应判断后续是否正好是一个 stop one bit 加全部 zero alignment bits;字节对齐的 SEI 循环也可在确认合法尾模式后结束。如果把 0x80 当 payloadType 128,随后会因缺少 payloadSize 报出伪截断错误。
长度 520 的编码
若未知 message 的类型为 300,payload 长度为 520,则字段编码为:
1 | payloadType = 300 = 255 + 45 -> FF 2D |
message 开头为:
1 | FF 2D FF FF 0A [520 payload bytes] |
不能使用普通 base-128 varint、LEB128 或两字节大端整数代替。解析器读到 FF 2D 时得到 300,而不是 0xFF2D=65325;读到 FF FF 0A 时得到 520,而不是 0xFFFF0A。
外层 AVCC NAL length 依然对整个 EBSP NAL 使用固定一、二或四字节大端长度。SEI 的扩展长度编码与 AVCC NAL length 是两套完全独立的机制:前者属于 H.264 RBSP message 语法,后者属于 AVC 容器表达。
Buffering Period 与 Picture Timing 的上下文依赖
字段宽度来自活动 SPS HRD
buffering_period 和 pic_timing 不能脱离 SPS/VUI 独立解析。buffering_period payload 首先给出 seq_parameter_set_id,解析器据此找到 SPS;如果该 SPS 的 VUI 声明 NAL HRD 或 VCL HRD,后续 initial CPB removal delay 字段的数量和位宽来自对应 hrd_parameters():
1 | cpb_count = cpb_cnt_minus1 + 1 |
每个 CPB entry 通常包含 initial_cpb_removal_delay 与 initial_cpb_removal_delay_offset。NAL HRD 和 VCL HRD 都存在时,两组条件字段都要读取,不能只解析第一组。cpb_cnt_minus1、delay width 和 HRD presence 都必须取自被引用的 SPS 版本;若同一 SPS ID 已更新,SEI 所在访问单元的参数集时间关系决定应使用哪个版本。
pic_timing 的 cpb_removal_delay 和 dpb_output_delay 位宽分别来自:
1 | cpb_removal_delay_length_minus1 + 1 |
如果 SPS 没有任何 HRD,这两个字段不存在。若 VUI 的 pic_struct_present_flag=1,后面才有 pic_struct、时钟时间戳标志及条件字段。解析器若无视条件直接按固定 24 或 32 位读取,会把后续字段或下一条 SEI message 吞掉。
HRD 时间不是容器 PTS 的替代文本
CPB/DPB delay 由假想参考解码器时钟模型解释,依赖 num_units_in_tick、time_scale、缓冲周期和可能的时钟时间戳。MP4 DTS/CTS、MPEG-TS PCR/PTS/DTS 或 RTP timestamp 属于外层系统时间轴。它们可以相互映射和交叉验证,但字段数值并非可直接复制。
验证器应分别报告码流 HRD 时间与容器时间,检查访问单元顺序、移除时刻和输出时刻是否一致;不应在缺失 HRD 时根据 MP4 时间凭空生成“原始 SEI”,也不应在有 HRD 时忽略 edit list、timescale 或 composition offset。重新封装但不重新编码时,若时间轴被裁剪或拼接,保留旧 buffering period/picture timing 可能产生不一致,需要明确重写或移除策略。
Recovery Point 的随机访问语义
与 IDR 的区别
recovery_point SEI 用于表示从某个访问单元开始解码,经过 recovery_frame_cnt 指示的恢复过程后可得到正确输出。它不把当前 VCL NAL 自动变成 IDR,也不执行 IDR 的参考图像缓冲清空语义。一个包含 recovery point 的非 IDR 图像仍可能依赖之前参考,只是解码器可通过错误隐藏或丢弃恢复期输出最终收敛。
核心字段包括:
| 字段 | 编码 | 作用 |
|---|---|---|
recovery_frame_cnt |
ue(v) |
按恢复点语义计算恢复输出所需帧计数 |
exact_match_flag |
1 bit | 恢复后的图像是否与从序列开头解码完全匹配 |
broken_link_flag |
1 bit | 前后链路是否断裂,影响恢复前图像处理 |
changing_slice_group_idc |
2 bit | 与恢复期间 slice group 变化有关 |
容器 sync sample 标志和 H.264 recovery point 应一起解释。将所有带 recovery point 的 sample 无条件标成 IDR 会丢失语义;反过来只接受 NAL type 5,又会错过标准允许的渐进恢复入口。播放器寻址策略可区分“立即无依赖随机访问”和“允许预卷的恢复点随机访问”,并向上层返回需要解码但不显示的预卷范围。
SEI 解析器的错误分类与验证
结构错误
SEI 验证至少覆盖:
| 检查项 | 失败含义 |
|---|---|
| NAL type 是否为 6 | 调用层传错 NAL 或 Header 损坏 |
| EBSP 防竞争序列是否合法 | 码流字节转义损坏 |
| payloadType 扩展是否终止 | message header 截断 |
| payloadSize 扩展是否终止 | message header 截断 |
| payloadSize 是否不超过 RBSP 剩余 | payload 截断或恶意长度 |
| 已知 payload 条件字段是否在子边界内 | 上下文或语法不一致 |
rbsp_trailing_bits 是否正确 |
NAL 截断、粘包或边界判断错误 |
未知 payload type 本身不是结构错误。已知类型缺少依赖 SPS 时,应报告“上下文缺失”,并可保存原始 message 等待参数集,而不是用默认位宽猜读。对于解析失败的单条 SEI,是否继续下一条取决于是否仍可信地知道 payload_end;如果 message header 和 size 已正确读取,通常可以跳到其声明末尾继续。如果 payloadSize 本身越界,则整个 NAL 后续边界都不可信,应停止该 NAL。
资源与日志安全
SEI 常承载大块用户数据、字幕、HDR 元数据或编码器信息。实现应限制单 message、单 NAL、单访问单元和缓存总量,采用切片视图或流式回调避免无条件复制。日志默认只打印 type、size、UUID 和有限长度十六进制前缀;用户数据可能包含隐私、密钥、设备标识或不可打印字节,不应完整写入公开日志。
解析器还应限制一个 SEI NAL 中 message 数量。攻击者可构造大量零长度未知 message 消耗 CPU;即使每条 message 都能推进 header 游标,也应受最大计数限制。对 Type/Size 的连续 0xFF 数量设置上限,并在任何错误路径保证游标单调前进或立即退出。
字节级回归测试
前述 UUID 实例适合作为最小黄金向量。测试应验证:
1 | NAL length = 23 |
然后构造系统性变体:删除最后 0x80;把 size 从 0x13 改为 0x14;截断 UUID;在用户数据加入 00 00 01 并验证 EBSP 插入/删除;增加第二条未知 message;用 FF 2D 表达 type 300;让扩展长度停在 0xFF;让 payloadSize 超过输入;在已知 HRD SEI 中替换活动 SPS。每个变体都应产生确定的解析结果和错误位置,不能仅以“解码器有没有崩溃”作为测试标准。
Parameter Set Cache 实现
Immutable Object
每次成功解析生成不可变SPS/PPS对象,包含原RBSP hash、派生字段和生效序号。ID map指向当前版本,已排队AU持有具体对象引用,避免同ID更新影响旧slice。
最大 ID
按标准范围使用有界数组或map。读取ID后先检查再索引。稀疏ID不分配巨大向量。重复完全相同RBSP可去重但仍记录出现位置。
更新通知
如果影响decoder配置的派生字段变化,向下游发reconfigure event:尺寸、chroma、bit depth、DPB、entropy等。只有VUI metadata变化时是否重建由API策略,但对象版本仍更新。
丢失引用
Slice引用不存在PPS或PPS引用不存在SPS时不猜最近对象。缓存可暂存NAL等待参数集到达,但有数量/时间上限,并保持原顺序。
H.264 Parser Fuzz 边界
Exp-Golomb
测试全零RBSP、64个前导零、stop bit缺失、最大合法/超范围ID、signed映射边界。每个失败不产生未定义移位。
Conditions
针对每flag生成present/absent组合,确保reader消费正确:POC三类、field、VUI两个HRD、PPS extension、FMO七类型、weighted/reference marking。
Arrays
测试scaling list、POC cycle、CPB、slice groups、reference modification/MMCO循环count上限和截断。分配前由标准/剩余bit限制。
EBSP
测试合法00000300/01/02/03、非法末尾03、无防竞争start-like序列、多03和file buffer跨界。RBSP与offset map正确。