一、为什么用音频做主时钟#
音频时钟:声卡以固定采样率播放,每一帧时长精确可计算
一帧 1024 个样本,48000Hz → 每帧时长 = 1024/48000 = 21.33ms
声卡中断或 DMA 传输时精确触发
视频时钟:显示器刷新率不稳定(VSync/Gsync/丢帧)
一帧 60fps → 16.67ms
实际可能因为解码延迟、渲染延迟而漂移
所以:
播放器初始化时记录音频开始播放的时钟
每一帧音频计算出当前的"期望位置"
视频根据音频位置决定"是丢帧还是重复帧"
二、实现逻辑#
// 最简单的音视频同步逻辑(伪代码)
struct SyncClock {
// 音频时钟(主时钟)
int64_t audio_start_time; // 开始播放时的系统时间(毫秒)
size_t total_samples_played; // 已播放的总采样数
int sample_rate; // 采样率
// 获取当前音频播放位置(毫秒)
double audio_clock() {
double seconds = (double)total_samples_played / sample_rate;
return seconds * 1000.0;
}
// 判断视频帧是否同步
SyncAction check_video_sync(double video_pts_ms) {
double audio_pts_ms = audio_clock();
double diff = video_pts_ms - audio_pts_ms;
// 偏差阈值
if (diff < -100.0) {
// 视频比音频快了 100ms 以上 → 需要等待(重复上一帧)
return SyncAction::DELAY;
} else if (diff > 100.0) {
// 视频比音频慢了 100ms 以上 → 需要丢帧
return SyncAction::DROP;
} else {
// 在同步范围内,正常显示
return SyncAction::SHOW;
}
}
};
enum class SyncAction { SHOW, DELAY, DROP };
三、同步策略对比#
视频快了(视频 PTS < 音频时钟):
策略一:把等待时间计入,推迟显示当前帧
策略二:重复显示上一帧(画面静止,但感觉不到)
后果:画面延迟增大,但不会卡顿
视频慢了(视频 PTS > 音频时钟):
策略一:直接丢弃当前帧(跳帧)
策略二:跳过解码,直接拿下一帧
后果:画面有轻微跳帧,但声音保持连续
经验法则:
快 20ms 以内 → 忽略(人眼感觉不到)
快 20-100ms → 延迟显示
快 100ms+ → 丢帧
播放器选择"丢帧保留音频同步":
声音卡顿是最容易被注意到的,画面稍微跳一下在可接受范围。