mp4 是一串「盒子」,索引盒子 moov 默认写在文件尾。
不搬到前面,网络播放就得下完整个文件才开始。
上一节在字节层面折腾了一圈,这一节回到「文件结构」。
faststart 是个很具体的优化:它只做一件事,却决定了一个网站的视频「点了能不能立刻播」。
这一节还会顺带建立「盒子」这个概念,后面看任何 mp4 相关的问题都用得上。
一、mp4 是一串盒子
mp4(ISO BMFF)的结构非常机械:每个盒子 = 4 字节长度 + 4 字节类型 + 内容,一层套一层。 所以「解析 mp4」在原理上就是「按这个格式一路读下去」。
| 盒子 | 装什么 | 为什么重要 |
|---|---|---|
ftyp |
文件类型与兼容品牌 | 告诉播放器「我是什么」 |
moov |
索引:几条流、什么编码、每帧在哪 | 没有它就没法播 |
mdat |
真正的音视频数据 | 占体积的绝大部分 |
free |
填充 | 给后续编辑留位置 |
关键点在于「moov 和 mdat 的先后」:默认封装是「先写数据、再补索引」,
因为编码的时候你并不知道索引要多大——得等所有帧都写完了才能算出来。
这个「先写后补」的顺序很方便,但对网络播放是灾难:
播放器必须先拿到 moov 才能知道「有几条流、每帧在哪」,而它在文件尾部——所以只能等下完。
二、faststart 只做一件事
-movflags +faststart 做的事只有一件:
写完文件之后,把 moov 从尾部搬到 ftyp 后面,并把里面所有偏移加一遍。
Demo 里两个文件的对比很直白:
| 版本 | 盒子顺序 | moov 偏移 |
起播要下 |
|---|---|---|---|
| 默认 | ftyp → free → mdat → moov |
6892 | 68.8% |
| faststart | ftyp → moov → free → mdat |
32 | 0.3% |
「起播要下」这一列就是全部意义:68.8% 对 0.3%, 在慢网络下是「等一分钟」和「立刻」的区别。
注意那个偏移 32 不是 0:ftyp 在前面占了 32 字节,所以 moov 紧跟在它后面。
这也说明 faststart 不是「把 moov 挪到最前面」,是「挪到 ftyp 后面」——ftyp 必须第一个,播放器靠它判断「这是不是 mp4」。
顺带说那个 free 盒子:它是「占位填充」,
faststart 挪完 moov 之后原来的位置会空出来,就用 free 填上。
所以两个文件大小完全一样(都是 10017 字节)——搬移不改变体积,只改顺序。
三、什么时候值得,什么时候不值
这个操作的代价要讲清:它要「多读一遍、多写一遍」, 所以封装时间会变长,而且需要额外的临时空间。 对一个大文件,这个代价不小。
| 场景 | 要不要 faststart | 原因 |
|---|---|---|
| 网络点播 | 要 | 能立刻起播 |
| 本地录像 | 不要 | 反正要等录完 |
| 直播切片(HLS) | 要 | 每个切片都要能快速起播 |
| 存档母版 | 不要 | 只求无损,不求起播 |
第三行值得一提:HLS 的每个 .ts 分片其实不需要 faststart(TS 没有 moov),
但如果是 fmp4 分片(CMAF),就必须。
所以「要不要 faststart」这个问题,答案是「看你的分发方式」。
还有一个更省事的替代方案:如果服务端支持 HTTP Range 请求,
播放器可以先请求「文件尾部最后几十 KB」拿到 moov,再回头请求数据段。
这就是很多播放器「不用 faststart 也能秒开」的原因。
但它依赖服务端,不如 faststart 稳。
所以工程上的建议是「两道保险都上」:压制时加 +faststart,服务端也开 Range。
前者保证文件本身没问题,后者能救「忘了加」的老文件。
四、动手 Demo:faststart 把索引从文件尾搬到文件头
这段用同样的源生成两个 mp4,一个默认、一个带 +faststart,然后解析顶层盒子的顺序与偏移。
解析代码只有十几行——因为 mp4 的结构就是「长度 + 类型」这么机械。
off = 0
while off < size:
f.seek(off)
sz, typ = struct.unpack(">I4s", f.read(8))
print(typ, off, sz)
off += sz
# 关键就看 moov 在什么位置
# 默认:ftyp -> free -> mdat -> moov
# faststart:ftyp -> moov -> free -> mdat
实跑输出:
--------------------------------------------------------------------
faststart 把索引从文件尾搬到文件头
--------------------------------------------------------------------
**mp4 的结构是「一串盒子」**,**其中最要紧的两个是 `mdat`(真正的数据)和 `moov`(索引)**。
**默认(不 faststart)**:共 10017 字节
box 顺序:**ftyp → free → mdat → moov**
`moov` 偏移 **6892**,要下完 **68.8%** 才能开始播
**faststart**:共 10017 字节
box 顺序:**ftyp → moov → free → mdat**
`moov` 偏移 **32**,要下完 **0.3%** 才能开始播
**默认那个文件的第二个盒子是 `free`**,**`moov` 落到最后面去了。**
**这意味着什么**:**播放器必须先拿到 `moov` 才知道「有几条流、每条流用什么编码、每帧在哪」**,
**所以它得先下完整个文件,再回头播。**
**在本地磁盘上看不出来(反正都读得到),一到网络就变成「转圈转到百分之百才开始」。**
**`+faststart` 做的事只有一件**:
**写完文件之后,把 `moov` 从尾部搬到 `ftyp` 后面,并把所有偏移加一遍。**
**它要多读一遍文件、多写一次**,所以封装会变慢一点——**但换来「边下边播」。**
**这个取舍在什么场景下值**:**网络点播值**(能立刻起播);**本地录像不值**(反正要等录完)。
**所以「压片给网站用」的流程里,`-movflags +faststart` 是标配的第二遍。**
两组输出的差别只有一行:盒子顺序变了,moov 的偏移从六千多变成 32。
而总字节数一模一样——这就是「搬移不改体积」的直接证据。
「要下完 68.8% 才能开始播」这个数要这么理解:它不是「下 68.8% 就够」,
而是「必须把 moov 下到,而它恰好在 68.8% 的位置」。
在真正的网络环境里,你还得考虑「下到 moov 之后,数据段还得重新请求」——所以实际体验比 68.8% 更差。
解析这段代码有个坑要提:盒子长度字段是 32 位,最大 4 GB,
所以大于 4 GB 的文件会用 size == 1 + 一个 64 位长度。
这段代码处理了这种情况(读 8 字节再解),写生产代码时不能省。
最后一个观察:free 盒子在两个文件里位置不同,但大小都在。
这说明「搬移」不是「删了再写」,是「原地换位置 + 留个空位」。
所以它确实需要能随机写的文件——推到管道里(比如直接推到 stdout)就没法 faststart 了。
30 秒实验
- 用
+faststart压一段更大的视频,看封装时间多了多少。 - 算一下:一个 100 MB 的文件,
moov在 90% 处,在 1 MB/s 的网络下要等多久才能起播? - 判断一下:本地录像的文件要不要加 faststart?为什么?
常见误区
- 以为 faststart 是「压缩」。 它只搬
moov的位置,文件大小完全不变。 - 以为
moov会挪到最前面。 它挪到ftyp后面,因为ftyp必须第一个。 - 所有文件都加 faststart。 要额外读写一遍,本地录像和存档母版没必要。
- 以为不加 faststart 就一定慢。 服务端支持 Range 请求时,播放器可以先去尾部取
moov。 但那是依赖服务端的保险。
自测三问(点开对答案)
1. mp4 的盒子结构是什么? —— 每个盒子是「4 字节长度 + 4 字节类型 + 内容」,可以嵌套。`moov` 存索引、`mdat` 存数据。 2. faststart 具体做了什么? —— 写完文件后把 `moov` 从尾部搬到 `ftyp` 后面,并把内部偏移全部加一遍, 让播放器一下就能拿到索引。 3. 为什么默认封装会把 `moov` 放在尾部? —— 因为编码时不知道索引会多大,必须等所有帧写完才能算出索引内容,所以只能「先写数据、 后补索引」。命令行这一层看完了,下面进入 API。先不写代码,先把「一次播放要经过哪八步」画出来——这个骨架后面会反复用。
- ☐ (本节完,下一节 07.03.01 FFmpeg播放流程简介)