MP4 文件结构:ISO-BMFF 的 box 逐层拆解
播放器凭什么秒开两 GB 的文件
双击一个两 GB 的 MP4,播放器瞬间就能显示时长、定位到中间任何一秒——它并没有把文件从头扫到尾。秘密在于 MP4 不是「装视频流的袋子」这么简单,而是一棵精心组织的索引树:标准名叫 ISO-BMFF(ISO 基础媒体文件格式),文件里的一切信息都装在叫 box 的单元里。理解了 box,MP4 的所有内幕——时长、编码、分辨率、帧率、甚至手机拍摄时的旋转角度——都有确切的位置可查。
一切皆 box
每个 box 的开头都是同样的 8 个字节:4 字节大端整数 size,加 4 字节 ASCII 类型名。size 是整个 box(含这 8 字节头)的长度,读完一个 box 就知道下一个从哪里开始;type 是四个可打印字符,比如 ftyp、moov、mdat。
有两个特殊约定。size 等于 1 时,表示 32 位装不下这个 box,真正的长度写在紧随其后的 64 位 largesize 字段里;size 等于 0 时,表示这个 box 一直延伸到文件末尾——实践中通常只有顶层的 mdat 会这么用。顺带一提,mov 扩展名的 QuickTime 文件是这套格式的直系祖先,底层结构同源,解析器读 mov 用的也是同一套规则。
| |
容器 box(moov、trak、mdia、minf、stbl……)内部装的还是一串 box,递归展开就是整棵树;叶子 box 才装真正的字段。整个文件里有两个重量级角色:mdat 装压缩后的音视频数据本体,moov 装索引——播放器能不能秒开,几乎完全取决于 moov 的位置和完整性。
这套设计最大的好处是可扩展与可跳过:解析器遇到不认识的 box 类型,只要照着 size 跳过去就行,永远不会因为出现新字段而失效——MP4 能一路演进、容纳更新的编码器而不破坏旧播放器,靠的就是这条规则。代价则是索引与数据彻底分离:索引可以和数据隔得很远,moov 排在文件尾时,完整索引要等数据写完才有,这也埋下了后面分片结构的伏笔。
moov:一棵值得逐层拆解的索引树
moov 的第一个子 box 通常是 mvhd(movie header):timescale 与 duration 两个字段给出全局时间坐标系,秒数 = duration ÷ timescale——比如 timescale 为 1000、duration 为 90000,片长就是 90 秒。mvhd 有 version 0 和 version 1 两代布局,主要差异在创建时间与时长等字段是 32 位还是 64 位:v1 就是为超长时长准备的扩容,解析时必须先看版本再决定每个字段读多少字节。
每个媒体流对应一个 trak。trak 里的 tkhd(track header)给出这条流的显示宽高——注意它是 16.16 定点数:高 16 位是整数部分,低 16 位是小数部分,取整要右移 16 位(等价于除以 65536)。为什么用定点数而不是浮点?因为这个格式要被各种年代、各种字长的硬件读到,整数运算没有平台歧义。tkhd 里还藏着一个 3×3 旋转矩阵:手机竖拍的视频,像素在文件里仍是按横向存的,旋转信息就写在这个矩阵里,播放器读到 90 度才把画面转过来。很多人遇到过「电脑播放器里正常、网页里躺着播」的视频,根子多半就在这一步没有被正确处理。tkhd 之后的 hdlr 声明这条流是视频(vide)还是音频(soun),决定后面字段该按哪套布局解释。
真正回答「用什么编码」的是 stsd(sample description)。它的条目以 fourcc 开头——四个 ASCII 字符当一个枚举值用,这是从 QuickTime 时代沿袭下来的命名法:avc1 表示 H.264,hvc1 表示 H.265,mp4a 表示 AAC。H.264 条目里还有个 avcC 子结构,携带 profile 与 level——profile 决定用到了哪些压缩工具,level 约束分辨率与码率的上限,播放器靠它判断自己解得动解不动。时间与体量的账则在 stts 和 stsz 里:stts 存相邻样本的时间戳增量,把增量平均一下就得到平均帧率;stsz 存每个样本(对视频就是每帧)的大小与总数,帧数、平均帧大小都从这里来,码率估算也有了分母。
分片 MP4:moof 与另一种索引
传统 MP4 把整棵索引集中在 moov 里,录制结束后才有完整索引。流媒体和现代相机(包括很多手机的录制管线)改用分片结构 fMP4:音视频切成一段段 fragment,每段前面挂一个 moof(movie fragment),其内的 mfhd 声明片段序号,段内自带这段数据的索引。好处是边录边写、边下边播——录制开始时不知道总时长也不碍事,写完一段就有一段的索引;代价是只认传统结构的播放器或剪辑软件对它支持不佳——「有些视频进度条拖不动、一拖就崩」,根子常常就在索引结构上:进度条要找的索引不在它以为的地方。
为什么 MP4 总是压不小
理解了结构,「MP4 为什么压不小」就不难回答:视频多大,在编码那一刻就由码率定死了,容器(box 树)只是把编码结果原样打包并加上索引——mdat 里的数据一个比特都不会因为换个封装方式而变少。想让它变小,唯一正路是重编码:降码率、降分辨率,或者换更高效的编码器。所以「无损压小 MP4」的宣传值得警惕:能无损省下的只有封装层冗余,通常只有个位数的百分比。反过来,转封装(比如 mp4 换 mov)之所以快得惊人,正是因为 mdat 原样搬运、只重排 box 树——它快,但它也不会小。
动手之前,先看清手里的文件。🔧 MP4信息查看器 用数百行零依赖纯 JS 实现了上面整棵树——大端字节序读取、容器递归展开、对异常尺寸的安全处理——拖入文件就能看到 box 树、编码参数、时长帧率码率与分片标记,全程本机解析。先看参数、判断该降码率还是降分辨率,再到 🔧 视频压缩 里重编码,压完回头对比效果,心里有数。这些能力连同抽帧、拼接、变速,都收在视频创作工具箱专题里。