封装格式与时间基:MP4/MKV/TS 的时间基解析

各位好,我是老周。今天咱们聊聊封装格式里的时间基。这玩意儿看着基础,但我在项目里见过太多人在这上面栽跟头。说白了,时间基就是音视频同步的「尺子」——尺子刻度不对,后面所有同步逻辑都是空中楼阁。

2.1 为什么需要时间基?

你想想看,一个视频文件里,视频帧可能是 30fps,音频采样率是 48000Hz。这两个东西的时间刻度完全不一样。封装格式必须有个统一的时间坐标系,才能把视频帧和音频样本对齐到同一个时间轴上。

这个统一坐标系,就是时间基(Time Base)。

核心概念:时间基定义了时间戳的最小刻度单位。比如时间基是 1/1000 秒,那时间戳 500 就代表 0.5 秒。所有音视频帧的时间戳,都基于这个刻度来计算。

2.2 三大封装格式的时间基设计

2.4.1 MP4:基于毫秒的优雅设计

MP4 的时间基设计得很干净。它用 timescale 来表示每秒的刻度数。比如 timescale = 1000,那每个刻度就是 1 毫秒。

我记得第一次解析 MP4 时,看到 moov box 里的 mdhd 字段,里面有个 timescale 值。当时我直接拿它当帧率用,结果音频对不上。后来才明白,timescale 是时间基,不是帧率。

// MP4 时间戳计算示例
// mdhd box 中 timescale = 1000
// 视频帧的 duration = 33(表示 33ms,约 30fps)
// 音频帧的 duration = 23(表示 23ms,约 43fps)

// 时间戳转换
uint64_t pts_ms = (uint64_t)pts * 1000 / timescale;
// 如果 timescale = 1000,pts 直接就是毫秒值
MP4 关键字段 含义 典型值
timescale 每秒的刻度数 1000, 90000, 48000
duration 当前帧占用的刻度数 33(30fps 视频)
composition_time_offset 解码时间到显示时间的偏移 0(B 帧场景下非零)

2.4.2 MKV:灵活的分数时间基

MKV 的时间基设计更灵活。它用 TimecodeScale 来表示每个刻度的纳秒数。默认是 1000000(1 毫秒),但你可以改成任意值。

我曾经处理过一个 MKV 文件,它的 TimecodeScale 设成了 10000000(10 毫秒)。结果所有时间戳都变成了 10 的倍数,精度直接丢了。嗯,这就是灵活性的代价——你得自己处理精度问题。

// MKV 时间戳转换
// TimecodeScale = 1000000(1ms)
// 视频帧的 Timecode = 33(表示 33ms)
// 音频帧的 Timecode = 23(表示 23ms)

// 转换为毫秒
uint64_t pts_ms = timecode * timecode_scale / 1000000;

// 转换为 90kHz 时间基(用于音视频同步)
uint64_t pts_90k = pts_ms * 90;

注意:MKV 的 TimecodeScale 可以是非整数吗?理论上可以,但实际编码器很少这么干。我建议你解析时直接取整,避免浮点精度问题。

2.4.3 TS:MPEG-2 的 90kHz 标准

TS 流的时间基最「硬核」。它基于 MPEG-2 系统层标准,固定使用 90kHz 作为时间基。为什么是 90kHz?因为 90kHz 能被常见的帧率(24、25、30、50、60)整除,计算起来方便。

TS 流里有两种时间戳:

  • PCR(Program Clock Reference):系统时钟参考,用于解码器恢复时钟
  • PTS/DTS(Presentation/Decoding Time Stamp):显示/解码时间戳
// TS 时间戳解析
// PCR 由两部分组成:PCR_base(33位) + PCR_ext(9位)
// PCR_base 单位是 1/90000 秒
// PCR_ext 单位是 1/27000000 秒

// 计算 PCR 值(单位:秒)
double pcr_seconds = pcr_base / 90000.0 + pcr_ext / 27000000.0;

// PTS 是 33 位整数,单位也是 1/90000 秒
uint64_t pts_seconds = pts / 90000;

2.3 tbn / tbr / tbc 概念

