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 帧,这件事就不成立了。
因为系统里至少可能同时存在三种顺序:
- 采集 / 生成顺序
- 解码顺序
- 显示顺序
先看一张总图。
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 | |
但由于 B1 和 B2 都要参考 P3,编码 / 解码侧常常更接近:
1 | |
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 | |
那它表示的实际时间就是:
1 | |
也就是说:
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_baseAVCodecContext.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 | |
这句话背后的本质是:
把同一个时间点,从一个坐标系转换到另一个坐标系。
这不是“格式化操作”,而是媒体系统里非常核心的逻辑。
十一、一个具体代码例子:把 packet 时间戳从 codec time_base 转到 stream time_base
1 | |
这段代码在编码输出后非常常见。
它的工程含义是:
- 编码器内部可能按
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
- 播放器时钟
- 音画同步
- 推流时间戳
都会顺很多。
因为到那时你已经不再把时间戳看成“几个神秘数字”,而会知道:
它们其实是在帮整条媒体链路维护节奏、顺序和时间坐标。