文档目录

一、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);
}