FFmpeg 时间戳体系入门:PTS、DTS、time_base 到底怎么理解

前言:为什么很多人学 FFmpeg,最后都死在时间戳上

如果你已经开始做 FFmpeg、播放器、转码器、推流器、直播链路或者音视频同步,几乎很快就会碰到下面这些词:

  • PTS
  • DTS
  • time_base
  • duration
  • 重排序
  • 音画同步
  • av_rescale_q

很多人第一次看这些概念时都会有一种很强的割裂感。

因为表面上它们都像“时间相关信息”,但一旦放到工程里,问题马上就会变复杂:

  • 为什么 packet 里既有 PTS 又有 DTS
  • 为什么有时候 DTS 小于 PTS
  • 为什么有 B 帧时顺序会变
  • 为什么 time_base 到处都不一样
  • 为什么明明图都能解出来,播放节奏还是不对
  • 为什么 mux 之后播放器会报 non monotonically increasing dts

我觉得这部分最容易让人痛苦的原因,不是公式多,而是:

很多人没有把“时间戳到底服务谁、描述什么顺序、在哪个时间单位里表示”这三件事分清楚。

所以这篇文章的目标,不是教你背几个 API,而是尽量把 FFmpeg 里最核心的时间戳体系讲顺。

重点讲清这些问题:

  • PTS 和 DTS 到底分别在描述什么
  • 为什么 B 帧会让显示顺序和解码顺序不一致
  • time_base 到底是什么,为什么它像单位换算基准
  • 为什么 packet / stream / codec context 里会出现不同时间语义
  • 工程里时间戳最容易在哪些地方出错

而且这篇会多放时序图和代码,尽量把“时间戳”这件事讲成有画面感的系统问题,而不是抽象公式题。


一、先给一句最重要的话:时间戳不是“当前时间”,而是媒体数据该在什么时候被处理或显示的标记

如果你现在只想先记一句最关键的话,那就是:

在音视频系统里,时间戳不是系统当前时间,而是媒体数据在链路中“应该在什么时候被解码、显示或播放”的标记。

这句话非常重要,因为它能立刻帮你摆脱很多误解。

也就是说:

  • 时间戳不是 time(NULL) 这种现实时间
  • 也不是“帧到了就随便记个数”
  • 它是媒体系统内部用来维持顺序和节奏的坐标系

所以你可以先把时间戳看成:

媒体数据的“时间坐标”。

而 PTS、DTS、time_base,就是这个坐标系里的几个核心元素。


二、先看全局:一段视频从编码到播放,为什么会出现不止一种“顺序”

这是理解 PTS / DTS 的前提。

很多人默认以为视频只有一种顺序:

第 1 帧、第 2 帧、第 3 帧……按顺序处理就完了。

但一旦有 B 帧,这件事就不成立了。

因为系统里至少可能同时存在三种顺序:

  1. 采集 / 生成顺序
  2. 解码顺序
  3. 显示顺序

先看一张总图。

flowchart LR
    A[采集/输入顺序] --> B[编码顺序]
    B --> C[解码顺序]
    C --> D[显示顺序]

在没有 B 帧时,这几种顺序可能比较接近。

但一旦有 B 帧:

  • 编码器为了完成双向预测,需要先看到后面的参考帧
  • 解码器为了正确还原,也要按参考依赖关系处理
  • 但最终显示还得回到用户真正应该看到的时间顺序

所以你必须接受一个现实:

视频系统里的“时间”不只是一条简单直线。

这也是 PTS 和 DTS 都必须存在的根本原因。


三、PTS 到底是什么,它更接近“什么时候该给用户看 / 听”

3.1 PTS 的本质

PTS 全称是 Presentation Timestamp。

如果不用教科书腔,我更建议你把它理解成:

这帧(或这段音频)应该在什么时候呈现给用户。

对视频来说,就是:

  • 什么时候该显示这一帧

对音频来说,就是:

  • 什么时候该播放这段采样

所以 PTS 更接近:

