时间戳原理:PTS与DTS的区别、时间基的概念、时间戳的计算与转换

做音视频开发,时间戳是绕不开的核心概念。我刚开始接触这个领域时,也被PTS、DTS、Timebase这些术语搞得一头雾水。说白了,时间戳就是给每一帧画面、每一段音频贴上一个"时间标签",告诉播放器:这一帧该在什么时候显示,那一帧该在什么时候解码。

但事情没那么简单。视频编码时,帧的解码顺序和显示顺序往往不一样。这就引出了我们今天要聊的两个关键概念——PTS和DTS。

PTS与DTS:解码时间和显示时间为什么不同?

先看一个实际场景。假设有一段视频,包含三帧:I帧、B帧、P帧。I帧是关键帧,可以独立解码;P帧依赖前面的I帧;B帧是双向预测帧,依赖前后的I帧和P帧。

编码器输出的顺序可能是:I帧 → P帧 → B帧。但显示顺序应该是:I帧 → B帧 → P帧。为什么会这样?因为B帧需要等到后面的P帧解码完成后,才能拿到完整的参考数据。

所以就有了两个时间戳:

  • DTS(Decoding Time Stamp):解码时间戳,告诉解码器什么时候开始解码这一帧。
  • PTS(Presentation Time Stamp):显示时间戳,告诉渲染器什么时候显示这一帧。

我遇到过不少新手,直接把PTS当成DTS用,结果画面卡顿、花屏。嗯,这里要注意:对于没有B帧的视频(比如大部分直播流),PTS和DTS是相等的。但一旦出现B帧,两者就不一样了。

核心结论:

  • 没有B帧时:PTS == DTS
  • 有B帧时:DTS ≤ PTS(解码早于显示)
  • 音频通常没有B帧概念,PTS == DTS

时间基(Timebase):时间戳的"刻度尺"

你可能会问:时间戳的单位是什么?秒?毫秒?都不是。时间戳的单位是时间基(Timebase)。

时间基本质上是一个分数,比如 1/90000、1/1000、1/44100。它表示"每个刻度代表多少秒"。举个例子:

  • Timebase = 1/90000,时间戳值 = 180000,那么实际时间 = 180000 × (1/90000) = 2秒
  • Timebase = 1/1000,时间戳值 = 2000,实际时间 = 2000 × (1/1000) = 2秒

我个人习惯把Timebase理解成一把尺子。尺子的最小刻度是1/90000秒,时间戳就是在这把尺子上读出的刻度数。不同封装格式、不同编码标准,用的Timebase不一样:

场景 常见Timebase 说明
MPEG-TS(H.264/H.265) 1/90000 MPEG标准定义,90kHz时钟
MP4容器 1/1000 或自定义 由mdhd/tkhd box中的timescale决定
音频(44100Hz采样) 1/44100 每个采样点一个刻度
音频(48000Hz采样) 1/48000 常见于HDMI、专业设备
RTMP/FLV 1/1000 毫秒精度

我曾经踩过的坑:有一次做多路流同步,一路流的Timebase是1/90000,另一路是1/1000。我直接拿时间戳做差值比较,结果差了90倍。后来才意识到,必须先统一Timebase再计算。切记:不同Timebase的时间戳不能直接比较。

时间戳的计算与转换

理解了Timebase,计算就简单了。核心公式就一个:

实际时间(秒) = 时间戳值 × Timebase

// 示例:Timebase = 1/90000,时间戳 = 45000
实际时间 = 45000 × (1/90000) = 0.5秒

反过来,从实际时间转回时间戳:

时间戳值 = 实际时间(秒) / Timebase

// 示例:Timebase = 1/90000,实际时间 = 2秒
时间戳值 = 2 / (1/90000) = 180000

不同Timebase之间的转换也很直接:

// 将Timebase A下的时间戳ts_A,转换为Timebase B下的时间戳ts_B
ts_B = ts_A × (Timebase_A / Timebase_B)

// 实际例子:将1/90000下的180000,转为1/1000下的值
ts_B = 180000 × (1/90000) / (1/1000) = 180000 × (1/90) = 2000

在FFmpeg中,这个转换有现成函数:

// FFmpeg时间基转换
int64_t av_rescale_q(int64_t src_ts, AVRational src_tb, AVRational dst_tb);

// 示例:将1/90000下的180000转为1/1000
int64_t new_ts = av_rescale_q(180000, (AVRational){1, 90000}, (AVRational){1, 1000});
// new_ts == 2000

我的调试小技巧:在排查音画不同步问题时,我通常会先打印每一帧的PTS、DTS和对应的Timebase。然后统一转成毫秒(Timebase=1/1000)做对比。如果发现相邻两帧的PTS差值不等于帧间隔(比如25fps应该是40ms),那基本就是时间戳出问题了。

时间戳与帧率的配合

帧率决定了相邻帧之间的时间戳增量。比如25fps的视频,每帧间隔40ms。如果Timebase是1/90000,那么每帧的时间戳增量应该是:

增量 = 帧间隔(秒) / Timebase = 0.04 / (1/90000) = 3600

所以一个25fps、Timebase=1/90000的视频流,理想的时间戳序列应该是:0, 3600, 7200, 10800, ...

但实际编码器输出的时间戳不一定这么规整。我见过一些编码器,因为码率控制、场景切换等原因,时间戳会出现抖动。这时候就需要做时间戳平滑处理。

时间戳的常见问题与避坑

  • 时间戳回绕(Wrap Around):32位时间戳最大约2^32,在1/90000的Timebase下约13小时。超过后会归零。我曾经在直播推流中遇到过,服务器端没处理回绕,导致播放器认为时间倒流,直接卡死。解决方案是用64位时间戳,或者检测回绕后做补偿。
  • 起始时间戳不为0:有些编码器从随机值开始。播放器需要记录第一帧的时间戳作为基准,后续帧减去这个基准。
  • B帧导致的PTS乱序:解码器收到的帧顺序是按DTS排的,但显示时要按PTS重排。如果播放器没做重排,画面就会来回跳。
  • 音频时间戳与采样率对齐:音频时间戳必须与采样率对应。比如48kHz的音频,每帧1024个采样,那么每帧的时间戳增量应该是 1024/48000 秒。如果增量不对,音频就会越播越快或越播越慢。
时间戳核心知识体系 时间戳(Timestamp) PTS(显示时间戳) DTS(解码时间戳) 时间基(Timebase) 无B帧时 = DTS 有B帧时 > DTS 决定解码器输入顺序 刻度单位(秒) 常见值 1/90000(TS流) 1/1000(MP4/FLV) 1/48000(音频) 核心公式 实际时间(秒)= 时间戳值 × Timebase 常见问题:时间戳回绕 | 起始不为0 | B帧乱序 | 音频对齐

这张图把时间戳的核心知识点串起来了。从上到下看:时间戳分为PTS和DTS,它们的关系取决于有没有B帧;Timebase是时间戳的刻度,不同场景用不同的值;底部是核心公式和常见问题。你调试时遇到问题,可以对照这张图排查。

最后说一句:时间戳这东西,理论不难,但实际项目中坑特别多。我建议你在开发播放器或推流器时,先把时间戳的打印日志做好。每帧的PTS、DTS、Timebase都打出来,转成毫秒后人工检查一遍。很多音画不同步的问题,根源就是时间戳算错了。