这三个缩写,我在面试时经常问。很多人答不上来,或者混为一谈。咱们一个一个说清楚。

缩写 全称 含义 典型值
tbn Time Base Numerator 时间基的分子(即时间基的倒数) 1000, 90000, 48000
tbr Time Base Rate 帧率(视频)或采样率(音频) 30, 25, 48000
tbc Time Base Codec 编解码器内部的时间基 90000(H.264), 48000(AAC)

tbn 是封装格式的时间基。比如 MP4 的 timescale = 1000,那 tbn = 1000。所有时间戳在封装层都用这个刻度。

tbr 是帧率或采样率。视频 30fps,音频 48000Hz。这个值决定了每秒钟有多少帧或样本。

tbc 是编解码器内部的时间基。H.264 编码器内部用 90kHz,AAC 用 48kHz。解码器输出的时间戳,通常基于 tbc。

我的经验:做音视频同步时,我习惯把所有时间戳统一到毫秒(tbn = 1000)。这样计算简单,调试也方便。但要注意精度——如果你处理的是 24fps 的视频,毫秒精度可能不够,建议用微秒或 90kHz。

2.4 时间基转换公式

实际开发中,你经常需要在不同时间基之间转换。公式其实很简单:

// 从时间基 A 转换到时间基 B
// pts_a:在时间基 A 下的时间戳
// tbn_a:时间基 A 的刻度数
// tbn_b:时间基 B 的刻度数

uint64_t pts_b = (uint64_t)pts_a * tbn_b / tbn_a;

// 示例:从 90kHz 转换到毫秒
// pts_90k = 45000(表示 0.5 秒)
// tbn_90k = 90000
// tbn_ms = 1000

uint64_t pts_ms = (uint64_t)45000 * 1000 / 90000; // = 500(毫秒)

这里有个坑:乘法可能溢出。pts_a 是 33 位或 64 位整数,乘以 tbn_b 后可能超过 64 位。我建议先除后乘,或者用 128 位中间变量。

// 安全的转换方式(先除后乘)
// 前提:pts_a 能被 tbn_a 整除
uint64_t pts_b = (pts_a / tbn_a) * tbn_b;

// 如果不能整除,用 128 位
// 或者用浮点(不推荐,精度问题)
double pts_b_double = (double)pts_a * tbn_b / tbn_a;

2.5 时间基转换的实战场景

我做过一个项目,需要把 TS 流里的音频同步到 MP4 容器里。TS 的时间基是 90kHz,MP4 是 1000。转换过程是这样的:

  1. 从 TS 解析出 PTS(90kHz 单位)
  2. 转换为毫秒:pts_ms = pts_90k * 1000 / 90000
  3. 写入 MP4 的 mdhd.timescale = 1000
  4. 计算 MP4 的 duration:duration = pts_ms

看起来简单?但实际遇到的问题是:TS 的 PTS 是 33 位整数,最大值约 9 小时。超过这个时间,PTS 会回绕。你得处理回绕逻辑,否则时间戳会突然变小。

避坑指南:我曾经在处理 24 小时直播流时,没处理 PTS 回绕,结果音频在 9 小时后突然跳到开头。从那以后,我所有时间戳处理都加了回绕检测。

2.6 知识体系图

下面这张图总结了本章的核心逻辑:

封装格式时间基知识体系 MP4 MKV TS timescale TimecodeScale 90kHz / PCR tbn / tbr / tbc 三兄弟 时间基转换公式 实战:TS→MP4 时间基转换 核心:所有时间戳最终都要统一到同一个时间基,才能做音视频同步

这张图展示了从封装格式到时间基,再到 tbn/tbr/tbc 概念,最后到转换公式和实战的完整链路。你写代码时,按这个顺序思考,基本不会乱。

2.7 小结

时间基是音视频同步的基石。MP4 用 timescale,MKV 用 TimecodeScale,TS 用 90kHz。tbn 是封装时间基,tbr 是帧率/采样率,tbc 是编解码器时间基。转换公式就是简单的比例计算,但要注意溢出和回绕。

嗯,这一章就到这儿。下回咱们聊聊时间戳的生成和校正——那才是真正考验细节的地方。