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

同一份素材转 AVI:音频成功,视频失败。 失败的原因不是「AVI 不支持 H.264」,而是「写法不对」。

📝 本节和前面几节的关系

4.3.1 换盒子很顺,这一节会撞墙——撞的正好是 4.2.2 讲过的那个墙。 这一节把「格式不支持」这句话拆开看。

一、一次失败,一次成功,差在哪

同一条命令,只换「转哪条流」,结果完全不同:

转哪条流 结果 字节或原因


只转音频 成功 40156 只转视频 失败 InvalidData

第二行那个 InvalidData 一句话都不解释,但原因很具体: AVI 要求视频流用起始码(Annex B),而 MP4 里存的是长度前缀(AVCC)。

所以「AVI 不支持 H.264」这句话是错的——AVI 能装 H.264,只是要 Annex B 写法。 加上 h264_mp4toannexb 过一遍,立刻就写进去了。

二、加一个位流滤镜就过了

两行只差一个滤镜:

做法 结果 字节


直接转视频 失败 — 加 annexb 滤镜后转 成功 37136

这就是「格式不支持」这句话要拆开看的原因:不支持的往往是「这种写法」。 分辨方法很简单——看报错是「不认这个名字」还是「不认这份数据」。

命令行里对应 -bsf:v h264_mp4toannexb,位置在输出侧。 这一条在把 mp4 喂给老设备、老软件时几乎是必备的。

三、AVI 的外壳与它的两个包袱

AVI 属于 RIFF 家族,头 12 个字节就能认出来:

位置 内容 含义


0~4 RIFF 这是 RIFF 家族的容器 8~12 AVI 加一个空格 具体是 AVI 这一种

RIFF 是 1992 年的格式,那个年代还没有「流」这个概念,只有「块」。 所以 AVI 里视频块和音频块是交替堆着的,时间戳粒度也比 MP4 粗。

包袱 表现


索引必须写在文件尾 录到一半断电,文件就废了 对 B 帧支持不好 老播放器可能花屏或卡顿

所以「转成 AVI 给老设备用」这件事,能成功不代表用得舒服。 真要给老设备,通常还要顺手把 B 帧关掉、把关键帧间隔调小。

四、动手 Demo:音频过得去,视频过不去,加滤镜就过去了

同一份素材,分别只转音频和只转视频到 AVI,看哪一半失败。 再给视频加一道 h264_mp4toannexb,看它从「拒绝」变成「写进去了」。

import av, os

def one(dst, keep):
    c0 = av.open(u"m4.mp4")
    c1 = av.open(dst, u"w", format=u"avi")
    mp = {s.index: c1.add_stream_from_template(s)
          for s in c0.streams if s.type == keep}
    for pk in c0.demux():
        if not pk.size or pk.stream.index not in mp:
            continue
        pk.stream = mp[pk.stream.index]
        c1.mux(pk)
    c1.close()
    c0.close()
    return os.path.getsize(dst)

for keep in (u"audio", u"video"):
    try:
        print(u"%s 成功 %d" % (keep, one(u"o_%s.avi" % keep, keep)))
    except Exception as e:
        print(u"%s 失败 %s" % (keep, type(e).__name__))

# 视频先过一遍 h264_mp4toannexb 再封装
c0 = av.open(u"m4.mp4")
c1 = av.open(u"ok.avi", u"w", format=u"avi")
st = c0.streams.video[0]
tgt = c1.add_stream_from_template(st)
bsf = av.BitStreamFilterContext(u"h264_mp4toannexb", st)
for pk in c0.demux(video=0):
    if not pk.size:
        continue
    for q in bsf.filter(pk):
        q.stream = tgt
        c1.mux(q)
c1.close()
c0.close()
print(u"加滤镜后 %d 字节" % os.path.getsize(u"ok.avi"))

实跑输出:

--------------------------------------------------------------------
4.3.2 MP4 转 AVI——失败的那一半正好是上一节讲的
--------------------------------------------------------------------

**① 直接转:音频成功,视频失败**

  转哪条流     结果     字节或原因
  -----------------------------------------------------
  只转音频     成功     40156
  只转视频     失败     InvalidData(拒绝这种写法)

**视频失败的原因不是「AVI 不支持 H.264」,而是「AVI 要求起始码,MP4 里存的是长度前缀」。**
**这正是 4.2.2 那个 `h264_mp4toannexb` 要解决的问题——先换写法,再封装。**

**② 先过一遍 h264_mp4toannexb,再转就成功**

  做法                   结果     字节
  -----------------------------------------
  直接转视频             失败     —
  加 annexb 滤镜后转     成功     37136

**两行只差一个位流滤镜,结果从「拒绝」变成「写进去了」。**
**所以「格式不支持」这句话要看清楚:不支持的往往是「这种写法」,不是「这种编码」。**

**③ AVI 的外壳是 RIFF,结构一目了然**

  位置     内容     含义
  ---------------------------------------
  0~4      RIFF     RIFF 家族的容器
  8~12     AVI      具体是 AVI 这一种

**RIFF 是 1992 年的格式,那个年代还没有「流」这个概念,只有「块」。**
**所以 AVI 里视频块和音频块是交替堆着的,时间戳粒度也比 MP4 粗。**

**④ AVI 的两个历史包袱**

  包袱                   表现
  ---------------------------------------------------
  索引必须写在文件尾     录到一半断电,文件就废了
  对 B 帧支持不好        老播放器可能花屏或卡顿

**所以「转成 AVI 给老设备用」这件事,能成功不代表能用得舒服。**
**真要给老设备,通常还要顺手把 B 帧关掉、把关键帧间隔调小。**

音频那一行成功、视频那一行抛 InvalidDataError,这就是本节的全部现象。 很多教程到这里就说「AVI 不支持 H.264」,那是把结论说反了——它支持,只是要 Annex B。

加滤镜那一段有两个细节:bsf.filter(pk) 返回列表要遍历, 每个输出包还要把 stream 改指向目标流,少一步都会报错。

结果 37136 字节,比原来的 mp4 小一点——因为 AVI 的索引比 MP4 的 moov 省。 这也顺带说明:换盒子时字节的差值是「记账方式」的差,不是画质的差。

30 秒实验

  1. 直接转:ffmpeg -i 片子.mp4 -c copy 出.avi,看它报什么。
  2. 加滤镜再转:ffmpeg -i 片子.mp4 -c:v copy -bsf:v h264_mp4toannexb 出.avi。
  3. 看头:xxd -l 16 出.avi,前四个字节应是 RIFF,8~12 是 AVI。

常见误区

  1. 以为 AVI 不支持 H.264。支持,只是要求 Annex B 写法,加个位流滤镜就行。
  2. 以为转成功了就能给老设备用。B 帧与索引位置这两个包袱还在,可能要额外关 B 帧。
  3. 把「格式不支持」当成一句话。要分清是不认编码、不认写法,还是不认参数组合。
自测三问(点开对答案) 1. MP4 转 AVI 时视频为什么会失败? —— AVI 要求视频用 Annex B 起始码写法,而 MP4 里存的是 AVCC 长度前缀,封装器拒绝。 2. 怎么让它成功? —— 先过一遍 `h264_mp4toannexb` 位流滤镜换成起始码写法,再封装。 3. AVI 的两个历史包袱是什么? —— 索引必须写在文件尾(中断就废),以及对 B 帧支持不好(老播放器可能花屏)。

相关笔记