MP4 / ISO BMFF 参考手册
MP4 / ISO BMFF 参考手册
MP4 是 ISO Base Media File Format(ISO BMFF)的常见文件扩展和配置。它是容器,不是 H.264 或 AAC 编码本身;同一 MP4 可以承载不同编码,必须读取 sample entry 和配置记录确认实际格式。
本文以 ISO Base Media File Format 的 Box、Track、Sample、Chunk 和时间表模型为主线,分别说明普通 MP4 文件与 fragmented MP4 在索引和媒体数据组织上的差异。
第一章 ISO BMFF 与 Box 模型
Box 基础结构
普通 box:
1 | size (4 bytes) + type (4 bytes) + payload |
当 size 为 1 时,后面跟 64 位 largesize;size 为 0 表示延伸到所在容器末尾(通常只适用于特定场景)。解析器必须检查 box size 不越过父 box 边界。
顶层 Box
| Box | 作用 |
|---|---|
ftyp |
文件类型、brand 和兼容品牌 |
moov |
元数据、轨道、样本表和时间信息 |
mdat |
实际媒体样本数据 |
free/skip |
可忽略空间 |
moof/mfra |
Fragmented MP4 的分片元数据/索引 |
moov 可能位于 mdat 之后;网络渐进播放常通过 faststart 将 moov 移到文件前部。
ftyp
ftyp 包含 major_brand、minor_version 和 compatible_brands。brand 表示兼容性声明,不等于编码格式。播放器应根据实际 box 和 sample entry 判断能力,而不能只检查 isom、mp42 等 brand。
moov 层级
典型层级:
1 | moov |
第二章 Track、Sample 与编码描述
stsd 与编码配置
stsd(Sample Description Box)描述样本格式。视频常见 sample entry:avc1、avc3、hev1、hvc1;音频常见:mp4a、Opus 等。
H.264 的 avc1 sample entry 通常包含 avcC,其中保存 SPS/PPS 和 NAL length size;AAC 的 mp4a 下通常有 esds,其 DecoderSpecificInfo 中包含 AudioSpecificConfig。
因此:
1 | MP4 H.264 样本通常是 AVCC 长度前缀格式,不是 Annex-B。 |
第三章 Sample Table 与时间
样本表 stbl
| Box | 作用 |
|---|---|
stts |
解码时间增量(DTS) |
ctts |
合成时间偏移(PTS-DTS) |
stsc |
chunk 与 sample 的映射 |
stsz/stz2 |
每个 sample 的大小 |
stco/co64 |
chunk 文件偏移 |
stss |
随机接入关键样本 |
stsd |
编码描述和初始化配置 |
读取某个 sample 需要综合 sample size、chunk offset、stsc、stts 等表,不能把 mdat 当作固定大小帧序列。
时间戳
每条轨道有自己的 timescale。样本解码时间为累加 stts 的 delta;若存在 ctts,显示时间还要加 composition offset:
1 | DTS = decode_time / track_timescale |
视频 B 帧常导致 PTS 与 DTS 不同。跨音视频轨道同步时要换算到统一时间基。
关键帧与随机访问
stss 记录同步样本。没有 stss 时,某些文件约定所有样本可同步,但播放器不能简单把第一帧当关键帧。H.264 还应结合 IDR/CRA 等编码语义判断真正的解码入口。
第四章 Fragmented MP4 概述
分片文件模型
fMP4 将媒体拆为:
1 | init segment: ftyp + moov |
moof 包含 traf、tfhd、tfdt、trun 等分片样本信息。它适合 DASH、HLS/CMAF 和低延迟分段传输。接收端必须先获得初始化段,不能只从任意 mdat 开始解码。
MP4 与 AAC/H.264 转换
- MP4 AAC → ADTS:根据 ASC 生成每帧 ADTS 头,再拼接 raw AAC。
- ADTS → MP4:去除 ADTS 头,建立
mp4a/esds和样本表。 - MP4 H.264 → Annex-B:从
avcC取 SPS/PPS,把每个长度前缀替换为起始码。 - Annex-B → MP4:提取 SPS/PPS 建立
avcC,每个 NAL 改为长度前缀。
转换时还要保留时间戳和关键帧信息,不能只转换字节格式。
第五章 解析、写入与验证
解析安全性
- 每个 box 的 size 必须小于父容器剩余长度。
- 处理 64 位 size 和大文件偏移。
- 对未知 box 做安全跳过。
- 防止递归层级过深和整数溢出。
- 检查 sample offset + size 不超过文件长度。
常见错误
- 看到
.mp4就假定内部是 H.264/AAC。 - 将 MP4 中的 AAC sample 当成带 ADTS 的数据。
- 将 MP4 H.264 sample 直接送给只接受 Annex-B 的解码器。
- 只读取
mdat,忽略moov样本表。 - 只看 DTS,不处理 B 帧 PTS。
- 移动
moov后没有修正 chunk offset。
FullBox 结构
很多 ISO BMFF box 是 FullBox,payload 开头额外有:
1 | version (8 bits) + flags (24 bits) |
解析 mvhd、tkhd、mdhd、stts、ctts、trun 时必须先根据 version 选择 32 位或 64 位字段,不能固定按一种结构读取。
Movie、Track 与 Media Header 字段表
| Box | 关键字段 | 作用 |
|---|---|---|
mvhd |
creation、timescale、duration、rate、volume | 全局 movie 参数 |
tkhd |
track_ID、duration、width、height、matrix | 轨道身份和显示属性 |
mdhd |
timescale、duration、language | 轨道媒体时间基 |
hdlr |
handler_type | vide、soun 等轨道类型 |
version 0 的时间字段通常为 32 位,version 1 使用 64 位。duration 为 0 或特殊值时,可能表示未知或实时流。
stts 解码时间表
stts 由 (sample_count, sample_delta) 条目组成。每个条目表示连续若干样本拥有相同解码时间增量:
1 | decode_time[i+1] = decode_time[i] + sample_delta |
样本数量之和应等于该轨道 sample 数量;若不一致,说明文件损坏或解析层级错误。
ctts 显示时间表
ctts 保存 composition offset,用于把解码顺序映射到显示顺序。version 0 的 offset 通常按无符号处理,version 1 支持有符号 offset。B 帧文件中错误忽略 ctts 会出现画面跳动、音画不同步或首帧负时间。
stsc 与 chunk 映射
stsc 每项包含:
| 字段 | 说明 |
|---|---|
| first_chunk | 该映射起始 chunk |
| samples_per_chunk | 每个 chunk 的样本数 |
| sample_description_index | 使用哪个 stsd 描述 |
从第一个 chunk 到下一个 first_chunk-1 都使用当前映射。计算 sample 偏移时必须先找到 chunk,再在 chunk 内累加前序 sample size。
stsz 与 stco/co64
stsz 可能给出统一 sample_size,也可能为每个样本提供数组;stco 是 32 位 chunk offset,co64 是 64 位 offset。大文件使用 stco 可能溢出,解析器应优先支持 co64。
mdat 中 H.264/AAC 样本
MP4 的 mdat 没有统一的“每帧头”。样本边界由样本表决定。H.264 样本前是 NAL length prefix;AAC 样本通常是 raw AAC,不带 ADTS。读取时应先通过轨道和 sample index 得到 offset/size,再交给对应解码器。
esds 与 AudioSpecificConfig
mp4a 轨道的 esds 通过 MPEG-4 Descriptor 保存 DecoderConfigDescriptor 和 DecSpecificInfo。DecSpecificInfo 通常就是 ASC 的字节串,包含 AOT、采样率索引、声道配置等。不能从 MP4 文件名或 mp4a 四字符码推断完整 AAC 参数。
avcC 到 Annex-B 的转换
1 | 读取 avcC 中 SPS/PPS |
转换时应在每个关键访问单元前确保参数集可用,并根据 lengthSizeMinusOne 使用 1~4 字节长度。
MP4 修复和验证
1 | ffprobe -v error -show_format -show_streams input.mp4 |
验证重点包括:box 层级闭合、sample 数量一致、offset 不越界、轨道 duration 合理、关键帧索引可用、编码配置与样本格式一致。
MP4 文件构成图
1 | MP4 |
Box Header 字节构成
普通 Box:
1 | 00..03 size uint32 big-endian |
当 size == 1:
1 | 00..03 00000001 |
当 size == 0 时,Box 延伸到父容器或文件末尾;解析器必须结合父 Box 边界判断,不能无条件读到整个磁盘文件尾。
ftyp 字段表
| 字段 | 长度 | 作用 |
|---|---|---|
| size/type | 8 | Box 公共头 |
| major_brand | 4 | 主要兼容品牌 |
| minor_version | 4 | 品牌版本 |
| compatible_brands | 4×N | 兼容品牌数组 |
compatible_brands 的数量由 Box 总长度计算:(size - 16) / 4。它不直接说明视频一定是 H.264,也不直接说明音频一定是 AAC。
Sample 定位公式
定位某个 sample 的过程为:
1 | 1. 根据 stsc 找到 sample 所属 chunk |
这就是为什么 MP4 不能只扫描 mdat 来获得完整帧列表。
MP4 中 H.264 与 AAC 的实际构成
1 | 视频轨:stsd/avc1/avcC + mdat 中的 length-prefixed NAL samples |
avcC 保存 SPS/PPS 和 NAL 长度字段大小;esds 的 DecoderSpecificInfo 保存 AAC AudioSpecificConfig。配置和媒体 payload 分离,是 MP4 与 ADTS/Annex-B 的重要区别。
读取 MP4 的完整顺序
一个真正的 MP4 解析器不能只按固定顺序寻找几个字符串。它首先从文件偏移 0 开始读取 Box Header,得到 size 和 type,并把该 Box 的结束位置记录为 box_start + box_size。如果 type 是 container,就在这个结束位置以前递归读取子 Box;如果是 FullBox,则在读取公共 Header 后先消费 version 和 flags,再根据具体类型解释 body。所有子 Box 的读取都必须受到父 Box 结束位置约束,这样即使文件中出现未知 Box,也能安全跳过而不把后续数据当作当前字段。
解析完成后,程序建立轨道表。每条 trak 都必须关联 track id、handler type、timescale、sample description 和 sample table。视频轨道的 handler_type 通常是 vide,音频轨道通常是 soun;不能根据 trak 出现顺序猜测。stsd 中的 entry 数量也可能大于 1,样本通过 sample-description-index 选择具体编码配置,因此同一条轨道在切换编码参数时可以存在多个 description。
时间表的实际计算
stts 不是每个 sample 一行,而是把连续 delta 相同的样本压缩成一组 (sample_count, sample_delta)。读取第 N 个样本的 DTS 时,应从零开始累加前面各组的 delta,或者建立前缀和缓存。ctts 用同样的压缩思想保存 composition offset;最终 PTS 为 DTS + offset。轨道内部使用自己的 timescale,只有换算到统一时钟后才能和另一条轨道比较。比如视频 timescale 为 90000、音频 timescale 为 48000,两个轨道的整数时间值不能直接相加或比较。
从 sample 找到 mdat 中的字节
假设某条轨道的第 100 个 sample 属于第 20 个 chunk,stco 给出该 chunk 的文件偏移,stsc 指示该 chunk 中有 3 个 sample,stsz 给出前三个 sample 的大小分别为 1200、800、960,那么第 100 个 sample 的起始地址就是 chunk offset 加上前两个 sample 的大小 2000,长度是 960。这里的 sample 是编码数据的完整单元;H.264 sample 内部还可能包含多个 length-prefixed NAL。容器负责定位 sample,编解码器负责解析 sample 内部语法,两个层次不能混淆。
moov 变化与索引修正
stco/co64 保存的是文件位置。当编辑器在 moov 前面插入数据、移动 moov、改变 Box 长度或重新排列 mdat 时,后续 chunk 的位置会整体移动。写文件时必须在所有 Box 大小确定后再计算 offset,或者使用占位和回填策略。只修改 ftyp 而不更新索引,会导致播放器读取到错误的 sample;这也是“文件能被工具识别但播放花屏”的常见原因。
MP4 字段的合法性检查
解析时应检查 Box size 至少包含 Header;FullBox 的 version 是否为实现支持的值;trak 的 track_ID 是否唯一;stts、stsz、stsc 推导出的 sample 总数是否一致;chunk offset 加 sample size 是否超出文件长度;avcC 的 SPS/PPS 长度是否在 Box 范围内;esds 的 descriptor 长度是否闭合。任何长度字段都必须先做整数溢出检查,再转换为内存索引。
普通 MP4 的写入顺序
写入普通 MP4 时,应用首先确定轨道数量、编码配置、时间基和 sample 表策略。可以先写 ftyp,为 moov 和 mdat 预留空间;也可以先把媒体 sample 写到临时区域,完成所有索引后再一次性输出。无论采用哪种方式,最终必须使 stsd 中的编码描述与 mdat 中的 sample 格式一致,使 stsz 的每个长度与实际 sample 字节数一致,使 stco/co64 的每个偏移指向真实 Chunk 起点。若使用 H.264,写入器还要保证 NAL 长度前缀的字节数和 avcC 的 lengthSizeMinusOne 相同。
为了实现渐进下载,常见做法是把 moov 放在 mdat 前面,也称 faststart。移动 moov 不是简单的文件块交换,因为所有 chunk offset 可能随位置变化;写入器必须重新计算 stco 或在偏移超出 32 位时升级为 co64。如果只把 moov 拷贝到文件开头而不调整索引,播放器会按照旧位置读媒体数据,结果可能是无法播放、画面花屏或音频噪声。
Fragmented MP4
Fragmented MP4 的数据时间关系
fMP4 将初始化信息放入 ftyp + moov,把每个媒体片段组织为 moof + mdat。moof 中的 traf 通过 tfhd 指定轨道,通过 tfdt 指定该片段的解码起始时间,通过 trun 列出样本数量、持续时间、大小、flags 和可选的 data offset。与普通 MP4 的 stts/stsc/stsz/stco 全局样本表相比,fMP4 的样本元数据分散在每个 fragment 中,更适合边生成边传输,但接收器必须先获得初始化段并正确维护跨片段时间。
MP4 的音视频同步
每条轨道都有独立 timescale,因此同步流程通常是先把视频 PTS 和音频 PTS 换算成共同的微秒或 90 kHz 时钟,再比较两个播放时刻。视频存在 B 帧时,DTS 用于解码顺序,PTS 用于显示顺序;音频通常没有 B 帧,但仍可能存在 encoder delay 和 edit list。播放器不能只比较 sample 序号,也不能把两个轨道的 duration 字段直接相加。正确的同步策略需要考虑轨道起始时间、编辑列表、负 CTS、空白样本以及音频首尾 padding。
容器、编码和传输的边界
Box 头和 FullBox 解析
32 位、64 位和 size 为零
普通 Box 头由 32 位大端 size 和 4 字节 type 组成。size 为 1 时,头部后追加 64 位大端 largesize;size 为 0 通常表示 Box 延伸到父容器或文件末尾,但只允许在规范定义的上下文中使用。解析器必须先确认基本头至少有 8 字节,再根据 size 值读取扩展字段,并检查 Box 结束位置不超过父范围。
1 | size == 1 → header = 8 + 8 + optional fields |
uuid 类型还会在普通头之后追加 16 字节 user type。未知 Box 可以按长度跳过,但不能把 uuid 的额外字段误当作子 Box。
FullBox 的 version 和 flags
许多 ISO BMFF 叶子 Box 使用 FullBox 结构:4 字节 version/flags,随后是该 Box 的专用字段。version 为 0 或 1 时字段宽度可能不同,例如 mvhd、tkhd 和 mdhd 在 version 1 中使用 64 位 creation time、modification time、duration。解析器必须先读取 version 再选择字段布局,不能总按 version 0 的固定偏移读取。
Movie、Track 和媒体层级
moov 的完整树
常见普通 MP4 的元数据树可以写成:
1 | moov |
并非每条轨道都需要所有 Box;例如没有 B 帧时可以省略 ctts,所有视频样本都是随机接入点时可以省略 stss。省略表示默认语义,而不是错误。验证器应根据轨道类型和 sample entry 判断必需与可选 Box。
mvhd、tkhd、mdhd
mvhd 定义电影级 timescale 和 duration;tkhd 定义轨道 ID、轨道 duration、显示层级、音量、变换矩阵和宽高;mdhd 定义媒体级 timescale、duration 和语言。轨道 duration 位于 movie timescale,媒体 duration 位于该轨道自己的 media timescale,二者不能直接比较。显示宽高通常是 16.16 定点数,旋转由 matrix 表达,不能只读取 width/height 而忽略矩阵。
Sample Description 与编码配置
stsd 条目计数
stsd 是 FullBox,包含 entry_count 和若干 sample entry。每个 sample entry 自带大小和类型,视频条目还包含宽高、分辨率、帧计数和 compressor name,音频条目包含声道数、样本大小、采样率等。entry_count 必须与实际可解析的 entry 数量一致;sample table 中的 sample description index 从 1 开始,不能按 C 数组的 0 起始索引直接使用。
AVC、HEVC 和 AAC 配置
AVC sample entry 通常包含 avcC,其中保存 profile、level、NAL 长度字段大小和 SPS/PPS;HEVC 使用 hvcC 的 array 结构;AAC 常见 mp4a 加 esds,其中 DecoderSpecificInfo 保存 ASC。配置 Box 的字节序和 descriptor 长度都必须单独验证。avcC 中的 SPS/PPS 不是 mdat sample,播放器初始化时应先加载它们。
Sample Table 的推导关系
stts 解码时间
stts 每项是 sample_count 和 sample_delta,表示连续若干 sample 使用相同解码时长。对第 i 个 sample,DTS 可由前面所有 entry 展开求和:
1 | dts[0] = 0 |
展开前应检查所有 count 之和等于轨道 sample 总数,乘法和累加使用宽整数。压缩视频的 sample duration 可以变化,不能只用第一项 delta 乘 sample 数量。
ctts 显示时间
ctts 给出 composition offset。version 0 的 offset 通常按无符号解释,version 1 允许有符号 offset,以表达负 CTS。显示时间为 DTS + offset;存在编辑列表时还要再应用 edit timeline。错误地把 signed offset 当 unsigned 会把 B 帧显示时间推到极大的正数。
stsc、stsz 和 chunk
stsc 把连续 chunk 映射为每 chunk 的 sample 数和 sample description index;每项从指定的 first_chunk 生效,直到下一项。stsz 可以给出统一 sample_size,也可以给出逐样本大小数组。推导时应先展开 chunk-to-sample 映射,再把 sample size 累加到 chunk 内偏移。stz2 使用 4、8 或 16 位紧凑大小,不能与 stsz 的 32 位数组混读。
stco 与 co64
stco 保存 32 位 chunk offset,co64 保存 64 位。偏移指向 chunk 数据的起始位置,而不是 sample 的任意字节;第一个 sample 的偏移通常等于 chunk offset,后续 sample 由前面 sample size 累加得到。将 moov 前移后,所有受影响的 chunk offset 都必须重新计算,超过 32 位范围时应转换为 co64。
stss 随机访问
stss 的 sample number 从 1 开始,列出同步样本。没有 stss 时,规范语义通常是所有样本均为同步样本;不能把缺失 stss 当成“没有关键帧”。H.264 还需结合编码层的 IDR/CRA 等语义,容器同步样本标志与 NAL 类型不是完全相同的字段。
从 sample 定位到 mdat
定位公式
对给定 sample s,先由 stsc 找到其所在 chunk c,再计算该 chunk 前面同 chunk 样本的大小:
1 | sample_offset(s) = chunk_offset(c) |
定位过程中必须检查每次加法不溢出,最终区间 [offset, offset + size) 在文件内。对于 interleaved 音视频,两个轨道的 chunk 可以交错,不能假定视频所有 sample 在一个连续区域。
sample 数量一致性
stts、ctts(若存在)、stsz、stsc 展开的 sample 数量和关键帧索引必须一致。stsd 的 description index 也要对每个 sample 有效。发现计数不一致时,解析器可以尝试按实际 mdat 扫描,但不能把扫描结果静默当作标准 sample table。
Fragmented MP4 的索引与时间
mvex 和默认值
fMP4 的 moov 通常包含 mvex 和 trex,为后续 fragment 提供默认 sample duration、size、flags 和 description index。tfhd 可以覆盖这些默认值,trun 再提供逐样本字段。解析器必须按“trun 显式值 → tfhd 值 → trex 默认值”的优先级推导,缺少必要字段时报告无法定位,而不是假设固定大小。
tfdt、trun 和数据偏移
tfdt 给出该 track fragment 的 decode time;version 1 使用 64 位 baseMediaDecodeTime。trun 的 flags 决定是否存在 data offset、first-sample-flags、duration、size、flags 和 composition time offset。data offset 通常相对于 moof 起点,样本数据常位于紧随其后的 mdat;必须依据标志和基准计算,不能固定写成 moof_size。
初始化段与媒体段
没有 ftyp/moov 初始化信息时,接收端通常无法知道轨道、编码配置和 timescale。媒体段可以独立缓存,但提交解码器前必须确认对应初始化段已经加载,且 track_ID、description index 和参数集仍然匹配。切换编码参数时应生成新的初始化段或明确的 period 边界。
MP4 构造、修复与验证
构造 Box 的长度回填
写入器可以先写占位 size,写完子 Box 后回填父 Box 长度;也可以在内存中先构造子树再一次性写出。回填时必须保留大端序,超过 32 位时升级为 largesize,并移动后续字段。直接修改 size 而不调整父级范围,会使所有后续 Box 失去闭合关系。
修复策略
如果 moov 尚在但某些 sample table 损坏,可以根据 mdat、编码帧边界和时间信息尝试重建;如果只有 mdat 而缺少初始化配置,不能凭字节扫描可靠恢复所有编码参数。修复工具应区分“结构可恢复”“编码配置缺失”和“数据本身截断”三种状态,并输出可审计的修复报告。
标准验证清单
1 | Box size / parent range / 64-bit extension |
MP4 的 Box 只负责描述和定位 sample。H.264 的 NAL 语法仍由 H.264 解码器解析,AAC raw sample 的语法仍由 AAC 解码器解析;avcC 和 esds 只提供初始化配置。把 MP4 文件中的 H.264 sample 直接发送到 RTP,通常还需要按照 RTP payload 规则拆分 NAL;把 ADTS 文件中的 AAC 帧放入 MP4,也需要去掉 ADTS Header 并生成 ASC。容器转换是元数据、字节组织和时间信息的共同转换,不能只对 payload 做 memcpy。
第六章 File Type 与品牌兼容性
ftyp 字段
ftyp payload 先给出四字节 major_brand、32 位 minor_version,随后是零个或多个四字节 compatible_brands。Box payload 长度减去 8 后必须是 4 的整数倍。brand 是文件遵循的规范与特性声明,不是视频 codec 名称,也不是扩展名。
| 字段 | 长度 | 作用 |
|---|---|---|
major_brand |
4 bytes | 主要兼容品牌 |
minor_version |
4 bytes | 品牌定义的次版本值 |
compatible_brands[] |
4 bytes each | 额外兼容规范集合 |
品牌与实际结构
声明某 brand 并不能替代结构验证。文件声称兼容 ISO BMFF/MP4 某版本,但 sample entry、fragment 或元数据不满足该品牌约束时,仍是不一致文件。修复器不应为了让播放器接受而随意增加 brand;必须确认文件使用的所有特性属于该品牌。
缺失 ftyp
早期或私有文件可能没有 ftyp,部分播放器仍能根据 moov 解析。严格验证应报告品牌信息缺失,但不要仅凭这一点断言所有 sample 损坏。写入新文件时应输出与实际特性一致的 ftyp。
第七章 Movie Header 逐字段语义
mvhd version 0 与 1
mvhd 是 FullBox。version 0 使用 32 位 creation/modification time、timescale 和 duration;version 1 将时间与 duration 扩为 64 位,timescale 仍为 32 位。creation time 使用标准定义的纪元,不能直接当 Unix time。duration 为未知或特殊值时需要按规范解释。
rate、volume 与矩阵
rate 通常是 16.16 定点数,默认 1.0;volume 是 8.8 定点数,默认 1.0。矩阵为 3×3 变换矩阵,其中部分成员使用不同定点格式。常见默认矩阵是单位变换,但旋转 90/180/270 度的视频会使用非默认矩阵。显示系统应组合轨道矩阵与宽高,而不是只交换宽高字段。
next_track_ID
next_track_ID 是新增轨道可用 ID 的提示,应大于当前已使用的 track ID,并避免零。它不决定现有轨道数量。解析器要从实际 trak 统计轨道并验证 ID 唯一。
Track Header 与轨道状态
tkhd flags
tkhd 的 flags 可表示轨道启用、参与电影、用于预览等状态。存在 trak 不等于默认播放;轨道选择器应结合 flags、handler、语言、alternate group 和用户偏好。修复器不应无条件把所有隐藏/备用轨道都设为 enabled。
track ID 和 duration
track ID 是分片、引用和加密辅助信息关联轨道的主键。ID 为零通常保留,重复 ID 会使 tfhd 等引用歧义。tkhd.duration 位于 movie timescale,并可能包含 edit list 后的呈现长度;不能用它代替 mdhd.duration 计算媒体 sample 解码时长。
layer、alternate group 与 volume
视频 layer 影响视觉合成层级,alternate group 可把互斥轨道分组,音频 volume 常为 1.0,视频通常为 0。字段值不是所有播放器都采用,但验证器仍需按 signed/unsigned 定点格式解析并保留。
width 与 height
宽高是 16.16 定点显示尺寸,可能与编码像素尺寸不同,例如像素宽高比或裁剪造成差异。avc1 sample entry 的 width/height 描述编码样本,而 tkhd 矩阵与显示尺寸描述电影画布上的呈现。
Media Header 与 Handler
mdhd
mdhd.timescale 定义该媒体轨道所有时间字段的基本单位。值为零非法,因为 duration、DTS 和 edit 映射无法换算。language 使用压缩语言码,不是以零结尾的 ASCII 字符串;未定义语言有标准约定值。
hdlr
hdlr.handler_type 常见 vide、soun、text、subt、meta 等,后面可有名称字符串。handler 说明媒体处理类别,sample entry 说明具体编码。vide 轨道中出现音频 sample entry 是结构矛盾,应报告而不是自动更改 handler。
Media Information 与数据引用
vmhd、smhd 与其他媒体头
视频 minf 常含 vmhd,音频常含 smhd。vmhd 有 graphics mode 和 opcolor,smhd 有 balance。它们不是 codec 初始化字段,但用于媒体表现。轨道类型与媒体头不匹配时应报告结构问题。
dinf/dref
dref 列出 sample 数据所在的数据引用。最常见 self-contained URL entry 通过 flags 表示数据就在当前文件;也可以引用外部 URL/URN。Sample entry 的 data_reference_index 从 1 开始索引该表。读取器若只支持自包含 MP4,应明确拒绝外部引用,而不是把外部 offset 当成本文件偏移。
数据引用安全
外部 URL 属于潜在网络访问,纯解析器不应自动访问。报告可以列出经过转义的引用字符串,解析 sample 前由上层策略决定是否允许加载。索引越界、空引用或自包含标志与 payload 位置矛盾都要单独报告。
Edit List 与电影时间线
edts/elst
elst 把轨道媒体时间映射到 movie timeline。每个 entry 包含 segment duration、media time 和 media rate。version 0/1 决定 duration/media time 的字段宽度;media time 是有符号值,特殊负值表示空编辑。
空编辑
空编辑在电影开始处插入一段没有该轨道媒体的时间,可用于延迟音频或视频起点。播放器在此期间输出静音、黑场或没有轨道内容。不能把负 media time 转成巨大无符号 sample 时间。
有效媒体编辑
非负 media time 表示从媒体时间轴的相应位置开始呈现,segment duration 使用 movie timescale。要定位第一个实际 sample,需要把 media time 换算到 media timescale,并结合 stts/ctts。编辑边界可能落在 sample 之间,播放器策略需遵循格式和 codec 随机访问要求。
Media Rate
常见 media rate 为 1.0。非标准播放速率或停帧语义只在规范允许范围内解释;很多实现仅支持 1.0。验证器应显示整数和小数部分,不应忽略字段后假定所有 edit 等速。
Visual Sample Entry
Visual Sample Entry 基础字段
VisualSampleEntry 在通用 sample entry 的保留字段与 data reference index 后,包含预定义字段、width、height、水平/垂直分辨率、frame_count、compressorname、depth 等。所有字段为大端,compressorname 常使用定长 Pascal 风格区域,不一定以 C 字符串结尾。
avc1 与 avc3
avc1 通常要求参数集通过 sample entry 中的 avcC 提供,媒体 sample 中的参数集处理遵循其定义;avc3 允许参数集在媒体 sample 中出现并支持变化。转换器不能只改 FourCC 而不调整参数集放置和随机访问语义。
Pixel Aspect Ratio 与 Clean Aperture
pasp 可给出水平/垂直像素间距比例,clap 描述 clean aperture 的宽高与偏移。最终显示比例可能由编码尺寸、clap、pasp 和 tkhd 矩阵共同决定。每个有理数字段的分母必须非零,组合计算使用有理数以减少舍入误差。
Colour Information
colr 可以指示颜色原色、传递特性、矩阵和 full range 等信息。它影响 YUV 到 RGB 和 HDR/SDR 显示,不改变 sample 边界。若 codec 码流 VUI 与容器颜色信息冲突,播放器需要有明确优先级并在诊断中显示两套值。
Audio Sample Entry
基础音频字段
AudioSampleEntry 包含 channelcount、samplesize、pre_defined、reserved 和 16.16 采样率。对压缩 AAC,这些字段提供容器级描述,真实对象类型和扩展采样率还来自 esds/ASC。HE-AAC 的输出采样率可能与基础 sample entry 的历史写法存在兼容差异,应结合配置分析。
mp4a 与 esds
esds 使用 MPEG-4 descriptor 层级,常见包括 ES_Descriptor、DecoderConfigDescriptor、DecoderSpecificInfo 和 SLConfigDescriptor。descriptor tag 后的长度采用最多若干字节的 7-bit continuation 编码,不是固定 32 位长度。解析每层时都要限制在父 descriptor 结束位置。
DecoderConfigDescriptor
该描述包含 object type indication、stream type、buffer size、最大/平均码率等。码率字段是描述值,不替代 sample table 时间计算。DecoderSpecificInfo 中的 ASC 才决定 AAC AOT、采样率与声道配置。
AVCDecoderConfigurationRecord
固定字段
avcC 开始通常为 configurationVersion、AVCProfileIndication、profileCompatibility、AVCLevelIndication 和带保留位的 lengthSizeMinusOne。保留位必须具有规定值,NAL length 字段字节数为 lengthSizeMinusOne + 1。
参数集数组
随后字段给出 SPS 数量,每个 SPS 使用 16 位大端长度;再给 PPS 数量及长度。High Profile 配置还可能有 chroma format、bit depth 和 sequence parameter set extension。解析器应依据 profile 和剩余长度进入扩展,不能把扩展字节当作下一个 Box。
参数一致性
avcC 外层 profile/level 应与 SPS 内容相容,sample 中每个 NAL length 必须使用同一长度宽度。参数集长度不包含 Annex-B 起始码。如果数组元素以 00 00 00 01 开头,说明封装器很可能错误地把起始码写入了配置。
第八章 Sample Table 展开算法
建立 Chunk Run
stsc entry 必须按 first_chunk 严格递增,第一个通常从 1 开始。对 entry i,其配置作用于从 first_chunk[i] 到下一 entry 的前一个 chunk。最后一项作用到 chunk offset 表的末尾。samples_per_chunk 和 description index 不能为零。
建立 Sample Offset
遍历 chunk 时维护全局 sample index:读取该 chunk 的 samples_per_chunk,设置首 sample 为 chunk offset,随后按 stsz/stz2 累加。每完成一个 chunk,sample index 增加相应数量。最终 sample index 必须恰好等于 sample count,不能多也不能少。
DTS Run
遍历 stts 时维护当前 DTS 和 sample index。每个 run 输出 count 个相同 delta 的 sample,DTS 每次加 delta。delta 为零在某些场景可能有定义,但大量零时应检查轨道类型和合法性。总 duration 应与 mdhd 在允许差异范围内一致。
CTS Run
ctts 的 count 展开数必须匹配 sample count。Version 1 用 signed offset;计算 PTS 时使用足够宽的 signed integer,并检查 DTS + offset 溢出。没有 ctts 时 composition time 默认等于 decode time。
Sync Sample
stss entry 必须在 1..sample_count 范围并通常递增。seek 时找到不晚于目标 decode/presentation 位置的适当 sync sample,再从那里按 codec 依赖解码。对于开放 GOP 或 roll recovery,还可能需要 sample group 辅助信息。
Compact Sample Size stz2
4、8 和 16 位字段
stz2.field_size 决定每项用 4、8 或 16 bit。4 bit 模式中两个 sample size 打包在一个字节的高低半字节,奇数 sample 数时最后低半字节是填充。其他 field_size 值非法。
与 stsz 的互斥
同一 sample table 通常使用 stsz 或 stz2 之一。两者同时出现会产生大小来源歧义,应按规范验证或拒绝。零大小 sample 是否允许取决于媒体与上下文,不能在偏移算法中无条件除零或跳过 sample index。
Sample Group
sbgp 与 sgpd
sbgp 用 run-length 映射把连续 sample 分配到 group description index,sgpd 保存该 grouping type 的描述。group index 可能表示不属于任何描述或使用 fragment-local 描述,version 和索引语义需要按规范读取。
常见用途
Sample group 可表达 roll recovery、随机访问、依赖、加密和 codec 特定属性。它不替代 stss 或 sample flags,而是提供额外分组语义。重封装时若丢弃 group box,可能破坏无缝 seek、恢复或解密。
Sample Dependency 与 Padding
sdtp
sdtp 每个 sample 使用一个字节的多个 2-bit 字段描述 is_leading、depends_on、is_depended_on 和 has_redundancy。值有“未知”和明确状态之分,不能把零统一解释为 false。它可辅助随机访问和丢帧策略。
Padding Bits 概览
padb 描述 sample 末尾 padding bit 数量,两个 sample 的值可打包在一个字节。多数字节对齐视频/音频无需它,但解析器仍应按 sample count 验证数组长度。
元数据容器
udta 与 meta
用户数据可放在 movie 或 track 层。meta 在某些品牌中是 FullBox,内部包含 handler、item 或键值结构;不同生态还使用 ilst 等约定。解析器必须依据父级/品牌决定结构,不能把所有 meta 都按同一私有布局读取。
字符串与未知数据
元数据可能包含 UTF-8、UTF-16、图片或二进制值。日志输出需限制长度并转义控制字符。未知元数据应按 Box size 保留或跳过,不应影响 sample table 解析。
第九章 Movie Fragment 字段继承
mfhd
每个 moof 通常有 mfhd,其中 sequence_number 用于片段顺序检查。序号跳跃可以提示缺片,但它不是每轨 sample 序号,也不能单独推导时间。
tfhd flags
tfhd 可携带 base_data_offset、sample_description_index、default_sample_duration、default_sample_size 和 default_sample_flags。default-base-is-moof 等 flags 会改变数据偏移基准。解析器必须按 flags 精确读取字段,否则一个漏读字段会使后续 tfdt/trun 全部错位。
trun sample 字段
trun flags 决定每 sample 是否有 duration、size、flags 和 composition time offset。first_sample_flags 与逐样本 flags 具有互斥/优先规则。version 决定 composition offset 的 signed 语义。entry_count 乘每 entry 字节数必须在 Box 剩余范围内。
数据起点计算
数据位置由 base data offset、moof 基准、trun data_offset 和前一 run 结束位置共同决定。一个 traf 可以有多个 trun,一个 moof 可以有多个 traf。实现应为每个 run 显式计算 [start,end),再验证落入对应 mdat,而不是假定所有 sample 紧跟 moof 且按轨道聚集。
第十章 Segment Index 与随机访问
sidx
Segment Index 给出 reference ID、timescale、earliest presentation time、first offset 和若干 reference。version 0/1 影响时间和偏移宽度。每个 reference 描述大小、时长、SAP 等信息,可指向媒体子段或另一级索引。
偏移基准
first_offset 相对于 sidx 结束位置,后续 reference 的范围依次累加。把它当作绝对文件偏移会定位错误。reference size 只有有限位宽,并带 reference type 标志,计算前必须分离标志位。
SAP
Starts With SAP、SAP type 和 delta time 描述随机接入属性。它们应与片段内 sample flags、sync sample 和 codec 随机访问点相容。索引声称可随机接入但首视频 sample 依赖前片段时,应报告不一致。
sidx Version 0 精确字段
FullBox后固定字段
| Payload偏移(含FullBox) | 长度 | 字段 |
|---|---|---|
| 0 | 4 | version/flags |
| 4 | 4 | reference_ID |
| 8 | 4 | timescale |
| 12 | 4 | earliest_presentation_time,Version0 |
| 16 | 4 | first_offset,Version0 |
| 20 | 2 | reserved |
| 22 | 2 | reference_count |
| 24 | 12×N | references |
Version0中时间和first_offset为32位;Version1各扩为64位,使reference数组起点从24变为32(以FullBox Payload开头计)。timescale必须非零。
两个Media Reference实例
逻辑值
1 | reference_ID = 1 |
两个媒体子段共5秒,字节总量11000。
完整56字节Box
1 | 00 00 00 38 73 69 64 78 |
大小:
1 | Box Header 8 |
Reference第一DWORD
Type与31-bit Size
1 | bit31 reference_type |
解析:
1 | word = read_u32be() |
5000=0x1388,type0:
1 | 00 00 13 88 |
6000=0x1770:
1 | 00 00 17 70 |
不能把整个DWORD当size。若type=1,最高位置1,表示引用另一个Index;此时大小word看起来大于2GiB,若不mask会严重越界。单Reference可表达size最大 0x7FFFFFFF,超过需更细分Segment/索引结构,不能截断。
SAP第三DWORD
位布局
1 | bit31 starts_with_SAP |
本例:starts=1、type=1、delta=0:
1 | (1<<31) | (1<<28) = 0x90000000 |
解析:
1 | starts = word >> 31 |
SAP delta使用sidx timescale。starts_with_SAP=0时,SAP可能位于Referenced Subsegment内部,由delta描述;不能仍假定第一个Sample是随机访问点。
first_offset坐标实例
sidx位于0x0100
1 | sidx_start = 0x0100 |
Reference0 Range:
1 | start0 = 0x0138 |
Reference1紧接:
1 | start1 = 0x14C0 |
Ranges:
1 | ref0 [0x0138,0x14C0) |
first_offset不是相对sidx start,也不是绝对文件位置。若错误从0x0100加,会把首Reference提前56字节并覆盖sidx自身。
非零First Offset
若sidx后有24字节free或其他允许区域,first_offset=24:
1 | first start = sidx_end + 24 |
这24字节不计入第一referenced_size。验证器可以列出gap中的Box/数据,确认offset确实跳过合法范围。
Reference时间轴
Earliest Presentation Time
EPT=0,Reference durations依次2000、3000:
1 | ref0 presentation range [0,2000) = [0s,2s) |
累计:
1 | time_cursor = earliest_presentation_time |
Reference Type1引用Index时,其duration仍参与当前层时间范围,但需要递归解析下级并验证汇总一致。递归深度、总Reference数和循环引用必须限制。
Version 1
64位EPT与Offset
Version1固定部分:
1 | reference_ID 4 |
相同两个Reference的Box比Version0多8字节:
1 | size = 64 = 0x40 |
Version字节为1:
1 | 01 00 00 00 |
Writer在EPT或first_offset超过uint32时必须升级Version1;Reference内部size/duration/SAP字段仍为各自32/31/28位,并没有随Version扩大。
Reference范围验证
文件边界
1 | cursor = checked_add(sidx_end, first_offset) |
用减法比较避免加法回绕。外部Segment/网络场景的Reference可能不在当前文件完整可用范围,此时验证器区分“本地缺失”与“语法越界”,根据数据源上下文处理。
子段结构
Type0 Reference通常覆盖一个媒体Subsegment,可含styp、sidx、moof、mdat、emsg等Box组合,referenced_size是整个被引用范围,不等于mdat Payload或Sample总Size。验证器从Range起点解析Box并确保恰好到Range end。
若首Box并非moof不自动判错;Segment Profile可能允许前置styp/emsg。关键是所有Box闭合、moof的Run Range落入Reference覆盖的数据、时间与duration一致。
SAP交叉验证
容器与Codec
对每个Type0 Reference:
1 | sidx starts_with_SAP/SAP_type/delta |
若sidx宣称starts_with_SAP=1,但首视频Sample flags为non-sync或Codec仍依赖前Segment,索引不可信。音频通常每Access Unit独立于预测视频意义,但Codec priming/config也需初始化段。
SAP Delta映射到Reference presentation timeline,不能按字节offset使用。delta超过Reference duration需要报告。
多级sidx
Type1递归
Reference Type1指向另一个sidx Range。解析器:
1 | parse_sidx(node, depth): |
记录已访问Range避免A引用B、B又引用A的循环。限制总节点和Reference,避免恶意指数展开。
sidx解析算法
1 | parse_sidx(box): |
保留reserved异常作为规范警告。Count由uint16限制,但仍在读取前用Box剩余反验。
sidx错误向量
1 | timescale=0 |
报告包含sidx文件Range、Version、Reference ID、timescale、每Reference byte/time Range、SAP字段、结构/Codec反验结果。
第十一章 Common Encryption 边界
保护后的 Sample Entry
CENC 文件可使用受保护 sample entry,并通过 sinf、frma、schm、schi/tenc 描述原始格式、scheme 和默认密钥/加密参数。解析器应先取得原始 codec 类型,再将加密作为外层保护属性;不能把 encrypted entry 当成未知 codec 后停止读取配置。
senc、saiz 与 saio
每 sample 的 IV 和 subsample encryption 信息可在 senc 或 auxiliary information 中描述,saiz 给大小,saio 给偏移。三者的 sample count、偏移和大小必须与媒体 sample 对齐。解析器可以验证结构而不执行解密,但不得把密文送给普通 H.264/AAC 解析器并误报 codec 损坏。
子样本范围
Subsample encryption entry 通常列出 clear bytes 和 encrypted bytes。各区段和必须等于对应 sample size,不能越界或剩余未描述字节(除非 scheme 明确定义)。IV 长度和 pattern encryption 参数来自默认配置或 sample group 覆盖。
PSSH Version 0 完整实例
36 字节Box
构造一个示例System ID和四字节系统私有Data:
1 | SystemID = 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF |
完整Box:
1 | 00 00 00 24 70 73 73 68 |
| 字段 | 长度 | 值 |
|---|---|---|
| Box Header | 8 | size36, type pssh |
| FullBox | 4 | Version0/Flags0 |
| system_ID | 16 | 示例UUID字节 |
| data_size | 4 | 4 |
| data | 4 | DE AD BE EF |
长度 8+4+16+4+4=36=0x24。SystemID是16字节二进制,不按UUID文本端序重排后再比较;在文件层按原16字节匹配注册的保护系统。
PSSH Data由对应DRM系统定义,ISO BMFF容器解析器只按data_size保存,不执行、不当字符串,也不把它当密钥。未知SystemID仍是结构合法Box。
PSSH Version 1
KID数组
Version1在SystemID后增加:
1 | KID_count uint32 |
使用一个KID 10 11 ... 1F 和四字节Data,大小:
1 | 8 + 4 + 16 + 4 + 16 + 4 + 4 = 56 = 0x38 |
完整逻辑前缀:
1 | 00 00 00 38 70 73 73 68 |
读取KID_count前用Box剩余反推最大项:至少为后续data_size保留四字节。Count乘16使用受检算术。Version0没有KID_count,不能因Data首四字节像小整数就误读为数组。
三Sample的IV辅助信息
假设配置
假设保护配置为每Sample 8字节IV,无Subsample Encryption。三个Media Sample对应IV:
1 | IV0 = 00 01 02 03 04 05 06 07 |
IV是字节串,不作为宿主整数增减或交换端序。实际IV生成/唯一性由加密系统负责;本例只验证容器布局。
senc 完整字节
无Subsample Flag
1 | 00 00 00 28 73 65 6E 63 |
大小:
1 | 8 Header + 4 FullBox + 4 sample_count + 3*8 IV = 40 = 0x28 |
sample_count=3必须与关联Fragment/Track范围中的三个Sample对应。IV大小不在每条senc Entry内重复,来自tenc或Sample Group覆盖;若配置说16字节,以上Box会截断,不能根据剩余“自动猜8”。
若Subsample Encryption Flag置位,每个IV后还有subsample_count及clear/encrypted pairs,Entry大小可变,saiz不能继续用统一8字节。
saiz 统一大小实例
每Sample 8字节
Flags为0、不带aux_info_type字段:
1 | 00 00 00 11 73 61 69 7A |
| 字段 | 值 |
|---|---|
| default_sample_info_size | 8 |
| sample_count | 3 |
Box size 8+4+1+4=17=0x11。ISO BMFF Box不使用RIFF式偶数Padding,下一Box起点就是start+17。若default size非零,后面没有逐Sample size数组;为零时才读取sample_count个一字节size。
saiz.sample_count=3、senc.sample_count=3、媒体Sample Count=3三者闭合。统一size8与三IV总24字节一致。
saio 单Offset实例
Version0
1 | 00 00 00 14 73 61 69 6F |
Box size20,entry_count1,Offset示例 0x1010。Offset基准由saio所在上下文、Fragment和相关标准版本/flags决定,不能一律解释为文件绝对位置。本受控示例明确约定非Fragment自包含文件、基准为文件起点,且senc从0x1000开始:
1 | senc Header [0x1000,0x1008) |
因此saio指向第一条Auxiliary Data。换到moof/traf场景时,应按该上下文计算基准并输出解析来源,不能复用本例绝对公式。
Version1使用64位offset,Box相应增大。超过32位时Writer必须升级,不能截断。
senc/saiz/saio 配对
逐Sample Range
1 | aux_start = resolved_saio_offset |
最后end等于senc Box end 0x1000+40=0x1028。解析出的三个Range分别对应三个IV。若saio指向senc Header而非第一IV,会把Box size/type当IV,结构表面仍有24字节但解密全部失败。
Subsample Encryption长度闭合
Entry结构
启用Subsample时,一条Entry概念上:
1 | IV[configured_iv_size] |
Entry辅助大小:
1 | iv_size + 2 + subsample_count*6 |
对Media Sample,所有clear+protected区段总和应覆盖Sample size(按scheme定义验证)。使用64位累计:
1 | covered += clear |
不能根据加密块大小擅自补齐未描述尾部。Pattern Encryption还需结合tenc/seig的crypt/skip block参数验证块处理,但容器Range仍必须闭合。
Sample Group覆盖
默认与每组配置
Track默认KID/IV规则来自tenc;sbgp/sgpd的seig可让特定Sample使用不同KID、IV大小、constant IV或pattern参数。解析每Sample最终配置:
1 | config = track tenc default |
不能先按Track默认8字节切分全部senc,再应用Group;若某组IV为16字节,后续所有Entry边界都会错位。
加密结构与Codec边界
不解密时能验证什么
1 | protected SampleEntry通过frma恢复原Codec类型 |
不能验证明文NAL/AAC语法或密钥正确。若Sample全加密,把密文字节当H.264 NAL length解析会产生伪错误;只有Subsample声明的clear区且包含相应结构时,才可在范围内做有限Codec检查。
CENC错误向量
1 | PSSH Version1 KID_count越过Box |
验证报告不输出密钥,KID/IV也应按日志安全策略控制;生产日志避免泄漏完整保护元数据。
第十二章 MP4 写入器事务模型
普通文件
写入器应把每个 sample 的轨道、DTS、PTS、duration、size、sync 属性和 description index 记录为内部样本表,再由该表生成 stts/ctts/stsc/stsz/stco/stss。直接在写 payload 时零散修改多个 Box 容易造成计数不一致。
崩溃恢复
若录制时先写 mdat、结束时才写 moov,崩溃可能留下没有索引的 payload。恢复需要 codec-aware 扫描和外部时间信息,并非所有格式都能无损重建。更可靠的录制可周期性写 fragment,使每个 moof 已含局部样本元数据。
原子发布
生成完整文件后再原子重命名,可以避免消费者看到半写 moov。流式 fMP4 则应只发布完整 Box/segment,不能让客户端读取到尚未写完的 trun 或 mdat。长度未知时使用允许的传输分段机制,而不是永久保留错误 Box size。
MP4 错误恢复策略
Box 层恢复
已知父 Box 范围内遇到非法子 size 时,不应全文件搜索任意四字符作为下一个 Box,因为 payload 中可能有大量假 type。可在明确的顶层边界和白名单类型下尝试重同步,同时把跳过区间标记为损坏。
Sample Table 恢复
部分表损坏时,先确定仍可信的 stsd、时间表、大小表和 chunk offset。只有 codec payload 存在可识别边界时,才能尝试重建缺失数据。重建结果应写入新文件,保留原件,并记录每个推断值。
Fragment 恢复
一个 fragment 损坏时,可从下一完整 moof/tfdt 恢复后续时间,但依赖前片段的非随机访问视频可能仍无法立即解码。播放器应等待新的随机接入点;音频通常可在下个完整 sample 恢复,并根据 tfdt 保留时间间隙。
MP4 测试矩阵
普通 MP4
应覆盖单/多轨、固定/可变 sample size、B 帧与负 CTS、edit list、stco/co64、多个 sample description、外部 data reference、缺失 stss、旋转矩阵、pasp/clap、AAC-LC/HE-AAC 和 AVC 参数变化。
Fragmented MP4 测试
应覆盖 trex 默认值、tfhd 覆盖、多个 traf/trun、显式/隐式 data offset、tfdt version 0/1、signed CTS、空 fragment、sidx、多 mdat、乱序/缺失 fragment 和初始化段切换。
损坏输入
应覆盖 Box size 小于头、largesize 溢出、父范围越界、descriptor 长度不闭合、sample 计数矛盾、chunk offset 指向文件外、NAL length 截断、CENC auxiliary 信息不匹配和超大尺寸资源攻击。所有测试都应证明解析器在报告错误前不会越界或分配失控。
顶层 Box 排列
mdat
mdat 保存媒体字节,但自身不说明哪些字节属于哪个轨道或 sample。普通 MP4 依靠 sample table 定位,fragmented MP4 依靠 moof/trun。一个文件可以有多个 mdat,chunk/sample 可以分布在不同 mdat 中;读取器不能只记录第一个 mdat 的起点。
free 与 skip
free/skip 提供可忽略空间,便于以后增长 moov 或写入其他 Box。其 payload 没有媒体语义。修改器可以在不移动后续数据的情况下把一部分 free 替换为新 Box,但剩余空间若仍作为 free,必须重新写合法 header 和 size。
wide
历史 QuickTime/兼容文件可能使用 wide 为 mdat 大小扩展或布局预留空间。现代解析器应按其 Box size 安全跳过,并以实际 mdat header 为准。不能把 wide payload 当媒体数据。
多个 moov
普通文件通常只有一个活动 moov。追加式更新或损坏文件可能出现多个 moov,生态行为不一致。验证器应报告全部位置和内容,不能未经规则选择最后一个并隐藏冲突。修复输出应生成唯一、闭合的新 moov。
第十三章 Box Parser 参考实现
受限 Reader
每个父 Box 创建一个 [start,end) 受限 reader。子 Box 只能在此 reader 中读取。叶子字段也使用相同边界,未知数据按子 Box end 跳过。这样即使内部 size 损坏,也不会跨入兄弟或父外区域。
1 | parse_children(parent_end): |
UUID Header
当 type 为 uuid,header 还需读取 16 字节 user type,最小 size 相应增加。用户类型应作为 16 原始字节保存,显示为 GUID 时处理字段端序约定。未知 UUID 按完整 size 跳过。
size=0 的父范围
顶层 size=0 可延伸到文件 EOF;嵌套 Box 的 size=0 若规范允许,则延伸到父结束。解析器不能在未知长度流中立即接受 size=0,除非上层能够提供 segment/EOF 边界。
递归限制
Box 树理论上可被恶意构造得很深。实现限制最大深度、Box 总数和单 Box 元数据读取量。大 mdat 不应读入内存,只登记范围并通过受限文件 view 按 sample 读取。
FullBox 参考实现
Version/Flags 字节
FullBox 的四字节由一个 version 字节和三个 flags 字节组成,整体大端读取时可写为:
1 | version = value >> 24 |
flags 只有 24 bit。不要用带符号 32 位右移后得到负 version,也不要把 version 纳入 flags 比较。
未支持 Version
同名 Box 的新 version 可能改变字段宽度或语义。解析器遇到未知 version 可保留原始 Box 并报告 unsupported version,但不应按已知 version 0 强行读取。父级仍可按 size 跳过继续分析。
ISO 时间和定点数
时间纪元
mvhd/tkhd/mdhd creation/modification time 常从 1904-01-01 UTC 计秒。转换到 Unix epoch 需要应用固定纪元差并检查早期/溢出值。字段为无符号 32/64 位,不能直接传给有符号 time_t。
16.16 定点
宽高、rate 和采样率等字段可能使用 16.16:高 16 位为整数,低 16 位为小数。解析值为 raw / 65536;写入需舍入并检查范围。音频采样率常是无符号 16.16,负值无意义。
8.8 定点
volume/balance 等字段可能用 8.8 signed fixed point。解析要先按 int16 做符号扩展,再除以 256。把它当 uint16 会把负 balance 显示为巨大正数。
Matrix 定点格式
变换矩阵的 a/b/c/d/x/y 与 u/v/w 并非全部相同 fixed-point 位数。常见二维仿射单位矩阵具有规范固定值。解析器应按各成员格式转浮点或保持原始整数,避免统一除 65536。
Movie Fragment Random Access
mfra
mfra 通常位于 fragmented 文件尾部,包含一个或多个 tfra 和 mfro。它提供按轨道的随机访问索引,不承载媒体。mfro 给出整个 mfra size,可帮助从文件尾定位,但仍要反向验证目标 header 和范围。
tfra
tfra 包含 track ID、长度字段编码和若干 entry。每项给出 time、moof offset,以及 traf/trun/sample number。version 0/1 决定 time 和 moof offset 的 32/64 位。三个 number 字段的字节数由紧凑长度字段加一决定。
Number 从一开始
traf_number、trun_number 和 sample_number 通常采用一基计数。映射到数组时先验证非零再减一。索引目标应指向指定 moof/traf/trun 中的随机访问 sample,并与 sample flags/codec 入口交叉验证。
mfro
mfro 是 FullBox,保存 mfra_size。读取文件尾时,可先找到 mfro,再向前跳 mfra_size 验证 mfra。损坏值不能用于未经检查的负向 seek。
Movie Extends Header
mvex
存在 movie fragments 的初始化 moov 通常含 mvex。其中 trex 为每轨提供默认 sample 属性,可能还有 mehd 给整个 fragmented presentation duration。没有 mvex 的普通 moov 不应被误判为 fMP4 初始化段。
mehd
mehd.fragment_duration 使用 movie timescale,version 决定 32/64 位。它是整体提示/声明,仍需用各 fragment tfdt+duration 验证。直播或尚未完成的文件可能没有最终 duration。
多个 trex
每个 fragment track 通常有一个对应 track_ID 的 trex。重复或缺失导致默认值歧义。tfhd/trun 完全显式提供必要字段时某些缺失可能仍可定位,但结构符合性应按品牌/规范判断。
Fragment Sample Flags
32 位布局
Sample flags 包含 leading、depends-on、is-depended-on、redundancy、padding、non-sync 和 degradation priority 等位域。实现用掩码/移位读取,不能依赖 C bit-field。零值的部分子字段表示 unknown,不一定表示“无依赖”。
Sync 判定
sample_is_non_sync_sample 为零只是容器随机访问信息的一部分,还要结合 depends_on、SAP/group 和 codec NAL。视频 segment 的首 sample若被标为 sync,但实际不是可随机解码点,播放器可能在 seek 后花屏。
默认值优先级
逐 sample flags 优先于 first_sample_flags,后者优先/适用于首 sample,tfhd 默认再回退到 trex。具体互斥由 trun flags 限制。解析器应为每个 sample输出“最终值”和“来源”,便于诊断错误默认继承。
Fragment 数据范围实例
单 Track 单 Run
假设 moof 起点为 1000,trun data_offset 为 200,sample size 依次 100、120、80,则三个 sample 起点为 1200、1300、1420,结束为 1500。验证 [1200,1500) 位于一个 mdat payload 内。
多 Run
第二个 trun 没有显式 data_offset 时,其数据起点可能接续前一个 run 结束,具体基准受 tfhd/default-base-is-moof 和规范规则影响。不要再次从 moof+0 开始。多 traf 交错时,各 run 可以指向不同区域。
Signed data_offset
trun data_offset 是 signed 字段。计算 base + offset 使用有符号宽整数并验证结果非负、可表示且位于文件。把负 offset 当 uint32 会跳到文件外巨大地址。
120 字节 Fragment 完整实例
目标结构
构造单轨、三个固定大小Sample的媒体Fragment:
1 | moof size 100 |
轨道ID=1,默认Sample duration=1000、size=4、flags=0;base decode time=0。tfhd.default-base-is-moof=1,trun.data_offset=108,所以数据从moof起点后108字节开始,即100字节moof加8字节mdat Header。
mfhd 完整字节
1 | 00 00 00 10 6D 66 68 64 |
| 字段 | 值 |
|---|---|
| size | 16 |
| type | mfhd |
| version/flags | 0 |
| sequence_number | 1 |
Box大小 8+4+4=16。Sequence Number用于Fragment顺序与缺失检查,不是Track Sample Number或DTS;按文件/传输顺序递增,并需处理32位边界策略。
tfhd 默认字段
Flags 0x020038
本例设置:
1 | 0x000008 default_sample_duration_present |
完整Box:
1 | 00 00 00 1C 74 66 68 64 |
逐字段:
| Payload偏移 | 字节 | 解释 |
|---|---|---|
| 0 | 00 02 00 38 |
Version0/Flags0x020038 |
| 4 | 00 00 00 01 |
track_ID=1 |
| 8 | 00 00 03 E8 |
default duration=1000 |
| 12 | 00 00 00 04 |
default size=4 |
| 16 | 00 00 00 00 |
default sample flags=0 |
总大小 8+4+4+4+4+4=28=0x1C。由于未置 base-data-offset-present,Box中不出现八字节 base offset;default-base-is-moof使数据基准明确为当前moof起点。
Parser必须严格按flags顺序消费条件字段。如果看见tfhd就无条件读取base_data_offset,会把track_ID和默认字段全部错位。
tfdt Version 1
64 位Base Decode Time
1 | 00 00 00 14 74 66 64 74 |
Version1用64位无符号 baseMediaDecodeTime=0。Box大小 8+4+8=20。Version0只用32位,总大小16;解析器由version决定宽度,不能按Box大小猜。
下一Fragment若紧接本例且没有时间空洞,其tfdt应为:
1 | 3 samples * 1000 = 3000 = 0x0BB8 |
连续性还要按Track独立检查,多轨tfdt使用各自Media timescale。
trun 的最小逐Run字段
只写Count与Data Offset
所有duration/size/flags从tfhd继承,trun只需显式sample_count和data_offset。Flags为 data-offset-present=0x000001:
1 | 00 00 00 14 74 72 75 6E |
| 字段 | 值 |
|---|---|
| version | 0 |
| flags | 0x000001 |
| sample_count | 3 |
| data_offset | +108,signed int32 |
Box大小 8+4+4+4=20。由于未置duration/size/flags/composition-offset flags,后面没有per-sample数组。解析器仍要生成三条最终Sample记录,从tfhd默认填充。
如果任何默认值在tfhd也缺失,则回退到初始化段 trex。若trun逐Sample字段存在,它覆盖默认。每个最终属性应保存来源,例如 size=4(source=tfhd)。
traf 与 moof 长度闭合
traf
1 | traf children = 28 + 20 + 20 = 68 |
Header:
1 | 00 00 00 4C 74 72 61 66 |
moof
1 | moof children = mfhd16 + traf76 = 92 |
Header:
1 | 00 00 00 64 6D 6F 6F 66 |
嵌套解析使用父范围:mfhd结束必须等于traf起点,traf结束必须等于moof end。不能根据固定顺序跳100字节而不验证child sizes。
mdat 和三个Sample
完整字节
1 | 00 00 00 14 6D 64 61 74 |
mdat size=20,其中Header8、payload12。Sample目录:
| Sample | 相对mdat Payload | 字节 |
|---|---|---|
| 1 | 0 | 11 12 13 14 |
| 2 | 4 | 21 22 23 24 |
| 3 | 8 | 31 32 33 34 |
这些是演示Payload,不声明具体Codec。真实轨道必须由初始化段stsd/trex和Track Handler解释。
Fragment 全部字节
1 | 00 00 00 64 6D 6F 6F 66 |
空格和缩进仅用于阅读,不在文件中。总大小:
1 | 100 + 20 = 120 bytes |
Data Offset 实算
Moof位于文件偏移 0x1000
1 | moof_start = 0x1000 |
物理布局:
1 | moof range [0x1000,0x1064) size100 |
Sample Range:
1 | sample1 [0x106C,0x1070) |
最后end恰好等于mdat payload end。data_offset=100 会错误指向mdat Box Header;data_offset=108 才跳过Header。
Signed受检加法
1 | resolve_data_start(base_u64, offset_i32): |
先把int32提升到int64再取负,避免 INT32_MIN 在32位域溢出。结果还必须落入当前文件/Segment允许的数据源范围。
Sample默认值继承
优先级
对每个Sample分别解析:
1 | duration: |
first_sample_flags 与逐Sample flags的相关present组合受语法约束,应验证互斥/适用性。零flags不等于所有依赖信息都明确为false,其中某些位域零表示unknown。
本例:
| Sample | Duration | Size | Flags | 来源 |
|---|---|---|---|---|
| 1 | 1000 | 4 | 0 | tfhd defaults |
| 2 | 1000 | 4 | 0 | tfhd defaults |
| 3 | 1000 | 4 | 0 | tfhd defaults |
Decode times从tfdt累加:0、1000、2000,Fragment decode end=3000。
多Run数据游标
无Data Offset的后续Run
一个traf可有多个trun。第一个Run显式定位后,后续Run若无data_offset,在适用规则下可从前一Run数据end继续。实现维护每个traf的data cursor:
1 | if trun has data_offset: |
不能对每个无offset trun都重置到moof end或mdat start。多个traf时,各Track Run可在mdat内交错,必须按各自显式/继承规则定位,不按Box出现顺序猜Payload顺序。
Fragment连续性
下一tfdt
如果下一同Track Fragment base decode time=3000,则与本例连续。小于3000表示Decode时间重叠/倒退,大于表示时间空洞:
1 | expected_next = current_tfdt + sum(sample_duration) = 3000 |
是否允许Gap由Presentation/Segment规范决定,但验证器必须报告。计算使用64位受检累计;tfdt version1可覆盖长时间线。
Sequence与Track独立性
mfhd sequence按Movie Fragment,不按Track。一个moof可含多个traf共享同一sequence。tfdt和duration按各Track timescale,不能把音频48000单位与视频90000单位直接相减;同步比较先转Movie时间或精确有理秒。
Fragment错误向量
Box结构
1 | moof size不等于child总和 |
数据定位
1 | data_offset指向mdat Header |
时间与默认值
1 | duration/size在trun、tfhd、trex均缺失 |
Fragment验证报告
本例应输出:
1 | mfhd.sequence_number = 1 |
再对三个Sample计算hash并与解码器/业务预期比较。Box边界合法不代表Payload Codec正确,两层验证分开报告。
Sub-sample Information
subs
SubSampleInformationBox 将某些 sample 分为多个子样本,entry 使用 sample_delta 定位目标 sample,再列出 subsample size、priority、discardable 等。version 决定 subsample size 字段宽度。
累计 Sample Delta
sample_delta 是相对前一个关联 sample 的增量,需要累计并检查不超过 sample count。第一个同样从当前计数累加,不是绝对 sample number。每个 subsample size 总和应符合 sample 大小或该 grouping 语义。
Codec 边界
子样本可辅助描述 NAL、层或其他编码单位,但不替代 codec parser。即使 subs 声明长度闭合,H.264 NAL length 仍需按 avcC 验证;两套边界冲突时报告容器元数据不一致。
Data Reference 与多文件媒体
Self-contained Flag
URL entry 的 self-contained flag 表示媒体数据包含在当前文件。此时 location 字符串通常为空。若 flag 未设置,payload 给外部位置。data_reference_index 为零或超过 dref entry count 均非法。
Offset 的文件归属
Chunk offset 应应用于 sample entry 指定的数据引用对象。外部引用轨道的 stco 不一定指当前 MP4。解析器内部的 sample location 应保存 (data_source_id, offset, size),不能只有裸 offset。
重封装
把外部引用文件做成 self-contained,需要复制所有 sample、重建 chunk offset,并将 data reference 更新为当前文件。只改 dref flag 会让旧 offset 指向错误位置。
Sample Description 切换
sample_description_index
stsc 或 fragment defaults 为 chunk/sample 选择 stsd entry。索引从 1 开始。一个轨道可在时间线上切换 codec 配置,例如分辨率或参数集,但播放器是否支持无缝切换取决于 codec 与品牌。
初始化边界
description index 变化前,应在可安全重新初始化的 sample 边界发生,视频通常需要随机访问点。解码器在提交该 sample 前加载新 avcC/esds,清理或迁移旧状态。只比较 FourCC 不足以发现配置变化。
参数去重
多个 stsd entry 可能字节相同;写入器可以复用一个 entry,但一旦已发布索引,不应随意重排 entry 导致旧 sample_description_index 改变。优化需重写全部引用并验证。
Chunk Interleave 设计
Chunk 不等于 Sample
一个 chunk 可包含一个或多个连续 sample,同一轨道可有许多 chunk。stsc 决定映射,stco/co64 只给 chunk 起点。将每个 sample 单独作为 chunk 最简单但索引较大;把过多 sample 放一个 chunk 会降低音视频交错与随机读取效率。
音视频交错
写入器按时间窗口安排音频/视频 chunk,使顺序读取不需要远距离 seek。每轨 sample 的 DTS/PTS不因物理交错改变。完成布局后才能确定 chunk offset 和 stsc run。
Faststart 迭代
把 moov 放在 mdat 前会改变 chunk offset,而 offset 从 stco 升级 co64 又会增大 moov,进一步改变 mdat 起点。写入器可迭代计算直到 Box 大小和 offset 稳定,或预留足够空间。一次估算可能在 32 位临界点失败。
Descriptor 长度编码
7-bit Continuation
MPEG-4 descriptor 的长度使用每字节低 7 位数据和高位 continuation,最多规范允许的字节数。解码过程为累计左移 7 再或低位,并检查字节数和溢出。它不同于 EBML VINT,也不是普通 LEB128 反向顺序。
1 | length = 0 |
父范围
descriptor length 只在父 descriptor/Box 剩余内有效。未知 descriptor 可按 length 跳过,known descriptor 的子字段也不得超出它。恶意 continuation 永不结束时,在最大字节数处失败。
AVC Sample 十六进制实例
受控AVC轨道
本例使用Constrained Baseline、Level 3.0、32×32、三帧测试序列。avcC保存一条22字节SPS和一条5字节PPS,NAL length width为4。三个MP4 Sample位于:
| Sample | 文件起点 | Size | 文件终点 | 类型 |
|---|---|---|---|---|
| 0 | 0x0363 |
1804 | 0x0A6F |
IDR访问单元 |
| 1 | 0x0A6F |
103 | 0x0AD6 |
Non-IDR P访问单元 |
| 2 | 0x0AD6 |
532 | 0x0CEA |
IDR访问单元 |
终点为半开区间:offset+size。0x0A6F是Sample1首字节,不属于Sample0。Sample Table解析器先建立这些范围,再在各自范围内解释NAL length;不能跨Sample继续读。
38 字节 AVCDecoderConfigurationRecord
avcC Payload
1 | 01 42 C0 1E FF E1 00 16 |
若作为普通Box,完整Header:
1 | 00 00 00 2E 61 76 63 43 |
Box总大小 8+38=46=0x2E。它通常是VisualSampleEntry avc1/avc3 的child,父SampleEntry边界必须覆盖这46字节。
逐字段解析
| Payload偏移 | 字节 | 字段 | 值 |
|---|---|---|---|
| 0 | 01 |
configurationVersion | 1 |
| 1 | 42 |
AVCProfileIndication | 66,Baseline |
| 2 | C0 |
profile_compatibility | constraint bits 0xC0 |
| 3 | 1E |
AVCLevelIndication | 30,Level3.0 |
| 4 | FF |
reserved + lengthSizeMinusOne | 3,width=4 |
| 5 | E1 |
reserved + numOfSPS | 1 |
| 6 | 00 16 |
SPS length | 22 |
| 8 | 67 ... 92 |
SPS NAL | 22字节,type7 |
| 30 | 01 |
numOfPPS | 1 |
| 31 | 00 05 |
PPS length | 5 |
| 33 | 68 CB 83 CB 20 |
PPS NAL | 5字节,type8 |
长度闭合:
1 | fixed prefix = 6 |
0xFF高六位是reserved 1,低两位才是lengthSizeMinusOne;不能把它当length width255。0xE1高三位reserved,低五位为1;不能循环225条SPS。
配置与SPS一致性
配置Header三个能力字节应与实际SPS profile/compatibility/level一致。解析器还检查SPS/PPS NAL Header type、参数集ID引用、SPS尺寸/位深和SampleEntry显示尺寸。Header一致不代表所有参数集相同;多SPS时逐条解析并管理ID。
avc1通常把初始化参数集作为out-of-band配置;avc3允许更多in-band参数集更新语义。转换器必须根据SampleEntry类型决定保留/提取策略,不只是改FourCC。
Sample 0 的六个NAL
Length目录
相对偏移以Sample0起点0计:
| 相对偏移 | 前缀 | NAL Size | Header/Type | 下一位置 |
|---|---|---|---|---|
0x0000 |
00 00 02 6C |
620 | 06, SEI |
0x0270 |
0x0270 |
00 00 00 02 |
2 | 09, AUD |
0x0276 |
0x0276 |
00 00 00 16 |
22 | 67, SPS |
0x0290 |
0x0290 |
00 00 00 05 |
5 | 68, PPS |
0x0299 |
0x0299 |
00 00 02 6C |
620 | 06, SEI |
0x0509 |
0x0509 |
00 00 01 FF |
511 | 65, IDR Slice |
0x070C |
0x070C=1804,恰好等于Sample size。独立求和:
1 | NAL payload = 620+2+22+5+620+511 = 1780 |
文件偏移 0x05D3
Sample0起点 0x0363 + 0x0270 = 0x05D3,从这里看到:
1 | 00 00 00 02 09 10 |
00 00 00 02是大端NAL length,随后 09 10 才是AUD。下一项 00 00 00 16=22,后面完整SPS。搜索 00 00 01 会把Length值或Payload中的字节误判为Annex-B start code。
两条SEI
本Sample有两条620字节SEI。Muxer/编码链可写入额外SEI或重复非VCL NAL,解析器不能硬编码“一个AU只能有一个SEI”。未知SEI默认透明保留;是否去重需要理解Message语义,不能仅按NAL type删除。
Sample 1 的两个NAL
1 | 00 00 00 02 09 30 |
| 相对偏移 | Size | Type | End |
|---|---|---|---|
| 0 | 2 | AUD type9 | 6 |
| 6 | 93 | Non-IDR Slice type1 | 103 |
长度:
1 | 4+2 + 4+93 = 103 |
Slice Header首字节0x41表示nal_ref_idc非零、type1。非IDR不等于非参考图像,随机访问仍结合stss/Sample Flags和Codec解析。
Sample 2 的四个NAL
1 | 00 00 00 02 09 10 |
| 相对偏移 | Size | Type | End |
|---|---|---|---|
0x000 |
2 | AUD | 0x006 |
0x006 |
22 | SPS | 0x020 |
0x020 |
5 | PPS | 0x029 |
0x029 |
487 | IDR Slice | 0x214 |
0x214=532。最后Length 00 00 01 E7=487 中前三字节看似Annex-B三字节start code,但第四字节是长度低字节。自动扫描会把E7误当NAL Header并得到forbidden bit异常。
三个Sample的表级闭合
Range连续
1 | 0x0363 + 1804 = 0x0A6F |
本例三个Sample在文件中紧邻,但ISO BMFF不要求同轨Sample永远连续;Chunk/Track交错可产生空洞。定位必须由stsc+stsz+stco/co64或Fragment字段得出,不能用上一Sample end猜下一Sample offset。
NAL闭合
1 | sample0: 1780 payload + 24 prefixes = 1804 |
每个Sample内游标必须恰好到end。少于end表示尾部垃圾/未知结构,多于end表示Length越界。任何NAL Length读取前验证剩余至少width字节。
AVC Sample解析算法
1 | parse_avc_sample(sample_view, avcc): |
零Length严格模式拒绝;兼容模式即使跳过也必须消费prefix并保证前进。Width=1只能表达255字节NAL,本例620字节SEI/511字节IDR不能转成一字节Length,Writer必须拒绝而非截断低字节。
MP4到Annex-B
外部边界替换
转换每个Length-Prefixed NAL:
1 | 00 00 00 02 09 10 |
NAL Header+EBSP Payload必须逐字节不变;转换不执行RBSP去竞争。对目标随机访问点,依据SampleEntry和参数集缓存,在Slice使用前确保所需SPS/PPS已输出。可从avcC注入:
1 | 00 00 00 01 + SPS(67...92) |
Sample自身已有相同SPS/PPS时,可按明确策略避免重复;同ID但内容更新不得误删。
时间旁路
Annex-B裸流没有stts/ctts/stss/Edit List等价表。导出时应把DTS/PTS/duration/Sync信息交给目标协议或旁路元数据;仅写.h264后再读回无法恢复可变帧率、B帧显示时间和Edit映射。
Annex-B到MP4
访问单元分组
先扫描start code得到NAL,再按H.264访问单元边界规则分组;每个目标MP4 Sample通常对应一个AU。为每NAL写固定宽度大端长度:
1 | write_be(nal.size, length_width) |
收集SPS/PPS生成avcC,从选定SPS派生profile/compatibility/level。选择avc1或avc3决定参数集放置/更新策略。Sample size写入stsz/trun;位置进入chunk/fragment索引;时间来自输入时间轴而非NAL字节猜测。
AVC转换验证
Payload Hash
分别解析源和目标外部边界,对每NAL记录:
1 | access_unit_index |
除声明的参数集移动/注入外,顺序、size和hash应完全相同。只比较解码画面不够:Decoder可能忽略未知SEI或隐藏损坏,NAL Hash能发现错误删除防竞争字节、包含Length/start code、截断一字节等问题。
Box与Codec双层
1 | stsz/trun size equals bytes consumed by all NALs |
容器合法但Slice损坏与NAL合法但Sample Table越界是不同错误层,报告分别归类。
越界向量
1 | Sample剩3字节但width=4 |
任何失败都输出Sample Number、文件Range、NAL ordinal、prefix相对偏移、声明Length和剩余字节,不能扫描后续mdat寻找start code“恢复”当前Sample。
AAC Sample 实例
受控AAC-LC配置
构造MPEG-4 AAC LC、44100 Hz、双声道、16-bit SampleEntry提示。mp4a中包含 esds,其DecoderSpecificInfo为两字节AudioSpecificConfig:
1 | 12 10 |
Sample Payload保存Raw AAC Access Unit,不含ADTS Header。Track的Sample边界由stsz/stsc/stco或trun给出,解码配置来自esds/ASC。
AudioSampleEntry mp4a
固定28字节Payload前缀
Version 0 AudioSampleEntry在Box Header之后、child Box之前有28字节:
| 相对Box Payload偏移 | 长度 | 字段 | 本例值 |
|---|---|---|---|
| 0 | 6 | reserved | 全零 |
| 6 | 2 | data_reference_index | 1 |
| 8 | 2 | version | 0 |
| 10 | 2 | revision_level | 0 |
| 12 | 4 | vendor | 0 |
| 16 | 2 | channelcount | 2 |
| 18 | 2 | samplesize | 16 |
| 20 | 2 | compression_ID | 0 |
| 22 | 2 | packet_size | 0 |
| 24 | 4 | samplerate | 44100的16.16值 |
44100以16.16 unsigned fixed表示:
1 | 44100 << 16 = 0xAC440000 |
AudioSampleEntry的channelcount/sample size是容器描述字段,最终解码参数仍应与ASC一致。若Entry写2声道而ASC配置1声道,必须报告冲突。
39 字节 esds Box
esds 完整字节
1 | 00 00 00 27 65 73 64 73 |
去掉阅读缩进后Box size为39=0x27。层级:
1 | esds FullBox |
Descriptor Length 闭合
DecoderConfigDescriptor 长度
Payload组成:
1 | objectTypeIndication 1 |
所以字节 04 11 合法。
ES Descriptor
1 | ES_ID 2 |
所以 03 19。esds整体:
1 | Box Header 8 |
Descriptor length使用7-bit continuation编码,并非Box的32位size。这里所有长度小于128,只占一字节;通用Parser仍要支持多字节并限制最多长度字节/父范围。
ES_Descriptor Flags
本例Flags为零
ES_ID后的一字节包含依赖/URL/OCR等条件标志与stream priority。本例为0,所以后面立即是tag04。若某标志置位,会出现条件字段:
1 | streamDependenceFlag -> dependsOn_ES_ID |
Parser必须按bit条件读取,否则会把条件字段首字节误当Descriptor tag。URL length必须在ES Descriptor父边界内,不能用于无界字符串读取或自动访问外部网络资源。
DecoderConfigDescriptor
Object Type与Stream Type
objectTypeIndication=0x40常表示MPEG-4 Audio。下一字节 0x15:
1 | 0x15 = 000101 0 1 |
必须用位域解析,不能把0x15当“Codec 21”。objectType与SampleEntry FourCC、ASC Audio Object Type共同确定解码器;冲突时不凭其中一个静默覆盖。
Buffer与Bitrate
1 | bufferSizeDB = 00 06 00 = 1536 |
三个/四字节均为big-endian。Bitrate字段是Decoder配置/速率描述,不定义每Sample固定大小;AAC VBR仍可有不同stsz。验证器可由媒体总bytes和duration估算平均率比较,但要区分容器开销、音频Payload和舍入。
AudioSpecificConfig 12 10
Bit布局
1 | 12 10 = 00010010 00010000 |
前16位可按BitReader:
1 | AOT = read_bits(5) = 2 |
freq_index=15 时后面有24-bit显式频率,不能查表;AOT=31时还有扩展AOT字段。HE-AAC可能通过Extension AOT/SBR/PS表达,不能只读前五位后称“全部都是LC”。
完整75字节 mp4a
SampleEntry加esds
1 | 00 00 00 4B 6D 70 34 61 |
大小:
1 | mp4a Box Header 8 |
esds起点为mp4a起点+36。AudioSampleEntry fixed字段之后可以还有其他child Box,Parser在mp4a size范围内循环,而不是假定余下全部属于esds。
包含mp4a的 stsd
单Entry
1 | stsd Box Header 8 |
Header前缀:
1 | 00 00 00 5B 73 74 73 64 |
entry_count=1,mp4a自身size=75。stsd Parser按Entry自身Box size跳到stsd end,不能按固定AudioSampleEntry 36字节误把esds当第二Entry。
Raw AAC Sample边界
不含ADTS
MP4 mp4a Sample通常直接保存AAC Access Unit的raw payload:
1 | stsz/trun declares N bytes |
不在每Sample前重复:
1 | FF F1 ... ADTS header |
若Payload以ADTS sync开头,可能是非标准/私有封装、错误SampleEntry或Muxer未剥Header。不能仅因前两字节恰好FF F?就删除七字节;先验证完整ADTS字段、Frame Length和ASC一致性。
AAC Sample时间
1024采样帧
常见AAC-LC每Access Unit对应每声道1024 PCM Sample。若mdhd timescale=44100,stts delta通常1024:
1 | sample duration = 1024 / 44100 |
100个Sample:
1 | duration units = 100 * 1024 = 102400 |
不能先把单帧舍入23ms再乘100,否则得到2.3s。所有时间在整数Media Units累计,显示时最后换算。
若Audio Object/FrameLengthFlag导致不同Frame长度,必须按Codec配置和stts验证,不能把所有AAC硬编码1024。
MP4 Raw AAC 转ADTS
七字节Header实例
假设一个Raw AAC Sample为100字节,配置AAC-LC、44100、stereo,无CRC。ADTS frame length包括Header:
1 | frame_length = 7 + 100 = 107 |
Header:
1 | FF F1 50 80 0D 7F FC |
逐字段:
1 | syncword = 0xFFF |
Length位拆分:
1 | byte3 low2 + byte4 + byte5 high3 |
生成算法:
1 | profile = audioObjectType - 1 |
ASC的显式频率、PCE/零channelConfiguration、HE-AAC扩展等情况不能都由基础ADTS短字段无损表达,转换器要验证目标可表示性。
ADTS转MP4
Header剥离与配置提取
逐帧:
1 | find/require sync at frame boundary |
从稳定ADTS字段构造ASC:AOT=profile+1、frequency index、channel configuration。跨帧配置若变化,不能仍用一个mp4a Entry覆盖全部Sample;应拒绝、分Track/Description或按目标标准处理配置切换。
ADTS每Frame含多个raw_data_block时,是否映射为一个MP4 Sample及duration要按AAC/封装语义处理,不能总假设字段为0。
ADTS与MP4长度边界
两套Length不能混用
MP4 Sample size来自stsz/trun;ADTS frame_length只存在导出后的ADTS Header。写MP4时:
1 | mp4_sample_size = raw_aac_size |
不是raw+7。若把ADTS Header留在mdat但stsd仍为标准mp4a/esds,Decoder会把sync字节当AAC语法开始而失败。
导出时若Sample size超过ADTS 13-bit Frame Length可表达上限:
1 | raw_size + header_size <= 8191 |
否则目标ADTS Frame不可表示,应报错,不能Length截断。
AAC配置一致性验证
容器字段
1 | mp4a channelcount == ASC channel config/derived PCE channels |
HE-AAC的core frequency和output frequency可能不同,简单相等检查会误报;验证器应输出两者及SampleEntry规则,而不是只保留一个“采样率”。
Payload与时间
对每Sample:Range在mdat、size非零/符合Codec、Raw AAC语法不越过Sample。总Sample Count与stts count闭合。解码后每帧输出Sample数与stts delta按timescale对应,考虑Encoder delay/Edit List后再验证最终Movie duration。
AAC错误向量
1 | mp4a size小于固定36字节 |
错误报告包含Track ID、Description Index、Sample Number、Box/Descriptor路径、文件Offset和声明/可用长度。
第十四章 MP4 时间线实例
DTS 与 CTS
假设三视频 sample 的 stts delta 均为 1000,DTS 为 0、1000、2000;ctts offset 为 2000、0、0,则 PTS 为 2000、1000、2000。实际合法编码会由排序和 offset 形成期望显示顺序,示例说明 PTS 不是仅从 sample index 单调推导。
Edit Mapping
若 movie timescale=1000,轨道 media timescale=48000,elst 先有 500 movie units 空编辑,再从 media time 24000 播放 2000 movie units,则轨道在电影 0.5 秒开始,跳过媒体前 0.5 秒并呈现 2 秒。换算必须在两个 timescale 间使用有理数。
舍入
时间基转换可用 round(value × dst_scale / src_scale),但边界选择需一致。大量逐 sample 独立舍入会改变总 duration;更稳妥是累计源时间后转换,或保留有理数余数进行误差扩散。
Edit List 的两套时间基
Segment Duration属于Movie Timescale
elst.segment_duration 使用 mvhd.timescale,而 media_time 使用所属轨道 mdhd.timescale。这是Edit List最容易混淆的规则:
1 | segment_duration seconds = segment_duration / movie_timescale |
不能用同一个timescale同时除两个字段。每条edit在Movie Timeline中按前一条结束位置连续排列;media_time给出该edit映射到Media Timeline的起点。
40 字节 elst Version 0 实例
时间目标
Movie timescale=1000,Media timescale=48000。Track先空白2秒,再从Media的1秒位置开始呈现5秒:
1 | Edit 0: |
Movie Track区间:
1 | [0,2000) no media |
第二段Media长度:5秒×48000=240000,起点48000,终点288000。
完整Box
1 | 00 00 00 28 65 6C 73 74 |
逐字段:
| 偏移 | 字节 | 解释 |
|---|---|---|
| 0 | 00 00 00 28 |
size=40 |
| 4 | 65 6C 73 74 |
elst |
| 8 | 00 00 00 00 |
Version0/Flags0 |
| 12 | 00 00 00 02 |
entry_count=2 |
| 16 | 00 00 07 D0 |
Edit0 duration=2000 |
| 20 | FF FF FF FF |
Edit0 media_time=-1 |
| 24 | 00 01 00 00 |
rate 1.0 |
| 28 | 00 00 13 88 |
Edit1 duration=5000 |
| 32 | 00 00 BB 80 |
Edit1 media_time=48000 |
| 36 | 00 01 00 00 |
rate 1.0 |
大小:
1 | 8 Box Header + 4 FullBox + 4 count + 2*12 entries = 40 |
Version0的segment_duration是uint32,media_time是signed int32。FF FF FF FF必须解析为-1;若读成uint32,会得到4294967295并错误定位到Media末尾外。
Media Rate
两个Signed 16-bit字段
每项末尾是:
1 | media_rate_integer signed int16 |
1.0:
1 | integer = 1 -> 00 01 |
它们在文件中big-endian,因为ISO BMFF多字节整数统一big-endian。不能把四字节整体当普通IEEE float。常规MP4播放最广泛支持1.0;其他rate语义和品牌支持需要按标准/Profile验证。Parser保留原值,不擅自按任意浮点速率重采样。
Movie Time 到 Media Time
Rate 1.0映射
Movie时间M若落在第二Edit:
1 | edit_movie_start = 2000 |
M=3500:
1 | movie_delta = 1500 |
即Movie 3.5秒对应Media 2.5秒。M=1000落在Empty Edit,没有Media Sample;播放器输出空白/静音或不呈现该Track。
反向映射
Media时间120000属于第二Edit映射区:
1 | media_delta = 120000 - 48000 = 72000 |
同一个Media区域可被多个Edit重复映射,或完全不被呈现,因此反向映射不一定唯一。API应返回所有候选或按播放顺序选择,而不是假设一一对应。
Version 1 Entry
64 位字段
Version1每项20字节:
| 长度 | 字段 |
|---|---|
| 8 | segment_duration uint64 |
| 8 | media_time int64 |
| 2 | media_rate_integer int16 |
| 2 | media_rate_fraction int16 |
Box最小大小:
1 | 8 + 4 + 4 + entry_count*20 |
当Movie duration超过uint32或Media起点超出signed32时使用Version1。Writer不得继续写Version0并截断高位。Reader由Version决定Entry宽度,未知Version按父Box安全跳过/拒绝语义解析。
Empty Edit的media_time在Version1为64位-1:
1 | FF FF FF FF FF FF FF FF |
Track Duration 与 Edit总和
tkhd.duration
有Edit List时,Track在Movie Timeline的呈现长度通常由edit segment durations总和表达:
1 | sum = 2000 + 5000 = 7000 movie units |
因此本例期望 tkhd.duration=7000(在Movie timescale中)。mdhd.duration使用Media timescale,至少要覆盖引用Media区到288000;它可还包含未呈现的前48000单位。
mvhd.duration是整部Movie所有Track/Edit后的整体持续时间,通常至少覆盖最长启用Track。不能用mdhd duration直接与mvhd/tkhd整数比较。
Edit与Sample Presentation
Sample区间映射
先由DTS+CTTS得到Media PTS,再根据Edit映射到Movie。对一个Media Sample显示区间 [PTS,PTS+duration),求它与每个有效Edit Media区间的交集,再换算到Movie区间。Sample可能部分落在Edit边界,需要裁剪显示时间,但Payload本身不切字节。
例如Media Sample区间 [47000,49000) 与第二Edit从48000开始相交 [48000,49000);Movie中只呈现后半段,对应从2000开始的一小段。音频需要Trim/Priming语义,视频可能需解码完整Sample但只显示交集。
Edit边界的Decode Preroll
Edit从Media时间48000开始显示,不代表Decoder一定可从第一个PTS≥48000的Sample独立启动。预测视频可能需要更早Sync Sample和Decode Preroll;AAC可能有Encoder Delay。Seek算法:
1 | movie target -> edit mapping -> media presentation target |
不能把Edit起点Sample无条件写入stss或Fragment sync flags。
时间换算与舍入
有理数累积
若timescale不能整除,使用宽整数/有理数:
1 | numerator = movie_delta * media_timescale |
边界映射应固定floor/ceil/nearest策略。为包含区间,起点常向下/按语义,终点需避免少覆盖最后一个tick;具体由API和标准规则决定。不要逐Sample把23.219ms四舍五入后再累计。
溢出
即使输入是32位,segment_duration*media_timescale可超过32位。先检查乘法或使用128位/大整数。恶意timescale=0直接拒绝;movie_delta不得在Empty Edit中参与Media换算。
多Edit与空洞
连续Movie Timeline
Edit entries在Movie时间按声明顺序连续排列,下一段Movie start是前面duration总和。Empty Edit产生没有Media的时间,而不是让Movie时间跳号。Media映射可前跳、后跳或重复,具体Profile可能限制。
验证器输出:
| Edit | Movie Range | Media Range | Rate | 类型 |
|---|---|---|---|---|
| 0 | [0,2000) |
无 | 1.0 | Empty |
| 1 | [2000,7000) |
[48000,288000) |
1.0 | Media |
这比只输出 media_time=-1 更容易发现Track为什么延迟出现。
Edit List解析算法
1 | parse_elst(box, movie_scale, media_scale): |
Entry数组后若Box还有扩展字节,按版本/Profile处理;不能让count越过父范围。
Edit List错误向量
1 | entry_count超过Box容量 |
修复器若Edit损坏,不能简单删除并声称时间不变;删除会改变Track Movie起点、时长和AV同步。应报告原始Edit、可证明的Sample时间以及采用的重建策略。
MP4 验证器输出模型
Box Tree
每个节点输出起点、header size、总 size、type、version/flags、父范围和状态。对于 mdat 只输出范围与引用覆盖,不 dump 全 payload。未知 Box 标记 unknown-but-well-formed,不等同 error。
Track Summary
每轨输出 track ID、handler、language、timescale、media/movie duration、edit list、sample entries、sample/chunk 数、DTS/PTS 范围、sync 数、数据引用和总字节。显示由 tkhd matrix/pasp/clap 推导的最终尺寸。
Sample Audit
逐 sample 或抽样输出 description index、DTS、PTS、duration、offset、size、sync/dependency flags、所在 mdat 和 codec 检查结果。大文件支持统计模式,并保留首个错误的完整证据。
Fragment Audit
列出 sequence number、每 traf track ID、tfdt、继承默认值、trun/run 数据范围、sample 时间范围和随机访问属性。检查 fragment 间 decode time 是否连续、重叠或有显式间隙。
Track Reference
tref
Track Reference Box 位于 trak 内,子 Box 的 type 表示引用关系,payload 是一个或多个 32 位 track ID。引用目标必须存在且不能按未定义方式形成非法自引用/循环。未知 reference type 可保留并显示原始 ID。
常见关系
引用可用于提示、章节、辅助、缩略图、依赖、时间码或其他规范定义的关系。具体 FourCC 的语义由相应品牌/扩展规定。解析器不能看到 tref 就假定所有目标是解码依赖。
删除轨道
编辑器删除 trak 时还要扫描所有 tref、alternate group、fragment default、metadata 和 index 对该 track ID 的引用。只删除 Box 会留下悬空 ID,某些播放器可能选择错误辅助轨。
Extended Language
elng
ExtendedLanguageTag Box 可用更完整语言标签补充 mdhd 的压缩三字符语言字段。字符串在 Box 边界内读取,编码/终止规则按规范。存在 elng 时应与 mdhd语言相容或报告冲突。
轨道选择
语言只是轨道选择因素之一,还要结合 handler、role、alternate group和用户偏好。und/未定义不等于英语。播放器不应因无法识别语言就丢弃媒体轨。
Sync 与 Partial Sync 表
stss
Sync Sample列出可随机访问sample number,一基、递增且在范围内。缺失表示全部sample是sync的默认语义只在该 Box语义/轨道上下文成立;codec依赖仍需验证。
stps
Partial Sync Sample可标识只满足部分同步条件的sample。它与完整sync不同,可能需要预卷或有限依赖。Seek算法应依据播放器能力与sample group/recovery信息选择。
stsh
Shadow Sync可把某些非随机sample关联到可用于同步的shadowed sample。每项含shadowed和sync sample number。两个编号均需范围检查,且映射不能导致 seek无限循环。
表之间一致性
同一sample若同时出现在多个同步表,解释由规范决定。验证器并列展示,不用其中一表静默覆盖另一表。重封装若不支持partial/shadow语义,应重新建立安全sync点而非直接丢Box。
Degradation Priority
stdp
Degradation Priority Box为每个sample提供16位priority,entry数通常与sample count相同。它可用于在资源不足时选择丢弃顺序,但不直接改变解码依赖。
与 Sample Flags
Fragment sample flags也有degradation priority字段。普通和fragmented存储位置不同,最终每sample属性要按上下文汇总。零值不一定表示最高/最低的自然语言含义,按规范顺序解释。
Padding Bits
padb
Padding Bits Box先给sample count,再以每字节两个4-bit值描述sample末尾padding bit。每个值有效范围受定义约束。奇数sample数时最后半字节处理必须正确。
Sample Size
stsz给出的sample size仍按整字节,padb只说明最后字节中无效bit。Codec parser应限制有效bit,不把padding解释为下个语法元素。多数现代字节对齐codec不会使用。
Composition to Decode Timeline
最早 PTS
含负 composition offset时,第一decode sample的PTS可能为负媒体时间。Edit list可将可见presentation移动到movie time零。播放器内部使用signed宽时间,不能在进入edit前夹到零。
Decode Preroll
目标显示时间之前可能需要解码若干sample建立参考/滤波状态。stss、sample group和codec recovery共同决定preroll。Seek UI时间与实际读取起点不同是正常的。
Duration 尾部
最后sample的presentation结束可由PTS+duration估计,但B帧重排时最大PTS不一定属于最后decode sample。轨道可见duration还受edit list和padding。不能简单取最后数组项。
Track Fragment Decode Time
tfdt 必要性
tfdt给traf首sample的baseMediaDecodeTime。没有tfdt时某些文件可由前fragment累计推导,但随机打开片段和丢片恢复更困难。CMAF类profile通常要求明确时间。
连续性
对同track,下个fragment tfdt应等于前fragment tfdt加所有sample duration,除非明确discontinuity/period切换。小于表示重叠/重置,大于表示时间洞。验证报告用media timescale显示差值。
64 位升级
长时间直播的decode time会超过32位,tfdt version 1使用64位。写入器在临界前升级,不能让version0环绕。切换version只改变Box字段宽度,不重置时间线。
第十五章 Sample Auxiliary Information
saiz
SampleAuxiliaryInformationSizesBox可给统一default_sample_info_size,或在其为零时给每sample的8位size数组。flags可使aux_info_type和parameter出现。sample_count与媒体sample数量/fragment范围匹配。
saio
SampleAuxiliaryInformationOffsetsBox给一个或多个aux data offset,version决定32/64位,flags同样可带type。偏移基准随文件/fragment上下文定义,不能统一当绝对文件offset。
配对
saiz说明每项长度,saio说明一个或多个数据块起点。解析器按目标scheme/type将两者配对并累计大小,验证范围不越界。多个aux类型可并存,不能只取第一个saiz/saio。
Fragment 场景
traf内aux信息通常只覆盖该fragment sample。Offset可能相对moof或其他基准。重写moof大小或移动mdat时必须更新saio,与trun data_offset一样参与布局迭代。
Protection System Specific Header
pssh
PSSH是FullBox,包含16字节system ID、可选KID列表和data size/payload。Version 1可列KID,version0没有相同数组。所有count和size绑定Box剩余。
PSSH 不是密钥
PSSH携带DRM系统初始化数据,不应包含可直接公开使用的内容密钥。解析器只显示system ID、KID数量和data长度;系统特定payload由受信DRM模块处理,不应未经转义打印。
多个 PSSH
文件可为不同DRM系统或不同位置放多个PSSH。播放器选择支持的system。去重不能只按system ID,因为payload/KID可能不同。
CENC Track Encryption
sinf
Protection Scheme Information通常含frma原始sample entry格式、schm scheme type/version和schi scheme-specific信息。受保护entry如encv/enca通过frma恢复原codec FourCC。
tenc
TrackEncryptionBox给默认isProtected、per-sample IV size、default KID以及某些version中的crypt/skip pattern或constant IV。字段存在性依version与IV size条件。KID固定16字节。
IV
Per-sample IV通常在senc/aux data;若默认IV size为零,配置可提供constant IV。解密器为每sample选正确IV,不能把前sample counter状态无条件延续。
Subsample Entry
Subsample encryption给若干clear/encrypted byte count对。Clear+encrypted总和应覆盖sample或按scheme规则闭合。H.264常保留部分NAL header/length清晰,但具体pattern由packager生成,codec parser需在解密后验证完整payload。
CENC Scheme 差异
cenc
常见cenc使用AES-CTR。CTR不需要块对齐,encrypted byte count可为任意长度。Counter/IV构造由scheme规定。重用IV与KID组合会破坏安全性,写入器必须保证唯一策略。
cbc1
CBC模式按完整块加密,尾部不足块的处理遵循scheme/subsample规则。不能使用普通文件CBC padding任意扩充sample,否则stsz和codec字节改变。
cens 与 cbcs
Pattern encryption只加密若干块、跳过若干块,crypt_byte_block/skip_byte_block在tenc中表达。CTR/CBC pattern语义不同。视频硬件解密需要这些参数与sample aux信息。
Clear Lead
一个period开始可有未加密sample或使用clear sample entry/group覆盖。播放器依据sample group/description切换保护状态。不能只看轨道默认tenc就断言每sample都加密。
Sample Encryption Box
senc
SampleEncryptionBox含sample_count及每sample IV,flags可指示subsample encryption。IV长度来自tenc或group覆盖,不在每entry重复声明。解析前必须解析保护配置。
Override 参数
某些版本/flags允许override track encryption参数,字段布局依规范。未知flags不应按最常见senc直接读取。Sample count与trun/stsz匹配。
位置
senc可位于traf等上下文,aux信息也可能在mdat并由saio指向。验证器抽象为每sample encryption record,不依赖单一物理位置。
Sample Group Encryption
seig
sgpd grouping type seig可为sample group覆盖isProtected、IV size、KID、pattern和constant IV。sbgp把sample映射到描述。Group index和fragment-local规则需精确处理。
Key Rotation
通过多个seig描述和sbgp映射可在同track轮换KID。播放器在sample边界选择key,缺少对应KID时可跳过/等待但保持时间。不能缓存首KID用于全轨。
配置验证
每sample最终保护配置来自default tenc和group覆盖。输出报告列明KID、scheme、IV来源和subsample count,但不输出实际密钥。
Producer Reference Time
prft
ProducerReferenceTimeBox将某个track的media time关联到NTP timestamp,可用于直播端到端时钟和同步。version决定media_time宽度,NTP为64位秒/分数格式。
NTP 转换
NTP高32位为秒、低32位为二进制小数。转换到纳秒使用宽整数/高精度,处理纪元与era。不能当作Unix 64位微秒。
同一时钟域
PRFT只有在producer NTP与media clock关联可靠时有意义。跨fragment多个点可估计漂移。Discontinuity后应建立新映射,不跨重启回归。
Event Message
emsg
EventMessageBox用于在分片媒体中携带定时事件,含scheme_id_uri、value、timescale、presentation time/delta、duration、id和message data。Version 0/1字段顺序与时间表达不同。
String 边界
URI/value字符串按Box内终止规则读取,必须在Box结束前找到终止。Message data是剩余二进制,不是C字符串。日志限制长度。
Event 时间
Version0常相对片段/earliest presentation time表达delta,version1可给绝对presentation time。换算到movie timeline需使用event timescale并结合period。事件不改变media sample DTS。
Segment Type
styp
Segment Type Box结构类似ftyp,用于媒体segment声明品牌兼容。它描述当前segment,不替代初始化段ftyp/moov。compatible brands数量由Box大小推导。
Segment 自包含性
普通fMP4 media segment通常依赖初始化段的track/sample entry/trex。styp存在不代表可独立解码。打包器应确保客户端在segment前获得匹配init。
Brand 验证
styp声明的segment profile应与moof/trun、SAP、时间连续性和加密结构相容。只添加brand不改变不符合的结构。
第十六章 CMAF 风格 Fragment 边界
说明边界
CMAF建立在ISO BMFF上并施加更严格profile。本文在MP4手册中只说明与Box/sample结构直接相关的通用原则,不把完整streaming manifest或网络协议混入MP4格式。
Chunk 与 Segment
低延迟场景可让一个segment包含多个更小chunk,每个chunk通常是可顺序处理的moof+mdat。发布时Box必须完整,tfdt/trun时间连续。HTTP传输分块不是Box size替代。
Random Access
Segment入口的SAP/视频随机接入、音频边界和初始化配置应符合profile。每个低延迟chunk不一定都是视频随机入口;播放器从segment/period入口选择。
Decode Time
同一switching set中轨道/representation时间对齐,使客户端切换时presentation连续。验证器比较tfdt、duration、timescale和SAP,而非文件名序号。
Item 与 Track 模型边界
Meta Item
ISO BMFF也能通过meta/item结构保存图像等item,使用iloc/iinf/iprp等Box。这与MP4传统trak/sample时间序列模型不同。一个文件可同时有item和track。
iloc
Item Location使用可变字段宽度描述data reference、base offset和extent。Version影响item ID与construction method。所有offset/length组合受对应data source范围限制。
iinf/infe
Item Information列出item ID、type、name和保护等信息,version布局不同。它不是sample entry,不能用stsd解析。
iprp
Item Properties通过property container和association把尺寸、颜色、codec配置等属性关联item。Association index和essential bit需验证。删除property需更新所有association。
MP4 手册取舍
传统音视频MP4主要使用track/sample;HEIF/AVIF等item profile有独立完整规范。解析器可识别并安全遍历item结构,但本手册不把静态图像profile冒充普通MP4轨道格式。
MP4 重写算法
解析到中间模型
先把Box树、track、sample、fragment、metadata和data ranges解析为中间模型,保留未知Box。所有修改基于模型生成新offset/size,避免原地零散patch造成父size不一致。
两遍布局
第一遍计算Box大小和顺序,确定mdat/moov/moof位置;第二遍写header/payload并生成offset。若stco升级co64或descriptor长度宽度变化导致大小改变,重新布局直到稳定。
Payload Copy
未改变codec sample时可按验证后的range复制,不能按原mdat整体copy后假定新chunk offset不变。外部data reference、自加密aux和多个mdat需分别处理。
Unknown Box
未知Box若是叶子且位置独立可原样保留;若其内部可能包含offset或track ID,移动/删轨可能使其失效。工具提供preserve/drop policy并警告,不能声称完全语义安全。
Commit
写到临时文件,fsync/关闭后用独立validator全量解析,比较sample hashes、时间和配置,再原子替换目标。失败保留原件和报告。
MP4 Repair Audit
证据等级
Box size与父范围是结构证据;sample table一致和payload codec解析是更强证据;播放器成功只是容错行为。修复报告给每个重建字段来源:原始、计算、codec扫描或用户输入。
时间恢复
缺stts时,仅凭H.264 NAL通常无法唯一恢复VFR DTS;VUI可给标称率但不是逐帧时间。AAC可由配置推常见sample duration但仍需考虑对象类型/丢帧。不能生成“精确原时间”承诺。
Offset 恢复
若stsz与codec边界可信,可在mdat扫描候选sample;但多个轨道交错、加密和无同步码codec使恢复歧义。候选需由已知chunk/时间/配置交叉验证。
Encryption
缺tenc/senc/saio/KID时,密文不能凭字节恢复保护元数据。结构修复可保留密文range并报告不可解密,不能尝试猜key或把密文标作clear codec。
第十七章 mvhd 精确字段布局
mvhd Version 0
FullBox头后字段依次为:
| 相对 FullBox payload 偏移 | 长度 | 字段 |
|---|---|---|
| 0 | 4 | creation_time |
| 4 | 4 | modification_time |
| 8 | 4 | timescale |
| 12 | 4 | duration |
| 16 | 4 | rate 16.16 |
| 20 | 2 | volume 8.8 |
| 22 | 2 | reserved |
| 24 | 8 | reserved[2] |
| 32 | 36 | matrix |
| 68 | 24 | pre_defined[6] |
| 92 | 4 | next_track_ID |
这里偏移从version/flags之后开始;整个Box还要加8/16字节通用Box header与4字节FullBox头。Parser应明确采用哪种offset基准。
mvhd Version 1
Version 1把creation/modification/duration改为64位,timescale仍32位,因此后续rate等字段比version0向后移动12字节。不能只把duration读64位而保留其他固定offset。
Timescale
Timescale必须非零。Movie duration秒数为duration/timescale,但特殊unknown duration按规范处理,不参与普通除法。所有track edit segment duration也使用movie timescale。
Rate 与 Volume
正常播放常见rate=0x00010000、volume=0x0100。非默认值不应导致parser失败,但播放器能力可能有限。视频文件的movie volume并不等于每轨音量自动相乘的唯一策略。
Matrix
九成员矩阵的a/b/u/c/d/v/x/y/w按标准定点格式。单位矩阵原始值具有固定pattern。旋转矩阵可有负值,必须按signed解析相应成员。
tkhd 精确字段布局
Version 0 时间字段
FullBox后依次creation time、modification time、track_ID、reserved、duration、两项reserved、layer、alternate_group、volume、reserved、matrix、width、height。Track ID与duration之间有一个32位reserved,漏掉会错读duration。
| 字段 | 长度 | 说明 |
|---|---|---|
| creation/modification | 4+4 | 1904纪元 |
| track_ID | 4 | 非零且movie内唯一 |
| reserved | 4 | 保留 |
| duration | 4 | movie timescale |
| reserved[2] | 8 | 保留 |
| layer | 2 | signed |
| alternate_group | 2 | signed/分组语义 |
| volume | 2 | 8.8 |
| reserved | 2 | 保留 |
| matrix | 36 | 变换 |
| width/height | 4+4 | 16.16 |
tkhd Version 1
Creation、modification、duration扩为64位,track_ID仍32位。解析version1时保持保留字段位置。Box最小size据此增加。
Flags
track_enabled、track_in_movie、track_in_preview等位于24-bit flags。未知flag保留。轨道选择时区分“存在”“启用”“参与movie”。
Display 尺寸
Width/height是unsigned fixed-point显示尺寸。若矩阵旋转90度,最终bounding box可交换。编码sample entry尺寸可能不同,输出报告同时列出。
mdhd 精确字段布局
mdhd Version 0
FullBox后:32位creation、32位modification、32位timescale、32位duration、16位language、16位pre_defined。最小专用payload24字节(含FullBox四字节时相应增加)。
mdhd Version 1
前三个时间中creation/modification和duration为64位,timescale32位,language/pre_defined保持16位。Timescale零非法。
Language 编码
Language字段由一个pad bit与三个5-bit字符值组成,通常字符通过加0x60映射小写字母。先分离bit,不把16位直接当两个ASCII。保留/未定义值按标准显示。
Duration 校验
将stts所有count×delta求和,与mdhd duration比较。Edit list不改变原media duration。Fragmented未最终封闭文件可unknown/0,使用fragment累计另行报告。
hdlr 精确字段布局
FullBox 字段
FullBox后通常有pre_defined、handler_type、三个reserved uint32,随后name字节到Box结束。handler_type按FourCC字节,reserved应按规范。
1 | pre_defined 4 |
Name
Name在不同生态可能C string或其他约定;核心轨道类型只依handler_type。按Box剩余读取并安全显示,不能因name无NUL越界。
elst 精确字段布局
Entry Count
Edit List是FullBox,首先32位entry_count。Version0每entry通常12字节,version1每entry20字节。entry_count×entry_size不能超过Box剩余。
Version 0 Entry
| 字段 | 长度 | 类型 |
|---|---|---|
segment_duration |
4 | unsigned, movie timescale |
media_time |
4 | signed, media timescale |
media_rate_integer |
2 | signed |
media_rate_fraction |
2 | signed/固定点部分 |
Version 1 Entry
Segment duration为64位unsigned,media_time为64位signed,rate两字段不变。读取media_time必须使用signed big-endian转换,-1空编辑不能变成2^64-1时间。
时间映射
Movie cursor从0开始,每entry推进segment_duration。空编辑不消费media;有效edit从media_time映射。Rate常为1:0。求sample可见区间时做两timescale有理换算。
stsd 精确字段布局
Entry Count 与范围
Sample Description Box是FullBox,之后32位entry_count,再串联entry Box。每entry有自己的32位size+4字节format。Entry size至少8且不超过stsd父范围。
通用 SampleEntry 前缀
Entry专用payload开头通常含6字节reserved和16位data_reference_index:
1 | reserved[6] = 0 |
之后由visual/audio/hint等类型定义。Index必须在dref范围。
Entry 切换
stsd数组只定义配置,stsc或fragment字段选择sample使用哪个entry。一条轨可有多个format/配置。Parser不应只解析第一entry并假定全轨使用。
VisualSampleEntry 精确字段
固定部分
通用前缀后依次pre_defined/reserved、pre_defined[3]、width/height、horizresolution/vertresolution、reserved、frame_count、32字节compressorname、depth、pre_defined=-1等。随后可嵌套avcC/pasp/colr/clap等child Box。
Compressor Name
32字节区域常以首字节长度的Pascal string表达,长度最多31,其余padding。不能把整个32字节按NUL string输出;损坏length>31需截断/报告。
Child Box 起点
固定VisualSampleEntry字段读完后,剩余按子Box解析。AvcC不是任意extradata裸尾;有4字节Box header/size。未知子Box安全跳过。
AudioSampleEntry 精确字段
AudioSampleEntry 基础字段
通用前缀后常见reserved[2]、channelcount、samplesize、pre_defined、reserved、samplerate 16.16。不同version/QuickTime音频entry可能有额外字段,解析需依据entry版本/规范。
Channel Count
容器channelcount与AAC ASC/PCE最终输出可因SBR/PS/生态约定存在复杂关系。验证冲突,不用基础字段覆盖codec config。
Child Box
mp4a后常有esds,也可能有btrt等。受保护enca还含sinf并通过frma找到原format。Parser在entry范围内递归。
stts 精确字段布局
stts Entry
FullBox后32位entry_count,每entry两个uint32 big-endian:sample_count、sample_delta。Entry size固定8。Count和delta的语义为连续run。
合法性
sample_count应非零;entry_count可为零于空轨。累计sample count与stsz/stz2一致,累计duration用至少64/128位:
1 | total_samples += sample_count |
随机访问时间
可给run建立前缀sample/time索引,二分定位sample DTS,避免把数百万sample全部展开。Run内DTS线性计算。
ctts 精确字段布局
ctts Version 0
每entry为sample_count uint32与sample_offset uint32。Offset非负。累计count应等于sample数(按存在语义)。
ctts Version 1
Offset字段按int32解释,允许负值。Entry仍8字节。PTS=DTS+signed_offset使用signed宽整数。
Run 索引
和stts一样建立count前缀,按sample index取得offset。不能假定ctts entry边界与stts run边界相同。
缺失
没有ctts则offset=0、PTS=DTS。空ctts与缺失的兼容行为按规范/文件验证,写新文件避免无意义空Box。
stsc 精确字段布局
stsc Entry
FullBox后entry_count,每entry三个uint32:first_chunk、samples_per_chunk、sample_description_index。Entry size12。
单调性
First_chunk一基且严格递增,第一个通常为1;samples_per_chunk>0;description index在stsd 1..entry_count。最后run延伸到chunk offset count。
展开实例
1 | (first_chunk=1, samples_per_chunk=4, desc=1) |
表示chunk1-2各4sample,从chunk3到末尾各2sample。若总chunk=5,总sample=4+4+2+2+2=14。
First Chunk 超界
某entry first_chunk大于chunk_count+1或run为空按严格规则报告。不能访问不存在chunk。计数推导和stsz比较。
stsz 精确字段布局
固定大小模式
FullBox后sample_size、sample_count各uint32。Sample_size非零时,所有sample都该大小,后面没有entry_size数组。Box若仍多出数组是结构异常/扩展。
可变大小模式
Sample_size=0时,后跟sample_count个32位entry_size。数组字节=count×4。每个size可按媒体语义为0,但offset推进仍处理sample index。
总字节
固定模式total=sample_size×count;可变模式累加。该总数是轨道媒体sample字节,不一定等于mdat大小,因为多轨交错、多个mdat和padding/aux。
stz2 精确字段布局
Header
FullBox后3字节reserved、1字节field_size、32位sample_count,再是packed entry。Field_size仅4、8、16。
4-bit
Sample偶数index通常取字节高nibble,奇数取低nibble(以规范顺序为准)。需要ceil(count/2)字节。最后unused nibble不作为额外sample。
8/16-bit
8-bit每sample一字节;16-bit每sample两字节big-endian。数组长度精确闭合。stz2 compact size有限,超范围必须用stsz。
stco 精确字段布局
stco Entry
FullBox后entry_count,随后count个32位unsigned chunk_offset。Offset从文件/数据源起点按数据引用解释,指向chunk首媒体字节。
Count
Count是chunk数量,必须能被stsc run覆盖。Offset不要求物理递增于所有特殊文件,但常见交错布局每轨自身递增;重复/逆序作为警告并按range验证。
32 位上限
任何offset超过0xFFFFFFFF需co64。Faststart布局变化后重新评估全部值。不能截断高位。
co64 精确字段布局
co64 Entry
结构与stco相同,但每entry为64位big-endian,数组字节=count×8。Parser内部统一uint64 chunk offset,来源类型单独记录。
与 stco 互斥
同一stbl通常只应有一套chunk offset表。两者同时存在会歧义,严格验证拒绝。转换stco→co64增大moov,需要重新布局。
stss 精确字段布局
stss Entry
FullBox后entry_count与若干32位sample_number。编号一基,范围1..sample_count,通常严格递增且不重复。
缺失语义
缺失stss表示所有sample是sync sample的标准语义用于相应轨道。存在但entry_count=0是否表示无sync/损坏需按规范和codec处理。Video轨至少应有可开始入口或依赖外部初始化。
Codec 反验
AVC sample标sync还需解析NAL/恢复语义。容器与codec冲突时,seek安全策略取更保守结果并报告。
sdtp 位字段
每 Sample 一字节
字节由四个2-bit字段组成:is_leading、sample_depends_on、sample_is_depended_on、sample_has_redundancy。具体bit位置按高到低规范映射。Entry没有显式sample_count,数量由Box payload长度与轨道sample count验证。
三态/未知
2-bit值可表达unknown、yes、no和保留,不是布尔。API用枚举,不能转value!=0。保留值报告。
Fragment
Fragment sample flags提供相似信息,但布局不同。普通stbl用sdtp,fragment用trun/tfhd/trex最终flags;不要混用字节掩码。
第十八章 Sample Table 一致性算法
输入集合
读取stsd、stts、可选ctts、stsc、stsz或stz2、stco或co64、可选stss/sdtp/padb/group。先验证每Box自身size/version/count,再做跨表关联。
样本数
主sample count来自stsz/stz2;stts count和、ctts count和、sdtp payload entries、padding/group映射应匹配。Stsc按chunk展开得到count也匹配。
Chunk 数
来自stco/co64 entry_count。Stsc first_chunk不得越界,最后run覆盖到chunk_count。每chunk samples_per_chunk累计形成全sample序列。
Description 一致性
每chunk的description index有效;由其sample继承。若某stsd entry从未被引用可保留但报告unused。配置变化点与随机访问检查。
Offset 一致性
对每chunk,从chunk_offset开始累加其中sample size,得到每sample range。每range在正确data source/mdat,互不非法重叠。下一chunk不一定紧邻。
Time 一致性
按stts生成DTS/duration,ctts生成PTS,edit映射movie time。Sync/dependency加入seek属性。所有运算用有理数/宽integer。
Sample Table 计算实例
受控轨道参数
构造六个 Sample、三个 Chunk、两个 Sample Description 的轨道。Media timescale 为1000单位/秒。表的逻辑值:
1 | stsz sizes = [100, 120, 80, 90, 110, 70] |
Sample Number 在 stss 中从1开始;内部数组索引可以从0开始,但两者转换要显式 sample_number=index+1。Chunk Number 在 stsc.first_chunk 中也从1开始。
stsc 完整字节
两个Run
完整 Box:
1 | 00 00 00 28 73 74 73 63 |
| 偏移 | 字节 | 解释 |
|---|---|---|
| 0 | 00 00 00 28 |
Box size 40 |
| 4 | 73 74 73 63 |
type stsc |
| 8 | 00 00 00 00 |
version=0, flags=0 |
| 12 | 00 00 00 02 |
entry_count=2 |
| 16 | 00 00 00 01 |
Run0 first_chunk=1 |
| 20 | 00 00 00 02 |
Run0 samples/chunk=2 |
| 24 | 00 00 00 01 |
Run0 description=1 |
| 28 | 00 00 00 03 |
Run1 first_chunk=3 |
| 32 | 00 00 00 02 |
Run1 samples/chunk=2 |
| 36 | 00 00 00 02 |
Run1 description=2 |
大小闭合:
1 | 8 Box Header + 4 FullBox + 4 entry_count + 2*12 entries = 40 |
Run0从Chunk1覆盖到下一个 first_chunk减一,即Chunk2;Run1从Chunk3覆盖到chunk_count=3。first_chunk必须严格递增,第一项必须适用于Chunk1,samples_per_chunk非零,description index落在 1..stsd.entry_count。
stsz 完整字节
可变Sample Size
1 | 00 00 00 2C 73 74 73 7A |
解析:
1 | sample_size = 0 # 启用逐项表 |
Box size:
1 | 8 + 4 + 4 + 4 + 6*4 = 44 = 0x2C |
若 sample_size 非零,后面不出现六个 size entries,Box仍声明 sample_count用于轨道Sample数量。解析器不能在固定大小模式继续读取数组。
总媒体Payload字节:
1 | 100+120+80+90+110+70 = 570 |
该总和不等于Chunk span,也不包含其他轨道数据、Box Header、mdat Header或Chunk之间空洞。
stco 完整字节
三个Chunk Offset
1 | 00 00 00 1C 73 74 63 6F |
三个大端 offset:
1 | 0x000003E8 = 1000 |
Box size 8+4+4+3*4=28=0x1C。Offset 是文件/数据引用坐标中的Chunk首Sample数据位置,不是 mdat 相对值,也不指向一个通用“Chunk Header”——ISO BMFF Sample Chunk在mdat中通常没有逐Chunk Box Header。
若任一 offset超过32位,使用 co64,每项八字节;同一 Sample Table 不应同时以冲突的 stco和co64作为有效来源。
stts 完整字节
两个Decode Time Run
1 | 00 00 00 20 73 74 74 73 |
两项分别是三Sample×1000和三Sample×2000。Box size 8+4+4+2*8=32。
展开 duration:
| Sample Number | Duration | DTS |
|---|---|---|
| 1 | 1000 | 0 |
| 2 | 1000 | 1000 |
| 3 | 1000 | 2000 |
| 4 | 2000 | 3000 |
| 5 | 2000 | 5000 |
| 6 | 2000 | 7000 |
轨道Media Duration:
1 | 3*1000 + 3*2000 = 9000 media units = 9 seconds |
DTS是Sample解码开始时间,最后 duration结束于9000。累计使用至少64位并检查 count*delta 与总和溢出。
ctts Version 1 完整字节
Signed Composition Offset
1 | 00 00 00 20 63 74 74 73 |
FullBox Version为1,因此 sample_offset 按 signed int32解释。FF FF FE 0C 是 -500。三Run的sample_count和 2+2+2=6,必须与stsz count一致。
PTS:
| Sample | DTS | CTS Offset | PTS |
|---|---|---|---|
| 1 | 0 | 0 | 0 |
| 2 | 1000 | 0 | 1000 |
| 3 | 2000 | +1000 | 3000 |
| 4 | 3000 | +1000 | 4000 |
| 5 | 5000 | -500 | 4500 |
| 6 | 7000 | -500 | 6500 |
PTS可以与DTS不同,Version 1可为负偏移。最终Movie时间还要经过 Edit List映射;不能为了让最早PTS非负而直接修改Sample字节。
stss 完整字节
Sync Sample 1与4
1 | 00 00 00 18 73 74 73 73 |
Box size 8+4+4+2*4=24。Entry严格递增、范围1..6。Sample1和4是容器声明的sync samples;验证器再用Codec配置和Sample Payload反验。存在stss时,未列出的Sample不应自动当同步点;缺失stss对所有Sample是否sync的语义要按媒体类型和标准处理。
Chunk 到 Sample 的展开
Run Cursor
1 | chunk 1, stsc run 0: 2 samples, description 1 |
Sample映射:
| Chunk | Chunk Offset | Sample | Size | Sample Offset | Description |
|---|---|---|---|---|---|
| 1 | 1000 | 1 | 100 | 1000 | 1 |
| 1 | 1000 | 2 | 120 | 1100 | 1 |
| 2 | 1300 | 3 | 80 | 1300 | 1 |
| 2 | 1300 | 4 | 90 | 1380 | 1 |
| 3 | 1600 | 5 | 110 | 1600 | 2 |
| 3 | 1600 | 6 | 70 | 1710 | 2 |
Chunk内从Chunk offset开始紧密累加 Sample size。Chunk1结束1220,下一个Chunk从1300开始,中间80字节不属于该轨道这两个Sample;可能是其他轨道Chunk、padding或其他mdat区域。Chunk2结束1470到1600也有空洞。不能把下一Chunk offset差值当Sample总大小覆盖空洞。
完整Sample目录
| No. | Offset Range | Size | DTS | Duration | CTS Offset | PTS | Desc | Sync |
|---|---|---|---|---|---|---|---|---|
| 1 | [1000,1100) |
100 | 0 | 1000 | 0 | 0 | 1 | yes |
| 2 | [1100,1220) |
120 | 1000 | 1000 | 0 | 1000 | 1 | no |
| 3 | [1300,1380) |
80 | 2000 | 1000 | 1000 | 3000 | 1 | no |
| 4 | [1380,1470) |
90 | 3000 | 2000 | 1000 | 4000 | 1 | yes |
| 5 | [1600,1710) |
110 | 5000 | 2000 | -500 | 4500 | 2 | no |
| 6 | [1710,1780) |
70 | 7000 | 2000 | -500 | 6500 | 2 | no |
所有Range使用半开区间,末字节地址为end-1。读取验证采用:
1 | require sample_offset <= data_source_size |
避免 offset+size 回绕。
Description 切换
只能在Chunk边界表达
sample_description_index 位于stsc entry,因而一个Chunk内的所有Sample共享同一Description。本例Sample1~4用stsd Entry1,Sample5~6用Entry2。配置切换发生在Chunk3/Sample5边界。
若Sample5是AVC且新Description带不同avcC,解码器在使用Sample5前加载Entry2配置。容器还应确保切换点满足Codec随机访问/重配置规则;本例stss只列1、4,Sample5不是Sync,因而“Description改变但非随机访问”是需要进一步审计的风险。不能仅因stsc合法就认定解码器可无缝切换。
六表数量闭合
独立计数
1 | stsz sample_count = 6 |
任何一个Count不闭合都应报错。不能取最小值后静默截断,因为这会把剩余mdat字节和时间含义丢弃;恢复模式可以产生候选,但必须标记推断。
Offset 与数据源验证
mdat Range不是单一假设
文件可有多个mdat,Sample还可通过Data Reference引用其他资源。对self-contained普通文件,建立所有mdat payload半开区间:
1 | mdat_ranges = [[start0,end0), [start1,end1), ...] |
每个Sample Range必须完整落在一个允许的数据源Range,不能只检查小于文件大小。Sample横跨mdat边界即使仍在文件内也无效。
本例若mdat payload覆盖 [900,1800),六个Sample都在范围内。若只覆盖 [900,1700),Sample5 [1600,1710) 已越界,Sample6更无效。
重叠
同一轨道普通Sample通常不应非法重叠;多轨道可能在异常/引用场景共享数据,但需格式语义支持。验证器对所有Range排序,报告:
1 | duplicate exact ranges |
不把Gap自动计为损坏。Faststart文件的moov在mdat前,Chunk offsets自然跨过不同Box区域。
时间与Seek实例
目标4.2秒
Media timescale1000,目标4200 units。按PTS寻找显示Sample,Sample4 PTS=4000、duration2000,覆盖显示区间 [4000,6000);但随机访问应回退到不晚于目标的Sync Sample。stss为1和4,所以可从Sample4开始。
如果目标3.5秒,Sample3 PTS=3000附近,但最近不晚于目标的Sync是Sample1;Sample4的PTS4000晚于目标。播放器可能从Sample1解码到目标,或按业务选择Sample4并产生晚启播,不能把Sample3 Index Flag伪改为Sync。
DTS调度
即使按PTS选择显示目标,解码器输入按DTS/Sample解码顺序提交。本例DTS严格递增。B Frame场景下PTS可能重排;按PTS排序读取mdat会破坏参考依赖。
表序列化验证
Box大小
1 | stsc = 40 |
每个Box的32位size都包含自身八字节Header。FullBox的Version/Flags属于payload前四字节。把size误写为仅payload会让下一个Box起点提前八字节。
Round-trip
Reader解析上述字节得到逻辑表,Writer从逻辑表重新生成字节。若保留相同run分组和版本,结果应bit-exact。逻辑等价Writer可能合并相邻相同stts/ctts run,但不能跨Description变化合并stsc。
Round-trip后再次展开六Sample目录,逐项比较Offset、Size、DTS、PTS、Description和Sync;并对mdat Sample Payload计算hash,证明重写moov没有改变媒体字节。
Sample Table Parser 内存策略
不完全展开
超长音频轨有数千万sample。可以保留stts/ctts/stsc run和stsz mmap/分页索引,按需计算;不必为每sample分配大对象。
Prefix Index
每隔固定sample建立checkpoint:sample index、chunk、offset、DTS和run cursor。Seek从最近checkpoint向前小范围展开,实现内存/速度折中。
不可信 Count
在分配前用Box payload推导最大entry数:例如stts最多remaining/8。声明count大于此立即失败。再应用全局memory cap。
Streaming Fragment
fMP4无需构建全局stbl;逐moof解析trun并产出sample,保留每track decode state/checkpoint。仍设置每fragment sample count和duration上限。