2、时间戳基础:PTS、DTS 与 time_base

做音画同步,说白了就是跟时间戳打交道。我刚开始接触多媒体开发时,总觉得时间戳不就是个数字嘛,有什么好研究的?结果第一次做播放器,音频视频各跑各的,画面都播到一半了,声音还没开始。嗯,从那以后我才明白——时间戳才是音画同步的命根子。

这一节我们就把 PTS、DTS 和时间基这三个概念彻底讲清楚。它们不复杂,但容易混淆。尤其是 DTS 和 PTS,很多人一开始都搞反过。

2.1 什么是 PTS(显示时间戳)

PTS 全称 Presentation Time Stamp,中文叫“显示时间戳”。它表示一帧数据应该被显示出来的时刻。

举个例子:你有一个视频文件,帧率是 30fps。理论上每帧应该间隔 33ms 显示。但实际编码时,每一帧的 PTS 值就是告诉播放器:“嘿,我这一帧应该在时间轴上的这个点出现。”

核心理解:PTS 是给显示设备看的。播放器拿到一帧数据后,会检查它的 PTS,然后决定什么时候把它送到屏幕上。

我在项目中遇到过一个问题:某个视频源的 PTS 不是从 0 开始的,而是从 1000 开始的。播放器没处理好,结果前 1000 个单位时间里画面一直是黑的。后来我加了个 PTS 归一化处理,才解决。

2.2 什么是 DTS(解码时间戳)

DTS 全称 Decoding Time Stamp,中文叫“解码时间戳”。它表示一帧数据应该被解码器处理的时刻。

你可能会问:解码和显示不是一回事吗?

还真不是。这就要说到视频编码里的一个关键概念——B 帧。

视频编码时,为了压缩效率,会用到三种帧类型:

  • I 帧:关键帧,可以独立解码
  • P 帧:向前参考帧,依赖前面的 I 帧或 P 帧
  • B 帧:双向参考帧,依赖前后两个方向的帧

B 帧的问题在于:它需要参考后面的帧才能解码。所以解码顺序和显示顺序是不一致的。

帧序号 帧类型 DTS(解码顺序) PTS(显示顺序)
1 I 帧 0 0
2 P 帧 1 2
3 B 帧 2 1
4 B 帧 3 3

看到没?B 帧的 DTS 是 2,但 PTS 是 1。也就是说,解码器先解码第 3 帧,但显示的时候要等到第 1 帧之后才显示它。

注意:不是所有编码格式都有 B 帧。比如 H.264 的 Baseline Profile 就不支持 B 帧。这种情况下 DTS 和 PTS 是相等的。但一旦遇到 B 帧,DTS 和 PTS 就会错开。

2.3 什么是时间基(time_base)

时间基,英文叫 time_base,是 FFmpeg 里最基础也最容易忽略的概念。

说白了,时间基就是一个刻度单位。它告诉系统:时间戳里的 1 个单位,到底对应多少秒。

比如:

  • time_base = 1/1000,表示每个单位是 1 毫秒
  • time_base = 1/90000,表示每个单位是 1/90000 秒(约 11.1 微秒)
  • time_base = 1/25,表示每个单位是 0.04 秒(常用于 25fps 视频)

我个人习惯用 AVRational 来表示时间基。FFmpeg 里是这样定义的:

typedef struct AVRational {
    int num;   // 分子
    int den;   // 分母
} AVRational;

所以 time_base = 1/1000 就是 {num=1, den=1000}。

为什么要用分数而不是浮点数?因为分数可以精确表示很多有理数。比如 1/25 用浮点数就是 0.040000...,但分数就是精确的 1/25。

2.4 PTS、DTS 与 time_base 的关系

这三者的关系其实很简单:

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

举个例子:

  • PTS = 3600
  • time_base = 1/90000
  • 实际显示时间 = 3600 × (1/90000) = 0.04 秒 = 40 毫秒

