一、MP4 的基本结构
MP4 由一个个 Box(也叫 Atom)组成,每个 Box 有类型和大小。
┌──────────────────────────────────────────┐
│ ftyp box(文件类型) │
│ 标识这是 MP4 文件,版本号 │
├──────────────────────────────────────────┤
│ moov box(元数据头,非常重要) │
│ ├── mvhd(电影头:时长、时间基) │
│ ├── trak(视频轨道) │
│ │ ├── tkhd(轨道头:分辨率) │
│ │ └── mdia(媒体信息) │
│ │ ├── mdhd(媒体头:语言) │
│ │ ├── hdlr(处理类型:vide) │
│ │ ├── minf(媒体信息) │
│ │ └── stbl(样本表——关键!) │
│ │ ├── stsd(编码器配置:H.264) │
│ │ ├── stts(帧时间戳) │
│ │ ├── stsc(帧分组) │
│ │ ├── stsz(每帧大小) │
│ │ └── stco(每帧在文件中的偏移) │
│ └── trak(音频轨道,类似结构) │
├──────────────────────────────────────────┤
│ mdat box(媒体数据) │
│ 实际的 H.264 码流 + AAC 码流 │
│ 按帧顺序排列,stco 告诉你每帧在 mdat 中的位置 │
└──────────────────────────────────────────┘
二、moov box 的位置——影响"秒开"的关键
moov 在文件头:
[ftyp][moov][mdat...]
播放器先读到 moov,知道所有帧的位置 → 可以直接跳到任意位置播放
适合"点播"场景(用户从头看)
moov 在文件尾(编码器默认行为):
[ftyp][mdat...][moov]
编码器写入时还不知道总帧数和每帧大小 → 先写 mdat,最后写 moov
播放器必须下载完整个文件才能读到 moov → 不能边下边播
对直播/流式播放不友好
解决方案(faststart):
ffmpeg -i input.mp4 -movflags faststart output.mp4
编码完后把 moov 从文件尾搬到文件头
三、手写解析 MP4 文件头(简化版)
#include <cstdio>
#include <cstdint>
#include <cstring>
#pragma pack(push, 1)
struct MP4BoxHeader {
uint32_t size; // Box 总大小(含 header)
uint32_t type; // 4 字符类型,如 'ftyp', 'moov', 'mdat'
};
#pragma pack(pop)
void print_box_type(uint32_t type) {
char t[5] = {
(char)((type >> 24) & 0xFF),
(char)((type >> 16) & 0xFF),
(char)((type >> 8) & 0xFF),
(char)(type & 0xFF),
'\0'
};
printf("%s", t);
}
void parse_mp4_boxes(const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) { perror("open"); return; }
printf("=== MP4 Box 结构 ===\n");
int depth = 0;
while (true) {
MP4BoxHeader hdr;
if (fread(&hdr, sizeof(hdr), 1, f) != 1) break;
if (hdr.size == 0) break; // 最后一个 box
for (int i = 0; i < depth; i++) printf(" ");
printf("+ ");
print_box_type(hdr.type);
printf(" size=%u", hdr.size);
if (hdr.type == 0x66747970) { // 'ftyp'
char brand[5] = {};
fread(brand, 4, 1, f);
printf(" brand=%s", brand);
}
printf("\n");
// 递归进入容器 box(moov、trak、mdia 等)
uint32_t container_types[] = {
0x6D6F6F76, // 'moov'
0x7472616B, // 'trak'
0x6D646961, // 'mdia'
0x6D696E66, // 'minf'
0x7374626C, // 'stbl'
0x65647473, // 'edts'
0x75647461, // 'udta'
};
bool is_container = false;
for (auto ct : container_types) {
if (hdr.type == ct) { is_container = true; break; }
}
if (is_container) {
depth++;
// 容器 box 的 header 后面就是子 box
// continue reading
} else {
// 跳过这个 box 的数据(header 已读,skip 剩余部分)
int remaining = hdr.size - sizeof(MP4BoxHeader);
if (remaining > 0) fseek(f, remaining, SEEK_CUR);
}
// 如果 is_container,子 box 的读取在下一轮 while 循环中继续
if (!is_container && depth > 0) {
// 检查是否回到父 box 的末尾
// 简化处理:没有很好地处理 box 嵌套边界
// 真实场景需要跟踪每个 box 的结束位置
}
}
fclose(f);
}