2、容器格式时间戳:MP4的mdhd/moof/traf盒子,TS的PCR/PTS/DTS,FLV的Timestamp/TimestampDelta
容器格式的时间戳,说白了就是封装层给音视频数据贴的「时间标签」。不同容器用不同的方式记录时间,但目标都一样——让播放器知道「这一帧该什么时候出来」。我这些年跟各种容器打交道,踩过的坑不少,今天把三个主流格式的核心时间戳机制掰开揉碎讲清楚。
2.1 MP4 容器:mdhd、moof、traf 盒子
MP4 的时间戳体系分两套:一套管静态文件(如本地 mp4),一套管流式传输(如 fMP4)。我个人习惯把前者叫「静态时间戳」,后者叫「分段时间戳」。
2.1.1 mdhd 盒子——媒体头时间基准
mdhd(Media Header Box)定义了整个轨道的时基(timescale)。它告诉解析器:这个轨道的时间单位是多少。
// mdhd 盒子的典型结构
Box Type: 'mdhd'
Container: Media Box ('mdia')
Mandatory: Yes
字段:
- version: 1 字节
- flags: 3 字节
- creation_time: 8 字节 (version=1) 或 4 字节 (version=0)
- modification_time: 8 或 4 字节
- timescale: 4 字节 —— 每秒的 tick 数
- duration: 8 或 4 字节 —— 轨道总时长(以 timescale 为单位)
- language: 2 字节
- quality: 2 字节
举个例子:如果 timescale = 90000,那么 1 秒 = 90000 个 tick。一个 30fps 的视频帧,每帧间隔就是 90000 / 30 = 3000 ticks。我在项目中遇到过有人把 timescale 设成 1000,结果跟音频的 44100 对不上,音画不同步了整整 3 秒——嗯,这种低级错误排查起来最费时间。
2.1.2 moof 与 traf 盒子——分段时间戳
fMP4(Fragmented MP4)用 moof(Movie Fragment Box)和 traf(Track Fragment Box)来管理分段时间。每个 moof 包含一个或多个 traf,每个 traf 对应一个轨道。
moof
├── mfhd (Movie Fragment Header)
│ └── sequence_number: 分片序号
└── traf (Track Fragment Box)
├── tfhd (Track Fragment Header)
│ ├── track_ID: 轨道 ID
│ └── base_data_offset: 基准偏移
├── tfdt (Track Fragment Base Media Decode Time)
│ └── base_media_decode_time: 该分片的起始解码时间
└── trun (Track Fragment Run)
├── sample_count: 样本数
└── sample_duration / sample_size / sample_composition_time_offset
关键在 tfdt 盒子的 base_media_decode_time——它定义了该分片的起始解码时间。而 trun 里的 sample_composition_time_offset 则用来计算 PTS(展示时间戳)。
DTS = base_media_decode_time + 累计 sample_duration
PTS = DTS + sample_composition_time_offset
我曾经调试过一个 HLS 流,视频每隔几秒就卡一下。抓包发现是 tfdt 的 base_media_decode_time 没有累加前一个分片的时长,导致播放器每次都要重新对齐——说白了就是时间戳断掉了。
2.2 TS 容器:PCR、PTS、DTS
TS(Transport Stream)是广播电视和直播领域的「老大哥」。它的时间戳体系比 MP4 复杂,但更健壮。我刚开始做 TS 解析时,被 PCR 和 PTS 的关系绕晕过好几次。
2.2.1 PCR——系统时钟的锚点
PCR(Program Clock Reference)是 TS 流的「心跳」。它告诉解码器:当前这个时刻,系统时钟应该走到哪个值。PCR 通常每 100ms 至少出现一次,精度高达 27MHz。
// PCR 的编码结构
PCR_base: 33 位 (90kHz 时钟)
PCR_ext: 9 位 (27MHz 时钟)
PCR = PCR_base * 300 + PCR_ext
// 单位:27MHz tick
为什么要有 PCR?因为解码器需要恢复出和编码端一致的时钟。如果 PCR 抖动太大,解码器的 PLL(锁相环)就会失锁,导致音画不同步。我记得有一次在直播现场,编码器 PCR 间隔突然跳到 500ms,所有机位的画面都开始抽动——嗯,那场面相当「精彩」。
2.2.2 PTS 与 DTS——展示与解码的博弈
PTS(Presentation Time Stamp)和 DTS(Decoding Time Stamp)是 TS 里最核心的两个时间戳。PTS 告诉解码器「什么时候显示」,DTS 告诉解码器「什么时候解码」。
| 时间戳 | 全称 | 作用 | 出现频率 |
|---|---|---|---|
| PTS | Presentation Time Stamp | 控制画面显示时机 | 每帧或每 GOP |
| DTS | Decoding Time Stamp | 控制解码器输入时机 | 每帧 |
| PCR | Program Clock Reference | 恢复系统时钟 | 至少每 100ms |
对于 I 帧和 P 帧,PTS 和 DTS 通常不同。因为 B 帧需要参考后面的帧,所以解码顺序和显示顺序不一样。举个例子:一个 GOP 的显示顺序是 I B B P,但解码顺序是 I P B B——DTS 跟着解码顺序走,PTS 跟着显示顺序走。
// PTS/DTS 在 PES 包中的编码
PTS: 33 位 (90kHz 时钟)
DTS: 33 位 (90kHz 时钟)
// 编码规则:
// 如果 PTS == DTS,只编码 PTS
// 如果 PTS != DTS,PTS 和 DTS 都编码
// 时间单位:1/90000 秒
2.3 FLV 容器:Timestamp 与 TimestampDelta
FLV 是 RTMP 和 HTTP-FLV 的「标配」。它的时间戳设计很简单,但简单不代表没坑。
2.3.1 Tag 的 Timestamp——绝对时间戳
每个 FLV Tag(音频或视频)都有一个 Timestamp 字段,表示该 Tag 相对于 FLV 文件起始的时间偏移。单位是毫秒。
// FLV Tag 头部结构
typedef struct {
uint8_t tag_type; // 8: 音频, 9: 视频, 18: 脚本
uint24_t data_size; // 数据大小
uint24_t timestamp; // 时间戳 (毫秒)
uint8_t timestamp_ext; // 时间戳扩展 (高 8 位)
uint24_t stream_id; // 流 ID (总是 0)
} FLVTagHeader;
// 实际时间戳 = (timestamp_ext << 24) | timestamp
// 单位:毫秒
注意:FLV 的 Timestamp 只有 24 位,最大只能表示 0xFFFFFF = 16777215 毫秒 ≈ 4.66 小时。超过这个值需要用 timestamp_ext 扩展。我遇到过一些老旧编码器,超过 4 小时后时间戳直接溢出归零——播放器以为回到了起点,画面开始乱跳。
2.3.2 TimestampDelta——音频的「小聪明」
FLV 的音频 Tag 有一个特殊机制:如果连续多个音频帧的采样率、声道数、采样位数相同,可以只发第一个 Tag 的完整头部,后续 Tag 用 TimestampDelta 表示相对时间偏移。这个机制在低延迟场景下很常见。
// 音频 Tag 的 TimestampDelta 使用场景
Tag 1: Timestamp = 0, 数据 = AAC 帧 1
Tag 2: Timestamp = 23, 数据 = AAC 帧 2 (实际时间戳 = 0 + 23)
Tag 3: Timestamp = 46, 数据 = AAC 帧 3 (实际时间戳 = 0 + 46)
// 注意:这里的 Timestamp 仍然是绝对时间戳,不是增量
其实 FLV 规范里并没有明确的「TimestampDelta」字段,这个说法来自 RTMP 协议。在 RTMP 中,音频包可以携带一个 timestamp delta 来表示相对于前一个包的时间增量。我刚开始做 RTMP 推流时,把音频的 delta 和视频的绝对时间戳混在一起算,结果音画不同步了 2 秒——嗯,这种低级错误犯一次就够了。
2.4 三种容器时间戳对比
| 特性 | MP4 (fMP4) | TS | FLV |
|---|---|---|---|
| 时间基准 | mdhd.timescale (自定义) | PCR (27MHz) | 毫秒 (固定) |
| 时间戳精度 | 1/timescale 秒 | 1/27000000 秒 | 1 毫秒 |
| PTS/DTS 分离 | 支持 (composition_time_offset) | 支持 (PES 头) | 不支持 (只有展示时间) |
| 时钟恢复 | 不需要 (文件/分段) | 需要 (PCR) | 不需要 (流式) |
| 常见问题 | 分片时间戳不连续 | PCR 抖动、PTS/DTS 差值过大 | 时间戳溢出、delta 计算错误 |
2.5 核心知识图谱
下面这张图展示了三种容器时间戳的核心关系。我把它画成流程图,方便你理解数据流转。
这张图从左到右展示了三种容器的时间戳机制,以及它们最终如何影响解码和播放。你可以看到,MP4 和 TS 都支持 PTS/DTS 分离(因为有 B 帧),而 FLV 只有展示时间。TS 的 PCR 是它独有的时钟恢复机制,这也是为什么 TS 在广播领域不可替代的原因。