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 判断能力,而不能只检查 isommp42 等 brand。

moov 层级

典型层级:

1
2
3
4
5
6
7
8
9
moov
├── mvhd 全局 movie header
└── trak 一条轨道
├── tkhd 轨道头
└── mdia
├── mdhd 媒体时间基
├── hdlr 轨道类型
└── minf
└── stbl 样本表

第二章 Track、Sample 与编码描述

stsd 与编码配置

stsd(Sample Description Box)描述样本格式。视频常见 sample entry:avc1avc3hev1hvc1;音频常见:mp4aOpus 等。

H.264 的 avc1 sample entry 通常包含 avcC,其中保存 SPS/PPS 和 NAL length size;AAC 的 mp4a 下通常有 esds,其 DecoderSpecificInfo 中包含 AudioSpecificConfig。

因此:

1
2
MP4 H.264 样本通常是 AVCC 长度前缀格式,不是 Annex-B。
MP4 AAC 样本通常不带 ADTS 头,配置在 esds/ASC 中。

第三章 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
2
DTS = decode_time / track_timescale
PTS = (decode_time + composition_offset) / track_timescale

视频 B 帧常导致 PTS 与 DTS 不同。跨音视频轨道同步时要换算到统一时间基。

关键帧与随机访问

stss 记录同步样本。没有 stss 时,某些文件约定所有样本可同步,但播放器不能简单把第一帧当关键帧。H.264 还应结合 IDR/CRA 等编码语义判断真正的解码入口。

第四章 Fragmented MP4 概述

分片文件模型

fMP4 将媒体拆为:

1
2
init segment: ftyp + moov
media segment: moof + mdat

moof 包含 traftfhdtfdttrun 等分片样本信息。它适合 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 改为长度前缀。

转换时还要保留时间戳和关键帧信息,不能只转换字节格式。

第五章 解析、写入与验证

解析安全性

  1. 每个 box 的 size 必须小于父容器剩余长度。
  2. 处理 64 位 size 和大文件偏移。
  3. 对未知 box 做安全跳过。
  4. 防止递归层级过深和整数溢出。
  5. 检查 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)

解析 mvhdtkhdmdhdsttscttstrun 时必须先根据 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 videsoun 等轨道类型

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。

stszstco/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
2
3
4
读取 avcC 中 SPS/PPS
输出 00 00 00 01 + SPS/PPS
读取每个 sample 的 NAL length
输出起始码 + NAL payload

转换时应在每个关键访问单元前确保参数集可用,并根据 lengthSizeMinusOne 使用 1~4 字节长度。

MP4 修复和验证

1
2
3
ffprobe -v error -show_format -show_streams input.mp4
ffmpeg -v error -i input.mp4 -f null -
mp4dump input.mp4

验证重点包括:box 层级闭合、sample 数量一致、offset 不越界、轨道 duration 合理、关键帧索引可用、编码配置与样本格式一致。

MP4 文件构成图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MP4
├── ftyp
├── moov
│ ├── mvhd
│ └── trak × N
│ ├── tkhd
│ └── mdia
│ ├── mdhd
│ ├── hdlr
│ └── minf
│ ├── vmhd/smhd
│ ├── dinf
│ └── stbl
│ ├── stsd
│ ├── stts
│ ├── ctts
│ ├── stss
│ ├── stsc
│ ├── stsz
│ └── stco/co64
└── mdat

Box Header 字节构成

普通 Box:

1
2
3
00..03  size       uint32 big-endian
04..07 type FourCC
08.. payload

size == 1

1
2
3
4
00..03  00000001
04..07 type
08..15 largesize uint64 big-endian
16.. payload

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
2
3
4
5
1. 根据 stsc 找到 sample 所属 chunk
2. 根据 stco/co64 得到 chunk_offset
3. 根据 stsz 累加该 chunk 内前序 sample 的长度
4. sample_offset = chunk_offset + intra_chunk_offset
5. sample_size = stsz[sample_index]

这就是为什么 MP4 不能只扫描 mdat 来获得完整帧列表。

MP4 中 H.264 与 AAC 的实际构成

1
2
视频轨:stsd/avc1/avcC + mdat 中的 length-prefixed NAL samples
音频轨:stsd/mp4a/esds + mdat 中的 raw AAC 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 是否唯一;sttsstszstsc 推导出的 sample 总数是否一致;chunk offset 加 sample size 是否超出文件长度;avcC 的 SPS/PPS 长度是否在 Box 范围内;esds 的 descriptor 长度是否闭合。任何长度字段都必须先做整数溢出检查,再转换为内存索引。

普通 MP4 的写入顺序

写入普通 MP4 时,应用首先确定轨道数量、编码配置、时间基和 sample 表策略。可以先写 ftyp,为 moovmdat 预留空间;也可以先把媒体 sample 写到临时区域,完成所有索引后再一次性输出。无论采用哪种方式,最终必须使 stsd 中的编码描述与 mdat 中的 sample 格式一致,使 stsz 的每个长度与实际 sample 字节数一致,使 stco/co64 的每个偏移指向真实 Chunk 起点。若使用 H.264,写入器还要保证 NAL 长度前缀的字节数和 avcClengthSizeMinusOne 相同。

为了实现渐进下载,常见做法是把 moov 放在 mdat 前面,也称 faststart。移动 moov 不是简单的文件块交换,因为所有 chunk offset 可能随位置变化;写入器必须重新计算 stco 或在偏移超出 32 位时升级为 co64。如果只把 moov 拷贝到文件开头而不调整索引,播放器会按照旧位置读媒体数据,结果可能是无法播放、画面花屏或音频噪声。

Fragmented MP4

Fragmented MP4 的数据时间关系

fMP4 将初始化信息放入 ftyp + moov,把每个媒体片段组织为 moof + mdatmoof 中的 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 位大端 largesizesize 为 0 通常表示 Box 延伸到父容器或文件末尾,但只允许在规范定义的上下文中使用。解析器必须先确认基本头至少有 8 字节,再根据 size 值读取扩展字段,并检查 Box 结束位置不超过父范围。

1
2
3
size == 1  → header = 8 + 8 + optional fields
size >= 8 → box_end = box_start + size
size == 0 → 由父 Box 或文件边界确定

uuid 类型还会在普通头之后追加 16 字节 user type。未知 Box 可以按长度跳过,但不能把 uuid 的额外字段误当作子 Box。

