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)

这是最常用的方案。音频时钟作为主时钟,视频去对齐音频。为什么?因为人耳对声音的抖动更敏感,画面稍微跳一下还能忍,声音卡顿一下立马出戏。

具体做法:

  1. 音频按自己的节奏播放,维护一个音频时钟。
  2. 视频帧渲染前,拿视频帧的PTS跟音频时钟比较。
  3. 如果视频帧晚了,就丢帧;如果早了,就等待。

我的经验:音频为主策略下,音频播放设备(比如扬声器)的延迟要提前测量。我曾经遇到一个项目,蓝牙耳机的延迟高达200ms,音频时钟直接偏了,视频反而要去追一个“假时钟”。后来我在音频时钟里加了一个设备延迟补偿,才搞定。

12.2.2 视频为主(Video Master)

这种方案用得少,但在某些场景下很实用。比如视频录制、视频编辑预览,或者音频流本身不稳定时。

视频为主时,视频时钟做主时钟,音频去对齐视频。音频可以通过变速(变调不变速)或者丢帧/插帧来追赶或等待。

注意:音频变速处理要小心。直接拉快或拉慢,声音会变调。必须用WSOLA(波形相似叠加算法)或者相位声码器来保持音调不变。我踩过这个坑,直接resample导致声音像“小黄人”,被测试吐槽了好久。

12.3 同步实现:核心逻辑

不管选哪种策略,核心逻辑就三步:

  1. 计算当前主时钟时间。
  2. 计算待渲染帧的期望显示时间。
  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 知识体系总览

下面这张图把音视频同步的核心逻辑串起来了:

音视频同步核心逻辑 音视频数据流 提取 PTS / DTS 同步策略选择 音频为主 / 视频为主 / 外部时钟 比较 PTS 与主时钟 视频快了 → 等待 同步 → 直接渲染 视频慢了 → 丢帧

12.6 实战中的坑与对策

常见问题 原因 对策
音画不同步逐渐增大 音频时钟和视频时钟的晶振频率有偏差 定期校准主时钟,或者用系统单调时钟做参考
丢帧后画面跳跃 连续丢帧导致画面不连续 丢帧时尽量均匀分布,不要集中丢
插帧后画面模糊 运动插值算法不够好 简单场景用重复帧,复杂场景才启用插值
蓝牙耳机延迟大 蓝牙传输有固定延迟 测量设备延迟,在音频时钟中补偿

小技巧:调试同步问题时,我习惯在画面角落显示一个“音频时钟 vs 视频PTS”的实时差值。肉眼观察比看日志直观得多。Android上可以用SurfaceView叠加TextView实现。

音视频同步没有银弹。不同场景、不同设备、不同网络条件,都要调整策略。但核心思路不变:选好主时钟,管好时间戳,该丢就丢,该等就等。嗯,做到这几点,你的播放器基本就稳了。

公众号:蓝海资料掘金营,微信deep3321