最终呈现顺序。

3.2 为什么 PTS 重要

因为用户最终感受到的是显示和播放节奏,而不是编码器内部怎么折腾。

所以:

  • 播放器靠它决定画面什么时候上屏
  • 音频设备靠它决定什么时候播放采样
  • 音画同步也强依赖它

3.3 一句话记忆

你可以先把 PTS 压成一句话:

PTS 决定“什么时候给用户看 / 听”。


四、DTS 到底是什么,它更接近“什么时候该先处理这份数据”

4.1 DTS 的本质

DTS 全称是 Decoding Timestamp。

它更接近:

这份压缩数据应该在什么时候送去解码。

为什么这和 PTS 不一样?

因为在有 B 帧时,显示顺序和解码顺序可能不同。

举个直觉例子:

  • 某个 B 帧显示上排在中间
  • 但要解它,系统得先把后面的参考 P 帧解出来

所以它不能只靠“显示顺序”来安排解码。

这时就需要 DTS。

4.2 DTS 的一句话记忆

你可以先记成:

DTS 决定“什么时候先把这份数据喂给解码器”。

这样它和 PTS 的区别就会立刻清楚很多:

  • PTS:什么时候呈现
  • DTS:什么时候解码

五、一张时序图看懂为什么有时 DTS 和 PTS 不一样

这个问题的核心几乎都和 B 帧有关。

我们先看一张抽象时序图。

sequenceDiagram
    participant Display as 显示顺序
    participant Decoder as 解码顺序

    Note over Display: I B B P
    Note over Decoder: I P B B

这张图虽然简化,但已经足够表达最核心的事实:

为了显示中间的 B 帧,解码器可能必须先把后面的 P 帧解出来。

于是就出现了:

  • 显示顺序按 PTS 走
  • 解码顺序按 DTS 走

这也是为什么:

  • 没有 B 帧时,PTS 和 DTS 常常相等或很接近
  • 有 B 帧时,PTS 和 DTS 往往会分开

六、一张更完整的例子:I / B / B / P 的顺序到底怎么理解

我们把刚才那件事再展开一点。

假设显示顺序是:

1
I0  B1  B2  P3

但由于 B1 和 B2 都要参考 P3,编码 / 解码侧常常更接近:

1
I0  P3  B1  B2

6.1 结构图理解

flowchart LR
    I0[I0] --> B1[B1]
    I0 --> B2[B2]
    P3[P3] --> B1
    P3 --> B2

这张图说明:

  • B1 和 B2 都依赖 I0 和 P3
  • 所以如果没有先拿到 P3,就很难正确解 B1 和 B2

6.2 时序上带来的直接结果

于是:

  • PTS 更像:I0 -> B1 -> B2 -> P3
  • DTS 更像:I0 -> P3 -> B1 -> B2

这也是为什么“时间戳”不是一个单纯计数器,而是和预测结构深度绑定。


七、time_base 到底是什么,为什么它像时间单位的尺子

这部分是另一个高频难点。

很多人看到 time_base 会下意识问:

为什么不直接用秒?

因为在媒体系统里,很多时间戳不是直接用浮点秒表示,而是:

用一个整数时间戳,再配一个时间单位去解释它。

这个时间单位就是 time_base

7.1 time_base 的本质

你可以把 time_base 理解成:

时间戳的基本单位。

如果:

1
2
time_base = 1/1000
pts = 500

那它表示的实际时间就是:

1
500 * (1/1000) = 0.5 秒

也就是说:

pts 本身只是一个整数坐标,真正把它解释成时间的是 time_base。

7.2 一句话记忆

  • pts/dts 是数字
  • time_base 是单位

这句话很值钱。


八、一张图看懂 pts 和 time_base 的关系

flowchart LR
    A[pts = 3000] --> B[time_base = 1/90000]
    B --> C[实际时间 = 3000 * 1/90000 秒]

这张图非常简单,但你以后做任何时间戳换算都绕不开它。