FullBox 的 version 和 flags

许多 ISO BMFF 叶子 Box 使用 FullBox 结构:4 字节 version/flags,随后是该 Box 的专用字段。version 为 0 或 1 时字段宽度可能不同,例如 mvhdtkhdmdhd 在 version 1 中使用 64 位 creation time、modification time、duration。解析器必须先读取 version 再选择字段布局,不能总按 version 0 的固定偏移读取。

Movie、Track 和媒体层级

moov 的完整树

常见普通 MP4 的元数据树可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
moov
├── mvhd
└── trak
├── tkhd
└── mdia
├── mdhd
├── hdlr
└── minf
├── vmhd/smhd/...
├── dinf
│ └── dref
└── stbl
├── stsd
├── stts
├── ctts
├── stsc
├── stsz/stz2
├── stco/co64
└── stss

并非每条轨道都需要所有 Box;例如没有 B 帧时可以省略 ctts,所有视频样本都是随机接入点时可以省略 stss。省略表示默认语义,而不是错误。验证器应根据轨道类型和 sample entry 判断必需与可选 Box。

mvhdtkhdmdhd

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 常见 mp4aesds,其中 DecoderSpecificInfo 保存 ASC。配置 Box 的字节序和 descriptor 长度都必须单独验证。avcC 中的 SPS/PPS 不是 mdat sample,播放器初始化时应先加载它们。

Sample Table 的推导关系

stts 解码时间

stts 每项是 sample_countsample_delta,表示连续若干 sample 使用相同解码时长。对第 i 个 sample,DTS 可由前面所有 entry 展开求和:

1
2
dts[0] = 0
dts[i+1] = dts[i] + delta[i]

展开前应检查所有 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 帧显示时间推到极大的正数。

stscstsz 和 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 位数组混读。

stcoco64

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
2
sample_offset(s) = chunk_offset(c)
+ sum(sample_size(k), k = first_sample_in_chunk .. s-1)

定位过程中必须检查每次加法不溢出,最终区间 [offset, offset + size) 在文件内。对于 interleaved 音视频,两个轨道的 chunk 可以交错,不能假定视频所有 sample 在一个连续区域。

sample 数量一致性

sttsctts(若存在)、stszstsc 展开的 sample 数量和关键帧索引必须一致。stsd 的 description index 也要对每个 sample 有效。发现计数不一致时,解析器可以尝试按实际 mdat 扫描,但不能把扫描结果静默当作标准 sample table。

Fragmented MP4 的索引与时间

mvex 和默认值

fMP4 的 moov 通常包含 mvextrex,为后续 fragment 提供默认 sample duration、size、flags 和 description index。tfhd 可以覆盖这些默认值,trun 再提供逐样本字段。解析器必须按“trun 显式值 → tfhd 值 → trex 默认值”的优先级推导,缺少必要字段时报告无法定位,而不是假设固定大小。

tfdttrun 和数据偏移

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
2
3
4
5
6
7
8
Box size / parent range / 64-bit extension
moov-trak-mdia-minf-stbl 层级闭合
track_ID 唯一且 handler 与 sample entry 一致
stts、ctts、stsc、stsz、stco/co64 数量一致
sample offset + size 不越界
avcC/esds 配置与 sample payload 一致
普通 MP4 的 moov/mdat 与 fMP4 的 moof/mdat 关系正确
时间基、编辑列表、关键帧和音视频同步可解释

MP4 的 Box 只负责描述和定位 sample。H.264 的 NAL 语法仍由 H.264 解码器解析,AAC raw sample 的语法仍由 AAC 解码器解析;avcCesds 只提供初始化配置。把 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 常见 videsountextsubtmeta 等,后面可有名称字符串。handler 说明媒体处理类别,sample entry 说明具体编码。vide 轨道中出现音频 sample entry 是结构矛盾,应报告而不是自动更改 handler。

Media Information 与数据引用

vmhdsmhd 与其他媒体头

视频 minf 常含 vmhd,音频常含 smhdvmhd 有 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 字符串结尾。

avc1avc3

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 的历史写法存在兼容差异,应结合配置分析。

mp4aesds

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 通常使用 stszstz2 之一。两者同时出现会产生大小来源歧义,应按规范验证或拒绝。零大小 sample 是否允许取决于媒体与上下文,不能在偏移算法中无条件除零或跳过 sample index。

Sample Group

sbgpsgpd

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 验证数组长度。

元数据容器

udtameta

用户数据可放在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
reference_ID = 1
timescale = 1000
earliest_presentation_time = 0
first_offset = 0
reference_count = 2

reference 0:
reference_type = 0
referenced_size = 5000
subsegment_duration = 2000
starts_with_SAP = 1
SAP_type = 1
SAP_delta_time = 0

reference 1:
reference_type = 0
referenced_size = 6000
subsegment_duration = 3000
starts_with_SAP = 1
SAP_type = 1
SAP_delta_time = 0

两个媒体子段共5秒,字节总量11000。

完整56字节Box

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
00 00 00 38 73 69 64 78
00 00 00 00
00 00 00 01
00 00 03 E8
00 00 00 00
00 00 00 00
00 00
00 02

00 00 13 88
00 00 07 D0
90 00 00 00

00 00 17 70
00 00 0B B8
90 00 00 00

大小:

1
2
3
4
5
6
7
Box Header             8
FullBox 4
reference_ID/timescale 8
EPT/first_offset 8
reserved/count 4
two references 24
total 56 = 0x38

Reference第一DWORD

Type与31-bit Size

1
2
bit31    reference_type
bits30:0 referenced_size

解析:

1
2
3
word = read_u32be()
reference_type = word >> 31
referenced_size = word & 0x7FFFFFFF

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
2
3
bit31       starts_with_SAP
bits30..28 SAP_type
bits27..0 SAP_delta_time

本例:starts=1、type=1、delta=0:

1
2
(1<<31) | (1<<28) = 0x90000000
bytes = 90 00 00 00

解析:

1
2
3
starts = word >> 31
sap_type = (word >> 28) & 0x7
sap_delta = word & 0x0FFFFFFF

SAP delta使用sidx timescale。starts_with_SAP=0时,SAP可能位于Referenced Subsegment内部,由delta描述;不能仍假定第一个Sample是随机访问点。

first_offset坐标实例

sidx位于0x0100

