🎬 第四章 FFmpeg命令行实现音视频转封装
📄 一句话概括

MP4 里用长度前缀标帧边界,其它场合用起始码。 h264_mp4toannexb 干的就是把前者换成后者,一个字节都不重压。

📝 本节和前面几节的关系

4.2.1 把流拆了出来,这一节讲拆出来之后为什么常常「放不了」。 4.3.2 转 AVI 时还会撞上同一个问题。

一、两种写法,开头四个字节就看得出来

同一个编码,装在不同地方,写法可以不一样:

写法 开头是什么 用在哪


长度前缀(AVCC) 00 00 02 A0 这样的长度 MP4、MKV 起始码(Annex B) 00 00 00 01 裸流、TS、AVI

第一行那四字节是「这一帧有多少字节」,读起来就能直接跳到下一帧。 第二行那四字节只是一个记号,说「从这里开始是一帧」,长度得靠往后扫。

两种写法各有好处:AVCC 能随机访问,Annex B 能一路顺序扫。 所以 MP4 选了前者,广播与裸流选了后者——不是谁更先进。

二、转一遍就能看到变化

用位流滤镜过一遍,前四个字节立刻变了:

项 值


MP4 里首包前 8 字节 00 00 02 A0 06 05 FF FF 过一遍滤镜后 00 00 00 01 06 05 FF FF

后面那四个字节 06 05 FF FF 一模一样,只有开头被替换了。 这正好证明它是「改记号」而不是「重编码」——画面数据一个字节没动。

所以这个操作极快,几乎等于内存里搬一遍。 命令行里对应 -bsf:v h264_mp4toannexb,bsf 就是 bitstream filter。

三、不转会怎样

症状很有迷惑性:文件能生成、大小也正常,就是放不出来。

场合 要哪种 不转的后果


MP4 内部 AVCC — 存成 .h264 裸流 Annex B 播放器解不出第一帧 封进 MPEG-TS Annex B 打包器认不出帧边界 绕进 AVI Annex B 封装时直接报 InvalidData

第二行是最常见的坑:从 mp4 里抠出 h264 存成 .h264,双击放不了。 文件没坏,是写法不匹配——播放器按起始码找帧,一个都找不到。

第四行更干脆:AVI 的封装器直接拒绝,报 InvalidData。 报错比静默坏文件好得多,4.3.2 会拿这个当场演示一遍。

四、动手 Demo:首包前后各 8 个字节,对照两种写法

取 MP4 里第一帧的包,先看它原始的开头,再用位流滤镜换一遍写法。 只对比前 8 个字节,就能看出「长度前缀」怎么变成「起始码」。

import av

c = av.open(u"m4.mp4")
pk0 = None
for pk in c.demux(video=0):
    if pk.size:
        pk0 = pk
        break
print(u"原始 %s" % u" ".join(u"%02X" % b for b in bytes(pk0)[:8]))

bsf = av.BitStreamFilterContext(u"h264_mp4toannexb", c.streams.video[0])
pkts = bsf.filter(pk0)
out = pkts[0]
print(u"转换 %s" % u" ".join(u"%02X" % b for b in bytes(out)[:8]))
c.close()

print(u"位流滤镜总数 %d" % len(av.bitstream_filters_available))

实跑输出:

--------------------------------------------------------------------
4.2.2 h264_mp4toannexb——两种码流的写法差在哪
--------------------------------------------------------------------

**① 同一个包里,开头四个字节说明了写法**

  项                      值
  ---------------------------------------------------
  MP4 里首包前 8 字节     00 00 02 A0 06 05 FF FF
  这 4 个字节的含义       后面这一帧有多长

**MP4 用的是「长度前缀」:先写 4 个字节说这一帧多长,再写帧本身。**
**这种写法叫 AVCC,好处是能直接定位到任意一帧,不用扫描。**

**② 换成 Annex B:把长度前缀换成起始码**

  项                  值
  ----------------------------------------------------
  支持的位流滤镜      49 个(含 h264_mp4toannexb)
  转换后前 8 字节     00 00 00 01 06 05 FF FF

**转换后的开头变成 `00 00 00 01`——这就是起始码,一个「从这里开始是一帧」的记号。**
**Annex B 常见于裸流文件(.h264)与 TS 流,因为它能一路顺序扫下去。**

**③ 什么时候必须转**

  场合                要哪种写法            不转的后果
  ----------------------------------------------------------------
  MP4 内部            长度前缀(AVCC)      —
  存成 .h264 裸流     起始码(Annex B)     播放器解不出第一帧
  封进 MPEG-TS        起始码(Annex B)     打包器认不出帧边界

**这就是「把 MP4 里的 H.264 直接抠出来存成 .h264 会放不了」的原因。**
**它不是坏了,是写法不匹配——所以 ffmpeg 里那个 `-bsf:v h264_mp4toannexb` 才有存在的必要。**

bsf.filter() 返回的是一个列表而不是单个包:一个输入包有可能变成多个输出包。 所以取 pkts[0];真实代码里应该把整个列表都写出去。

构造滤镜时必须把输入流传进去:h264_mp4toannexb 需要读 SPS/PPS 才知道帧长怎么算。 只传名字不传流,它会初始化失败。

本机这份构建里有 49 个位流滤镜,最常用的三个是: h264_mp4toannexb、hevc_mp4toannexb、aac_adtstoasc(反向,把 ADTS 转成 ASC)。

30 秒实验

  1. 抠裸流:ffmpeg -i 片子.mp4 -c:v copy -bsf:v h264_mp4toannexb 出.h264,再双击试试。
  2. 抠音频 ADTS:ffmpeg -i 片子.mp4 -c:a copy -f adts 出.aac,这是另一个方向的转换。
  3. 看有哪些滤镜:ffmpeg -bsfs,输出就是那四十几个名字。

常见误区

  1. 以为 .h264 放不了是文件坏了。是写法(Annex B)与播放器预期不符,过一遍滤镜就好。
  2. 把 bsf.filter() 的返回值当单个包用。它返回列表,一个输入包可能变成多个输出包。
  3. 以为转一遍会掉画质。它只替换开头那 4 个字节,画面数据一个字节没动。
自测三问(点开对答案) 1. AVCC 与 Annex B 最直观的区别在哪? —— AVCC 用 4 字节长度前缀标帧长,Annex B 用 `00 00 00 01` 起始码标帧开始。 2. 为什么 MP4 选 AVCC? —— 因为长度写在前面,可以直接跳到任意一帧,适合随机访问与拖动进度条。 3. `h264_mp4toannexb` 会重编码吗? —— 不会。它只把长度前缀换成起始码,画面数据原样保留。

相关笔记