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

MP4 是盒子套盒子:顶层只有 ftyp、free、mdat、moov 几个。 关键是 moov 与 mdat 谁在前——它决定能不能边下边播。

📝 本节和前面几节的关系

4.3 一直在换盒子,这一节把最常用的那个盒子拆开看内部。 4.4.2 讲的 faststart,就是给这一节的顺序做手术。

一、顶层盒子,一眼看清

每个盒子都是「4 字节长度 + 4 字节类型」,所以能一路数下去:

盒子名 起始偏移 大小 放什么


ftyp 0 32 文件类型与兼容品牌 free 32 8 填充,方便后续修改 mdat 40 34891 真正的数据 moov 34931 3051 索引(每条流的表)

第三行的 mdat 占了 92%——这就是 4.1 里那个「92% 是包」的同一个数。 剩下 8% 里,moov 又占了大头,ftyp 与 free 只是零头。

所以一个 MP4 的结构可以背下来:开头一小段说明身份,中间一大坨数据,末尾一张目录。 这个「末尾」的位置,就是接下来所有问题的根源。

二、moov 在 mdat 后面,是默认行为

默认转封装出来就是这个顺序,不是 bug:

项 值


盒子顺序 ftyp → free → mdat → moov moov 在第几个 4 mdat 在第几个 3 能不能边下边播 不能

原因很朴素:写索引必须知道每一帧在文件里的偏移,而偏移要等数据写完才知道。 所以先写数据、最后回头写索引,是最省内存的做法。

代价是「必须拿到文件尾才能开始播」。本地播放完全没问题,网络点播就要命了—— 浏览器通常是边下边播,下载到 10% 时它还没拿到 92% 处的索引。

三、顺序决定能不能边下边播

同样是 mp4,两种顺序对应两种场景:

顺序 下载到 10% 时 适合


moov 在前 能算出每帧位置,直接开播 网络点播 moov 在后 得先下完整份才能读索引 本地播放

第一行就是所谓「faststart」:索引挪到最前面,播放器一进门就能拿到地图。 注意挪的只是索引,mdat 里一个字节都没动,所以这个操作很快。

判断一个 mp4 适不适合上网,最简单的办法就是看它前几个字节里有没有 moov。 有就是 faststart 过的,没有就得重新转一遍。

四、动手 Demo:数 MP4 的顶层盒子:从偏移 0 一路数到文件尾

按「4 字节长度加 4 字节类型」的规矩,从文件头一路读出每个顶层盒子。 读出来就能看清 moov 到底排在 mdat 前面还是后面。

import os, struct

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

for typ, off, size in boxes(u"m4.mp4"):
    print(u"%-6s 偏移 %6d  大小 %6d" % (typ, off, size))

实跑输出:

--------------------------------------------------------------------
4.4.1 MP4 格式简介——一个盒子套一个盒子
--------------------------------------------------------------------

**① 顶层只有几个盒子,位置一眼看清**

  盒子名     起始偏移     大小      放什么
  --------------------------------------------------------
  ftyp       0            32        文件类型与兼容品牌
  free       32           8         填充,方便后续修改
  mdat       40           34891     真正的数据
  moov       34931        3051      索引(每条流的表)

**② 关键看 moov 和 mdat 谁在前**

  项                      值
  -----------------------------------------------------
  盒子顺序                ftyp → free → mdat → moov
  moov 在第几个           4
  mdat 在第几个           3
  moov 在 mdat 之前吗     否

**这一份的 moov 排在后面,这不是 bug,是默认行为:**
**编码时先把 mdat 一路写完,最后才回头写索引——因为写索引要知道每一帧的偏移。**

**③ 这个顺序决定「能不能边下边播」**

  顺序          下载到 10% 时                适合
  -------------------------------------------------------
  moov 在前     能算出每帧位置,直接开播     网络点播
  moov 在后     得先下完整份才能读索引       本地播放

**这就是下一节 `-movflags faststart` 要解决的问题——把 moov 挪到前面去。**
**注意挪的只是索引,mdat 里一个字节都没动,所以转一遍很快。**

这段代码就是 MP4 解析器的雏形:off += size 一路往后跳,四个字段就够用。 真正的解析器要做的是「进到 moov 里再按同样规矩数子盒子」,逻辑完全一样。

注意 off + 8 <= total 这个条件:末尾可能只剩不足 8 个字节的填充, 不判就会读到半截头,解出一个荒唐的长度。

读出来是 ftyp、free、mdat、moov 四个盒子,顺序与本节第一张表一致。 mdat 那段 34891 字节占了 92%,正是「数据区」该有的体量。

30 秒实验

  1. 看盒子列表:ffprobe -v trace 片子.mp4 2>&1 | grep type 能看到盒子名的踪迹。
  2. 数偏移:用十六进制工具跳到 mdat 的偏移处,能看到画面数据的开头。
  3. 看 moov 大小:ffprobe -show_format 里的 size 减去 mdat 大约就是索引开销。

常见误区

  1. 以为 moov 在文件头才正常。默认转封装出来它在尾部,那是「边写边算」的必然结果。
  2. 以为 faststart 会重编码。它只挪索引,数据区一个字节都不动。
  3. 解析时不判剩余长度。末尾不足 8 字节时会解出荒唐的盒子大小,然后死循环。
自测三问(点开对答案) 1. MP4 的每个盒子开头几个字节是什么? —— 4 字节长度加 4 字节类型,长度是包含自己在内的整个盒子大小。 2. 为什么默认转封装会把 moov 写在文件尾? —— 因为索引要记录每帧的偏移,而偏移要等数据写完才知道,边写边算是省内存的做法。 3. moov 在尾部对网络播放有什么影响? —— 播放器必须拿到文件尾才能读到索引,所以没法边下边播,只能下完再放。

相关笔记