1
2
3
4
5
sidx_start = 0x0100
sidx_size = 0x38 = 56
sidx_end = 0x0138
first_offset = 0
first_reference_start = sidx_end + first_offset = 0x0138

Reference0 Range:

1
2
3
start0 = 0x0138
end0 = 0x0138 + 5000
= 0x14C0

Reference1紧接:

1
2
3
start1 = 0x14C0
end1 = 0x14C0 + 6000
= 0x2C30

Ranges:

1
2
ref0 [0x0138,0x14C0)
ref1 [0x14C0,0x2C30)

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
2
ref0 presentation range [0,2000) = [0s,2s)
ref1 presentation range [2000,5000) = [2s,5s)

累计:

1
2
3
4
time_cursor = earliest_presentation_time
for reference:
range = [time_cursor, time_cursor + duration)
time_cursor += duration

Reference Type1引用Index时,其duration仍参与当前层时间范围,但需要递归解析下级并验证汇总一致。递归深度、总Reference数和循环引用必须限制。

Version 1

64位EPT与Offset

Version1固定部分:

1
2
3
4
5
6
reference_ID 4
timescale 4
earliest_presentation_time 8
first_offset 8
reserved 2
reference_count 2

相同两个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
2
3
4
5
6
7
cursor = checked_add(sidx_end, first_offset)
for i in references:
size = entry.referenced_size
require size > 0 under target profile
require size <= file_end - cursor
range[i] = [cursor,cursor+size)
cursor += size

用减法比较避免加法回绕。外部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
2
3
4
5
sidx starts_with_SAP/SAP_type/delta
moof/trun final sample flags
sample group random access metadata
codec IDR/CRA/recovery point or audio independence
tfdt/PTS timeline

若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
2
3
4
5
6
7
8
parse_sidx(node, depth):
require depth <= max_depth
require node range not in active recursion stack
for ref:
if type==1:
require referenced range contains expected sidx
parse child
compare child time/size coverage

记录已访问Range避免A引用B、B又引用A的循环。限制总节点和Reference,避免恶意指数展开。

sidx解析算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
parse_sidx(box):
read FullBox
require version in {0,1}
reference_id = u32be()
timescale = u32be()
require timescale != 0

if version==0:
ept = u32be()
first_offset = u32be()
else:
ept = u64be()
first_offset = u64be()

reserved = u16be()
count = u16be()
require count <= remaining / 12

byte_cursor = checked_add(box.end, first_offset)
time_cursor = ept
for each entry:
word0 = u32be()
size = word0 & 0x7fffffff
type = word0 >> 31
duration = u32be()
word2 = u32be()
parse SAP bits
emit byte/time ranges with checked arithmetic

保留reserved异常作为规范警告。Count由uint16限制,但仍在读取前用Box剩余反验。

sidx错误向量

1
2
3
4
5
6
7
8
9
10
11
12
timescale=0
Version0却只剩Version1/截断字段
reference_count大于remaining/12
referenced_size=0
size最高位未mask
first_offset加法溢出
累计Reference超文件/Segment
Type1形成循环
SAP type超出Profile允许
SAP delta大于Subsegment duration
starts_with_SAP与首Sample/Codec冲突
Reference Range未在Box边界闭合

报告包含sidx文件Range、Version、Reference ID、timescale、每Reference byte/time Range、SAP字段、结构/Codec反验结果。

第十一章 Common Encryption 边界

保护后的 Sample Entry

CENC 文件可使用受保护 sample entry,并通过 sinffrmaschmschi/tenc 描述原始格式、scheme 和默认密钥/加密参数。解析器应先取得原始 codec 类型,再将加密作为外层保护属性;不能把 encrypted entry 当成未知 codec 后停止读取配置。

sencsaizsaio

每 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
2
SystemID = 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
Data = DE AD BE EF

完整Box:

1
2
3
4
5
00 00 00 24 70 73 73 68
00 00 00 00
00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
00 00 00 04
DE AD BE EF
字段 长度
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
2
3
4
KID_count uint32
KIDs[KID_count][16]
data_size uint32
data[data_size]

使用一个KID 10 11 ... 1F 和四字节Data,大小:

1
8 + 4 + 16 + 4 + 16 + 4 + 4 = 56 = 0x38

完整逻辑前缀:

1
2
3
4
5
6
00 00 00 38 70 73 73 68
01 00 00 00
[16-byte SystemID]
00 00 00 01
10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
00 00 00 04 DE AD BE EF

读取KID_count前用Box剩余反推最大项:至少为后续data_size保留四字节。Count乘16使用受检算术。Version0没有KID_count,不能因Data首四字节像小整数就误读为数组。

三Sample的IV辅助信息

假设配置

假设保护配置为每Sample 8字节IV,无Subsample Encryption。三个Media Sample对应IV:

1
2
3
IV0 = 00 01 02 03 04 05 06 07
IV1 = 10 11 12 13 14 15 16 17
IV2 = 20 21 22 23 24 25 26 27

IV是字节串,不作为宿主整数增减或交换端序。实际IV生成/唯一性由加密系统负责;本例只验证容器布局。

senc 完整字节

无Subsample Flag

1
2
3
4
5
6
00 00 00 28 73 65 6E 63
00 00 00 00
00 00 00 03
00 01 02 03 04 05 06 07
10 11 12 13 14 15 16 17
20 21 22 23 24 25 26 27

大小:

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
2
3
4
00 00 00 11 73 61 69 7A
00 00 00 00
08
00 00 00 03
字段
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=3senc.sample_count=3、媒体Sample Count=3三者闭合。统一size8与三IV总24字节一致。

saio 单Offset实例

Version0

1
2
3
4
00 00 00 14 73 61 69 6F
00 00 00 00
00 00 00 01
00 00 10 10

Box size20,entry_count1,Offset示例 0x1010。Offset基准由saio所在上下文、Fragment和相关标准版本/flags决定,不能一律解释为文件绝对位置。本受控示例明确约定非Fragment自包含文件、基准为文件起点,且senc从0x1000开始:

1
2
3
4
senc Header      [0x1000,0x1008)
FullBox [0x1008,0x100C)
sample_count [0x100C,0x1010)
first IV starts 0x1010

因此saio指向第一条Auxiliary Data。换到moof/traf场景时,应按该上下文计算基准并输出解析来源,不能复用本例绝对公式。

