MP4 文件结构深度解析:Box 结构、ftyp、moov、mdat、moof
MP4 文件,说白了就是一个「盒子套盒子」的嵌套结构。每个盒子(Box)都有自己的类型、大小和职责。我刚开始接触 MP4 时,总觉得它就是个普通的视频文件,直到有一次在流媒体服务器上遇到一个「只播放声音不出画面」的诡异问题……排查到最后,才发现是 moov box 里的 track 信息写错了。
嗯,从那以后,我再也不敢小看这些盒子了。
2.1 Box 基础结构:一切皆盒子
MP4 文件由一系列 Box 组成。每个 Box 都有一个固定的头部格式:
typedef struct {
uint32_t size; // Box 总大小(包含头部和数据)
uint32_t type; // Box 类型,如 'ftyp', 'moov', 'mdat'
uint8_t data[]; // 实际数据(可能包含子 Box)
} Box;
这里有个细节:size 字段是 4 字节无符号整数,最大只能表示 4GB。如果文件超过 4GB,就需要用 largeSize 扩展字段。我遇到过一些老旧播放器,它们只读 size,遇到大文件直接崩溃——这就是典型的「32 位兼容性陷阱」。
size 是否为 1。如果是 1,说明后面还有 8 字节的 largeSize。这个判断逻辑虽然简单,但漏掉它,整个解析流程就会跑偏。
2.2 ftyp box:文件的身份证
ftyp(File Type Box)永远是 MP4 文件的第一个 Box。它告诉解析器:这个文件是什么格式、兼容哪些标准。
| 字段 | 大小 | 说明 |
|---|---|---|
| major_brand | 4 字节 | 主品牌标识,如 'isom'、'mp42'、'avc1' |
| minor_version | 4 字节 | 次版本号,通常为 0 |
| compatible_brands | 可变 | 兼容品牌列表,每个 4 字节 |
举个例子,一个典型的 ftyp box 内容可能是:
major_brand: 'isom'
minor_version: 512
compatible_brands: ['isom', 'iso2', 'avc1', 'mp41']
这表示文件遵循 ISO Base Media File Format 标准,同时兼容 MPEG-4 Part 14 和 AVC 编码。我在做多平台兼容性测试时发现,有些 Android 设备只认 compatible_brands 里的第一个品牌——所以顺序很重要。
2.3 moov box:文件的目录
moov(Movie Box)是整个 MP4 文件的「目录」或「索引」。它不包含音视频数据本身,但记录了所有轨道(track)的元信息:时长、分辨率、编码参数、采样率、关键帧位置等。
moov box 内部包含多个子 Box,最重要的几个是:
- mvhd(Movie Header Box):全局信息,如时长、时间基、播放速率
- trak(Track Box):每个轨道一个,包含视频轨、音频轨、字幕轨等
- tkhd(Track Header Box):轨道头部信息,如轨道 ID、宽高、音量
- mdia(Media Box):媒体信息容器,包含采样描述、时间戳映射等
这里有个关键概念:moov box 的位置决定了文件的「流式友好度」。如果 moov 放在文件开头(称为「快速启动」或「moov first」),播放器可以立即获取元信息,实现边下边播。如果 moov 放在文件末尾(很多编码器的默认行为),播放器必须下载完整个文件才能开始播放。
实战经验:我在做视频点播平台时,要求所有上传的 MP4 文件必须将 moov box 前置。我们用 qt-faststart 工具或 FFmpeg 的 -movflags faststart 参数来实现。这个改动让首屏加载时间从 15 秒降到了 2 秒以内。
2.4 mdat box:真正的数据
mdat(Media Data Box)存放实际的音视频帧数据。它通常体积最大,占整个文件的 95% 以上。
mdat box 内部没有固定的结构——它只是一大块二进制数据。解析器需要根据 moov box 里的采样表(stbl)来定位每一帧的位置和大小。
采样表的结构大致如下:
stbl (Sample Table Box)
├── stsd (Sample Description Box) // 编码参数
├── stts (Time-to-Sample Box) // 时间戳映射
├── stsc (Sample-to-Chunk Box) // 采样到块的映射
├── stsz (Sample Size Box) // 每个采样的大小
└── stco (Chunk Offset Box) // 每个块在文件中的偏移
举个例子,如果 stco 告诉你「第 3 个块在文件偏移 102400 字节处」,stsc 告诉你「第 3 个块包含 50 个采样」,stsz 告诉你「每个采样的大小是 2048 字节」,那么你就可以精确地从 mdat 中提取出这 50 个帧。
2.5 moof box:分片文件的灵魂
moof(Movie Fragment Box)是「分片 MP4」(fMP4)的核心。它用于流媒体场景,比如 DASH 或 HLS 的 CMAF 封装。
传统的 MP4 文件只有一个 moov 和一个 mdat。而 fMP4 文件的结构是:
ftyp
moov (包含全局信息,但不包含采样表)
moof (第 1 个分片的元信息)
mdat (第 1 个分片的数据)
moof (第 2 个分片的元信息)
mdat (第 2 个分片的数据)
...
每个 moof box 内部包含:
- mfhd(Movie Fragment Header Box):分片序号
- traf(Track Fragment Box):每个轨道一个,包含该分片内的采样信息
- trun(Track Fragment Run Box):采样偏移、大小、时间戳等
这种结构的好处是:播放器可以逐个分片下载和播放,不需要等待整个文件下载完成。我在做直播转码服务时,就用 fMP4 实现了低延迟的 HLS 输出——每个分片只有 2 秒,播放器几乎可以实时播放。
2.6 核心结构关系图
下面这张图展示了 MP4 文件中各 Box 的层级关系和依赖路径:
2.7 总结:记住这几条
- ftyp 是身份证,告诉你是谁
- moov 是目录,告诉你怎么找数据
- mdat 是仓库,存放真正的音视频帧
- moof 是分片索引,用于流媒体场景
解析 MP4 文件时,我个人的习惯是:先读 ftyp 确认格式,再找 moov 的位置(如果 moov 在末尾,先跳到末尾读取),然后根据 stbl 里的偏移信息去 mdat 里提取帧数据。这个流程虽然简单,但每一步都有坑——比如 Box 大小溢出、品牌不兼容、采样表与数据不对齐等。
嗯,掌握了这些基础,后面我们就能深入讨论如何高效地解析和操作这些盒子了。