H.264 的码流有两种装法:AVCC 用 4 字节长度、AnnexB 用起始码。 mp4 里存 AVCC,推流要 AnnexB,差这一层远端就解不出来。
上一节把视频流抽成了裸流,这一节看这堆字节到底长什么样。 顺手会看到 H.264 里最基础的结构:NAL。 「一层皮」这个说法不夸张——转换前后字节数几乎不变,但能不能播完全取决于它。
一、两种装法,解决两个不同的问题
| 装法 | 每个 NAL 前面 | 用在哪 |
|---|---|---|
| AnnexB | 00 00 00 01 起始码 |
文件(.h264)、TS、RTP |
| AVCC | 4 字节大端长度 | mp4、flv(部分) |
为什么要分两种:mp4 是「按索引随机访问」的,用长度前缀才能「知道这一段多长、跳过去」;
而直播流是「顺序读」的,
起始码更省事,而且能做「字节对齐重同步」——收流出错时,只要往后找到 00 00 00 01 就能重新对上。
这个差异导致一个很具体的分工:h264_mp4toannexb 这个 bsf(比特流滤镜)专门干转换,
FFmpeg 推 RTMP 时会自动挂上它,
所以你从来不用手动调——但如果你自己写推流代码,这一步必须自己做。
「bsf」这个词要解释一下:它是「不碰编码,只改码流包装」的一类滤镜,
名字叫比特流滤镜(bitstream filter)。
跟 sws_scale、libavfilter 都不同层级——它连帧都不解,只动字节。
二、拆开看:一个包里其实有四个 NAL
Demo 把第一个视频包拆开了,结果比预想的热闹:一个包里有四段。
| 段 | 长度 | NAL 类型 |
|---|---|---|
| 第 1 段 | 677 字节 | 6:SEI 补充增强 |
| 第 2 段 | 59 字节 | 5:IDR 图像分片 |
| 第 3 段 | 60 字节 | 5:IDR 图像分片 |
| 第 4 段 | 61 字节 | 5:IDR 图像分片 |
857 + 4 × 4 = 873,跟包长正好对上,说明四段读完刚好到尾巴。
这张表说明两件事:第一,一个包里可以有多个 NAL; 第二,同一帧的图像数据还可以被切成几个分片。
第二件事是编码器的多线程切片(sliced threads)干的:每个线程编一条 slice,最后拼成一帧。 好处是并行,代价是「一帧的 NAL 数量不固定」。
所以「N 个 NAL = N 帧」这种假设一定错——后三段其实是一帧,只是被切成了三片。
还有一处值得盯:体积分配。 SEI 677 字节,三段图像加起来才 180 字节,补充信息居然是图像的将近四倍。 别被这个比例带走——这是 320×240 的小画面、而且刚开播的第一帧, SEI 里写的是编码器版本这类只写一次的东西;帧数一多,它马上被摊薄。
这也解释了为什么「抽流」出来的文件通常比原 mp4 略大: 少了容器索引,多了起始码,一进一出量级相当。
三、转换为什么是「等长替换」
Demo 里转完的字节数跟原包一样(873 对 873), 因为 4 字节长度换成 4 字节起始码,长度正好相等。
所以这个转换的性质是「换标记」,不是「压缩」也不是「转码」: 码流内容一个字节没变,只是「怎么切段」的记号变了。
这也意味着它是可逆的:把 00 00 00 01 换回长度就行(h264_annexb2mp4 干这个)。
双向都无损——
这一点很重要,因为它说明「两种装法只是外壳不同」。
典型的故障现象要记住:本地用 ffplay 放 mp4 一切正常,一推流对方就黑屏。
因为本地播放器读的是 mp4 的索引,根本不需要起始码;而服务端拿到 AVCC 之后,找不到起始码,直接把流判成坏的。
这类 bug 的排查思路:先抓包看前几十个字节。
如果开头是 00 00 02 A5 这种「像长度」的数,就是 AVCC;如果开头是 00 00 00 01,就是 AnnexB。
一眼就能看出来,不用猜。
四、动手 Demo:h264_mp4toannexb —— 一层皮,决定远端能不能解
这段从 mp4 里取第一个视频包,先按 AVCC 的规矩拆成段(前四字节当长度), 再手工转成 AnnexB(换成起始码),最后比较两边的字节数与前八个字节。 拆段那一步会给你一个意外:一个包里有四段。
raw = bytes(first_video_packet)
# AVCC:前四字节是长度
pos = 0
while pos + 4 <= len(raw):
ln = struct.unpack(">I", raw[pos:pos+4])[0]
nal_type = raw[pos+4] & 0x1F
print(pos, ln, nal_type)
pos += 4 + ln
# 转 AnnexB:把长度换成 00 00 00 01
out = b"\x00\x00\x00\x01" + raw[pos+4:pos+4+ln]
实跑输出:
--------------------------------------------------------------------
h264_mp4toannexb —— 一层皮,决定远端能不能解
--------------------------------------------------------------------
**H.264 的码流有两种装法,名字很学术,区别很朴素**:
**AnnexB**:每个 NAL 前面放 `00 00 00 01` 起始码。**文件、直播流用这个。**
**AVCC**:每个 NAL 前面放「4 字节长度」。**mp4 里用这个。**
**为什么要分两种**:**mp4 是「按索引随机访问」的,用长度前缀才能跳着读**;
**而直播流是「顺序读」的,起始码更省事、还能做字节对齐重同步。**
mp4 里第一个视频包:**873 字节**
前八个字节:**00 00 02 A5 06 05 FF FF**
**按 AVCC 的规矩读它:前四字节是长度,跳过去再读四字节**。
**一个包可以被切好几段,一段就是一个 NAL。**
段 偏移 长度 NAL 类型
-------------------------------------------------
第 1 段 0 677 6:SEI 补充增强
第 2 段 681 59 5:IDR 图像分片
第 3 段 744 60 5:IDR 图像分片
第 4 段 808 61 5:IDR 图像分片
三段加起来 **857 + 4 × 4 = 873 字节**,跟包长对上了。
**这张表说明「一个包 ≠ 一个 NAL」**。
**SPS 与 PPS 是解码器必须先拿到的东西**,**它们就跟第一个图像分片挤在同一个包里。**
**「前四字节是长度」这句话就是 AVCC 的全部**。
**转 AnnexB 就是「把这四字节换成 `00 00 00 01`」**——**长度信息被丢掉,换成起始码。**
**手工转一遍,看要换几处**:
这个包里一共 **4 个 NAL**
转出来的字节数:**873**(原 873)
前八个字节:**00 00 00 01 06 05 FF FF**
**注意字节数几乎没变**:**4 字节长度换成 4 字节起始码,长度一样。**
**所以这个转换不是「压缩」,是「换标记」。**
**这就是 `h264_mp4toannexb` 这个 bsf 的全部工作。**
**它为什么非做不可**:**因为 RTMP、TS、RTP 这些传输格式要的是 AnnexB**,**而 mp4 里存的是 AVCC。**
**差这一层,远端收到的是一堆「长度前缀」,解复用器找不到起始码,直接判成坏流。**
**这个坑的典型症状**:**本地放 mp4 一切正常,一推流对方就黑屏。**
**因为本地播放器读的是 mp4 的索引,根本不需要起始码。**
「前八个字节 00 00 02 A5 06 05 FF FF」这行是本节的核心证据:00 00 02 A5 就是 677,
06 的低五位是 6,也就是 SEI。
两个字段一读,AVCC 的结构就落地了。
注意第一段是 SEI,不是图像,所以「包里的第一段就是帧数据」这个默认假设是错的。 这种写法在小片子上不报错、在大片子上必崩,最难查。
顺序也值得看一眼:SEI → IDR → IDR → IDR。 打完头的补充信息,后面三段全是同一帧的切片,整包最前面的东西不是参数集,而是补充信息。
那 SPS 和 PPS 去哪了:mp4 把它们放在 stsd 里的 avcC 盒子里,
码流里不必再带——这也是为什么「从 mp4 抽出来的裸流」常被老播放器判成缺参数。
换到直播场景就完全不同:RTMP 和 TS 都没有 stsd,
FFmpeg 推流时自动挂上 h264_mp4toannexb,顺手把 avcC 里的 SPS/PPS 拼成 NAL 塞到流开头。
这一层你平时看不见,但它一直在跑。
转出来字节数不变这一点,是「无损」的直接证据。
如果哪天你看到转换后字节数少了一大截,那说明代码丢东西了——通常是把 ln <= 0 的段跳过了,而那种段往往是有意义的。
30 秒实验
- 把
00 00 00 01换回长度前缀,验证是等长的(这就是反向的 annexb2mp4)。 - 算一下:一个 1000 字节的包里有 3 个 NAL,转成 AnnexB 后是多少字节?
- 判断一下:抓包看到流的开头是
00 00 02 A5,这是 AVCC 还是 AnnexB?
常见误区
- 以为 mp4 里存的是 AnnexB。 mp4 存 AVCC(4 字节长度),推流要 AnnexB(起始码),中间必须转。
- 以为一个包里只有一个 NAL。 实测第一个包里有四段,而且第一段是 677 字节的 SEI, 图像被切成了三片。
- 以为参数集一定在码流开头。 mp4 把 SPS/PPS 放在
stsd的avcC里, 码流的第一个包可以完全没有它们。 - 手工推流时忘了挂 bsf。 FFmpeg 命令行会自动加,自己写代码不加,远端直接判成坏流。
自测三问(点开对答案)
1. AVCC 与 AnnexB 的区别是什么? —— AVCC 在每个 NAL 前放 4 字节长度,用于 mp4 这种可随机访问的容器; AnnexB 放 `00 00 00 01` 起始码,用于文件裸流和直播传输。 2. 为什么 `h264_mp4toannexb` 是等长替换? —— 因为 4 字节长度正好换成 4 字节起始码。它只换标记,不动码流内容,所以无损且可逆。 3. 推流黑屏但本地播放正常,最可能是什么问题? —— 忘了做 AVCC 到 AnnexB 的转换。 本地播放器读 mp4 索引不需要起始码,服务端拿不到起始码就判成坏流。码流这一层解决了,下一节回到容器。mp4 有个「索引在文件尾」的老毛病——它让「边下边播」变成了一件需要额外处理的事。
- ☐ (本节完,下一节 07.02.04 MP4格式的faststart快速播放模式)