Version1使用64位offset,Box相应增大。超过32位时Writer必须升级,不能截断。

senc/saiz/saio 配对

逐Sample Range

1
2
3
4
5
6
aux_start = resolved_saio_offset
sizes = [8,8,8] from saiz

sample0 aux [0x1010,0x1018)
sample1 aux [0x1018,0x1020)
sample2 aux [0x1020,0x1028)

最后end等于senc Box end 0x1000+40=0x1028。解析出的三个Range分别对应三个IV。若saio指向senc Header而非第一IV,会把Box size/type当IV,结构表面仍有24字节但解密全部失败。

Subsample Encryption长度闭合

Entry结构

启用Subsample时,一条Entry概念上:

1
2
3
4
5
IV[configured_iv_size]
subsample_count uint16
repeat subsample_count:
bytes_of_clear_data uint16
bytes_of_protected_data uint32

Entry辅助大小:

1
iv_size + 2 + subsample_count*6

对Media Sample,所有clear+protected区段总和应覆盖Sample size(按scheme定义验证)。使用64位累计:

1
2
3
4
covered += clear
covered += protected
require covered <= sample_size at every step
require covered == sample_size at end

不能根据加密块大小擅自补齐未描述尾部。Pattern Encryption还需结合tenc/seig的crypt/skip block参数验证块处理,但容器Range仍必须闭合。

Sample Group覆盖

默认与每组配置

Track默认KID/IV规则来自tencsbgp/sgpdseig可让特定Sample使用不同KID、IV大小、constant IV或pattern参数。解析每Sample最终配置:

1
2
3
4
config = track tenc default
if sample maps to seig group entry:
config = group override
parse sample auxiliary info using final config

不能先按Track默认8字节切分全部senc,再应用Group;若某组IV为16字节,后续所有Entry边界都会错位。

加密结构与Codec边界

不解密时能验证什么

1
2
3
4
5
6
7
8
protected SampleEntry通过frma恢复原Codec类型
sinf/schm/schi/tenc Box边界和版本
PSSH Descriptor长度与SystemID
saiz/saio/senc Sample Count和Aux Range
IV/constant-IV长度
Subsample clear+protected总长度
KID和Group映射结构
Sample Offset/Size位于mdat

不能验证明文NAL/AAC语法或密钥正确。若Sample全加密,把密文字节当H.264 NAL length解析会产生伪错误;只有Subsample声明的clear区且包含相应结构时,才可在范围内做有限Codec检查。

CENC错误向量

1
2
3
4
5
6
7
8
9
10
11
PSSH Version1 KID_count越过Box
PSSH data_size大于剩余
senc count与媒体/saiz不一致
配置IV 16字节但senc只够8
saiz default size与实际Entry不同
saio Range指向Box Header或文件外
Subsample count乘6溢出
clear+encrypted累计超过Sample
seig覆盖后IV边界未重算
constant IV size为0或越界
未知Scheme却按cenc CTR规则解读

验证报告不输出密钥,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 的起点。

freeskip

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
parse_children(parent_end):
while position < parent_end:
require parent_end - position >= 8
child_start = position
size32 = read_be32()
type = read_fourcc()
if size32 == 1:
require remaining >= 8
size = read_be64()
header = 16
else if size32 == 0:
size = parent_end - child_start
header = 8
else:
size = size32
header = 8
require size >= header
require size <= parent_end - child_start
dispatch(type, child_start + header, child_start + size)
position = child_start + size

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
2
version = value >> 24
flags = value & 0x00FFFFFF

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 文件尾部,包含一个或多个 tframfro。它提供按轨道的随机访问索引,不承载媒体。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
2
3
4
5
6
7
8
moof size 100
├── mfhd size 16
└── traf size 76
├── tfhd size 28
├── tfdt size 20
└── trun size 20
mdat size 20
└── 3 samples × 4 bytes

轨道ID=1,默认Sample duration=1000、size=4、flags=0;base decode time=0。tfhd.default-base-is-moof=1trun.data_offset=108,所以数据从moof起点后108字节开始,即100字节moof加8字节mdat Header。

mfhd 完整字节

1
2
3
00 00 00 10 6D 66 68 64
00 00 00 00
00 00 00 01
字段
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
2
3
4
5
6
0x000008 default_sample_duration_present
0x000010 default_sample_size_present
0x000020 default_sample_flags_present
0x020000 default_base_is_moof
--------------------------------------
0x020038

完整Box:

1
2
3
4
5
6
00 00 00 1C 74 66 68 64
00 02 00 38
00 00 00 01
00 00 03 E8
00 00 00 04
00 00 00 00

逐字段:

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
2
3
00 00 00 14 74 66 64 74
01 00 00 00
00 00 00 00 00 00 00 00

Version1用64位无符号 baseMediaDecodeTime=0。Box大小 8+4+8=20。Version0只用32位,总大小16;解析器由version决定宽度,不能按Box大小猜。

下一Fragment若紧接本例且没有时间空洞,其tfdt应为:

1
2
3
3 samples * 1000 = 3000 = 0x0BB8

00 00 00 00 00 00 0B B8

连续性还要按Track独立检查,多轨tfdt使用各自Media timescale。

trun 的最小逐Run字段

只写Count与Data Offset

所有duration/size/flags从tfhd继承,trun只需显式sample_count和data_offset。Flags为 data-offset-present=0x000001

1
2
3
4
00 00 00 14 74 72 75 6E
00 00 00 01
00 00 00 03
00 00 00 6C
字段
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)

trafmoof 长度闭合

traf

1
2
traf children = 28 + 20 + 20 = 68
traf size = 8 + 68 = 76 = 0x4C

Header:

1
00 00 00 4C 74 72 61 66

moof

1
2
moof children = mfhd16 + traf76 = 92
moof size = 8 + 92 = 100 = 0x64

Header:

1
00 00 00 64 6D 6F 6F 66

嵌套解析使用父范围:mfhd结束必须等于traf起点,traf结束必须等于moof end。不能根据固定顺序跳100字节而不验证child sizes。

mdat 和三个Sample

完整字节

