时间戳基础:PTS、DTS、time_base 的概念与关系

做流媒体的人都知道一句话:同步是灵魂,时间戳是骨架。没有时间戳,你拿到的就是一坨乱序的字节流,谁也分不清哪帧该什么时候显示。

我记得刚入行那会儿,接手一个多路摄像头拼接的项目。四路 1080p 视频流,硬生生被我拼成了“鬼畜”画面——每路各跑各的节奏,有的快有的慢。后来排查了一整天,才发现是时间戳单位没统一。嗯,从那以后,我再也不敢小看 time_base 了。

2.1 什么是 PTS 和 DTS?

先看两个最核心的概念:

  • PTS(Presentation Time Stamp):显示时间戳。告诉解码器:这帧画面应该在什么时候显示给用户。
  • DTS(Decoding Time Stamp):解码时间戳。告诉解码器:这帧数据应该在什么时候开始解码。

你可能会问:为什么需要两个时间戳?直接一个 PTS 不就行了?

原因在于视频编码里的 B 帧(双向预测帧)。B 帧依赖前后的 I 帧和 P 帧才能解码,所以解码顺序和显示顺序往往不一致。举个例子:

显示顺序: I₁ → B₂ → P₃ → B₄ → P₅

解码顺序: I₁ → P₃ → B₂ → P₅ → B₄

你看,B₂ 虽然显示在 P₃ 之前,但解码时必须等 P₃ 先解完。这就是 DTS 存在的意义。

说白了,DTS 管“什么时候解”,PTS 管“什么时候放”。对于没有 B 帧的编码(比如大部分音频编码),PTS 和 DTS 是相等的。

2.2 time_base:时间戳的“刻度尺”

时间戳本身是个整数,比如 3600、7200。但 3600 代表多少秒?这取决于你用什么单位去量。

time_base 就是那个单位。它是一个有理数,通常用分数表示,比如 1/90000、1/1000。

在 FFmpeg 里,每个流都有自己的 time_base。常见的几种:

媒体类型 常见 time_base 精度 适用场景
视频(MPEG-TS) 1/90000 高 广播电视、直播流
视频(MP4) 1/1000 或 1/90000 中/高 文件封装、点播
音频(AAC) 1/44100 或 1/48000 极高 采样率对齐
音频(通用) 1/1000 中 简单场景

我个人习惯把 time_base 想象成一把尺子。1/90000 意味着尺子上每格是 1/90000 秒,约 11 微秒。对于 25fps 的视频,每帧间隔是 3600 个刻度(因为 90000/25 = 3600)。

小技巧:在 FFmpeg 里用 av_rescale_q() 或 av_rescale_q_rnd() 做 time_base 转换。千万别自己手算,容易翻车。我曾经在项目里手写除法,结果精度丢失导致音画不同步,排查了整整两天。

2.3 PTS、DTS、time_base 的关系

这三者的关系可以用一个公式概括:

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

举个例子:

  • PTS = 3600,time_base = 1/90000 → 实际时间 = 3600 × (1/90000) = 0.04 秒
  • PTS = 7200,time_base = 1/90000 → 实际时间 = 7200 × (1/90000) = 0.08 秒

你看,只要 time_base 统一,不同流的时间戳就能换算到同一个时间轴上。这就是同步的基石。

但在实际项目中,不同来源的流往往 time_base 不同。比如摄像头输出的是 1/90000,音频采集卡给的是 1/48000。这时候必须做一次 时间基转换,否则直接比较 PTS 就是鸡同鸭讲。

避坑指南:我曾经遇到过一个坑——某个 RTSP 流的 time_base 是 1/90000,但它的 PTS 值却以毫秒为单位递增。这明显是封装不规范。遇到这种情况,别硬算,先检查一下 av_stream->time_base 和实际 PTS 的步长是否匹配。

2.4 为什么时间戳是同步的基石?

多路流同步,说白了就是让各路媒体在同一个时间线上对齐。没有时间戳,你根本不知道:

  • 这帧画面应该和哪段音频配对
  • 当前播放进度到了哪里
  • 是否需要丢帧或插帧来补偿延迟

我参与过一个远程医疗项目,需要同步传输医生的视频、患者的生命体征数据和实时语音。三路流,三种不同的采样率,三种 time_base。最后靠一套统一的时间戳映射机制,才把画面、波形和声音对齐到毫秒级。

说白了,时间戳就是各路流的“共同语言”。没有它,多路流就是一群各说各话的哑巴。

2.5 核心知识图谱

下面这张图总结了 PTS、DTS、time_base 的关系,以及它们在同步中的位置:

时间戳核心知识结构图 时间戳核心知识结构图 PTS 显示时间戳 DTS 解码时间戳 time_base 时间基(刻度单位) 决定画面何时显示 决定数据何时解码 时间戳的刻度单位 实际时间(秒)= 时间戳 × time_base 例如:3600 × (1/90000) = 0.04 秒 多路流同步的基石 统一时间轴 → 音画对齐 → 丢帧/插帧决策 💡 关键:不同流的 time_base 必须统一换算后才能比较 PTS/DTS

2.6 实战中的常见问题

光讲理论不够,我列几个实际项目中踩过的坑:

  1. 时间戳回绕(Wrap Around):PTS 是 32 位或 64 位整数,总有上限。达到最大值后会归零。如果不做回绕检测,同步逻辑会瞬间崩溃。
  2. 非单调递增的 PTS:有些编码器输出的 PTS 会偶尔倒退。FFmpeg 的 ffplay 遇到这种情况会直接报错,但生产环境里你得自己处理。
  3. time_base 精度不足:比如用 1/1000 做视频 time_base,对于 29.97fps 的 NTSC 制式,每帧间隔是 33.3667 毫秒,但 1/1000 只能精确到 33 或 34 毫秒,累积误差会越来越大。

我的建议:做多路流同步时,统一用一个高精度的 time_base 作为“主时钟”。我个人偏爱 1/90000,兼容性好,精度也够。音频流通过重采样或插值对齐到这个主时钟上。