12、音视频同步:时间戳管理、同步策略(音频为主/视频为主)、丢帧与插帧
音视频同步,圈里人常说的“A/V sync”,是播放器开发里最磨人的问题之一。我刚开始做播放器时,觉得解码、渲染搞定就完事了。结果第一次联调,画面里人嘴都闭上了,声音还在那“啊——”,那叫一个尴尬。说白了,音视频同步就是让声音和画面在时间轴上对齐,别让观众出戏。
12.1 时间戳:同步的基石
时间戳是同步的命根子。每个音频帧、每个视频帧,都带着一个时间戳,告诉播放器“我该在什么时候被展示”。
常见的两种时间戳:
- PTS(Presentation Time Stamp):显示时间戳,决定帧什么时候展示。
- DTS(Decode Time Stamp):解码时间戳,决定帧什么时候解码。
对于视频来说,因为有B帧的存在,DTS和PTS可能不一样。音频一般没有B帧,所以DTS和PTS通常相同。
核心原则:同步比较的是PTS,不是DTS。解码器只管解码,渲染器才管显示。
时间戳的单位通常是微秒(μs)或者毫秒(ms)。FFmpeg里用AVRational来表示时间基,比如{1, 1000}表示毫秒精度。我个人习惯在内部统一用微秒,避免精度丢失。
12.2 同步策略:音频为主 vs 视频为主
同步策略说白了就是“谁当老大”。主流方案有两种:
12.2.1 音频为主(Audio Master)
这是最常用的方案。音频时钟作为主时钟,视频去对齐音频。为什么?因为人耳对声音的抖动更敏感,画面稍微跳一下还能忍,声音卡顿一下立马出戏。
具体做法:
- 音频按自己的节奏播放,维护一个音频时钟。
- 视频帧渲染前,拿视频帧的PTS跟音频时钟比较。
- 如果视频帧晚了,就丢帧;如果早了,就等待。
我的经验:音频为主策略下,音频播放设备(比如扬声器)的延迟要提前测量。我曾经遇到一个项目,蓝牙耳机的延迟高达200ms,音频时钟直接偏了,视频反而要去追一个“假时钟”。后来我在音频时钟里加了一个设备延迟补偿,才搞定。
12.2.2 视频为主(Video Master)
这种方案用得少,但在某些场景下很实用。比如视频录制、视频编辑预览,或者音频流本身不稳定时。
视频为主时,视频时钟做主时钟,音频去对齐视频。音频可以通过变速(变调不变速)或者丢帧/插帧来追赶或等待。
注意:音频变速处理要小心。直接拉快或拉慢,声音会变调。必须用WSOLA(波形相似叠加算法)或者相位声码器来保持音调不变。我踩过这个坑,直接resample导致声音像“小黄人”,被测试吐槽了好久。
12.3 同步实现:核心逻辑
不管选哪种策略,核心逻辑就三步:
- 计算当前主时钟时间。
- 计算待渲染帧的期望显示时间。
- 比较两者,决定是等待、渲染还是丢帧。
伪代码大概长这样:
// 音频为主策略
double audio_clock = get_audio_clock(); // 当前音频播放位置
double video_pts = frame->pts; // 视频帧的PTS
double diff = video_pts - audio_clock;
if (diff > SYNC_THRESHOLD) {
// 视频快了,等待
usleep((int)(diff * 1000000));
render_frame(frame);
} else if (diff < -SYNC_THRESHOLD) {
// 视频慢了,丢帧
skip_frame(frame);
} else {
// 在阈值内,直接渲染
render_frame(frame);
}
阈值一般设多少?我个人习惯设40ms到100ms之间。太小人眼反而觉得抖,太大能明显感觉到不同步。
12.4 丢帧与插帧
丢帧和插帧是同步的“急救手段”。
12.4.1 丢帧
视频帧积压太多,或者解码速度跟不上,就得丢帧。丢帧不是乱丢,要遵循原则:
- 优先丢B帧,因为B帧依赖前后帧,丢了不影响后续解码。
- P帧尽量不丢,丢了会影响后面的P帧和B帧。
- I帧绝对不能丢,丢了整个GOP都废了。
避坑指南:我曾经在低端机上做直播播放器,解码速度跟不上,直接丢I帧。结果画面卡住不动,用户以为死机了。后来改成丢B帧和部分P帧,I帧保命,流畅度明显提升。
12.4.2 插帧
插帧是丢帧的反向操作。当视频帧不够,或者需要慢动作播放时,就得插帧。插帧有两种方式:
- 简单重复:把上一帧再显示一次。实现简单,但画面会感觉“卡顿”。
- 运动插值:根据前后帧的运动矢量,计算中间帧。效果好,但计算量大。
运动插值在移动端要谨慎。我试过在手机上做实时运动插值,骁龙835勉强能跑,但发热严重。后来只在播放本地高码率视频时才启用。
12.5 知识体系总览
下面这张图把音视频同步的核心逻辑串起来了:
12.6 实战中的坑与对策
| 常见问题 | 原因 | 对策 |
|---|---|---|
| 音画不同步逐渐增大 | 音频时钟和视频时钟的晶振频率有偏差 | 定期校准主时钟,或者用系统单调时钟做参考 |
| 丢帧后画面跳跃 | 连续丢帧导致画面不连续 | 丢帧时尽量均匀分布,不要集中丢 |
| 插帧后画面模糊 | 运动插值算法不够好 | 简单场景用重复帧,复杂场景才启用插值 |
| 蓝牙耳机延迟大 | 蓝牙传输有固定延迟 | 测量设备延迟,在音频时钟中补偿 |
小技巧:调试同步问题时,我习惯在画面角落显示一个“音频时钟 vs 视频PTS”的实时差值。肉眼观察比看日志直观得多。Android上可以用SurfaceView叠加TextView实现。
音视频同步没有银弹。不同场景、不同设备、不同网络条件,都要调整策略。但核心思路不变:选好主时钟,管好时间戳,该丢就丢,该等就等。嗯,做到这几点,你的播放器基本就稳了。