很多 bug,本质上不是 pts 错了,而是:

你用错了 time_base 去解释它。


九、为什么 FFmpeg 里到处都有 time_base,看起来特别乱

这是很多人一进 FFmpeg 就懵的地方。

因为你会看到:

  • AVStream.time_base
  • AVCodecContext.time_base
  • 某些 frame / packet 相关时间语义

看起来像“到处都是时间基”。

9.1 为什么会这样

因为不同模块处在不同语境里:

  • 封装层有自己的时间单位习惯
  • 编码器有自己的内部时间表达
  • packet 的时间戳通常相对于 stream 的 time_base

所以它不是“故意搞乱”,而是:

不同层都需要一个自己的时间坐标系。

9.2 最实用的工程理解

先粗记这条就很好用:

  • AVPacket.pts/dts 通常配合 AVStream.time_base 来解释
  • 编码器 / 解码器内部有时会以 AVCodecContext.time_base 为参考

然后真正做换算时,用 av_rescale_q 去转。


十、最经典的一件事:为什么一定要做时间基换算

因为同一个“0.5 秒”,在不同时间基下,数值可能完全不同。

比如:

  • 1/1000 下,0.5 秒对应 500
  • 1/90000 下,0.5 秒对应 45000

所以如果你直接拿一个时间戳整数跨模块传,而不做换算,就很容易炸。

10.1 一段最常见的 FFmpeg 代码

1
int64_t new_pts = av_rescale_q(old_pts, src_tb, dst_tb);

这句话背后的本质是:

把同一个时间点,从一个坐标系转换到另一个坐标系。

这不是“格式化操作”,而是媒体系统里非常核心的逻辑。


十一、一个具体代码例子:把 packet 时间戳从 codec time_base 转到 stream time_base

1
2
3
pkt->pts = av_rescale_q(pkt->pts, enc_ctx->time_base, out_stream->time_base);
pkt->dts = av_rescale_q(pkt->dts, enc_ctx->time_base, out_stream->time_base);
pkt->duration = av_rescale_q(pkt->duration, enc_ctx->time_base, out_stream->time_base);

这段代码在编码输出后非常常见。

它的工程含义是:

  • 编码器内部可能按 enc_ctx->time_base 给出时间戳
  • 但写入容器时,mux 层更关心 out_stream->time_base
  • 所以必须换算

如果这里不做,很容易出现:

  • 时长不对
  • 播放节奏不对
  • mux 报时间戳异常

十二、为什么“non monotonically increasing dts”这么常见

这是 FFmpeg 新手非常爱见到的一类报错。

它的直觉意思是:

后来的 DTS 不能比前面的还小。

也就是:

  • 解码顺序时间戳必须整体向前走
  • 不能乱跳回去

12.1 为什么会触发

常见原因包括:

  • B 帧重排序没处理好
  • 时间基换算错了
  • 手工改 pts/dts 时没维护顺序
  • mux 输入 packet 顺序不对

12.2 为什么 mux 层这么在意 DTS

因为 mux 层需要按“可解码顺序”把数据组织起来。

如果 DTS 乱了,后面解码和播放就容易出大问题。

所以你可以先把这类报错理解成:

系统在提醒你:解码顺序的时间坐标坏了。


十三、PTS / DTS / time_base 三者关系图

flowchart TD
    A[媒体数据 packet/frame] --> B[PTS]
    A --> C[DTS]
    B --> D[表示最终呈现时间]
    C --> E[表示解码处理时间]
    B --> F[都必须结合 time_base 解释]
    C --> F
    F --> G[实际秒数 / 媒体时刻]

这张图非常适合你后面反复回忆。


十四、音视频同步为什么最后也会落到时间戳上

如果你继续往后学播放器或 RTC,很快就会碰到音画同步。

而音画同步的本质,也离不开时间戳。

14.1 为什么音画不同步不是“谁快谁慢”这么简单

因为音频和视频:

  • 采样方式不同
  • 编解码复杂度不同
  • buffer 深度不同
  • 渲染设备行为不同

