时间戳与同步:音画不同步的罪魁祸首
音视频同步的核心在于时间戳管理。PTS(呈现时间戳)决定帧的显示时间,DTS(解码时间戳)控制解码顺序,两者差异尤其在B帧处理中显现。视频和音频采用不同时间基(如视频90kHz,音频44.1kHz),需通过时间基转换实现统一时钟。FFmpeg通过av_rescale_q()处理容器与流的时间基差异,确保精确同步。典型场景中,B帧因依赖前后帧会导致DTS>PTS,需通过缓冲区重排序。实验显示I
时间戳与同步:音画不同步的罪魁祸首
专栏导读:你有没有遇到过视频中人物"对不上口型"的情况?这背后涉及音视频播放器最核心的技术——时间戳管理和同步。这一篇带你理解 PTS/DTS,掌握音视频同步的数学原理。
⏱️ 开场:音画不同步有多难受?
想象这样的场景:
视频画面: 演员张嘴说话 👄
音频声音: 0.5 秒后才传来声音 🔊
→ 像在看配音不准的烂片 😖
为什么会不同步?
视频流: 每秒 30 帧,每帧间隔 33.3 ms
音频流: 每秒 44100 个样本,每个样本间隔 0.023 ms
问题:
1. 视频和音频的"时钟"不一样
2. 解码速度不一致(视频慢,音频快)
3. 网络传输有延迟(直播场景)
人眼的容忍度:
音频超前视频 < 20 ms: 基本察觉不到 ✅
视频超前音频 < 40 ms: 可以接受 🆗
差距 > 100 ms: 明显不同步 ❌
核心问题:如何确保视频帧和音频样本在正确的时刻播放?
答案:时间戳(Timestamp)!
📏 核心概念:PTS 和 DTS
PTS(Presentation Time Stamp)
定义:这一帧/样本应该呈现(显示/播放)的时间。
视频 PTS 示例:
帧 0: PTS = 0 ms → 立即显示
帧 1: PTS = 33.3 ms → 33.3 ms 时显示
帧 2: PTS = 66.6 ms → 66.6 ms 时显示
帧 3: PTS = 100 ms → 100 ms 时显示
音频 PTS 示例:
样本 0-1023: PTS = 0 ms
样本 1024-2047: PTS = 23.2 ms (1024/44100 ≈ 23.2 ms)
样本 2048-3071: PTS = 46.4 ms

DTS(Decoding Time Stamp)
定义:这一帧应该解码的时间。
为什么需要 DTS?
因为视频编码中有 B 帧(双向预测帧),它依赖未来的帧:
解码顺序 (DTS): I₀ P₃ B₁ B₂ P₆ B₄ B₅
显示顺序 (PTS): I₀ B₁ B₂ P₃ B₄ B₅ P₆
解释:
B₁ 需要参考 I₀ 和 P₃,所以必须先解码 P₃
→ DTS(B₁) > DTS(P₃),但 PTS(B₁) < PTS(P₃)

关键规律:
对于 I 帧和 P 帧:
DTS = PTS (解码即显示)
对于 B 帧:
DTS > PTS (解码后要等待,按 PTS 显示)
🔢 时间基(Time Base):统一的时钟
问题:不同流的时间单位不同
视频流: 1 秒 = 90000 ticks (常见的 90 kHz 时间基)
音频流: 1 秒 = 44100 ticks (采样率)
系统时钟: 1 秒 = 1000 ms
如何统一?
FFmpeg 使用**时间基(Time Base)**的概念:
Time Base = 1 / frequency
示例:
视频 Time Base = 1/90000 (每 tick = 1/90000 秒)
音频 Time Base = 1/44100 (每 tick = 1/44100 秒)
时间戳转换公式
核心公式:
实际时间 (秒) = PTS × Time Base
示例 1 (视频):
PTS = 3000 (ticks)
Time Base = 1/90000
实际时间 = 3000 / 90000 = 0.0333 秒 = 33.3 ms
示例 2 (音频):
PTS = 1024 (样本数)
Time Base = 1/44100
实际时间 = 1024 / 44100 = 0.0232 秒 = 23.2 ms
转换到毫秒:
// ZenPlay 项目中的实际代码(简化版)
double pts_seconds = packet->pts * av_q2d(stream->time_base);
int64_t pts_ms = static_cast<int64_t>(pts_seconds * 1000);

