一段 H.264 裸码流的顺序很朴素:起始码、SPS、PPS、IDR 片、后续片——其中起始码是解码器找回边界的唯一线索,所以数据里绝不允许出现它。
9.3.1 给了五级骨架,这一节把它们按顺序摆成一段真实码流。
一、一段码流的顺序
把一段最简单的 H.264 裸码流按顺序写出来: 起始码 0x000001——每个 NAL 单元的分隔标记; SPS——全局参数,整段只要一份; PPS——图像参数,整段只要一份; IDR 片——第一个可独立解码的画面; 非 IDR 片——后续的 P/B 片,要靠前面的帧。 顺序本身没有商量余地:参数必须在用它的数据之前。
二、起始码与【防止竞争】
起始码 0x000001 是这一节最需要留意的一处设计: 解码器在码流里顺序扫描,见到 0x000001 就认为【一个新 NAL 开始了】; 那么如果真正的数据里也出现这串字节,解码器就会误判边界; 所以编码时要做防止竞争(emulation prevention):数据里出现连续两个 0x00 时,插入一个 0x03 把它打断; 解码时再把 0x03 去掉,还原出原始数据——这一步叫去竞争; 所以你看到的裸码流是【变换过】的,不等于原始字节。 这是很多【自己写解析器】的人最先踩的坑。
三、两种边界标记方式
同样是划边界,H.264 有两套写法,用在不同封装里:
起始码方式(Annex B)——用 0x000001 或 0x00000001 分隔,常见于 TS、裸码流、RTP 负载;
长度前缀方式(AVCC)——每个 NAL 前面写一个长度字段,常见于 MP4、MKV 这类带索引的封装;
两者装的是同一批 NAL 单元,只是【分隔方式】不同;
所以把 MP4 里的 H.264 取出来塞进 TS 时,必须做一次格式转换——这就是实际工作中 ffmpeg 的 -bsf h264_mp4toannexb 干的事。
四、为什么转封装有时也要动数据
这一条正好回答了第 5.2.1 节留下的一个疑问:转封装不是【一个字节都不碰】吗? 纯换容器时确实只搬包——因为包的边界信息在容器里已经记好了; 但当两边的边界标记方式不同(MP4 的长度前缀 vs TS 的起始码),就必须重写每个 NAL 的分隔; 这时虽然没碰音频视频的编码内容,但数据确实被改写过了; 所以严谨的说法是:转封装不改编码,但可能改变数据的组织方式。 这也是【转封装比转码快得多、但不是零成本】的原因。
五、看码流的实操顺序
把这一节用到实际排查上,顺序是固定的: 第一步看有没有 SPS/PPS——没有就直接判定文件不可解; 第二步看第一个片是不是 IDR——不是的话拖动会失败; 第三步看类型序列是否规整——乱了说明码流被裁坏了; 用 3.4 节的码流分析器或十六进制工具都能看出这几条。 先看结构、再怀疑内容——这是处理码流问题的通用策略。
六、动手 Demo:一段 H.264 码流长什么样
把 NAL 单元按顺序摆出来,就是一段最朴素的 H.264 码流。
seq = [("起始码 0x000001", "每个 NAL 单元的分隔标记"),
("SPS", "全局参数,整段只要一份"),
("PPS", "图像参数,整段只要一份"),
("IDR 片", "第一个可独立解码的画面"),
("非 IDR 片", "后续的 P/B 片,要靠前面的帧")]
for i, (name, note) in enumerate(seq, 1):
print("%d. %-16s %s" % (i, name, note))
实跑输出:
--------------------------------------------------------------------
一段 H.264 码流长什么样
--------------------------------------------------------------------
把 NAL 单元按顺序摆出来,就是一段最朴素的 H.264 码流。
顺序 内容 作用
----------------------------------------------------------
1 起始码 0x000001 每个 NAL 单元的分隔标记
2 SPS 全局参数,整段只要一份
3 PPS 图像参数,整段只要一份
4 IDR 片 第一个可独立解码的画面
5 非 IDR 片 后续的 P/B 片,要靠前面的帧
两个实战要点:
**一是起始码 0x000001**——解码器靠它找 NAL 边界;
所以真正的数据里不能出现这个字节序列,编码时要**防止竞争**;
**二是 SPS/PPS 必须在 IDR 之前**——否则解码器不知道分辨率与熵编码方式,一个宏块也解不了。
起始码 0x000001 是解码器找回边界的唯一线索, 所以真正的数据里不能出现这个字节序列,编码时要防止竞争。 SPS/PPS 必须在 IDR 之前,否则解码器一个宏块也解不了。
30 秒实验
- 用
ffmpeg -i in.mp4 -c copy -bsf:v h264_mp4toannexb out.ts转一次封装。 - 注意参数里
-c copy没有变——变的只是 NAL 的分隔方式。
常见误区
- 以为起始码只是【好看的分隔线】。它是解码器找边界的唯一依据。
- 以为转封装绝对不会改数据。边界标记方式不同时必须重写分隔。
- 以为 SPS/PPS 可以随便挪。它们必须在使用它们的片之前。
自测三问(点开对答案)
1. 一段 H.264 裸码流的基本顺序是什么? —— 起始码、SPS、PPS、IDR 片、后续非 IDR 片。 2. 防止竞争要解决什么问题? —— 数据里出现 0x000001 会被误认为 NAL 边界,需插入 0x03 打断。 3. MP4 与 TS 里的 NAL 分隔方式有何不同? —— MP4 用长度前缀,TS 用起始码;互转时需要重写分隔。- ☐ 下一节(9.3.3 H.264的NAL Unit)
相关笔记
- H.264的NAL Unit — 单元的字节组成
- H.264分层结构 — 五级骨架
- 数据封装和解封装 — 转封装到底动没动数据
- 简介 — 码流分析工具能看什么
- 🎬 FFmpeg入门详解 · 索引