1
2
3
4
00 00 00 14 6D 64 61 74
11 12 13 14
21 22 23 24
31 32 33 34

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
2
3
4
5
6
7
8
9
10
11
12
13
00 00 00 64 6D 6F 6F 66
00 00 00 10 6D 66 68 64
00 00 00 00 00 00 00 01
00 00 00 4C 74 72 61 66
00 00 00 1C 74 66 68 64
00 02 00 38 00 00 00 01
00 00 03 E8 00 00 00 04 00 00 00 00
00 00 00 14 74 66 64 74
01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 14 74 72 75 6E
00 00 00 01 00 00 00 03 00 00 00 6C
00 00 00 14 6D 64 61 74
11 12 13 14 21 22 23 24 31 32 33 34

空格和缩进仅用于阅读,不在文件中。总大小:

1
100 + 20 = 120 bytes

Data Offset 实算

Moof位于文件偏移 0x1000

1
2
3
4
moof_start   = 0x1000
base = moof_start # default-base-is-moof
data_offset = +108 = 0x6C
run_start = 0x1000 + 0x6C = 0x106C

物理布局:

1
2
3
moof range        [0x1000,0x1064)  size100
mdat header [0x1064,0x106C) size8
mdat payload [0x106C,0x1078) size12

Sample Range:

1
2
3
sample1 [0x106C,0x1070)
sample2 [0x1070,0x1074)
sample3 [0x1074,0x1078)

最后end恰好等于mdat payload end。data_offset=100 会错误指向mdat Box Header;data_offset=108 才跳过Header。

Signed受检加法

1
2
3
4
5
6
7
8
9
resolve_data_start(base_u64, offset_i32):
if offset_i32 >= 0:
require uint64(offset_i32) <= UINT64_MAX - base
result = base + uint64(offset_i32)
else:
magnitude = uint64(-(int64(offset_i32)))
require magnitude <= base
result = base - magnitude
return result

先把int32提升到int64再取负,避免 INT32_MIN 在32位域溢出。结果还必须落入当前文件/Segment允许的数据源范围。

Sample默认值继承

优先级

对每个Sample分别解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
duration:
trun.sample_duration if present
else tfhd.default_sample_duration if present
else trex.default_sample_duration

size:
trun.sample_size
else tfhd.default_sample_size
else trex.default_sample_size

flags:
trun.sample_flags for that sample
else first_sample_flags for first sample when present
else tfhd.default_sample_flags
else trex.default_sample_flags

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
2
3
4
5
6
7
8
if trun has data_offset:
run_start = resolved_base + signed_offset
else:
require previous run end or applicable implicit base is known
run_start = previous_run_end

run_end = run_start + sum(final sample sizes)
previous_run_end = run_end

不能对每个无offset trun都重置到moof end或mdat start。多个traf时,各Track Run可在mdat内交错,必须按各自显式/继承规则定位,不按Box出现顺序猜Payload顺序。

Fragment连续性

下一tfdt

如果下一同Track Fragment base decode time=3000,则与本例连续。小于3000表示Decode时间重叠/倒退,大于表示时间空洞:

1
2
expected_next = current_tfdt + sum(sample_duration) = 3000
gap = next_tfdt - expected_next

是否允许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
2
3
4
5
6
moof size不等于child总和
traf缺tfhd或track_ID不存在
重复冲突tfhd/tfdt
trun sample_count超过payload可容纳字段
flags声明字段但Box提前结束
tfdt version未知

数据定位

1
2
3
4
5
6
7
data_offset指向mdat Header
signed负偏移下溢
sample size累计溢出
Run横跨mdat end
两个Run非法重叠
Sample落在moof或其他Box
下一无offset Run缺少可信前序cursor

时间与默认值

1
2
3
4
5
duration/size在trun、tfhd、trex均缺失
最终Sample size为零但Codec不允许空Sample
tfdt与上一Fragment不连续
sample composition offset version解释错误
flags把非随机访问视频声明为sync

Fragment验证报告

本例应输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mfhd.sequence_number = 1
track_ID = 1
base rule = default-base-is-moof
tfdt = 0
trun count = 1
sample count = 3
data start = moof+108 = 0x106C
data end = 0x1078
mdat payload range = [0x106C,0x1078)
decode range = [0,3000)
size source = tfhd
duration source = tfhd
flags source = tfhd
all sample ranges contained = true

再对三个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
2
3
4
5
length = 0
repeat:
b = read_u8()
length = (length << 7) | (b & 0x7F)
until (b & 0x80) == 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+size0x0A6F是Sample1首字节,不属于Sample0。Sample Table解析器先建立这些范围,再在各自范围内解释NAL length;不能跨Sample继续读。

38 字节 AVCDecoderConfigurationRecord

avcC Payload

1
2
3
4
01 42 C0 1E FF E1 00 16
67 42 C0 1E D9 09 6C 04 40 00 00 03 00 40 00 00 03 01 03 C5 8B 92
01 00 05
68 CB 83 CB 20

若作为普通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
2
3
4
5
fixed prefix                   = 6
SPS array = 2 + 22 = 24
PPS count = 1
PPS array = 2 + 5 = 7
total = 6 + 24 + 1 + 7 = 38

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
2
3
NAL payload = 620+2+22+5+620+511 = 1780
prefixes = 6*4 = 24
sample = 1780+24 = 1804

文件偏移 0x05D3

Sample0起点 0x0363 + 0x0270 = 0x05D3,从这里看到:

1
2
3
00 00 00 02 09 10
00 00 00 16 67 42 C0 1E D9 09 6C 04 40 00 00 03 00 40 00 00 03 01 03 C5 8B 92
00 00 00 05 68 CB 83 CB 20

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
2
00 00 00 02 09 30
00 00 00 5D 41 ...
相对偏移 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
2
3
4
00 00 00 02 09 10
00 00 00 16 67 42 C0 1E D9 09 6C 04 40 00 00 03 00 40 00 00 03 01 03 C5 8B 92
00 00 00 05 68 CB 83 CB 20
00 00 01 E7 65 88 82 17 C4 18 45 40 ...
相对偏移 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
2
3
0x0363 + 1804 = 0x0A6F
0x0A6F + 103 = 0x0AD6
0x0AD6 + 532 = 0x0CEA

本例三个Sample在文件中紧邻,但ISO BMFF不要求同轨Sample永远连续;Chunk/Track交错可产生空洞。定位必须由stsc+stsz+stco/co64或Fragment字段得出,不能用上一Sample end猜下一Sample offset。

NAL闭合

1
2
3
sample0: 1780 payload + 24 prefixes = 1804
sample1: 95 payload + 8 prefixes = 103
sample2: 516 payload + 16 prefixes = 532

