3、Box结构详解:Box头、大小、类型、用户数据、容器Box

核心原理:一切皆Box,Box是MP4的“原子”

MP4文件本质上是一个层级嵌套的Box(或称Atom)序列。整个文件就是一个大的Box(通常为ftyp或moov),其内部又包含若干子Box,子Box还可以继续嵌套。理解Box结构是解析MP4的基石——无论是时长、分辨率、编码参数还是音视频样本数据,全部以Box的形式存储。

每个Box由两部分组成:Box头(Header)和Box数据(Payload)。Box头描述“这个Box有多大、是什么类型”,Box数据则承载实际内容。Box头的基本结构如下:

偏移量  长度(字节)  字段        说明
0       4          size        整个Box的大小(含头)
4       4          type        Box类型(如moov、trak、mdat)
8       …          payload     Box数据(可能包含子Box或具体字段)

当size == 1时,表示Box大小需要扩展:在type字段后追加一个8字节的largesize字段,用于存储超过4GB的超大Box(常见于mdat媒体数据Box)。当size == 0时,表示该Box一直延伸到文件末尾(通常用于最后一个mdat)。

Box头的最小尺寸为8字节(size + type),但某些Box(如uuid)会额外携带16字节的UserType,此时头部为24字节。解析时必须先读取前8字节,再根据type和size值决定后续读取策略。

Box类型与用户数据(UUID Box)

Box的type字段是4个ASCII字符,用于标识Box的语义。常见的Box类型包括:

  • ftyp:文件类型与兼容性声明,位于文件起始位置。
  • moov:容器Box,存放所有元数据(trak、mvhd等)。
  • mdat:媒体数据Box,存放实际的音视频样本帧。
  • trak:轨道容器,描述一条音/视频轨道的属性。
  • mdia:媒体信息容器,包含mdhd、hdlr、minf等。
  • stbl:样本表容器,包含stts、stsc、stsz、stco等关键索引。

除了标准类型,MP4规范允许扩展自定义Box,通过uuid类型实现。此时Box头结构变为:

size (4字节) | type='uuid' (4字节) | usertype (16字节) | payload

usertype是一个16字节的全局唯一标识符(GUID),用于区分不同的自定义扩展。例如,Apple的com.apple.quicktime.*系列元数据就通过UUID Box存储。解析时若遇到uuid,必须跳过或读取这16字节的UserType,否则后续Box偏移会全部错乱。

容器Box与叶子Box

根据是否包含子Box,Box可分为两类:

类别特点典型示例
容器Boxpayload中不直接存储数据,而是嵌套若干子Boxmoov、trak、mdia、minf、stbl、edts
叶子Boxpayload中直接存储具体字段或数据,不再包含子Boxmvhd、tkhd、mdhd、hdlr、stts、stsz、mdat

容器Box的解析逻辑是:读取头部后,进入payload区域,将其视为一个新的Box序列,循环解析每个子Box,直到子Box的总长度等于父Box的payload长度。这种递归结构使得MP4可以灵活表达复杂的媒体信息。

方法流程:手写解析器的核心步骤

解析一个MP4文件时,通常采用递归下降的方式遍历Box树。以下是一个典型的解析流程:

  1. 打开文件,读取前8字节,解析出size和type。
  2. 若size == 1,继续读取8字节的largesize;若size == 0,则Box延伸至文件末尾。
  3. 根据type判断是否为容器Box(如moov、trak)。若是,则进入payload区域,递归执行步骤1~4。
  4. 若是叶子Box,则根据其类型解析具体字段(如mvhd中的版本、时长、时间刻度)。
  5. 处理完当前Box后,将文件指针移动到起始偏移 + size处,继续解析下一个兄弟Box。

关键点在于:Box的size字段是绝对长度,包含头部自身。因此,子Box的解析范围不能超出父Box的边界。若子Box的size之和大于父Box的payload长度,说明文件损坏或解析逻辑有误。

