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

faststart 只做一件事:把 moov 挪到 mdat 前面。 字节数几乎不变,因为它只是重排,不是重压。

📝 本节和前面几节的关系

4.4.1 看清了默认顺序的毛病,这一节就是那台手术。 也是第四章的收尾:所有盒子的知识,最后都落到「这个文件能不能边下边看」。

一、两次转封装,只差一个参数

同一份素材,一次默认、一次加 faststart:

文件 字节 盒子顺序


默认转封装 37982 ftyp → free → mdat → moov 加 faststart 37982 ftyp → moov → free → mdat

字节完全相同,只有顺序变了——这是「重排不重压」最直接的证据。 moov 从第 4 位挪到第 2 位,mdat 被推到后面去了。

命令行里就是 -movflags +faststart,注意前面有个加号。 这个参数是 MP4 封装器私有的,别的盒子没有对应的东西。

二、它到底做了什么

三步,全在文件层面,不碰码流:

步骤 做什么 代价


1 先按常规写完整个文件 同普通转封装 2 把 moov 单独留在内存或临时区 索引有多大就占多少 3 最后整体重写一遍,把 moov 放最前 多一次全文件写盘

第三行是唯一真实的代价:文件要重写一遍。 所以大文件加 faststart 会明显变慢,尽管它没有重编码。

这也解释了为什么「字节不变但耗时变长」——它在磁盘上多做了一遍搬运。 很多人以为 faststart 很轻,其实它对 I/O 是有实际要求的。

三、什么时候该加,什么时候别加

判断只有一句话:这个文件是「边下边看」还是「下完再看」。

场景 要不要 理由


网页播放 要 浏览器边下边播 手机端拖动 要 要能跳到任意位置 本地存档 不必 整份在硬盘上,索引在哪都一样 还要再转码 不必 下一趟会被整个重写

第三四行是最常见的浪费:本地素材加了 faststart,白多一次全文件重写。 尤其是「先转 mp4 再转别的」这种流水线,中间那次加了完全没用。

所以这条参数应该放在「流水线的最后一步」,而不是每一次转封装都加。 这也是第四章的收尾结论:所有盒子知识最后都落到「给谁看、怎么看」。

四、动手 Demo:加一个 movflags,看 moov 从第 4 位挪到第 2 位

同一份素材转两次:一次默认,一次在输出容器上带 movflags=faststart。 然后各自再数一遍顶层盒子,对比顺序与字节。

import av, os, struct

def remux(dst, opts):
    c0 = av.open(u"m4.mp4")
    c1 = av.open(dst, u"w", format=u"mp4", options=opts)
    mp = {s.index: c1.add_stream_from_template(s) for s in c0.streams}
    for pk in c0.demux():
        if not pk.size:
            continue
        pk.stream = mp[pk.stream.index]
        c1.mux(pk)
    c1.close()
    c0.close()

remux(u"plain.mp4", {})
remux(u"fast.mp4", {u"movflags": u"faststart"})

def order(p):
    out, off = [], 0
    total = os.path.getsize(p)
    with open(p, u"rb") as f:
        while off + 8 <= total and len(out) < 8:
            f.seek(off)
            h = f.read(8)
            size = struct.unpack(u">I", h[:4])[0]
            out.append(h[4:8].decode(u"latin-1"))
            off += size
    return out

for p in (u"plain.mp4", u"fast.mp4"):
    print(u"%-10s %d 字节  %s"
          % (p, os.path.getsize(p), u" → ".join(order(p))))

实跑输出:

--------------------------------------------------------------------
4.4.2 faststart——把索引挪到最前面
--------------------------------------------------------------------

**① 同一份素材,带不带 faststart 转两遍**

  文件             字节      盒子顺序
  --------------------------------------------------------
  默认转封装       37982     ftyp → free → mdat → moov
  加 faststart     37982     ftyp → moov → free → mdat

**② 两个文件的字节数几乎一样,顺序却不一样**

  项              默认      faststart
  ---------------------------------------
  moov 排第几     4         2
  mdat 排第几     3         4
  字节            37982     37982

**第三行的两个数几乎相等,说明 faststart 只是「重排」:**
**把索引写到一个临时位置,最后再整体搬到文件头,数据区一个字节没重压。**

**③ 什么时候必须加,什么时候加了没意义**

  场景                   要不要 faststart
  -----------------------------------------------
  网页播放、边下边播     要
  手机端快进拖动         要
  本地存档、剪辑素材     不必
  准备再转一次码         不必,下一趟会被重写

**判断标准只有一句:这个文件是「边下边看」还是「下完再看」。**
**前者必须加,后者加了只是白白多一次文件重写。**

options={"movflags": "faststart"} 是容器级选项, 对应命令行的 -movflags +faststart;这里是字典,所以不用写那个加号。

两个文件字节完全相同(37982),顺序却是 mdat → moov 对 moov → mdat。 字节相同这件事本身就排除了「重编码」的可能。

order() 里那个 limit=8 是防死循环的小保险: 一旦遇到长度字段为 0 的坏盒子,没有上限就会一直读下去。

30 秒实验

  1. 转一次:ffmpeg -i 片子.mp4 -c copy -movflags +faststart 出.mp4。
  2. 验顺序:用十六进制工具看前 64 字节,moov 应出现在 mdat 之前。
  3. 对比大小:两个文件大小几乎相同,说明没有重编码。

常见误区

  1. 以为 faststart 能减小体积。它只重排,字节几乎不变,甚至会略大几个字节。
  2. 在流水线中间就加 faststart。后面还要转码的话,这次重写完全是白费。
  3. 以为对所有格式都有效。它是 MP4 封装器私有选项,别的盒子没有对应参数。
自测三问(点开对答案) 1. faststart 改了文件的什么? —— 只改顶层盒子的顺序,把 moov 从 mdat 后面挪到前面。 2. 为什么字节数几乎不变? —— 因为只是重排,没有任何数据被重新编码,大小自然不变。 3. 它的真实代价是什么? —— 文件要整体重写一遍,大文件时 I/O 开销明显,尽管不耗 CPU 编码。

相关笔记