每个Sample内游标必须恰好到end。少于end表示尾部垃圾/未知结构,多于end表示Length越界。任何NAL Length读取前验证剩余至少width字节。

AVC Sample解析算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
parse_avc_sample(sample_view, avcc):
width = avcc.lengthSizeMinusOne + 1
require width in supported values
cursor = 0
ordinal = 0

while cursor < sample_view.size:
require sample_view.size - cursor >= width
prefix_offset = cursor
n = read_unsigned_be(sample_view[cursor:cursor+width])
cursor += width
require n > 0
require n <= sample_view.size - cursor

nal = sample_view[cursor:cursor+n]
require (nal[0] & 0x80) == 0
emit ordinal, prefix_offset, cursor, n, nal_type(nal[0])
cursor += n
ordinal += 1

require cursor == sample_view.size

零Length严格模式拒绝;兼容模式即使跳过也必须消费prefix并保证前进。Width=1只能表达255字节NAL,本例620字节SEI/511字节IDR不能转成一字节Length,Writer必须拒绝而非截断低字节。

MP4到Annex-B

外部边界替换

转换每个Length-Prefixed NAL:

1
2
3
00 00 00 02 09 10
->
00 00 00 01 09 10

NAL Header+EBSP Payload必须逐字节不变;转换不执行RBSP去竞争。对目标随机访问点,依据SampleEntry和参数集缓存,在Slice使用前确保所需SPS/PPS已输出。可从avcC注入:

1
2
00 00 00 01 + SPS(67...92)
00 00 00 01 + PPS(68...20)

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
2
write_be(nal.size, length_width)
write(nal.header_and_ebsp)

收集SPS/PPS生成avcC,从选定SPS派生profile/compatibility/level。选择avc1avc3决定参数集放置/更新策略。Sample size写入stsz/trun;位置进入chunk/fragment索引;时间来自输入时间轴而非NAL字节猜测。

AVC转换验证

Payload Hash

分别解析源和目标外部边界,对每NAL记录:

1
2
3
4
5
6
access_unit_index
nal_ordinal
nal_type
nal_size
SHA-256(NAL Header + EBSP Payload)
source = sample / avcC injected / in-band

除声明的参数集移动/注入外,顺序、size和hash应完全相同。只比较解码画面不够:Decoder可能忽略未知SEI或隐藏损坏,NAL Hash能发现错误删除防竞争字节、包含Length/start code、截断一字节等问题。

Box与Codec双层

1
2
3
4
5
6
7
stsz/trun size equals bytes consumed by all NALs
sample ranges lie in mdat
avcC arrays close exactly
SPS/PPS references valid
stss/fragment sync flags agree with IDR/recovery semantics
DTS/PTS count equals sample count
converted AU count unchanged

容器合法但Slice损坏与NAL合法但Sample Table越界是不同错误层,报告分别归类。

越界向量

1
2
3
4
5
6
7
8
9
Sample剩3字节但width=4
Length=0
Length=100但只剩80
最后有1~3字节垃圾
NAL Header forbidden_zero_bit=1
avcC width与实际Sample编码不一致
一字节width承载620字节NAL
参数集长度超出avcC Box
Sample跨mdat end

任何失败都输出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
2
44100 << 16 = 0xAC440000
bytes = AC 44 00 00

AudioSampleEntry的channelcount/sample size是容器描述字段,最终解码参数仍应与ASC一致。若Entry写2声道而ASC配置1声道,必须报告冲突。

39 字节 esds Box

esds 完整字节

1
2
3
4
5
6
7
8
9
10
11
12
13
00 00 00 27 65 73 64 73
00 00 00 00
03 19
00 01
00
04 11
40
15
00 06 00
00 01 F4 00
00 01 F4 00
05 02 12 10
06 01 02

去掉阅读缩进后Box size为39=0x27。层级:

1
2
3
4
5
6
7
8
9
10
11
12
esds FullBox
└── ES_Descriptor tag 0x03, payload length 25
├── ES_ID = 1
├── flags = 0
├── DecoderConfigDescriptor tag 0x04, payload length 17
│ ├── objectTypeIndication = 0x40
│ ├── streamType/upStream/reserved = 0x15
│ ├── bufferSizeDB = 1536
│ ├── maxBitrate = 128000
│ ├── avgBitrate = 128000
│ └── DecoderSpecificInfo tag 0x05, length 2, bytes 12 10
└── SLConfigDescriptor tag 0x06, length 1, predefined 2

Descriptor Length 闭合

DecoderConfigDescriptor 长度

Payload组成:

1
2
3
4
5
6
7
objectTypeIndication            1
streamType byte 1
bufferSizeDB 3
maxBitrate 4
avgBitrate 4
DecoderSpecificInfo descriptor 2 header + 2 payload = 4
total 17 = 0x11

所以字节 04 11 合法。

ES Descriptor

1
2
3
4
5
ES_ID                           2
flags 1
DecoderConfig descriptor 2 + 17 = 19
SLConfig descriptor 2 + 1 = 3
total 25 = 0x19

所以 03 19。esds整体:

1
2
3
4
Box Header 8
FullBox Version/Flags 4
ES Descriptor Header+Payload 2+25
= 39

Descriptor length使用7-bit continuation编码,并非Box的32位size。这里所有长度小于128,只占一字节;通用Parser仍要支持多字节并限制最多长度字节/父范围。

ES_Descriptor Flags

本例Flags为零

ES_ID后的一字节包含依赖/URL/OCR等条件标志与stream priority。本例为0,所以后面立即是tag04。若某标志置位,会出现条件字段:

1
2
3
streamDependenceFlag -> dependsOn_ES_ID
URL_Flag -> URLlength + URLstring
OCRstreamFlag -> OCR_ES_Id

Parser必须按bit条件读取,否则会把条件字段首字节误当Descriptor tag。URL length必须在ES Descriptor父边界内,不能用于无界字符串读取或自动访问外部网络资源。

DecoderConfigDescriptor

Object Type与Stream Type

objectTypeIndication=0x40常表示MPEG-4 Audio。下一字节 0x15

1
2
3
4
5
6
0x15 = 000101 0 1
stream up reserved

streamType = 5 (AudioStream)
upStream = 0
reserved = 1

必须用位域解析,不能把0x15当“Codec 21”。objectType与SampleEntry FourCC、ASC Audio Object Type共同确定解码器;冲突时不凭其中一个静默覆盖。