FFmpeg 中的时间基
常见时间基:
| 流类型 | 常见时间基 | 含义 |
|---|---|---|
| 视频 | 1/90000 | MPEG 标准时钟频率 |
| 音频 | 1/采样率 | 例如 1/44100, 1/48000 |
| 字幕 | 1/1000 | 毫秒级精度 |
| 容器 | 1/1000 或 1/90000 | 取决于容器格式 |
注意:同一视频文件中,容器的时间基 ≠ 流的时间基!
示例:
MP4 容器: Time Base = 1/1000 (毫秒)
H.264 流: Time Base = 1/90000 (90 kHz)
转换: 需要用 av_rescale_q()
🎬 实战案例:B 帧的时间戳顺序
编码顺序 vs 显示顺序
假设视频帧序列:
I₀ B₁ B₂ P₃ B₄ B₅ P₆ ...
依赖关系:
- B₁ 依赖 I₀ 和 P₃
- B₂ 依赖 I₀ 和 P₃
- B₄ 依赖 P₃ 和 P₆
- B₅ 依赖 P₃ 和 P₆
实际编码和传输顺序(DTS):
I₀ → P₃ → B₁ → B₂ → P₆ → B₄ → B₅
播放器的处理流程:
关键点:
- 解码器必须提前解码 P₃
- 解码后的帧放入缓冲区
- 渲染器按 PTS 排序后显示
📊 实战:FFmpeg 时间戳分析
实验 1:查看数据包的时间戳
# 显示视频流的前 20 个数据包的 PTS/DTS
ffprobe -show_packets -select_streams v:0 -read_intervals "%+#20" input.mp4
# 只显示关键字段(PTS, DTS, 帧类型)
ffprobe -show_packets -show_entries packet=pts,dts,flags -select_streams v:0 \
-read_intervals "%+#20" input.mp4
输出示例(H.264 视频):
[PACKET]
pts=0
dts=0
flags=K_ # K = 关键帧 (I 帧)
[/PACKET]
[PACKET]
pts=3003
dts=1001 # DTS < PTS,这是 B 帧
flags=__
[/PACKET]
[PACKET]
pts=1001
dts=2002 # DTS > PTS,B 帧解码后需要重排序
flags=__
[/PACKET]
[PACKET]
pts=2002
dts=3003
flags=__
[/PACKET]
[PACKET]
pts=6006
dts=4004 # P 帧
flags=__
[/PACKET]
观察:
- I 帧:
pts == dts,标记K_ - B 帧:
pts < dts,标记__ - P 帧:
pts == dts,标记__
🧠 思考题
Q1:为什么 I 帧一定是 DTS == PTS,而 B 帧一定是 DTS > PTS?
I 帧(独立帧):
I 帧不依赖任何其他帧
→ 收到即可解码
→ 解码完成即可显示
→ DTS == PTS
B 帧(双向预测帧):
B₁ 依赖 I₀ 和 P₃
→ 必须等 P₃ 先解码完成
→ 传输顺序: I₀, P₃, B₁
→ 解码时间 DTS(B₁) 晚于 DTS(P₃)
→ 但显示时间 PTS(B₁) 早于 PTS(P₃)
→ 因此 DTS(B₁) > PTS(B₁)
数值示例(时间单位:毫秒):
帧序列: I₀ B₁ B₂ P₃
显示顺序 (PTS):
I₀: PTS=0
B₁: PTS=33
B₂: PTS=67
P₃: PTS=100
传输顺序 (DTS):
I₀: DTS=0 (第一个解码)
P₃: DTS=100 (第二个解码)
B₁: DTS=133 (第三个解码)
B₂: DTS=167 (第四个解码)
对比:
I₀: DTS=0, PTS=0 → DTS == PTS ✅
P₃: DTS=100, PTS=100 → DTS == PTS ✅
B₁: DTS=133, PTS=33 → DTS > PTS ✅
B₂: DTS=167, PTS=67 → DTS > PTS ✅
Q2:为什么播放器通常选择"音频为主"的同步策略,而不是"视频为主"?
点击查看答案原因 1:人耳对延迟更敏感
音频断续阈值: 50 ms (人耳能明显察觉)
视频丢帧阈值: 100 ms (人眼不易察觉)
原因 2:音频必须连续输出
音频硬件 (声卡): 每秒需要 44100 个样本
→ 如果数据供应不及时 → 产生"咔咔"爆音
→ 无法容忍中断
视频硬件 (显卡): 可以偶尔重复帧/跳帧
→ 人眼不易察觉(视觉暂留效应)
原因 3:音频时钟更稳定
音频时钟: 由硬件晶振提供,精度 ±10 ppm
视频解码: 受 CPU 负载影响,时间不固定
- I 帧解码慢 (10-20 ms)
- P/B 帧解码快 (2-5 ms)
实际策略:
正常播放: 音频为主,视频追音频 ✅
静音模式: 视频为主(无音频参考)
直播场景: 外部时钟为主(服务器时间)
Q3:如果视频文件没有 B 帧(例如 -bf 0 编码),是否就不需要 DTS 了?
理论上:是的!
如果只有 I 帧和 P 帧:
I₀ P₁ P₂ P₃ P₄ ...
依赖关系:
P₁ 依赖 I₀ → 必须先解码 I₀
P₂ 依赖 P₁ → 必须先解码 P₁
...
解码顺序 = 显示顺序
→ DTS == PTS(始终相等)
→ 不需要单独的 DTS 字段
实际情况:
某些容器格式 (如 raw H.264 bitstream) 确实不存储 DTS
→ 解码器根据帧依赖关系推算
但大多数容器 (MP4/MKV) 仍然会写入 DTS
→ 便于 seek 和流处理
→ 即使 DTS == PTS,也会冗余存储
ZenPlay 的处理:
// 即使没有 B 帧,也优先使用 PTS
if (frame->pts != AV_NOPTS_VALUE) {
use_timestamp = frame->pts;
} else if (frame->pkt_dts != AV_NOPTS_VALUE) {
use_timestamp = frame->pkt_dts; // 回退到 DTS
} else {
use_timestamp = frame->best_effort_timestamp;
}
📚 下一篇预告
下一篇《解封装:打开视频文件的第一步》,我们将深入探讨:
- 容器格式(MP4/MKV/FLV)的结构
- Demuxer 的工作原理(如何分离音视频流)
- Seek 操作的实现(快进/快退的秘密)
- ZenPlay 项目中的
Demuxer类实现
敬请期待!📦
🔗 相关资源
- FFmpeg 时间基文档:https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax
- MPEG 时间戳标准:ISO/IEC 13818-1 (PTS/DTS 定义)
- 推荐阅读:《FFmpeg 从入门到精通》
- ZenPlay 项目地址:https://github.com/Sunshine334419520/zenplay
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)