所以系统最后必须依赖时间戳,把它们都放回同一个“媒体时间轴”上。

14.2 一句话理解

可以先这样记:

PTS 是音画同步时最接近“该在什么时候呈现”的公共语言。

所以后面你学同步时,会发现:

  • 不懂时间戳,几乎不可能真正懂同步

十五、一张时序图看懂播放器为什么总在围绕 PTS 做决策

sequenceDiagram
    participant Demux as Demux
    participant Dec as Decoder
    participant Clock as Master Clock
    participant Render as Renderer

    Demux->>Dec: packet (pts/dts)
    Dec->>Render: frame (pts)
    Clock->>Render: 当前播放时钟
    Render->>Render: 比较 frame pts 与当前时钟
    Render->>Render: 决定立即显示 / 等待 / 丢帧

这张图最关键的意思是:

播放器最终显示逻辑,通常更看重 PTS 与当前播放时钟的关系。

这也是为什么你可以把 PTS 理解成“呈现计划表”。


十六、工程里最容易踩的几个坑

16.1 误区一:PTS 和 DTS 总是一样的

不是。

  • 没 B 帧时可能接近甚至相同
  • 有 B 帧时往往会分开

16.2 误区二:time_base 不重要,pts 就是时间

不对。

没有 time_base,pts/dts 只是整数。

16.3 误区三:只要 packet 有时间戳,随便拷过去就行

不行。

跨模块、跨容器、跨 stream 时,常常必须做时间基换算。

16.4 误区四:出现同步问题一定是播放器问题

也不一定。

很多同步问题根源可能在:

  • 编码时间戳就错了
  • mux 时转换错了
  • packet 顺序不对
  • DTS 单调性被破坏

十七、面试里怎么讲,才不会显得只会背定义

如果面试官问:

你怎么理解 PTS、DTS 和 time_base?

我建议你按下面这个结构讲。

17.1 先讲本质

PTS 表示媒体数据最终应该被呈现的时间,DTS 表示压缩数据应该被解码处理的时间。在没有 B 帧时它们可能接近,但有 B 帧时由于显示顺序和解码顺序不同,它们通常会分开。

17.2 再讲 time_base

time_base 可以理解成时间戳的单位,PTS 和 DTS 本身只是整数,必须结合对应的 time_base 才能解释成真正的时间。

17.3 最后讲工程意义

在 FFmpeg 里,packet、stream、codec context 可能处在不同时间基语境下,所以时间戳经常需要做换算。如果处理不好,就容易出现音画不同步、播放节奏异常或者 non monotonically increasing dts 这类问题。

如果你能这么讲,面试官会很容易判断你是真的理解过时间戳,而不是只会说“PTS 是显示时间,DTS 是解码时间”。


总结:FFmpeg 时间戳体系真正难的,不是公式,而是顺序和单位

回到这篇文章最开始的问题:

PTS、DTS、time_base 到底该怎么理解?

我觉得最稳的主线是:

  • 媒体系统需要给数据打时间坐标
  • PTS 负责“什么时候呈现”
  • DTS 负责“什么时候解码”
  • time_base 负责“这些时间戳的单位是什么”
  • 一旦有 B 帧,显示顺序和解码顺序就可能不同
  • 一旦跨模块、跨容器、跨 stream,就必须做时间基换算

只要这条线立住,后面你再去看:

  • B 帧重排序
  • mux / demux
  • 播放器时钟
  • 音画同步
  • 推流时间戳

都会顺很多。

因为到那时你已经不再把时间戳看成“几个神秘数字”,而会知道:

它们其实是在帮整条媒体链路维护节奏、顺序和时间坐标。


FFmpeg 时间戳体系入门:PTS、DTS、time_base 到底怎么理解
https://breaker505.github.io/2026/04/19/ffmpeg-timestamps-pts-dts-timebase/
作者
爱发呆的鱼
发布于
2026年4月19日
许可协议