所以 PTS 和 DTS 本身只是整数,真正有意义的是它们乘以 time_base 之后得到的实际时间。

经验之谈:做音画同步时,我建议把所有时间戳都统一换算成微秒(microseconds)或毫秒(milliseconds)来处理。因为不同流的时间基可能不一样,直接比较 PTS 值会出问题。统一换算后再比较,就安全多了。

2.5 知识体系图

下面这张图把 PTS、DTS 和 time_base 的关系画清楚了:

PTS、DTS 与 time_base 关系图 PTS 显示时间戳 DTS 解码时间戳 time_base 时间基 实际时间(秒)= 时间戳值 × time_base 例如:PTS=3600, time_base=1/90000 → 实际时间=0.04秒 解码与显示流程 解码器 缓冲区 显示设备 按 DTS 解码 按 PTS 显示 关键点: • 解码器按 DTS 顺序解码,显示设备按 PTS 顺序显示 • time_base 是连接时间戳与实际时间的桥梁

2.6 常见问题与避坑

做时间戳处理时,有几个坑我踩过,分享给你:

  • 时间基不一致:音频流和视频流的时间基可能不同。比如视频用 1/90000,音频用 1/48000。直接比较 PTS 值会出错。必须先统一换算。
  • PTS 回绕:时间戳是整数,总有上限。当 PTS 达到最大值后会回绕到 0。我曾经在一个长时间录制的项目中遇到过这个问题,画面突然跳回开头。解决方案是用相对时间戳,或者用 64 位整数。
  • B 帧导致的 DTS/PTS 错位:解码器输出帧的顺序是按 DTS 来的,但显示必须按 PTS。如果你直接把解码后的帧送去显示,画面顺序就乱了。必须做重排序。

曾经踩过的坑:有一次我写播放器,没处理 B 帧的重排序。结果画面里的人物动作是倒着走的——先张嘴再闭嘴,先抬手再放下。排查了半天才发现是 DTS 和 PTS 搞混了。从那以后,我每次处理视频帧都会先检查有没有 B 帧。

2.7 代码示例:时间戳换算

下面是一个简单的 C 语言示例,演示如何把 PTS 换算成实际时间:

#include <stdio.h>
#include <stdint.h>

typedef struct {
    int num;
    int den;
} AVRational;

// 将时间戳换算为微秒
int64_t pts_to_us(int64_t pts, AVRational time_base) {
    // pts * num / den,先乘后除避免精度损失
    return pts * time_base.num * 1000000 / time_base.den;
}

int main() {
    // 视频流:time_base = 1/90000
    AVRational video_tb = {1, 90000};
    int64_t video_pts = 3600;
    
    // 音频流:time_base = 1/48000
    AVRational audio_tb = {1, 48000};
    int64_t audio_pts = 1920;
    
    int64_t video_us = pts_to_us(video_pts, video_tb);
    int64_t audio_us = pts_to_us(audio_pts, audio_tb);
    
    printf("视频 PTS 实际时间:%ld 微秒\n", video_us);
    printf("音频 PTS 实际时间:%ld 微秒\n", audio_us);
    printf("时间差:%ld 微秒\n", audio_us - video_us);
    
    return 0;
}

输出结果:

视频 PTS 实际时间:40000 微秒
音频 PTS 实际时间:40000 微秒
时间差:0 微秒

你看,虽然两个流的 PTS 值不同(3600 vs 1920),但换算成实际时间后都是 40 毫秒。这就是 time_base 的作用——它让不同时间基的流可以统一比较。

2.8 小结

这一节我们讲了三个核心概念:

  • PTS:告诉播放器什么时候显示
  • DTS:告诉解码器什么时候解码
  • time_base:告诉系统时间戳的单位是什么

记住一句话:PTS 和 DTS 是整数,time_base 是刻度,实际时间 = 整数 × 刻度。搞懂这个,后面做音画同步就顺了。