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 秒实验
- 看盒子列表:
ffprobe -v trace 片子.mp4 2>&1 | grep type能看到盒子名的踪迹。 - 数偏移:用十六进制工具跳到 mdat 的偏移处,能看到画面数据的开头。
- 看 moov 大小:
ffprobe -show_format里的 size 减去 mdat 大约就是索引开销。
常见误区
- 以为 moov 在文件头才正常。默认转封装出来它在尾部,那是「边写边算」的必然结果。
- 以为 faststart 会重编码。它只挪索引,数据区一个字节都不动。
- 解析时不判剩余长度。末尾不足 8 字节时会解出荒唐的盒子大小,然后死循环。
自测三问(点开对答案)
1. MP4 的每个盒子开头几个字节是什么? —— 4 字节长度加 4 字节类型,长度是包含自己在内的整个盒子大小。 2. 为什么默认转封装会把 moov 写在文件尾? —— 因为索引要记录每帧的偏移,而偏移要等数据写完才知道,边写边算是省内存的做法。 3. moov 在尾部对网络播放有什么影响? —— 播放器必须拿到文件尾才能读到索引,所以没法边下边播,只能下完再放。- ☐ (本节完,下一节 4.4.2 faststart 参数介绍)