实操建议:在调试解析器时,可以先用十六进制工具(如010 Editor或HxD)打开一个MP4文件,手动定位ftyp、moov、mdat三个关键Box的偏移和大小,验证你的解析逻辑是否正确。例如,ftyp通常位于文件偏移0处,其size一般为24或32字节。

案例场景:从MP4中提取H.264样本的Box路径

假设我们需要从MP4中提取H.264视频帧(样本),典型的Box嵌套路径如下:

文件
└── moov (容器)
    └── trak (容器,视频轨道)
        └── mdia (容器)
            └── minf (容器)
                └── stbl (容器)
                    ├── stsd (叶子:编码格式声明,如avc1)
                    ├── stts (叶子:样本时间戳增量表)
                    ├── stsc (叶子:样本到块的映射)
                    ├── stsz (叶子:每个样本的字节大小)
                    └── stco (叶子:每个块的起始文件偏移)
mdat (叶子:实际样本数据)

解析时,先通过trak中的tkhd判断轨道类型(视频/音频),再进入stbl读取stsc和stco,得到每个块(Chunk)的文件偏移。然后根据stsz中的样本大小,从mdat中按偏移切出每个样本。整个过程完全依赖Box头的正确解析——任何一层Box的size计算错误,都会导致后续所有偏移错位。

误区与复盘:Box解析中的常见陷阱

  • 误区1:忽略size==1的扩展长度。某些工具生成的MP4中,mdat超过4GB时会使用largesize。若只读取4字节size,会得到1,导致解析崩溃。必须判断size==1并读取后续8字节。
  • 误区2:将容器Box当作叶子Box处理。例如,直接读取moov的payload并尝试解释为具体字段,结果得到一堆乱码。正确做法是先判断type是否为已知容器,再递归解析子Box。
  • 误区3:未处理Box头中的版本与标志位。部分Box(如mvhd、tkhd)在payload开头有1字节version和3字节flags,version不同(0或1)会导致后续字段宽度不同(如时间戳从32位变为64位)。解析时必须先读取version,再决定字段布局。
  • 误区4:假设所有Box都是8字节对齐。虽然大多数Box是4字节对齐,但某些扩展Box可能包含非对齐数据。解析时应始终基于size字段进行偏移计算,而不是依赖固定步长。

复盘提醒:在解析stsc(样本到块映射)时,其条目是“每N个样本属于一个块”的压缩表示,而不是每个样本一条记录。初学者常误以为stsc条目数等于样本数,导致读取样本偏移时越界。务必理解stsc的“分段压缩”语义,结合stco和stsz才能正确还原每个样本的文件位置。

MP4 Box 结构总览 文件级 Box ftyp (文件类型) | moov (元数据容器) | mdat (媒体数据) moov (容器Box) 包含 mvhd、trak 等子Box trak (容器Box) 轨道信息,含 tkhd、mdia mdia (容器Box) 媒体信息,含 mdhd、hdlr、minf stbl (容器Box) 样本表,含 stsd、stts、stsc、stsz、stco mvhd (叶子Box) 时长、时间刻度 tkhd (叶子Box) 轨道宽度、高度 stsd (叶子Box) 编码格式 (avc1) stsz (叶子Box) 每个样本的大小 stco (叶子Box) 块的文件偏移 Box头结构 size (4B) type (4B) [largesize] [usertype] payload (子Box或数据) size==1 → largesize type=='uuid' → usertype size==0 → 到文件末尾 容器Box(含子Box) 叶子Box(直接存数据) 文件级Box

上图展示了MP4中典型的Box嵌套层级:moov作为顶层容器,向下递归包含trak、mdia、stbl等容器Box,最终到达stsd、stsz等叶子Box。右侧同时标出了Box头的可变结构——当size==1时需读取largesize,当type=='uuid'时需读取16字节的usertype。理解这一递归模型后,解析任何MP4文件都只需按部就班地读取头部、判断类型、递归进入payload即可。