H.264 码流结构
概述
H.264 码流由一系列 NAL(Network Abstraction Layer,网络抽象层)单元组成。每个 NAL 单元包含一个完整的编码数据单元,可以是视频帧数据、参数集或其他控制信息。理解 H.264 码流结构对于视频编解码、传输和存储都至关重要。
NAL 单元结构
基本结构
NAL 单元是 H.264 码流的基本组成单位,每个 NAL 单元由以下部分组成:
[Start Code] [NAL Header] [NAL Payload]
1. Start Code(起始码)
- Annex-B 格式:使用起始码分隔 NAL 单元
- 3 字节起始码:
0x00 0x00 0x01 - 4 字节起始码:
0x00 0x00 0x00 0x01(推荐,避免与数据冲突)
- 3 字节起始码:
- AVCC 格式:使用长度前缀(Length Prefix)代替起始码
- 1 字节长度:
0x00–0xFF(最大 255 字节) - 2 字节长度:
0x0000–0xFFFF(最大 64KB) - 4 字节长度:
0x00000000–0xFFFFFFFF(最大 4GB)
- 1 字节长度:
2. NAL Header(NAL 头)
- 固定 1 字节,包含 NAL 单元类型和标志位
- 结构:
[F] [NRI] [Type]- F(Forbidden bit):1 位,通常为 0,表示无错误
- NRI(NAL Reference IDC):2 位,表示 NAL 单元的重要性
00:非参考帧,可丢弃01:参考帧,但非关键帧10:参考帧,重要11:关键帧(IDR),最重要
- Type(NAL 单元类型):5 位,表示 NAL 单元类型(0-31)
3. NAL Payload(NAL 负载)
- 包含实际的编码数据(如视频帧数据、参数集等)
- 大小可变,取决于编码内容和码率
NAL Header 解析示例
// NAL Header 解析伪代码
uint8_t nal_header = 0x67; // 示例:SPS 的 NAL Header
uint8_t forbidden_bit = (nal_header >> 7) & 0x01; // 最高位
uint8_t nri = (nal_header >> 5) & 0x03; // 中间2位
uint8_t nal_type = nal_header & 0x1F; // 低5位
// 0x67 = 0110 0111
// forbidden_bit = 0
// nri = 3 (11)
// nal_type = 7 (SPS)
NAL 单元类型
H.264 标准定义了多种 NAL 单元类型,每种类型对应不同的数据内容:
| NAL Type | 类型名称 | 说明 | NRI 典型值 | 应用场景 |
|---|---|---|---|---|
| 0 | 未指定 | 保留,不应使用 | – | – |
| 1 | 非IDR图像 | 普通视频帧(I/P/B帧) | 01/10/11 | 视频数据 |
| 2 | 数据分区A | 数据分区(已废弃) | – | 已废弃 |
| 3 | 数据分区B | 数据分区(已废弃) | – | 已废弃 |
| 4 | 数据分区C | 数据分区(已废弃) | – | 已废弃 |
| 5 | IDR图像 | 关键帧(Instantaneous Decoder Refresh) | 11 | 随机访问点 |
| 6 | SEI | 补充增强信息 | 00 | 元数据、时间戳 |
| 7 | SPS | 序列参数集 | 11 | 解码配置 |
| 8 | PPS | 图像参数集 | 11 | 解码配置 |
| 9 | 访问单元分隔符 | 帧边界标记 | 00 | 帧同步 |
| 10 | 序列结束 | 序列结束标记 | 00 | 序列结束 |
| 11 | 流结束 | 流结束标记 | 00 | 流结束 |
| 12 | 填充数据 | 填充字节 | 00 | 对齐填充 |
| 13-23 | 保留 | 保留未使用 | – | – |
| 24-31 | 未指定 | 保留未使用 | – | – |
重要 NAL 单元类型详解
1. SPS(Sequence Parameter Set,序列参数集)
作用:包含整个视频序列的全局参数,解码器需要 SPS 才能正确解码视频。
包含信息:
- 编码 Profile 和 Level
- 图像尺寸(宽度、高度)
- 色度格式(4:2:0、4:2:2、4:4:4)
- 帧率信息
- GOP 结构参数
- 参考帧数量
- 熵编码方式(CAVLC/CABAC)
特点:
- NAL Type = 7
- NRI = 11(最重要,不可丢弃)
- 通常出现在码流开头,IDR 帧之前
- 序列参数变化时需要发送新的 SPS
示例:
Start Code: 00 00 00 01
NAL Header: 67 (0110 0111)
- F = 0
- NRI = 11 (最重要)
- Type = 7 (SPS)
Payload: [SPS 数据...]
2. PPS(Picture Parameter Set,图像参数集)
作用:包含图像级别的解码参数,通常与 SPS 配合使用。
包含信息:
- 熵编码方式(CAVLC/CABAC)
- 量化参数
- 去块效应滤波器参数
- 参考帧列表参数
- 加权预测参数
特点:
- NAL Type = 8
- NRI = 11(最重要,不可丢弃)
- 通常出现在码流开头,IDR 帧之前
- 一个 SPS 可以对应多个 PPS
示例:
Start Code: 00 00 00 01
NAL Header: 68 (0110 1000)
- F = 0
- NRI = 11 (最重要)
- Type = 8 (PPS)
Payload: [PPS 数据...]
3. IDR 帧(Instantaneous Decoder Refresh)
作用:关键帧,解码器可以从 IDR 帧开始独立解码,不依赖之前的帧。
特点:
- NAL Type = 5
- NRI = 11(最重要,不可丢弃)
- 是随机访问点,可以用于 Seek 操作
- IDR 帧之前通常会重复发送 SPS/PPS(确保解码器能正确解码)
- 所有参考帧缓冲区在 IDR 帧后清空
示例:
Start Code: 00 00 00 01
NAL Header: 65 (0110 0101)
- F = 0
- NRI = 11 (最重要)
- Type = 5 (IDR)
Payload: [IDR 帧数据...]
4. 非IDR图像(普通视频帧)
作用:包含实际的视频帧数据(I帧、P帧、B帧)。
特点:
- NAL Type = 1
- NRI 值取决于帧类型:
- I帧:NRI = 10 或 11
- P帧:NRI = 01 或 10
- B帧:NRI = 00 或 01
- 包含 Slice 数据(一个或多个 Slice)
示例:
Start Code: 00 00 00 01
NAL Header: 41 (0100 0001) // P帧示例
- F = 0
- NRI = 01 (参考帧)
- Type = 1 (非IDR图像)
Payload: [Slice 数据...]
5. SEI(Supplemental Enhancement Information,补充增强信息)
作用:包含额外的元数据信息,不影响解码,但可用于播放优化。
常见 SEI 类型:
- 用户数据 SEI:自定义数据(如时间戳、版权信息)
- 缓冲周期 SEI:HRD(Hypothetical Reference Decoder)参数
- 图像时序 SEI:帧率、时间戳信息
- 恢复点 SEI:随机访问点信息
特点:
- NAL Type = 6
- NRI = 00(可丢弃,不影响解码)
- 可选,解码器可以忽略
码流格式
H.264 码流有两种主要格式:
1. Annex-B 格式
特点:
- 使用起始码(Start Code)分隔 NAL 单元
- 起始码:
0x00 0x00 0x01或0x00 0x00 0x00 0x01 - 文件格式:
.264、.h264 - 传输格式:RTP、TS 流
优点:
- 结构简单,易于解析
- 支持流式传输
- 广泛用于实时传输
缺点:
- 需要扫描起始码,解析相对复杂
- 起始码可能与数据冲突(虽然概率很低)
示例码流:
00 00 00 01 67 64 00 1E AC ... // SPS
00 00 00 01 68 E9 78 2C 8B ... // PPS
00 00 00 01 65 88 84 00 10 ... // IDR 帧
00 00 00 01 41 9A 26 01 10 ... // P 帧
00 00 00 01 01 9A 26 01 10 ... // B 帧
2. AVCC 格式(MP4 格式)
特点:
- 使用长度前缀(Length Prefix)代替起始码
- 长度前缀:1/2/4 字节(在容器中配置)
- 文件格式:
.mp4、.mov、.mkv - SPS/PPS 存储在容器元数据中(
avcCbox)
优点:
- 解析速度快(直接读取长度,无需扫描)
- 适合随机访问(可以快速定位帧)
- 容器格式支持完善
缺点:
- 需要容器格式支持
- 不适合纯流式传输
示例码流(4字节长度前缀):
00 00 00 23 67 64 00 1E AC ... // SPS (长度 0x23 = 35 字节)
00 00 00 05 68 E9 78 2C 8B ... // PPS (长度 0x05 = 5 字节)
00 00 01 23 65 88 84 00 10 ... // IDR 帧 (长度 0x123 = 291 字节)
00 00 00 45 41 9A 26 01 10 ... // P 帧 (长度 0x45 = 69 字节)
格式转换
Annex-B → AVCC:
- 移除起始码
- 添加长度前缀
- 将 SPS/PPS 提取到容器元数据
AVCC → Annex-B:
- 移除长度前缀
- 添加起始码
- 将 SPS/PPS 插入到 IDR 帧之前
码流结构示例
完整的 H.264 码流结构(Annex-B 格式)
┌─────────────────────────────────────────┐
│ Start Code: 00 00 00 01 │
│ NAL Header: 67 (SPS) │
│ SPS Payload: [序列参数...] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Start Code: 00 00 00 01 │
│ NAL Header: 68 (PPS) │
│ PPS Payload: [图像参数...] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Start Code: 00 00 00 01 │
│ NAL Header: 06 (SEI, 可选) │
│ SEI Payload: [补充信息...] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Start Code: 00 00 00 01 │
│ NAL Header: 65 (IDR 帧) │
│ IDR Payload: [关键帧数据...] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Start Code: 00 00 00 01 │
│ NAL Header: 41 (P 帧) │
│ P Payload: [P帧数据...] │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Start Code: 00 00 00 01 │
│ NAL Header: 01 (B 帧) │
│ B Payload: [B帧数据...] │
└─────────────────────────────────────────┘
... (更多帧)
访问单元(Access Unit)结构
一个访问单元(Access Unit)对应一个完整的视频帧,可能包含多个 NAL 单元:
访问单元(一帧):
├─ SPS (可选,通常只在序列开始或参数变化时)
├─ PPS (可选,通常只在序列开始或参数变化时)
├─ SEI (可选)
├─ IDR/I/P/B 帧 (必需,一个或多个 Slice)
└─ 访问单元分隔符 (可选,NAL Type 9)
注意:
- IDR 帧前通常会重复发送 SPS/PPS(确保解码器能正确解码)
- 一个帧可能包含多个 Slice(分片编码),每个 Slice 是一个独立的 NAL 单元
- 访问单元分隔符用于明确标记帧边界
码流解析流程
Annex-B 格式解析
flowchart TD
A[开始解析] --> B[查找起始码]
B --> C{找到起始码?}
C -->|否| D[继续扫描]
D --> B
C -->|是| E[读取 NAL Header]
E --> F[解析 NAL Type]
F --> G{类型判断}
G -->|SPS| H[保存 SPS]
G -->|PPS| I[保存 PPS]
G -->|IDR/I/P/B| J[解码视频帧]
G -->|SEI| K[处理元数据]
G -->|其他| L[跳过或处理]
H --> M[继续解析下一个 NAL]
I --> M
J --> M
K --> M
L --> M
M --> B
解析步骤详解
1. 查找起始码
// 查找起始码伪代码
uint8_t* find_start_code(uint8_t* data, size_t size) {
for (size_t i = 0; i < size - 3; i++) {
// 查找 3 字节起始码
if (data[i] == 0x00 && data[i+1] == 0x00 && data[i+2] == 0x01) {
return data + i;
}
// 查找 4 字节起始码
if (i < size - 4 &&
data[i] == 0x00 && data[i+1] == 0x00 &&
data[i+2] == 0x00 && data[i+3] == 0x01) {
return data + i;
}
}
return NULL;
}
2. 解析 NAL Header
// 解析 NAL Header
typedef struct {
uint8_t forbidden_bit; // 1 bit
uint8_t nri; // 2 bits
uint8_t type; // 5 bits
} NALHeader;
NALHeader parse_nal_header(uint8_t header_byte) {
NALHeader nal;
nal.forbidden_bit = (header_byte >> 7) & 0x01;
nal.nri = (header_byte >> 5) & 0x03;
nal.type = header_byte & 0x1F;
return nal;
}
3. 处理不同类型的 NAL 单元
- SPS/PPS:保存到解码器配置中
- IDR 帧:清空参考帧缓冲区,开始新序列
- 普通帧:根据帧类型进行解码
- SEI:提取元数据信息(可选)
AVCC 格式解析
flowchart TD
A[开始解析] --> B[读取长度前缀]
B --> C[根据长度读取 NAL 单元]
C --> D[解析 NAL Header]
D --> E[处理 NAL 单元]
E --> F{还有数据?}
F -->|是| B
F -->|否| G[解析完成]
解析步骤:
- 读取长度前缀(1/2/4 字节,由容器配置决定)
- 根据长度读取完整的 NAL 单元
- 解析 NAL Header 和处理数据
- 重复直到数据结束
关键要点
1. SPS/PPS 的重要性
- 必须保存:解码器必须保存 SPS/PPS 才能正确解码
- 及时更新:当序列参数变化时,需要发送新的 SPS/PPS
- IDR 前发送:IDR 帧前通常会重复发送 SPS/PPS,确保解码器能正确解码
2. IDR 帧的作用
- 随机访问点:可以从 IDR 帧开始独立解码
- 清空缓冲区:IDR 帧后所有参考帧缓冲区清空
- Seek 支持:视频播放器的 Seek 操作通常定位到 IDR 帧
3. NRI 值的含义
- NRI = 11:关键帧(IDR)或参数集(SPS/PPS),不可丢弃
- NRI = 10:重要参考帧(I帧、重要P帧),不应丢弃
- NRI = 01:参考帧(P帧),可以丢弃但会影响后续帧
- NRI = 00:非参考帧(B帧)或可丢弃数据(SEI),可以安全丢弃
4. 码流格式选择
- 实时传输(RTP、RTMP、WebRTC):使用 Annex-B 格式
- 文件存储(MP4、MOV、MKV):使用 AVCC 格式
- 流媒体(HLS、DASH):根据容器格式选择
5. 错误处理
- 起始码冲突:虽然概率很低,但需要处理(使用 4 字节起始码可降低概率)
- NAL 单元损坏:根据 NRI 值决定是否丢弃
- 参数集丢失:无法解码,需要重新获取或使用默认参数
实际应用
FFmpeg 处理 H.264 码流
提取 SPS/PPS:
# 从 MP4 文件提取 SPS/PPS
ffmpeg -i input.mp4 -vcodec copy -bsf:v h264_mp4toannexb -f h264 output.264
# 查看 NAL 单元信息
ffprobe -v error -show_entries packet=pts_time,dts_time,flags -select_streams v:0 input.mp4
转换格式:
# Annex-B 转 AVCC(MP4)
ffmpeg -i input.264 -c:v copy output.mp4
# AVCC 转 Annex-B
ffmpeg -i input.mp4 -vcodec copy -bsf:v h264_mp4toannexb output.264
代码示例:解析 H.264 码流
// 简化的 H.264 码流解析示例
typedef struct {
uint8_t* data;
size_t size;
size_t pos;
} H264Stream;
int parse_h264_stream(H264Stream* stream) {
uint8_t* start_code = NULL;
while ((start_code = find_start_code(stream->data + stream->pos,
stream->size - stream->pos)) != NULL) {
// 跳过起始码
stream->pos = (start_code - stream->data) + 4;
// 查找下一个起始码或数据结束
uint8_t* next_start = find_start_code(stream->data + stream->pos,
stream->size - stream->pos);
size_t nal_size = (next_start ? (next_start - stream->data) : stream->size) - stream->pos;
// 读取 NAL Header
if (nal_size < 1) break;
uint8_t nal_header = stream->data[stream->pos];
NALHeader nal = parse_nal_header(nal_header);
// 处理不同类型的 NAL 单元
switch (nal.type) {
case 7: // SPS
handle_sps(stream->data + stream->pos + 1, nal_size - 1);
break;
case 8: // PPS
handle_pps(stream->data + stream->pos + 1, nal_size - 1);
break;
case 5: // IDR
handle_idr(stream->data + stream->pos + 1, nal_size - 1);
break;
case 1: // 非IDR图像
handle_frame(stream->data + stream->pos + 1, nal_size - 1, nal.nri);
break;
default:
// 其他类型
break;
}
stream->pos += nal_size;
}
return 0;
}
相关资源
标准文档
- ITU-T H.264:ITU-T Recommendation H.264 (Advanced Video Coding)
- ISO/IEC 14496-10:ISO/IEC 14496-10 (MPEG-4 Part 10, AVC)
参考实现
- x264:H.264 编码器参考实现
- FFmpeg:H.264 编解码和格式转换
- OpenH264:Cisco 开源的 H.264 编解码器
相关文档
最后更新:2026-01-20
