🎬 第五章 FFmpeg命令行实现音视频转码
📄 一句话概括

同一个编码器、同一批帧,写进 AVI 比写进 MP4 大了一倍。 差的是封装层,不是编码器——比较效率必须在同一个容器里比。

📝 本节和前面几节的关系

第四章转封装到 AVI 时撞过 AVCC 的墙,这一节改用重编码,墙没了。 但新的问题来了:换个盒子,同样的编码体积会变。

一、三行对比:换编码与换容器

同一批帧,三种组合:

容器 编码 字节


AVI mpeg4 16708 AVI libx264 11038 MP4 libx264 5150

第一二行是同一个容器换编码:MPEG-4 比 H.264 大了一半,这就是换代的价值。 但老设备只认 MPEG-4 的时候,你只能接受这个代价。

第二三行更值得注意:同一个编码器、同一档参数、同一批帧,只换个容器,字节差一倍。 差的是封装层——AVI 没有放参数集的规范位置,编码器只能把参数集一路重发。

所以比较编码效率时,一定要在同一个容器里比,否则结论全被封装带偏。

二、重编码能进,搬包进不去

同一个 AVI、同一种编码,两种做法的结果完全相反:

做法 结果


重新编码成 H.264 成功 从 MP4 直接搬包 InvalidDataError

差别在「写法」:编码器吐出来的是起始码(Annex B), 而 MP4 里存的是长度前缀(AVCC)。AVI 只认前者。

所以搬包要过 h264_mp4toannexb 才成,重新编码反而不用管—— 因为编码器本来就是从 Annex B 开始写的。 这就是为什么「同一种编码」不等于「同一种数据」。

三、AVI 的两个老包袱

它当年很好用,现在只剩两个明显短板:

包袱 后果


索引放在文件末尾 没下完就不能拖进度 全局头不标准 扩展参数经常丢

第一条在本地播放无所谓,放到网络上就成了硬伤: 播放器必须先拿到整个文件尾部,才能算出每一帧在哪里。

第二条更坑:AVI 里的 H.264 常常缺 SPS/PPS 的规范位置,换个播放器就黑屏。 所以今天除非对方明确要求 AVI,否则不该主动选它。

四、动手 Demo:同一批帧写进 AVI 与 MP4,比字节

先用 MPEG-4 与 H.264 各写一个 AVI,再用同一个 H.264 写一个 MP4。 最后试一次「从 MP4 搬包进 AVI」,看它怎么拒绝。

import av, os

def to(dst, fmt, codec):
    c1 = av.open(dst, u"w", format=fmt)
    s1 = c1.add_stream(codec, rate=25)
    s1.width, s1.height, s1.pix_fmt = 160, 90, u"yuv420p"
    c0 = av.open(u"s5.mp4")
    for f in c0.decode(video=0):
        fr = f.reformat(width=160, height=90, format=u"yuv420p")
        for o in s1.encode(fr):
            c1.mux(o)
    for o in s1.encode(None):
        c1.mux(o)
    c1.close()
    c0.close()
    return os.path.getsize(dst)

print(u"avi mpeg4  %d" % to(u"m.avi", u"avi", u"mpeg4"))
print(u"avi h264   %d" % to(u"h.avi", u"avi", u"libx264"))
print(u"mp4 h264   %d" % to(u"h.mp4", u"mp4", u"libx264"))

try:
    c0 = av.open(u"s5.mp4")
    c1 = av.open(u"copy.avi", u"w", format=u"avi")
    mp = {s.index: c1.add_stream_from_template(s) for s in c0.streams}
    for pk in c0.demux(video=0):
        if pk.size:
            pk.stream = mp[pk.stream.index]
            c1.mux(pk)
    c1.close()
except Exception as e:
    print(u"搬包失败:%s" % type(e).__name__)

实跑输出:

--------------------------------------------------------------------
5.4.3 MP4 格式转码为 AVI
--------------------------------------------------------------------

**① MPEG-4 与 H.264 两条路各转一遍**

  容器     编码        字节
  ---------------------------
  AVI      mpeg4       16708
  AVI      libx264     11038
  MP4      libx264     5150

**第一二行是同一个容器换编码:MPEG-4 比 H.264 大了 1.5 倍,**
**这就是换代的价值——同样的画质,位数少一半。**
**但老设备只认 MPEG-4 的时候,你只能接受这个代价。**

**第二三行更值得注意:同一个编码器、同一档参数、同一批帧,**
**只换个容器,字节就从 5150 变成 11038。**
**差的是封装层:AVI 没有放参数集的规范位置,编码器只能把参数集一路重发。**
**所以比较编码效率时,一定要在同一个容器里比,否则结论全被封装带偏。**

**② 同一个 AVI,搬包就进不去**

  做法                 结果
  --------------------------------------
  重新编码成 H.264     成功,11038 字节
  从 MP4 直接搬包      InvalidDataError

**同一个容器、同一种编码,一个能进一个不能进——差别在「写法」:**
**编码器吐出来的是起始码(Annex B),而 MP4 里存的是长度前缀(AVCC)。**
**AVI 只认前者,所以搬包要过 `h264_mp4toannexb` 才成,重新编码反而不用管。**

**③ AVI 的两个老包袱**

  包袱                 后果
  ----------------------------------------
  索引放在文件末尾     没下完就不能拖进度
  全局头不标准         扩展参数经常丢

**第二条尤其坑:AVI 里的 H.264 常常缺 SPS/PPS 的规范位置,换个播放器就黑屏。**
**所以今天除非对方明确要求 AVI,否则不该主动选它。**

三个文件用的是同一批解码帧,所以三行字节的差别只能来自两个变量:编码与容器。

add_stream_from_template 会原样抄走编码参数与附加数据。 抄不走的是「写法」:MP4 里的长度前缀到了 AVI 里没人认。

如果真要把 MP4 的 H.264 搬进 AVI,得先过 h264_mp4toannexb 位流滤镜。 这一步在 4.2.2 里讲过,代码里对应 BitStreamFilterContext。

30 秒实验

  1. 命令行对照:ffmpeg -i 片子.mp4 -c:v mpeg4 出.avi。
  2. 只换盒子:-c copy,看 InvalidData。
  3. 加滤镜:-c copy -bsf:v h264_mp4toannexb,这次能成。

常见误区

  1. 跨容器比编码效率。AVI 里同样的 H.264 比 MP4 大一倍,比出来的结论是假的。
  2. 以为「H.264 不能放 AVI」。能放,只是要 Annex B 写法。
  3. 拿 AVI 做网络分发。索引在末尾,必须下完才能拖。
自测三问(点开对答案) 1. 为什么同样的 H.264 写进 AVI 比 MP4 大? —— AVI 没有放参数集的规范位置,编码器只能把 SPS/PPS 一路重发。 2. 为什么重编码能进 AVI,搬包反而不能? —— 编码器吐的是起始码写法,MP4 里存的是长度前缀写法,AVI 只认前者。 3. AVI 最不适合什么场景? —— 边下边播、在线拖动。它的索引在文件末尾,必须整份拿到才能定位。

相关笔记