H.265/HEVC NALU结构快速入门:从起始码到Type值识别
📅 更新时间:2025年10月11日
🏷️ 标签:H.265 | HEVC | NALU | 视频编码 | 编解码原理 | WebRTC
🎯 目标:快速理解H.265 NALU基本结构,掌握Type值识别方法
文章目录
- 📖 前言
- 🎯 NALU是什么?(快递包裹类比)
- 简单理解
- NALU的作用
- 📦 NALU完整结构
- 三部分组成
- 1️⃣ 起始码(Start Code)
- 是什么?
- 类比
- 作用
- 注意事项
- 2️⃣ NALU Header(2字节)
- H.265 vs H.264 核心差异
- 2字节Header的bit位分布
- 调试时只需关注Type
- 3️⃣ NALU数据(Payload)
- 🔑 Type值详解
- 什么是Type值?
- 常见Type值速查表
- Type值的重要性
- 对解码器:
- 对RTP传输(WebRTC):
- 🔢 Type值提取方法
- 位运算公式
- 手工计算步骤
- 示例:NALU Header第1字节 = `40`
- 🛠️ 实战:用HxD分析H.265文件
- 工具准备
- 实战步骤
- 第1步:准备H.265文件
- 第2步:用HxD打开文件
- 第3步:查找起始码
- 第4步:查看第一个NALU(VPS)
- 第5步:继续查看后续NALU
- 快速识别表(不用计算)
- 🎯 实战应用场景
- 场景1:学习H.265格式
- 场景2:调试WebRTC H.265传输(重要!)
- 场景3:RTP分片处理
- 📚 H.264 vs H.265 对比
- NALU结构差异
- 为什么差异重要?
📖 前言
在开发WebRTC、视频传输或调试H.265相关项目时,你一定遇到过这样的问题:
- ❓ H.265文件是怎么组织的?
- ❓ 什么是NALU?和H.264有什么区别?
- ❓ 为什么RTP打包/解包需要理解NALU?
- ❓ 如何用工具查看和分析H.265文件结构?
本文用最简单的方式,通过类比和实战,帮你快速理解H.265 NALU的核心概念。
🎯 NALU是什么?(快递包裹类比)
简单理解
NALU = Network Abstraction Layer Unit(网络抽象层单元)
用快递包裹类比最容易理解:
原始视频 → H.265编码 → 一堆NALU包裹就像:
商品 → 打包发货 → 快递包裹1、快递包裹2、快递包裹3...
H.265文件 = 一串NALU包裹连起来
NALU的作用
作用 | 说明 |
---|---|
方便传输 | 每个NALU是独立单元,可以单独传输 |
方便解析 | 起始码分隔,容易定位每个NALU |
适应网络 | 支持分片(大NALU可以拆成小包传输) |
灵活处理 | 不同Type的NALU可以分别处理 |
📦 NALU完整结构
三部分组成
┌─────────────┬─────────────┬──────────────┐
│ 起始码 │ NALU Header │ NALU数据 │
│ 4字节 │ 2字节 │ 可变长度 │
│ 00 00 00 01 │ 40 01 │ Payload │
└─────────────┴─────────────┴──────────────┘
1️⃣ 起始码(Start Code)
是什么?
NALU之间的分隔符,固定为 00 00 00 01
(4字节)
类比
快递包裹前的标签(标识开始):
[──标签1──][包裹1][──标签2──][包裹2][──标签3──][包裹3]↑ 这是1号 ↑ 这是2号 ↑ 这是3号NALU前的起始码(标识开始):
[00 00 00 01][NALU1][00 00 00 01][NALU2][00 00 00 01][NALU3]↑ 标识开始 ↑ ↑ 标识开始 ↑ ↑ 标识开始
作用
作用 | 说明 |
---|---|
标识开始 | 告诉解析器"新的NALU开始了" |
定位边界 | 确定一个NALU在哪结束,下一个在哪开始 |
方便搜索 | 用工具(如HxD)快速定位所有NALU |
注意事项
两种起始码:
00 00 00 01
(4字节)- 常见00 00 01
(3字节)- 较少见
本文统一使用4字节起始码。
2️⃣ NALU Header(2字节)
H.265 vs H.264 核心差异
编码格式 | NALU Header大小 | 说明 |
---|---|---|
H.264 | 1字节 | 结构简单 |
H.265 | 2字节 | 支持分层编码,更复杂 |
这是H.265和H.264在数据格式上最重要的差异!
2字节Header的bit位分布
NALU Header = 2字节 = 16 bits第1字节(8 bits):
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ ← 示例:40 (hex)
└───┴───┴───┴───┴───┴───┴───┴───┘7 6 5 4 3 2 1 0 ← bit位编号↓ └────────Type────────┘ ↓F (bit 1-6) LayerId高1位第2字节(8 bits):
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ ← 示例:01 (hex)
└───┴───┴───┴───┴───┴───┴───┴───┘7 6 5 4 3 2 1 0 ← bit位编号└────LayerId低5位────┘ └TID─┘(bit 3-7) (bit 0-2)
字段说明:
字段 | 位置 | 长度 | 值范围 | 说明 |
---|---|---|---|---|
F | 第1字节 bit 7 | 1位 | 0或1 | 禁止位,通常为0 |
Type | 第1字节 bit 1-6 | 6位 | 0-63 | NALU类型 ← 重点! |
LayerId | 第1字节 bit 0 + 第2字节 bit 3-7 | 6位 | 0-63 | 空间层ID(分层编码) |
TID | 第2字节 bit 0-2 | 3位 | 0-7 | 时间层ID(时间分层) |
示例分析(40 01):
- F = 0(bit 7)
- F字段实际意义:
网络传输时,如果检测到错误,可以设置F=1
接收端看到F=1,直接丢弃这个NALU
正常情况下,F永远是0 - Type = 100000 (二进制) = 32 (VPS) ← 我们只关心这个
- LayerId = 000000 = 0
- TID = 001 = 1
调试时只需关注Type
Type(6位)= NALU类型,这是最重要的信息!
其他字段(F/LayerId/TID)在基础调试中不太需要关注。
3️⃣ NALU数据(Payload)
存储实际的视频编码数据:
- 参数信息(VPS/SPS/PPS)
- 图像数据(I帧/P帧/B帧)
长度可变,从几十字节到几十KB不等。
🔑 Type值详解
什么是Type值?
Type值 = NALU的"标签",告诉解码器这个包裹里装的是什么
类比:
快递标签:
- "易碎品" → 小心轻放
- "食品" → 注意保质期
- "文件" → 重要文件NALU Type值:
- 32(VPS) → 视频参数,解码前必须有
- 33(SPS) → 序列参数,解码前必须有
- 34(PPS) → 图像参数,解码前必须有
- 19/20(IDR) → 关键帧,完整画面
- 0/1 → 普通帧,差异数据
常见Type值速查表
Type值 | 名称 | 全称 | 作用 | 特点 |
---|---|---|---|---|
32 | VPS | Video Parameter Set | 视频级别参数 | H.265特有 ✨ |
33 | SPS | Sequence Parameter Set | 序列参数(分辨率等) | 解码必需 |
34 | PPS | Picture Parameter Set | 图像参数 | 解码必需 |
19/20 | IDR | Instantaneous Decoder Refresh | I帧(关键帧) | 独立解码 |
0/1 | TRAIL | Trailing Picture | P/B帧 | 依赖其他帧 |
49 | FU | Fragmentation Unit | RTP分片标识 | WebRTC必知 ⚠️ |
Type值的重要性
对解码器:
解码器收到NALU → 检查Type值:
├── Type=32/33/34 → 保存参数,用于后续解码
├── Type=19/20 → 这是I帧,独立解码
└── Type=0/1 → 这是P/B帧,需要参考帧
对RTP传输(WebRTC):
发送端打包:
├── Type=32/33/34 → 优先发送,确保接收端有参数
├── Type=49 → 这是FU分片,需要重组
└── 其他 → 单NALU模式接收端解包:
├── 检查Type=49 → 进入FU重组逻辑
└── 其他 → 直接输出NALU
🔢 Type值提取方法
位运算公式
uint8_t nalu_type = (nalu_header[0] >> 1) & 0x3F;└────┬────┘ └─┬──┘右移1位 取低6位
手工计算步骤
示例:NALU Header第1字节 = 40
第1步:转二进制
40 (十六进制) = 0100 0000 (二进制)
第2步:右移1位
0100 0000 >> 1 = 0010 0000↑ 去掉了最右边的bit 0
第3步:取低6位(& 0x3F)
0010 0000 & 0011 1111 = 0010 0000└─0x3F──┘ └─结果──┘
第4步:转十进制
0010 0000 (二进制) = 32 (十进制)
第5步:查表
32 → VPS ✅
🛠️ 实战:用HxD分析H.265文件
工具准备
HxD - 免费十六进制编辑器
- 下载:https://mh-nexus.de/en/hxd/
- 大小:约3MB
- 功能:查看二进制文件、搜索、对比
实战步骤
第1步:准备H.265文件
方法1:用FFmpeg生成测试文件
ffmpeg -i input.mp4 -c:v libx265 -t 10 -frames:v 60 test.h265
方法2:从你的项目获取
// 编码时保存
FILE* file = fopen("output.h265", "wb");
fwrite(packet->data, 1, packet->size, file);
第2步:用HxD打开文件
- 右键
test.h265
→ 打开方式 → HxD - 或拖拽文件到HxD窗口
第3步:查找起始码
按 Ctrl+F
打开查找对话框
设置:
- 点击 “Hex-values” 或 “字节序列” 标签
- 在输入框输入:
00 00 00 01
(空格分隔) - 点击 “查找全部” 或 “Find all”
结果:
- 底部显示所有匹配位置
- 显示找到的数量(如:Found 62 occurrences)
第4步:查看第一个NALU(VPS)
双击底部第一个搜索结果(偏移量0)
你会看到:
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F00000000 00 00 00 01 40 01 0C 01 FF FF 01 60 00 00 03 00└起始码(4B)┘ └NALU Header─┘ └─NALU数据开始...偏移0-3 偏移4-5↑看这2个字节!
识别:
- 起始码:
00 00 00 01
✅ - NALU Header:
40 01
- 第1字节
40
→ Type = 32 → VPS ✅
第5步:继续查看后续NALU
典型顺序:
NALU 1: 00 00 00 01 40 01 ... → VPS (32)
NALU 2: 00 00 00 01 42 01 ... → SPS (33)
NALU 3: 00 00 00 01 44 01 ... → PPS (34)
NALU 4: 00 00 00 01 26 01 ... → IDR (19)
NALU 5: 00 00 00 01 02 01 ... → TRAIL (1)
...
快速识别表(不用计算)
记住常见的Header第1字节值:
Header第1字节 | 计算过程 | Type值 | NALU类型 | 记忆 |
---|---|---|---|---|
40 | (40>>1)&3F=20=32 | 32 | VPS | 4开头→参数 |
42 | (42>>1)&3F=21=33 | 33 | SPS | 4开头→参数 |
44 | (44>>1)&3F=22=34 | 34 | PPS | 4开头→参数 |
26 | (26>>1)&3F=13=19 | 19 | IDR | 2开头→帧 |
28 | (28>>1)&3F=14=20 | 20 | IDR | 2开头→帧 |
62 | (62>>1)&3F=31=49 | 49 | FU | 必记! |
🎯 实战应用场景
场景1:学习H.265格式
用HxD分析自己编码的文件:
1. 编码生成test.h265
2. 用HxD打开
3. 找起始码
4. 识别NALU类型
5. 理解H.265文件组织方式
场景2:调试WebRTC H.265传输(重要!)
问题:发送端编码正常,接收端无法解码
排查步骤:
步骤1:保存编码输出
FILE* f1 = fopen("debug_encode_output.h265", "wb");
fwrite(packet->data, ...);步骤2:保存RTP重组输出
FILE* f2 = fopen("debug_reassembled.h265", "wb");
// RTP解包后写入步骤3:用HxD对比两个文件
- 打开两个文件
- 搜索起始码
- 对比NALU数量
- 对比NALU类型(Type值)
- 定位差异位置
可能发现的问题:
- ❌ VPS/SPS/PPS丢失 → 参数集传输有问题
- ❌ NALU数量不一致 → 丢包
- ❌ Type值错误 → FU重组逻辑错误
- ❌ 某个字节开始不同 → 数据损坏位置
场景3:RTP分片处理
识别FU分片:
void ProcessRTPPayload(uint8_t* payload, int size) {// 提取Type值uint8_t nalu_type = (payload[0] >> 1) & 0x3F;if (nalu_type == 49) {// 这是FU分片!需要重组uint8_t fu_header = payload[2];bool S = (fu_header >> 7) & 0x01; // 开始片bool E = (fu_header >> 6) & 0x01; // 结束片// 重组逻辑...} else {// 单NALU模式,直接输出}
}
Type=49的处理是WebRTC H.265的关键难点!
📚 H.264 vs H.265 对比
NALU结构差异
项目 | H.264 | H.265 | 影响 |
---|---|---|---|
NALU Header | 1字节 | 2字节 | RTP打包逻辑不同 |
参数集 | SPS+PPS | VPS+SPS+PPS | H.265多一个VPS |
起始码 | 00 00 00 01 | 00 00 00 01 | 相同 |
Type位数 | 5位 (0-31) | 6位 (0-63) | H.265支持更多类型 |
FU类型 | 28 | 49 | RTP分片Type不同 |
为什么差异重要?
WebRTC项目常见问题:
问题:用H.264的RTP打包/解包逻辑处理H.265错误示例:
if (nalu_type == 28) { // ❌ 这是H.264的FU Type// FU处理...
}正确示例:
if (nalu_type == 49) { // ✅ H.265的FU Type// FU处理...
}
结果:
- H.264逻辑无法正确处理H.265的FU分片
- 导致接收端组帧失败,无法解码
如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多音视频开发实战技巧将持续更新 🔥!