Buffer与Bitrate

1
2
3
bufferSizeDB = 00 06 00 = 1536
maxBitrate = 00 01 F4 00 = 128000
avgBitrate = 00 01 F4 00 = 128000

三个/四字节均为big-endian。Bitrate字段是Decoder配置/速率描述,不定义每Sample固定大小;AAC VBR仍可有不同stsz。验证器可由媒体总bytes和duration估算平均率比较,但要区分容器开销、音频Payload和舍入。

AudioSpecificConfig 12 10

Bit布局

1
2
3
4
5
6
12 10 = 00010010 00010000

audioObjectType 00010 = 2 (AAC LC)
samplingFrequencyIndex 0100 = 4 (44100 Hz)
channelConfiguration 0010 = 2 channels
后续GA specific bits...

前16位可按BitReader:

1
2
3
4
5
6
AOT = read_bits(5) = 2
freq_index = read_bits(4) = 4
channel_config = read_bits(4) = 2
frameLengthFlag = read_bits(1) = 0
dependsOnCoreCoder = read_bits(1) = 0
extensionFlag = read_bits(1) = 0

freq_index=15 时后面有24-bit显式频率,不能查表;AOT=31时还有扩展AOT字段。HE-AAC可能通过Extension AOT/SBR/PS表达,不能只读前五位后称“全部都是LC”。

完整75字节 mp4a

SampleEntry加esds

1
2
3
4
5
6
7
8
9
10
11
00 00 00 4B 6D 70 34 61
00 00 00 00 00 00 00 01
00 00 00 00 00 00 00 00
00 02 00 10 00 00 00 00 AC 44 00 00

00 00 00 27 65 73 64 73
00 00 00 00
03 19 00 01 00
04 11 40 15 00 06 00 00 01 F4 00 00 01 F4 00
05 02 12 10
06 01 02

大小:

1
2
3
4
mp4a Box Header          8
AudioSampleEntry Payload 28
esds child 39
total 75 = 0x4B

esds起点为mp4a起点+36。AudioSampleEntry fixed字段之后可以还有其他child Box,Parser在mp4a size范围内循环,而不是假定余下全部属于esds。

包含mp4a的 stsd

单Entry

1
2
3
4
5
stsd Box Header      8
FullBox 4
entry_count 4
mp4a 75
total 91 = 0x5B

Header前缀:

1
2
3
4
00 00 00 5B 73 74 73 64
00 00 00 00
00 00 00 01
[75-byte mp4a]

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
2
stsz/trun declares N bytes
mdat contains exactly N Raw AAC 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
2
sample duration = 1024 / 44100
≈ 0.0232199546485 s

100个Sample:

1
2
duration units = 100 * 1024 = 102400
seconds = 102400 / 44100 ≈ 2.32199546485

不能先把单帧舍入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
2
3
4
5
6
7
8
9
10
syncword = 0xFFF
ID = 0 (MPEG-4)
layer = 0
protection_absent = 1
profile = AOT-1 = 1 (AAC LC)
sampling_frequency_index = 4
channel_configuration = 2
aac_frame_length = 107
adts_buffer_fullness = 0x7FF (VBR convention)
number_of_raw_data_blocks_in_frame = 0

Length位拆分:

1
2
byte3 low2 + byte4 + byte5 high3
107 = 0x006B

生成算法:

1
2
3
4
5
6
7
8
9
10
11
12
profile = audioObjectType - 1
full_length = raw_size + 7
require full_length <= 0x1FFF

h[0] = 0xFF
h[1] = 0xF1
h[2] = (profile << 6) | (freq_index << 2) | (channel_config >> 2)
h[3] = ((channel_config & 3) << 6) | (full_length >> 11)
h[4] = (full_length >> 3) & 0xFF
h[5] = ((full_length & 7) << 5) | 0x1F
h[6] = 0xFC
write h + raw_sample

ASC的显式频率、PCE/零channelConfiguration、HE-AAC扩展等情况不能都由基础ADTS短字段无损表达,转换器要验证目标可表示性。

ADTS转MP4

Header剥离与配置提取

逐帧:

1
2
3
4
5
6
7
find/require sync at frame boundary
parse protection_absent -> header length 7 or 9
parse 13-bit frame_length
require frame_length >= header_length
require frame_length <= input remaining
raw_size = frame_length - header_length
copy raw payload as one MP4 sample

从稳定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
2
3
4
5
6
mp4a channelcount == ASC channel config/derived PCE channels
mp4a samplerate compatible with ASC core/output frequency
objectTypeIndication compatible with MPEG-4 Audio
stts duration compatible with AAC frame sample count
esds descriptor lengths close exactly
stsc description index selects correct mp4a

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
2
3
4
5
6
7
8
9
10
11
12
mp4a size小于固定36字节
esds size越过mp4a
Descriptor continuation永不终止
ES flags声明URL但长度截断
DecoderConfig length不含完整DSI
ASC只有一字节
ASC frequency index15但缺24-bit频率
mp4a双声道与ASC单声道冲突
stts delta与Frame样本数不符
mdat中错误保留ADTS Header
ADTS frame_length小于Header或超过输入
Raw Sample导出ADTS超过8191

错误报告包含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
2
segment_duration seconds = segment_duration / movie_timescale
media_time seconds = media_time / media_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
2
3
4
5
6
7
8
9
Edit 0:
segment_duration = 2000 movie units
media_time = -1 (empty edit)
media_rate = 1.0

Edit 1:
segment_duration = 5000 movie units
media_time = 48000 media units
media_rate = 1.0

Movie Track区间:

1
2
[0,2000)    no media
[2000,7000) maps media [48000,288000)

第二段Media长度:5秒×48000=240000,起点48000,终点288000。

完整Box

1
2
3
4
5
6
7
8
9
10
11
00 00 00 28 65 6C 73 74
00 00 00 00
00 00 00 02

00 00 07 D0
FF FF FF FF
00 01 00 00

00 00 13 88
00 00 BB 80
00 01 00 00

逐字段:

偏移 字节 解释
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
2
media_rate_integer  signed int16
media_rate_fraction signed int16

1.0:

1
2
integer = 1 -> 00 01
fraction = 0 -> 00 00

它们在文件中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
2
3
4
edit_movie_start = 2000
movie_delta = M - 2000
media_delta = movie_delta * media_timescale / movie_timescale
media_time = 48000 + media_delta

