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 秒——嗯,这种低级错误排查起来最费时间。

经验之谈: 视频轨道的 timescale 建议用 90000(MPEG 标准),音频轨道用采样率(如 44100、48000)。这样能保证时间精度,避免舍入误差。

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 没有累加前一个分片的时长,导致播放器每次都要重新对齐——说白了就是时间戳断掉了。

避坑指南: 分片时间戳必须连续。每个 moof 的起始时间 = 上一个 moof 的起始时间 + 上一个 moof 的总时长。断掉的话,播放器会跳帧或卡顿。

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,所有机位的画面都开始抽动——嗯,那场面相当「精彩」。

PCR 间隔要求: 标准规定 PCR 间隔不超过 100ms,抖动不超过 ±500ns。实际项目中我建议控制在 40ms 以内,给解码器留足余量。

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 秒
关键点: PTS 和 DTS 的差值不能超过一个 GOP 的长度。如果差值过大,解码器缓冲区会溢出或下溢。我曾经见过一个编码器把 PTS 和 DTS 差到 10 秒,播放器直接崩溃。

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 小时后时间戳直接溢出归零——播放器以为回到了起点,画面开始乱跳。

避坑指南: 直播流如果超过 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 计算错误
一句话总结: MP4 靠盒子结构管理时间,TS 靠 PCR 恢复时钟,FLV 靠简单毫秒计数。理解它们的差异,才能在封装、解封装、转码时不出错。

2.5 核心知识图谱

下面这张图展示了三种容器时间戳的核心关系。我把它画成流程图,方便你理解数据流转。

容器格式时间戳核心关系图 容器格式时间戳核心关系图 MP4 (fMP4) mdhd.timescale → 时基 tfdt.base_media_decode_time trun.sample_composition_offset TS (Transport Stream) PCR → 系统时钟恢复 PES 头 → PTS / DTS 90kHz 时钟基准 FLV Tag Timestamp (毫秒) TimestampDelta (RTMP) 24位 + 8位扩展 核心关系 MP4 用盒子结构管理时间戳,TS 用 PCR 恢复时钟,FLV 用简单毫秒计数 三者最终目标一致:让播放器知道「这一帧该什么时候出来」 封装层时间戳 解码器时间同步 音画同步输出

这张图从左到右展示了三种容器的时间戳机制,以及它们最终如何影响解码和播放。你可以看到,MP4 和 TS 都支持 PTS/DTS 分离(因为有 B 帧),而 FLV 只有展示时间。TS 的 PCR 是它独有的时钟恢复机制,这也是为什么 TS 在广播领域不可替代的原因。

我的建议: 如果你在做直播,优先考虑 TS 或 fMP4(HLS)。如果做低延迟互动,FLV(RTMP)更合适。但无论选哪种,时间戳的连续性和精度都是生命线——我见过太多因为时间戳问题导致的线上事故了。