H.264 句法分析
H.264 句法分析
1. 什么是句法元素
1.1 句法的概念
句法(Syntax) 是 H.264 标准定义的码流组织规则,规定了编码数据如何被序列化为二进制比特流,以及解码器如何从比特流中还原出编码数据。
可以把句法理解为一种”协议”或”格式约定”:
- 编码端:按照句法规则将图像信息(预测模式、运动矢量、残差等)打包成二进制流
- 解码端:按照相同的句法规则从二进制流中解析出图像信息
1.2 为什么需要句法
视频编码涉及大量信息需要传输:
编码一帧图像需要表达的信息:
┌─────────────────────────────────────────────────────┐
│ 1. 这是什么帧?(I/P/B 帧) │
│ 2. 用了什么预测方式?(帧内/帧间) │
│ 3. 参考了哪一帧?(参考帧索引) │
│ 4. 物体移动了多少?(运动矢量) │
│ 5. 预测误差是多少?(残差系数) │
│ 6. 量化参数是多少?(QP) │
│ 7. ... 更多参数 │
└─────────────────────────────────────────────────────┘
问题:这些信息如何组织成一个有序的二进制流?
答案:通过句法定义每个信息的存储位置和编码方式
句法的作用:
| 作用 | 说明 |
|---|---|
| 标准化 | 确保不同编码器产生的码流可被任何解码器解码 |
| 压缩 | 通过高效的数据组织方式减少存储空间 |
| 容错 | 通过分层结构支持错误检测和恢复 |
| 随机访问 | 提供关键点标记,支持 Seek 操作 |
1.3 句法元素的位置
句法元素存在于 H.264 码流的各个层次中:
H.264 文件/传输流
│
▼
┌───────────────────────────────────────────────────────┐
│ NAL 单元序列 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ NAL #1 │ │ NAL #2 │ │ NAL #3 │ │ ... │ │
│ │ (SPS) │ │ (PPS) │ │ (IDR) │ │ │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └─────────┘ │
└───────┼────────────┼────────────┼────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌────────────┐ ┌──────────────────┐
│ NAL Header │ │ NAL Header │ │ NAL Header │
│ (1字节句法) │ │ (1字节) │ │ (1字节) │
├───────────────┤ ├────────────┤ ├──────────────────┤
│ SPS 句法元素 │ │ PPS 句法 │ │ Slice Header │
│ - profile_idc │ │ 元素 │ │ 句法元素 │
│ - level_idc │ │ - pps_id │ │ - slice_type │
│ - width/height│ │ - qp_init │ │ - frame_num │
│ - ... │ │ - ... │ │ - ... │
│ │ │ │ ├──────────────────┤
│ │ │ │ │ 宏块句法元素 │
│ │ │ │ │ - mb_type │
│ │ │ │ │ - mvds │
│ │ │ │ │ - residual │
└───────────────┘ └────────────┘ └──────────────────┘
1.4 句法的组织形式
H.264 句法采用分层结构,每一层负责不同粒度的信息:
flowchart TB
subgraph 码流
A[NAL 单元]
end
A --> B{NAL Type}
B -->|7| C[SPS<br>序列参数]
B -->|8| D[PPS<br>图像参数]
B -->|5| E[IDR Slice<br>关键帧]
B -->|1| F[Non-IDR Slice<br>非关键帧]
C --> G[整个视频序列的参数<br>分辨率、帧率、Profile...]
D --> H[图像级参数<br>QP、熵编码模式...]
E --> I[Slice Header + 宏块数据]
F --> I
I --> J[宏块层<br>预测模式、运动矢量、残差]
句法元素编码方式:
| 编码方式 | 符号 | 说明 |
|---|---|---|
| 固定长度 | u(n) | n 位固定长度,直接读取 |
| 固定长度(1位) | u(1) | 单个标志位,0 或 1 |
| 无符号指数哥伦布 | ue(v) | 变长编码,适合小值概率高的情况 |
| 有符号指数哥伦布 | se(v) | 同上,支持负数 |
| 指数哥伦布映射 | me(v) | 映射到预定义表 |
| 截断指数哥伦布 | te(v) | 有上限的指数哥伦布,节省比特 |
| 映射指数哥伦布 | me(v) | CodedBlockPattern 等使用 |
各编码方式详解:
u(n) - 固定长度编码
直接读取 n 位,按二进制解析为无符号整数
示例: u(8) 读取 1 字节
0x64 → profile_idc = 100
示例: u(1) 读取 1 位标志
1 → entropy_coding_mode_flag = true
ue(v) - 无符号指数哥伦布编码
编码规则:
codeNum = 0: 编码 1 → 1 bit
codeNum = 1: 编码 010 → 3 bits
codeNum = 2: 编码 011 → 3 bits
codeNum = 3: 编码 00100 → 5 bits
codeNum = 4: 编码 00101 → 5 bits
codeNum = 5: 编码 00110 → 5 bits
codeNum = 6: 编码 00111 → 5 bits
codeNum = 7: 编码 0001000 → 7 bits
...
规律: value = 2^leadingZeroBits - 1 + read_bits(leadingZeroBits)
特点:
- 小值用短码,大值用长码
- 适合大多数句法元素值较小的情况
- 编码长度 = 2 × floor(log2(codeNum+1)) + 1
se(v) - 有符号指数哥伦布编码
将 ue(v) 的 codeNum 映射为有符号值:
codeNum → value
0 → 0
1 → 1
2 → -1
3 → 2
4 → -2
5 → 3
6 → -3
...
映射公式:
value = (-1)^(codeNum+1) × ceil(codeNum / 2)
用途: 运动矢量差值 (MVD)、QP 差值等可正可负的值
te(v) - 截断指数哥伦布编码
当值的范围有上限时使用,节省比特
如果上限 max = 1:
直接用 u(1) 编码
如果上限 max > 1:
使用 ue(v),但如果编码后值 > max,则无效
示例: 帧内预测模式选择,最多 8 种
用 te(v) 编码,上限为 7
me(v) - 映射指数哥伦布编码
将 codeNum 映射到预定义的表
示例: CodedBlockPattern (CBP) 映射
不同的宏块类型有不同的 CBP 表
mb_type = I_4x4 时:
codeNum → CBP_luma + CBP_chroma 组合
编码方式选择原则:
| 数据类型 | 推荐编码方式 | 原因 |
|---|---|---|
| 标志位 | u(1) | 只有 0/1 两种值 |
| 固定范围枚举 | u(n) | 值范围固定,直接编码最高效 |
| 小值概率高的整数 | ue(v) | 变长编码,小值用短码 |
| 可正可负的小值 | se(v) | 支持有符号,小值高效 |
| 有上限的值 | te(v) | 节省比特 |
| 复杂映射 | me(v) | 需要查表映射 |
1.5 句法拆解实例
下面以一个实际的 H.264 码流片段为例,演示句法分析过程。
原始十六进制数据(一帧 IDR 图像的开头):
00 00 00 01 67 64 00 1F AC D9 40 50 05 BB 01 10
00 00 00 01 68 EE 3C 80
00 00 00 01 65 88 80 40 00 5F FE FD F8 ...
│ │ │
│ │ └─ NAL Header + RBSP
│ └──── NAL 单元起始码
└──────────── 起始码 (4 字节)
第一步:识别 NAL 单元边界
码流结构:
┌─────────────┬───────────────────┐
│ 起始码 │ NAL 单元内容 │
│ 00 00 00 01│ 67 64 00 1F ... │ ← SPS
├─────────────┼───────────────────┤
│ 00 00 00 01│ 68 EE 3C 80 │ ← PPS
├─────────────┼───────────────────┤
│ 00 00 00 01│ 65 88 80 40 ... │ ← IDR Slice
└─────────────┴───────────────────┘
第二步:解析 NAL Header
SPS 的 NAL Header: 0x67 = 0110 0111
┌─┬────┬─────────┐
│F│NRI │ Type │
│0│ 11 │ 00111 │
└─┴────┴─────────┘
F = 0 → 正常,无错误
NRI = 3 → 重要,不可丢弃
Type = 7 → SPS(序列参数集)
PPS 的 NAL Header: 0x68 = 0110 1000
Type = 8 → PPS(图像参数集)
IDR 的 NAL Header: 0x65 = 0110 0101
Type = 5 → IDR Slice(关键帧)
第三步:解析 SPS 句法元素
SPS 数据: 64 00 1F AC D9 40 50 05 BB 01 10 ...
字节拆解:
64 → profile_idc = 100 (High Profile)
00 → constraint_set flags = 0
1F → level_idc = 31 (Level 3.1)
后续字节使用指数哥伦布编码:
AC D9 40 ... → 需要按 ue(v) 逐位解析
解析结果:
┌──────────────────────────┬─────────┬───────────────────┐
│ 句法元素 │ 值 │ 含义 │
├──────────────────────────┼─────────┼───────────────────┤
│ profile_idc │ 100 │ High Profile │
│ level_idc │ 31 │ Level 3.1 │
│ seq_parameter_set_id │ 0 │ SPS ID = 0 │
│ chroma_format_idc │ 1 │ 4:2:0 色度格式 │
│ log2_max_frame_num_minus4│ 4 │ frame_num 用 8 位 │
│ pic_order_cnt_type │ 0 │ POC 类型 0 │
│ max_num_ref_frames │ 4 │ 最多 4 个参考帧 │
│ pic_width_in_mbs_minus1 │ 119 │ 宽度 = 1920 像素 │
│ pic_height_in_map_units │ 67 │ 高度 = 1080 像素 │
│ frame_mbs_only_flag │ 1 │ 逐行扫描 │
└──────────────────────────┴─────────┴───────────────────┘
计算分辨率:
宽度 = (119 + 1) × 16 = 1920 像素
高度 = (67 + 1) × 16 = 1080 像素
第四步:解析 Slice Header
IDR Slice 数据: 88 80 40 00 5F ...
88 → first_mb_in_slice + slice_type 组合
first_mb_in_slice = 0 (从第一个宏块开始)
slice_type = 7 (I 帧, 当前帧编号)
80 → pic_parameter_set_id = 0 (引用 PPS #0)
40 → frame_num = 0 (IDR 帧, frame_num 重置)
解析结果:
┌──────────────────────────┬─────────┬───────────────────┐
│ 句法元素 │ 值 │ 含义 │
├──────────────────────────┼─────────┼───────────────────┤
│ first_mb_in_slice │ 0 │ 从帧首部开始 │
│ slice_type │ 7 (I) │ I 帧 (帧内编码) │
│ pic_parameter_set_id │ 0 │ 引用 PPS #0 │
│ frame_num │ 0 │ IDR 帧, 编号 0 │
│ idr_pic_id │ 0 │ IDR 图像标识 │
│ slice_qp_delta │ X │ QP 调整值 │
└──────────────────────────┴─────────┴───────────────────┘
第五步:解析宏块数据
Slice Header 之后的字节是宏块数据:
每个宏块包含:
┌────────────────────────────────────────────────┐
│ mb_type (宏块类型) │
│ → 决定后续句法元素的种类 │
├────────────────────────────────────────────────┤
│ 如果是帧内预测: │
│ - Intra4x4PredMode[] 或 Intra16x16PredMode │
│ - CBP (Coded Block Pattern) │
├────────────────────────────────────────────────┤
│ 如果是帧间预测: │
│ - ref_idx (参考帧索引) │
│ - mvd (运动矢量差值) │
│ - CBP │
├────────────────────────────────────────────────┤
│ 残差数据 (如果 CBP 指示有系数): │
│ - residual_block() 系数 │
└────────────────────────────────────────────────┘
I 帧第一个宏块解析示例:
mb_type = 0 (I_NxN) → 使用 4×4 帧内预测
后续需要解析:
- 16 个 4×4 子块的预测模式
- 残差系数
完整解析流程图:
flowchart TB
A[读取码流] --> B[查找起始码 00 00 00 01]
B --> C[读取 NAL Header 1 字节]
C --> D{NAL Type?}
D -->|7 SPS| E[解析 SPS 句法元素<br>获取分辨率/帧率等]
D -->|8 PPS| F[解析 PPS 句法元素<br>获取 QP/熵编码模式等]
D -->|5 IDR| G[解析 IDR Slice]
D -->|1 Non-IDR| H[解析普通 Slice]
E --> I[保存参数供后续使用]
F --> I
G --> J[解析 Slice Header]
H --> J
J --> K[逐个解析宏块]
K --> L[解析 mb_type]
L --> M{帧内/帧间?}
M -->|帧内| N[解析预测模式 + 残差]
M -->|帧间| O[解析运动矢量 + 残差]
N --> P[重建像素]
O --> P
P --> Q{还有宏块?}
Q -->|是| K
Q -->|否| R[完成一帧解码]
通过这个实例可以看到:
- 句法元素是分层组织的:从 NAL → Slice → 宏块 → 块
- 每个层次有特定的句法定义:规定了该层需要传输哪些信息
- 解析是递归的:外层句法元素的值决定内层句法的结构
- 编码方式多样:固定长度、指数哥伦布等,根据数据特性选择
2. 句法层次结构
H.264 采用分层句法结构,从上到下依次为:
┌─────────────────────────────────────┐
│ 序列层 (Sequence) │
│ ┌───────────────────────────────┐ │
│ │ 图像层 (Picture) │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 片层 (Slice) │ │ │
│ │ │ ┌───────────────────┐ │ │ │
│ │ │ │ 宏块层 (MB) │ │ │ │
│ │ │ │ ┌───────────────┐ │ │ │ │
│ │ │ │ │ 子宏块/块层 │ │ │ │ │
│ │ │ │ └───────────────┘ │ │ │ │
│ │ │ └───────────────────┘ │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
2.1 各层作用
| 层次 | 作用 | 主要句法元素 |
|---|---|---|
| 序列层 | 定义整个视频序列的参数 | SPS、PPS |
| 图像层 | 定义单帧图像的编码方式 | Picture Header、Slice Header |
| 片层 | 定义一组宏块的编码方式 | Slice Header、宏块数据 |
| 宏块层 | 定义 16×16 像素块的编码 | 宏块类型、预测模式、残差 |
| 块层 | 定义子块的变换量化数据 | 残差系数、CBP |
3. NAL 单元结构
3.1 NAL 单元格式
H.264 码流由 NAL(Network Abstraction Layer)单元组成:
NAL 单元结构:
┌────────────┬──────────────────────────┐
│ NAL Header │ RBSP Payload │
│ (1 字节) │ (变长) │
└────────────┴──────────────────────────┘
NAL Header 结构 (1 字节):
┌─┬─────┬───────────┐
│0│1│2│3│4│5│6│7│8│
├─┼─────┼───────────┤
│F│NRI │ Type │
└─┴─────┴───────────┘
F (forbidden_zero_bit): 禁止位,必须为 0
NRI (nal_ref_idc): 2 bits,重要性指示
- 00: 可丢弃(无参考价值)
- 01-11: 重要性递增,不可随意丢弃
Type (nal_unit_type): 5 bits,NAL 单元类型
3.2 NAL 类型定义
| Type | 名称 | NRI | 说明 |
|---|---|---|---|
| 1 | 非 IDR 切片 | 2-3 | P/B 帧的编码数据 |
| 2 | 切片数据分区 A | 2 | 数据分区模式(较少使用) |
| 3 | 切片数据分区 B | 2 | 数据分区模式 |
| 4 | 切片数据分区 C | 2 | 数据分区模式 |
| 5 | IDR 切片 | 3 | I 帧(即时解码刷新) |
| 6 | SEI | 0 | 补充增强信息 |
| 7 | SPS | 3 | 序列参数集 |
| 8 | PPS | 3 | 图像参数集 |
| 9 | AUD | 0 | 访问单元分隔符 |
| 10 | 序列结束 | 0 | 表示序列结束 |
| 11 | 流结束 | 0 | 表示流结束 |
| 12 | 填充数据 | 0 | 填充字节 |
3.3 起始码与访问单元
Annex B 格式(裸流格式):
起始码 + NAL 单元序列:
┌──────────┬──────────────┬──────────┬──────────────┐
│ 0x000001 │ NAL Unit │ 0x000001 │ NAL Unit │
│ (起始码) │ │ (起始码) │ │
└──────────┴──────────────┴──────────┴──────────────┘
起始码:
- 0x 00 00 01 (3 字节)
- 0x 00 00 00 01 (4 字节,用于随机访问点)
防竞争 (Emulation Prevention):
- 如果 RBSP 中出现 0x 00 00 00/01/02/03,插入 0x03
- 0x 00 00 03 00 → 解码时移除 0x03 → 0x 00 00 00
AVCC 格式(MP4 容器):
┌────────────┬──────────────┐
│ Length (4B)│ NAL Unit │
│ NAL 长度 │ │
└────────────┴──────────────┘
每个 NAL 前面是 4 字节大端序长度,无起始码
4. SPS(序列参数集)
4.1 SPS 作用
SPS 包含整个视频序列的公共参数,一个序列中所有图像共享同一 SPS。
4.2 主要句法元素
seq_parameter_set_rbsp() {
profile_idc // u(8) Profile 标识
constraint_set0_flag // u(1) 约束标志 0
constraint_set1_flag // u(1) 约束标志 1
constraint_set2_flag // u(1) 约束标志 2
constraint_set3_flag // u(1) 约束标志 3
constraint_set4_flag // u(1) 约束标志 4
constraint_set5_flag // u(1) 约束标志 5
reserved_zero_2bits // u(2) 保留位
level_idc // u(8) Level 标识
seq_parameter_set_id // ue(v) SPS ID
// 高级 Profile 扩展
if (profile_idc == 100 || ...) {
chroma_format_idc // ue(v) 色度格式
bit_depth_luma_minus8 // ue(v) 亮度位深
bit_depth_chroma_minus8 // ue(v) 色度位深
qpprime_y_zero_transform_bypass_flag // u(1)
seq_scaling_matrix_present_flag // u(1)
}
log2_max_frame_num_minus4 // ue(v) frame_num 位数
pic_order_cnt_type // ue(v) POC 类型
if (pic_order_cnt_type == 0)
log2_max_pic_order_cnt_lsb_minus4 // ue(v)
if (pic_order_cnt_type == 1) {
delta_pic_order_always_zero_flag // u(1)
offset_for_non_ref_pic // se(v)
offset_for_top_to_bottom_field // se(v)
num_ref_frames_in_pic_order_cnt_cycle // ue(v)
for (i = 0; i < ...; i++)
offset_for_ref_frame[i] // se(v)
}
max_num_ref_frames // ue(v) 最大参考帧数
gaps_in_frame_num_value_allowed_flag // u(1)
pic_width_in_mbs_minus1 // ue(v) 图像宽度(MB) - 1
pic_height_in_map_units_minus1 // ue(v) 图像高度 - 1
frame_mbs_only_flag // u(1) 是否只有帧宏块
if (!frame_mbs_only_flag)
mb_adaptive_frame_field_palette_flag // u(1)
direct_8x8_inference_flag // u(1)
frame_cropping_flag // u(1) 是否裁剪
if (frame_cropping_flag) {
frame_crop_left_offset // ue(v)
frame_crop_right_offset // ue(v)
frame_crop_top_offset // ue(v)
frame_crop_bottom_offset // ue(v)
}
vui_parameters_present_flag // u(1) VUI 存在标志
if (vui_parameters_present_flag)
vui_parameters() // VUI 参数
}
4.3 关键参数解析
Profile IDC 解读:
| profile_idc | Profile 名称 |
|---|---|
| 66 | Baseline |
| 77 | Main |
| 88 | Extended |
| 100 | High |
| 110 | High 10 |
| 122 | High 4:2:2 |
| 244 | High 4:4:4 |
分辨率计算:
图像宽度 (像素) = (pic_width_in_mbs_minus1 + 1) × 16
图像高度 (像素) = (pic_height_in_map_units_minus1 + 1) × 16
× (2 - frame_mbs_only_flag)
示例:
pic_width_in_mbs_minus1 = 119 → 宽度 = 120 × 16 = 1920
pic_height_in_map_units_minus1 = 67 → 高度 = 68 × 16 = 1080
frame_mbs_only_flag = 1 → 逐行扫描
Frame Num 位数:
frame_num 位数 = log2_max_frame_num_minus4 + 4
示例:
log2_max_frame_num_minus4 = 0 → frame_num 用 4 bits (0-15)
log2_max_frame_num_minus4 = 4 → frame_num 用 8 bits (0-255)
log2_max_frame_num_minus4 = 12 → frame_num 用 16 bits (0-65535)
5. PPS(图像参数集)
5.1 PPS 作用
PPS 包含单帧图像的编码参数,多个图像可共享同一 PPS。
5.2 主要句法元素
pic_parameter_set_rbsp() {
pic_parameter_set_id // ue(v) PPS ID
seq_parameter_set_id // ue(v) 引用的 SPS ID
entropy_coding_mode_flag // u(1) 熵编码模式
// 0=CAVLC, 1=CABAC
bottom_field_pic_order_in_frame_present_flag // u(1)
num_slice_groups_minus1 // ue(v) 片组数 - 1
if (num_slice_groups_minus1 > 0) {
slice_group_map_type // ue(v) 片组映射类型
// 片组相关参数...
}
num_ref_idx_l0_default_active_minus1 // ue(v) List0 参考帧数
num_ref_idx_l1_default_active_minus1 // ue(v) List1 参考帧数
weighted_pred_flag // u(1) 加权预测
weighted_bipred_idc // u(2) B 帧加权预测
pic_init_qp_minus26 // se(v) 初始 QP - 26
pic_init_qs_minus26 // se(v) SP/SI 帧初始 QS
chroma_qp_index_offset // se(v) 色度 QP 偏移
deblocking_filter_control_present_flag // u(1) 去块滤波控制
constrained_intra_pred_flag // u(1) 受限帧内预测
redundant_pic_cnt_present_flag // u(1) 冗余图像计数
}
5.3 关键参数说明
熵编码模式:
entropy_coding_mode_flag:
0 → CAVLC (Baseline Profile 使用)
1 → CABAC (Main/High Profile 使用)
初始 QP:
Slice QP = pic_init_qp_minus26 + 26 + slice_qp_delta
示例:
pic_init_qp_minus26 = 0 → 基准 QP = 26
slice_qp_delta = -4 → 实际 QP = 22
6. Slice Header
6.1 Slice 类型
| slice_type | 名称 | 说明 |
|---|---|---|
| 0, 5 | P Slice | 使用前向预测 |
| 1, 6 | B Slice | 使用双向预测 |
| 2, 7 | I Slice | 只使用帧内预测 |
| 3, 8 | SP Slice | 切换 P 帧 |
| 4, 9 | SI Slice | 切换 I 帧 |
注:0-4 用于后续帧,5-9 用于当前帧
6.2 Slice Header 句法
slice_header() {
first_mb_in_slice // ue(v) 片中第一个宏块地址
slice_type // ue(v) 片类型
pic_parameter_set_id // ue(v) 引用的 PPS ID
frame_num // u(v) 帧号
if (!frame_mbs_only_flag) {
field_pic_flag // u(1) 场图像标志
if (field_pic_flag)
bottom_field_flag // u(1) 底场标志
}
if (nal_unit_type == 5) // IDR 帧
idr_pic_id // ue(v) IDR 图像 ID
if (pic_order_cnt_type == 0) {
pic_order_cnt_lsb // u(v) POC 低位
if (pic_order_present_flag)
delta_pic_order_cnt_bottom // se(v)
}
if (pic_order_cnt_type == 1 && !delta_pic_order_always_zero_flag) {
delta_pic_order_cnt[0] // se(v)
delta_pic_order_cnt[1] // se(v)
}
if (redundant_pic_cnt_present_flag)
redundant_pic_cnt // ue(v)
if (slice_type == B)
direct_spatial_mv_pred_flag // u(1)
if (slice_type == P || slice_type == SP || slice_type == B) {
num_ref_idx_active_override_flag // u(1)
if (num_ref_idx_active_override_flag) {
num_ref_idx_l0_active_minus1 // ue(v)
if (slice_type == B)
num_ref_idx_l1_active_minus1 // ue(v)
}
}
// 参考帧列表重排序
ref_pic_list_modification()
// 加权预测参数
if (weighted_pred_flag || (weighted_bipred_idc == 1 && slice_type == B))
pred_weight_table()
// 参考帧标记
if (nal_ref_idc != 0)
dec_ref_pic_marking()
if (entropy_coding_mode_flag && slice_type != I && slice_type != SI)
cabac_init_idc // ue(v)
slice_qp_delta // se(v) QP 增量
if (deblocking_filter_control_present_flag) {
disable_deblocking_filter_idc // ue(v)
if (disable_deblocking_filter_idc != 1) {
slice_alpha_c0_offset_div2 // se(v)
slice_beta_offset_div2 // se(v)
}
}
}
6.3 关键参数解析
first_mb_in_slice:
表示该片第一个宏块在一帧中的位置(光栅扫描顺序)
first_mb_in_slice = 0 → 从帧的左上角开始
first_mb_in_slice = 120 → 从第 121 个宏块开始
计算宏块坐标:
mb_x = first_mb_in_slice % (pic_width_in_mbs)
mb_y = first_mb_in_slice / (pic_width_in_mbs)
frame_num:
帧号,用于参考帧管理和解码顺序
特性:
- 范围: 0 ~ 2^(log2_max_frame_num_minus4+4) - 1
- IDR 帧后重置为 0
- 循环使用,超过最大值后回到 0
IDR 识别:
IDR (Instantaneous Decoding Refresh) 帧:
- nal_unit_type == 5
- 之后的所有帧不参考 IDR 之前的帧
- 用于随机访问点(Seek 点)
7. 宏块层句法
7.1 宏块头
macroblock_layer() {
mb_type // ue(v) 宏块类型
if (mb_type == I_PCM) {
// PCM 模式:直接传输原始像素
pcm_alignment_zero_bit // 填充对齐
pcm_byte[i][j] // 原始像素值
return
}
// 宏块类型决定后续句法
if (mb_type != I_NxN)
Intra16x16_Pred_Mode // u(2) 16x16 帧内预测模式
else
Intra4x4_Pred_Mode[i] // 帧内 4x4 预测模式
// 参考帧索引(帧间预测)
if (MbPartPredMode != Intra) {
for (each reference index)
ref_idx_l0[i] // te(v) List0 参考索引
if (slice_type == B)
ref_idx_l1[i] // te(v) List1 参考索引
}
// 运动矢量
if (MbPartPredMode != Intra) {
for (each motion vector)
mvd_l0[i][j] // se(v) 运动矢量差值
if (slice_type == B)
mvd_l1[i][j] // se(v)
}
// 残差
residual()
}
7.2 宏块类型(I 帧)
I_Slice 中的 mb_type:
| mb_type | 名称 | 说明 |
|---|---|---|
| 0 | I_NxN | 使用 4×4 或 8×8 帧内预测 |
| 1 | I_16x16_0_0_0 | 16×16 预测,CBP=0 |
| 2 | I_16x16_1_0_0 | 16×16 预测,模式 1 |
| … | … | … |
| 24 | I_PCM | 原始像素直接传输 |
I_16x16 类型编码:
I_16x16_X_Y_Z:
X = 帧内预测模式 (0-3)
Y = luma 16×16 CBP (0-2)
Z = chroma CBP (0-1)
预测模式:
0 = Vertical (垂直)
1 = Horizontal (水平)
2 = DC
3 = Plane (平面)
7.3 宏块类型(P 帧)
P_Slice 中的 mb_type:
| mb_type | 名称 | 说明 |
|---|---|---|
| 0 | P_L0_16x16 | 1 个 16×16 前向预测 |
| 1 | P_L0_L0_16x8 | 2 个 16×8 分割 |
| 2 | P_L0_L0_8x16 | 2 个 8×16 分割 |
| 3 | P_8x8 | 8×8 分割(进一步细分) |
| 4 | P_8x8ref0 | 8×8 分割(无参考) |
| 5+ | Intra | 使用帧内预测 |
7.4 子宏块类型
当 mb_type = P_8x8 时,每个 8×8 块可进一步细分:
sub_mb_type:
┌────────────┬───────────────────────┐
│ sub_type │ 说明 │
├────────────┼───────────────────────┤
│ 0 │ P_L0_8x8 (不细分) │
│ 1 │ P_L0_8x4 (上下) │
│ 2 │ P_L0_4x8 (左右) │
│ 3 │ P_L0_4x4 (四等分) │
└────────────┴───────────────────────┘
8. 残差数据句法
8.1 残差层结构
residual() {
// 亮度残差
if (CbPatternLuma & 1)
residual_block(0, 0) // 亮度 DC 系数(16x16 模式)
for (i = 0; i < 4; i++)
for (j = 0; j < 4; j++)
if (CbPatternLuma & (1 << (i*4+j)))
residual_block(i*4+j, 0) // 亮度 AC
// 色度残差
for (iCbCr = 0; iCbCr < 2; iCbCr++) {
if (CbPatternChroma & 1)
residual_block(0, iCbCr+1) // 色度 DC
for (i = 0; i < 2; i++)
for (j = 0; j < 2; j++)
if (CbPatternChroma & (1 << (i*2+j)))
residual_block(i*2+j+1, iCbCr+1) // 色度 AC
}
}
8.2 Coded Block Pattern (CBP)
CBP 指示哪些块有非零残差系数:
CBP 编码 (帧内 16x16 以外):
┌────────────────────────────────────┐
│ bits 0-3: 亮度 4 个 8x8 块 │
│ bits 4-5: 色度 Cb/Cr │
│ bit 6: 色度 DC │
└────────────────────────────────────┘
示例:
CBP = 0x1F = 00 011111
│ │ ││││
│ │ │││└─ 亮度块 0 有系数
│ │ ││└── 亮度块 1 有系数
│ │ │└─── 亮度块 2 有系数
│ │ └──── 亮度块 3 有系数
│ └─────── 色度 AC 有系数
└───────────── 色度 DC 有系数
8.3 残差系数编码(CAVLC)
residual_block_cavlc() {
// 1. 非零系数总数和拖尾 1 个数
coeff_token // 根据 nC 选择码表
// 2. 拖尾 1 的符号
for (i = 0; i < trailing_ones; i++)
trailing_ones_sign_flag // u(1)
// 3. 剩余非零系数的幅值
for (i = 0; i < total_coeff - trailing_ones; i++) {
level_prefix // u(v) 前缀
level_suffix // u(v) 后缀
}
// 4. 最后一个非零系数前的零个数
if (total_coeff < block_len)
total_zeros // 编码零的总数
// 5. 每个非零系数前的连续零
for (i = 0; i < total_coeff - 1; i++)
run_before[i] // 编码游程
}
系数扫描顺序:
4×4 块的 Zig-Zag 扫描:
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
扫描顺序: 0→1→4→8→5→2→3→6→9→12→13→10→7→11→14→15
低频系数在前,高频系数在后
便于利用高频系数为零的特性
9. VUI 参数
9.1 VUI 作用
VUI(Video Usability Information)提供视频显示相关的参数,用于播放器正确显示视频。
9.2 主要句法元素
vui_parameters() {
aspect_ratio_info_present_flag // u(1)
if (aspect_ratio_info_present_flag) {
aspect_ratio_idc // u(8)
if (aspect_ratio_idc == 255) {
sar_width // u(16)
sar_height // u(16)
}
}
overscan_info_present_flag // u(1)
if (overscan_info_present_flag)
overscan_appropriate_flag // u(1)
video_signal_type_present_flag // u(1)
if (video_signal_type_present_flag) {
video_format // u(3)
video_full_range_flag // u(1)
colour_description_present_flag // u(1)
if (colour_description_present_flag) {
colour_primaries // u(8)
transfer_characteristics // u(8)
matrix_coefficients // u(8)
}
}
chroma_loc_info_present_flag // u(1)
if (chroma_loc_info_present_flag) {
chroma_sample_loc_type_top_field // ue(v)
chroma_sample_loc_type_bottom_field // ue(v)
}
timing_info_present_flag // u(1)
if (timing_info_present_flag) {
num_units_in_tick // u(32)
time_scale // u(32)
fixed_frame_rate_flag // u(1)
}
// 其他参数...
}
9.3 帧率计算
帧率 = time_scale / (2 × num_units_in_tick)
示例:
num_units_in_tick = 1000 (时间单位为 1/27000000 秒)
time_scale = 27000000
帧率 = 27000000 / (2 × 1000) = 13500 / 1000 = 13.5 fps
常见配置:
┌─────────────┬─────────────┬─────────────┐
│ num_units │ time_scale │ 帧率 │
├─────────────┼─────────────┼─────────────┤
│ 1001 │ 60000 │ 29.97 fps │
│ 1000 │ 60000 │ 30 fps │
│ 1001 │ 50000 │ 24.975 fps │
│ 1000 │ 50000 │ 25 fps │
│ 1001 │ 24000 │ 11.988 fps │
│ 1000 │ 24000 │ 12 fps │
└─────────────┴─────────────┴─────────────┘
9.4 宽高比
aspect_ratio_idc 预定义值:
┌──────┬───────────────┐
│ 值 │ 宽高比 │
├──────┼───────────────┤
│ 1 │ 1:1 (方形) │
│ 2 │ 12:11 │
│ 3 │ 10:11 │
│ 4 │ 16:11 │
│ 5 │ 40:33 │
│ 6 │ 24:11 │
│ 7 │ 20:11 │
│ 8 │ 32:11 │
│ 9 │ 80:33 │
│ 10 │ 18:11 │
│ 11 │ 15:11 │
│ 12 │ 64:33 │
│ 13 │ 160:99 │
│ 14 │ 4:3 │
│ 15 │ 3:2 │
│ 16 │ 2:1 │
│ 255 │ 自定义 │
└──────┴───────────────┘
显示宽高比计算:
DAR = (width / height) × (sar_width / sar_height)
10. SEI(补充增强信息)
10.1 SEI 结构
sei_message() {
payloadType // SEI 载荷类型
payloadSize // 载荷大小
sei_payload(payloadType, payloadSize)
}
10.2 常见 SEI 类型
| Type | 名称 | 用途 |
|---|---|---|
| 0 | buffer_period | 缓冲周期 |
| 1 | pic_timing | 图像时序 |
| 2 | pan_scan_rect | Pan-scan 矩形 |
| 3 | filler_payload | 填充数据 |
| 4 | user_data_registered_itu_t_t35 | 用户数据 |
| 5 | user_data_unregistered | 未注册用户数据 |
| 6 | recovery_point | 恢复点 |
| 19 | sub_layer_info | 子层信息 |
10.3 实时通信中的 SEI
SEI 在 WebRTC 中的应用:
1. 用户数据传输
- 自定义元数据
- 时间戳同步信息
- 端到端延迟测量
2. 恢复点
- 标识可独立解码的位置
- 用于错误恢复
3. 帧时间信息
- 显示时间戳
- 用于音视频同步
11. 句法分析流程
11.1 解码器初始化流程
flowchart TB
A[开始] --> B[读取起始码]
B --> C[解析 NAL Header]
C --> D{NAL Type?}
D -->|SPS| E[解析并保存 SPS]
D -->|PPS| F[解析并保存 PPS]
D -->|IDR Slice| G[解析 Slice Header]
D -->|Non-IDR Slice| G
D -->|SEI| H[处理 SEI]
D -->|其他| I[跳过]
E --> B
F --> B
G --> J[解析宏块数据]
J --> K[重建图像]
K --> B
H --> B
I --> B
11.2 宏块解码流程
flowchart TB
A[读取 mb_type] --> B{帧内/帧间?}
B -->|帧内| C[解析帧内预测模式]
C --> D[获取预测块]
B -->|帧间| E[解析运动矢量]
E --> F[获取参考帧索引]
F --> G[运动补偿获取预测块]
D --> H[解析残差系数]
G --> H
H --> I[反量化]
I --> J[反变换 IDCT]
J --> K[预测 + 残差]
K --> L[去块滤波]
L --> M[存储重建帧]
12. 实际码流示例
12.1 SPS 示例解析
SPS 原始数据 (十六进制):
67 64 00 1F AC D9 40 50 05 BB 01 10
解析:
67 → NAL Header: Type=7 (SPS), NRI=3
64 → profile_idc = 100 (High Profile)
00 → constraint_set flags = 0
1F → level_idc = 31 (Level 3.1)
AC D9 40... → Exp-Golomb 编码的参数
解码后的关键参数:
- Profile: High (100)
- Level: 3.1
- 分辨率: 1280×720
- 帧率: 30fps
12.2 IDR Slice 示例
Slice Header (十六进制):
65 88 80 40 00 5F ...
解析:
65 → NAL Header: Type=5 (IDR), NRI=3
88 → first_mb_in_slice = 0, slice_type = 7 (I)
80 → pic_parameter_set_id = 0
40 → frame_num = 0
00 5F → idr_pic_id, QP 等
关键信息:
- IDR 帧(关键帧)
- I_Slice 类型
- 第一个宏块地址 = 0
- 引用 PPS ID = 0
13. 总结
13.1 句法层次要点
| 层次 | 关键句法元素 | 作用 |
|---|---|---|
| NAL | NAL Header | 标识数据类型和重要性 |
| SPS | profile, level, 分辨率 | 序列级参数 |
| PPS | 熵编码模式, QP | 图像级参数 |
| Slice | slice_type, frame_num | 片级参数 |
| MB | mb_type, 残差 | 宏块级编码数据 |
13.2 解码要点
- 参数集优先:必须先解析 SPS/PPS 才能解码 Slice
- IDR 是起点:随机访问从 IDR 帧开始
- 参考帧管理:正确维护参考帧列表是解码关键
- 残差解码:CAVLC/CABAC 选择由 PPS 决定
13.3 调试技巧
- 使用工具(如 Elecard StreamEye, FFmpeg)分析码流
- 检查 SPS/PPS 参数是否正确
- 验证 NAL 单元边界和起始码
- 确认帧类型和参考关系