M=3500:

1
2
3
movie_delta = 1500
media_delta = 1500 * 48000 / 1000 = 72000
media_time = 48000 + 72000 = 120000

即Movie 3.5秒对应Media 2.5秒。M=1000落在Empty Edit,没有Media Sample;播放器输出空白/静音或不呈现该Track。

反向映射

Media时间120000属于第二Edit映射区:

1
2
3
media_delta = 120000 - 48000 = 72000
movie_delta = 72000 * 1000 / 48000 = 1500
movie_time = 2000 + 1500 = 3500

同一个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
2
3
4
movie target -> edit mapping -> media presentation target
-> find required sync/preroll decode start
-> decode earlier samples
-> suppress output until mapped presentation target

不能把Edit起点Sample无条件写入stss或Fragment sync flags。

时间换算与舍入

有理数累积

若timescale不能整除,使用宽整数/有理数:

1
2
3
numerator = movie_delta * media_timescale
quotient = numerator / movie_timescale
remainder = numerator % movie_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
parse_elst(box, movie_scale, media_scale):
read FullBox
require version in {0,1}
count = read_u32be()
entry_bytes = 12 if version==0 else 20
require count <= remaining / entry_bytes
require count <= configured_max_edits

movie_cursor = 0_u64
for i in 0..count-1:
duration = read_u32/u64
media_time = read_i32/i64
rate_i = read_i16
rate_f = read_i16
require duration <= UINT64_MAX - movie_cursor
movie_end = movie_cursor + duration

if media_time == -1:
classify empty edit
else:
require media_time >= 0 under supported profile
validate rate and mapped media range
emit [movie_cursor,movie_end)
movie_cursor = movie_end

require consumed entries fit box

Entry数组后若Box还有扩展字节,按版本/Profile处理;不能让count越过父范围。

Edit List错误向量

1
2
3
4
5
6
7
8
9
10
11
entry_count超过Box容量
Version0 Entry只有11字节
Version1错误按12字节读取
movie/media timescale为0
segment duration累计溢出
media_time小于-1
Empty Edit错误尝试Media换算
rate分母/语义不支持
引用Media范围超过mdhd/Sample presentation范围
tkhd.duration与Edit总和冲突
首显示Sample不是可安全Decode入口

修复器若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字节改变。

censcbcs

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_enabledtrack_in_movietrack_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
2
3
4
pre_defined      4
handler_type 4
reserved[3] 12
name remaining

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
2
reserved[6] = 0
data_reference_index (16-bit, 1-based)

之后由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
2
total_samples += sample_count
total_duration += uint64(sample_count) × sample_delta

随机访问时间

可给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_chunksamples_per_chunksample_description_index。Entry size12。

单调性

First_chunk一基且严格递增,第一个通常为1;samples_per_chunk>0;description index在stsd 1..entry_count。最后run延伸到chunk offset count。

展开实例

1
2
(first_chunk=1, samples_per_chunk=4, desc=1)
(first_chunk=3, samples_per_chunk=2, 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_sizesample_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 精确字段布局

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
stsz sizes = [100, 120, 80, 90, 110, 70]

stco chunk offsets = [1000, 1300, 1600]

stsc entries:
first_chunk=1, samples_per_chunk=2, description_index=1
first_chunk=3, samples_per_chunk=2, description_index=2

stts entries:
sample_count=3, sample_delta=1000
sample_count=3, sample_delta=2000

ctts version 1 entries:
sample_count=2, sample_offset=0
sample_count=2, sample_offset=1000
sample_count=2, sample_offset=-500

stss sync samples = [1, 4]

Sample Number 在 stss 中从1开始;内部数组索引可以从0开始,但两者转换要显式 sample_number=index+1。Chunk Number 在 stsc.first_chunk 中也从1开始。

stsc 完整字节

两个Run

完整 Box:

1
2
3
4
5
00 00 00 28 73 74 73 63
00 00 00 00
00 00 00 02
00 00 00 01 00 00 00 02 00 00 00 01
00 00 00 03 00 00 00 02 00 00 00 02
偏移 字节 解释
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
2
3
4
5
6
7
8
9
10
00 00 00 2C 73 74 73 7A
00 00 00 00
00 00 00 00
00 00 00 06
00 00 00 64
00 00 00 78
00 00 00 50
00 00 00 5A
00 00 00 6E
00 00 00 46

解析:

1
2
3
sample_size = 0       # 启用逐项表
sample_count = 6
sizes = 100,120,80,90,110,70

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
2
3
4
5
6
00 00 00 1C 73 74 63 6F
00 00 00 00
00 00 00 03
00 00 03 E8
00 00 05 14
00 00 06 40

三个大端 offset:

1
2
3
0x000003E8 = 1000
0x00000514 = 1300
0x00000640 = 1600

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
2
3
4
5
00 00 00 20 73 74 74 73
00 00 00 00
00 00 00 02
00 00 00 03 00 00 03 E8
00 00 00 03 00 00 07 D0

两项分别是三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
2
3
4
5
6
00 00 00 20 63 74 74 73
01 00 00 00
00 00 00 03
00 00 00 02 00 00 00 00
00 00 00 02 00 00 03 E8
00 00 00 02 FF FF FE 0C

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
2
3
4
5
00 00 00 18 73 74 73 73
00 00 00 00
00 00 00 02
00 00 00 01
00 00 00 04

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
2
3
chunk 1, stsc run 0: 2 samples, description 1
chunk 2, stsc run 0: 2 samples, description 1
chunk 3, stsc run 1: 2 samples, description 2

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
2
require sample_offset <= data_source_size
require sample_size <= data_source_size - sample_offset

避免 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
2
3
4
5
6
stsz sample_count                    = 6
sum(stts.sample_count) = 3 + 3 = 6
sum(ctts.sample_count) = 2 + 2 + 2 = 6
sum(stsc samples over 3 chunks) = 2 + 2 + 2 = 6
stss entries all in [1,6] = true
stsc description indices in [1,2] = true

任何一个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
2
3
4
5
duplicate exact ranges
partial overlaps
sample inside Box Header
sample inside moov/free instead of mdat
gap sizes between chunks

不把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
2
3
4
5
6
stsc = 40
stsz = 44
stco = 28
stts = 32
ctts = 32
stss = 24

每个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上限。