3、MP4关键元数据:Track、Sample、Chunk、Sample Table(stbl)详解
MP4文件的结构,说白了就是一棵树。你打开一个MP4文件,最先看到的是ftyp,然后是moov和mdat。但真正决定播放器能不能正确解码的,是moov里的那堆元数据。今天我们就来啃最核心的几个概念:Track、Sample、Chunk,以及那个让人又爱又恨的Sample Table(stbl)。
我个人习惯把MP4的元数据比作一本字典。你查一个字,得知道它在哪一页、哪一行。Track就是章节,Sample就是每个字,Chunk就是连续的几页,而stbl就是索引目录。没有这个目录,播放器就得从头到尾扫描整个文件——那体验,你想想看。
核心关系速览:
- Track:一条独立的媒体流(视频、音频、字幕等)
- Sample:一个独立的媒体单元(一帧视频、一段音频)
- Chunk:连续存储的一组Sample
- stbl:描述Sample如何映射到文件偏移的索引表
3.1 Track:媒体流的容器
一个MP4文件可以包含多个Track。最常见的,一个视频Track加一个音频Track。每个Track都有自己的tkhd(Track Header),里面记录了时长、宽高、音量等基本信息。
我记得有一次接手一个项目,播放器总是卡在开头几秒。排查了半天,发现是Track的duration字段写错了,导致播放器预加载了过多数据。嗯,这里要注意:tkhd里的duration是媒体时间单位,不是毫秒,也不是秒。你得结合mdhd里的timescale才能换算成真实时间。
| Track类型 | 常见子类型 | 关键Box |
|---|---|---|
| 视频 | h264, hevc, av1 | stsd(Sample Description) |
| 音频 | aac, mp3, opus | stsd + mp4a/mp4v等 |
| 字幕 | tx3g, wvtt | stsd + 字体描述 |
| 元数据 | meta, timed metadata | stsd + 自定义描述 |
避坑指南:我曾经在解析一个多Track文件时,发现第二个Track的tkhd里track_ID居然和第一个重复了。虽然规范不允许,但有些编码器确实会犯这种错。解析时一定要做去重校验,否则后面查stbl会乱套。
3.2 Sample:最小的媒体单元
Sample是什么?对于视频,一帧就是一个Sample。对于音频,一段连续的音频数据(通常是1024或2048个采样点)就是一个Sample。Sample是不可再分的媒体数据单元。
你可能会问:那一个Sample在文件里占多少字节?这得看编码格式和编码参数。H.264的I帧可能几十KB,B帧可能只有几KB。所以,每个Sample的大小是不固定的。这就引出了一个问题:播放器怎么知道每个Sample从哪里开始、到哪里结束?
答案就在stbl里。stbl里有一个叫stsz(Sample Size)的Box,它记录着每个Sample的字节大小。如果所有Sample大小相同(比如某些音频流),stsz可以只写一个默认值,省空间。
注意:Sample和NAL单元(H.264)不是一回事。一个Sample可能包含多个NAL单元(比如一个IDR帧包含SPS、PPS和Slice数据)。解析时不要混淆。
3.3 Chunk:Sample的物理分组
Sample是逻辑单元,Chunk是物理存储单元。编码器在写文件时,会把连续的几个Sample打包成一个Chunk,然后写入mdat。为什么这么做?为了减少文件碎片,提高读写效率。
举个例子:一个视频Track有1000个Sample,编码器可能每10个Sample组成一个Chunk。这样在mdat里就只有100个Chunk,而不是1000个零散的数据块。播放器读取时,一次I/O操作就能拿到10个Sample,效率高得多。
Chunk的信息记录在stco(Chunk Offset)里。每个Chunk有一个起始文件偏移,以及包含的Sample数量。嗯,这里有个细节:stco记录的是Chunk在mdat里的绝对偏移,不是相对于moov的偏移。
| Box名称 | 作用 | 关键字段 |
|---|---|---|
| stco | Chunk在文件中的偏移 | chunk_offset(数组) |
| stsc | Sample到Chunk的映射 | first_chunk, samples_per_chunk, sample_description_index |
| stsz | 每个Sample的大小 | sample_size(统一值或数组) |
| stts | Sample的时间戳 | sample_delta(数组) |
关键理解:Chunk是物理概念,Sample是逻辑概念。播放器通过stsc知道:第N个Chunk包含哪些Sample;通过stco知道:这个Chunk在文件哪里;通过stsz知道:每个Sample多大。三者结合,就能精确定位任意一个Sample的数据。
3.4 Sample Table(stbl):元数据的核心
stbl是Track里最重要的Box。它下面有多个子Box,每个负责一类元数据。我把它叫做「五虎上将」:
- stsd(Sample Description):描述Sample的编码格式、分辨率、采样率等。没有它,解码器不知道该怎么解码。
- stts(Time-to-Sample):记录每个Sample的时长(delta)。播放器靠它计算时间戳。
- stsc(Sample-to-Chunk):记录每个Chunk包含多少个Sample。这是连接逻辑和物理的桥梁。
- stsz(Sample Size):记录每个Sample的字节大小。
- stco(Chunk Offset):记录每个Chunk在文件中的起始偏移。
你想想看,播放器要播放第100帧视频,它需要做几步?
- 通过
stts找到第100帧对应的Sample索引(因为时间戳可能不是均匀的)。 - 通过
stsc找到这个Sample属于哪个Chunk。 - 通过
stco找到这个Chunk的偏移。 - 通过
stsz找到这个Sample在Chunk内的偏移和大小。 - 从
mdat里读取数据,交给解码器。
每一步都离不开stbl。所以我说,stbl是MP4解析的「心脏」。我曾经在优化一个播放器时,发现随机seek特别慢。查到最后,是stco用了32位偏移,而文件超过4GB了。换成64位的co64后,问题解决。
大文件陷阱:如果文件超过4GB,stco必须使用64位版本(co64)。但有些老编码器会忽略这个限制,导致解析时偏移溢出。解析时一定要检查ftyp里的brand,如果是"isom"或"mp42",大概率支持64位。
3.5 整体结构图
下面这张图展示了Track、Sample、Chunk和stbl之间的关系。我建议你多看几遍,理解这个层级结构,后面解析代码写起来就顺了。
3.6 实战:如何快速定位一个Sample
假设我们要读取视频Track的第50个Sample。伪代码如下:
// 1. 通过 stsz 获取第50个Sample的大小
sample_size = stsz.entry_sizes[49]; // 索引从0开始
// 2. 通过 stsc 找到第50个Sample属于哪个Chunk
// stsc 是一个分段表,需要遍历查找
chunk_index = find_chunk_for_sample(50, stsc.entries);
// 3. 通过 stco 获取该Chunk的文件偏移
chunk_offset = stco.chunk_offsets[chunk_index];
// 4. 计算Sample在Chunk内的偏移
// 需要知道该Chunk中前几个Sample的总大小
offset_within_chunk = 0;
for (i = 0; i < sample_index_within_chunk; i++) {
offset_within_chunk += stsz.entry_sizes[first_sample_index + i];
}
// 5. 最终文件偏移
final_offset = chunk_offset + offset_within_chunk;
这段代码看起来简单,但实际项目中坑不少。比如stsc的first_chunk是从1开始计数的,不是0。还有stsz如果用了统一大小模式,entry_sizes数组可能为空,你得用sample_size字段代替。
我的习惯:在解析stbl时,我会先构建一个「Sample索引表」,把每个Sample的偏移和大小提前算好缓存起来。虽然占点内存,但seek时直接查表,速度飞快。对于4K视频,100万个Sample的索引表也就几十MB,值得。
3.7 常见问题与避坑
- stco溢出:文件超过4GB时,必须用
co64。解析时先检查stco的版本号,version=1就是64位。 - stsc分段混乱:有些编码器写的
stsc分段不合理,导致一个Chunk包含的Sample数量忽多忽少。解析时要做边界检查。 - stts时间戳跳跃:如果
stts里的delta为0,说明有连续多个Sample时间戳相同(比如B帧)。播放器要注意处理。 - stsd多描述:一个Track可以有多个Sample Description(比如不同分辨率的视频)。解析时要根据
stsc里的sample_description_index选择正确的描述。
嗯,今天就先聊到这里。Track、Sample、Chunk、stbl这四个概念,是MP4解析的基石。你把这套逻辑吃透了,后面讲moof(电影片段)和fragment(碎片化MP4)时,就会轻松很多。