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可分为两类:
| 类别 | 特点 | 典型示例 |
|---|---|---|
| 容器Box | payload中不直接存储数据,而是嵌套若干子Box | moov、trak、mdia、minf、stbl、edts |
| 叶子Box | payload中直接存储具体字段或数据,不再包含子Box | mvhd、tkhd、mdhd、hdlr、stts、stsz、mdat |
容器Box的解析逻辑是:读取头部后,进入payload区域,将其视为一个新的Box序列,循环解析每个子Box,直到子Box的总长度等于父Box的payload长度。这种递归结构使得MP4可以灵活表达复杂的媒体信息。
方法流程:手写解析器的核心步骤
解析一个MP4文件时,通常采用递归下降的方式遍历Box树。以下是一个典型的解析流程:
- 打开文件,读取前8字节,解析出
size和type。 - 若
size == 1,继续读取8字节的largesize;若size == 0,则Box延伸至文件末尾。 - 根据
type判断是否为容器Box(如moov、trak)。若是,则进入payload区域,递归执行步骤1~4。 - 若是叶子Box,则根据其类型解析具体字段(如mvhd中的版本、时长、时间刻度)。
- 处理完当前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嵌套层级:moov作为顶层容器,向下递归包含trak、mdia、stbl等容器Box,最终到达stsd、stsz等叶子Box。右侧同时标出了Box头的可变结构——当size==1时需读取largesize,当type=='uuid'时需读取16字节的usertype。理解这一递归模型后,解析任何MP4文件都只需按部就班地读取头部、判断类型、递归进入payload即可。