🎬 第七章 FFmpeg解码音视频及流媒体
📄 一句话概括

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 秒实验

  1. 把 00 00 00 01 换回长度前缀,验证是等长的(这就是反向的 annexb2mp4)。
  2. 算一下:一个 1000 字节的包里有 3 个 NAL,转成 AnnexB 后是多少字节?
  3. 判断一下:抓包看到流的开头是 00 00 02 A5,这是 AVCC 还是 AnnexB?

常见误区

  1. 以为 mp4 里存的是 AnnexB。 mp4 存 AVCC(4 字节长度),推流要 AnnexB(起始码),中间必须转。
  2. 以为一个包里只有一个 NAL。 实测第一个包里有四段,而且第一段是 677 字节的 SEI, 图像被切成了三片。
  3. 以为参数集一定在码流开头。 mp4 把 SPS/PPS 放在 stsd 的 avcC 里, 码流的第一个包可以完全没有它们。
  4. 手工推流时忘了挂 bsf。 FFmpeg 命令行会自动加,自己写代码不加,远端直接判成坏流。
自测三问(点开对答案) 1. AVCC 与 AnnexB 的区别是什么? —— AVCC 在每个 NAL 前放 4 字节长度,用于 mp4 这种可随机访问的容器; AnnexB 放 `00 00 00 01` 起始码,用于文件裸流和直播传输。 2. 为什么 `h264_mp4toannexb` 是等长替换? —— 因为 4 字节长度正好换成 4 字节起始码。它只换标记,不动码流内容,所以无损且可逆。 3. 推流黑屏但本地播放正常,最可能是什么问题? —— 忘了做 AVCC 到 AnnexB 的转换。 本地播放器读 mp4 索引不需要起始码,服务端拿不到起始码就判成坏流。

码流这一层解决了,下一节回到容器。mp4 有个「索引在文件尾」的老毛病——它让「边下边播」变成了一件需要额外处理的事。

相关笔记