17、音视频同步:同步策略、时间戳管理、A/V同步算法、音画同步实战

音视频同步,圈里人常说的“A/V Sync”,是播放器开发里最磨人的问题之一。你想想看,画面和声音对不上,哪怕只差200毫秒,观众都会觉得“怪怪的”。我早年做直播SDK时,就因为这个被用户骂过“你们这播放器是不是坏了”。嗯,今天我们就把它彻底讲透。

17.1 为什么音视频会不同步?

说白了,音频和视频是两个独立的“生产线”。音频采集、编码、传输、解码、渲染是一条线;视频是另一条线。两条线各自有各自的时钟,稍微一错位,就不同步了。

常见的不同步原因有:

  • 采集时钟不同:摄像头和麦克风可能用不同的晶振,时间基准不一样。
  • 编码/解码延迟差异:视频编码通常比音频慢,解码时也可能有差异。
  • 网络抖动:直播场景下,音视频包可能走不同的网络路径,到达时间不一致。
  • 渲染延迟:音频输出设备和视频显示设备的缓冲机制不同。

核心认知:音视频同步不是让音频和视频“同时”到达,而是让它们按照同一个时间轴对齐播放。这个时间轴,就是PTS(Presentation Time Stamp)。

17.2 同步策略:三种主流方案

业界常见的同步策略有三种。我个人习惯把第一种作为默认方案,但具体选哪个,得看场景。

策略 原理 适用场景 缺点
音频为主 以音频时钟为基准,视频向音频对齐 直播、通话(人耳对音频抖动更敏感) 视频帧率可能不稳定
视频为主 以视频时钟为基准,音频向视频对齐 视频编辑、录播(画面流畅优先) 音频可能出现卡顿或拉伸
外部时钟 使用系统时钟或NTP作为统一基准 多设备同步、专业广播 实现复杂,需要高精度时钟

我在项目中遇到过最典型的场景是直播。直播里用户对声音的敏感度远高于画面,所以我通常选音频为主策略。音频播放的时钟作为主时钟,视频帧根据音频的PTS来调整显示时机。

17.3 时间戳管理:PTS与DTS

时间戳是同步的命根子。两个最重要的概念:

  • PTS(Presentation Time Stamp):显示时间戳,告诉播放器这一帧“什么时候该显示”。
  • DTS(Decoding Time Stamp):解码时间戳,告诉播放器这一帧“什么时候该解码”。

为什么要有两个?因为视频编码里有B帧(双向预测帧)。B帧的显示顺序和解码顺序不一样。举个例子:

// 解码顺序(DTS顺序): I(0)  P(3)  B(1)  B(2)  P(6)  B(4)  B(5)
// 显示顺序(PTS顺序): I(0)  B(1)  B(2)  P(3)  B(4)  B(5)  P(6)

你看,解码器必须先解出P帧,才能解出它后面的B帧。但显示时,B帧要排在P帧前面。所以DTS和PTS必须分开管理。

我的经验:在Android MediaCodec里,你只需要给每个输入buffer设置正确的PTS。解码器内部会自动处理DTS。但如果你用FFmpeg做软解,就得自己维护DTS和PTS的映射关系。我曾经在这上面栽过跟头——忘了给B帧设置正确的PTS,结果画面乱跳。

17.4 A/V同步算法:核心逻辑

同步算法的核心就一句话:比较当前音频和视频的PTS差值,决定视频是快进还是等待

具体实现分三步:

  1. 获取主时钟:如果音频为主,就用音频设备当前播放的位置作为主时钟。
  2. 计算差值:视频帧的PTS减去主时钟,得到delta。
  3. 决策
    • 如果delta > 阈值(比如40ms),说明视频快了,让视频等一等。
    • 如果delta < -阈值,说明视频慢了,直接丢帧或加速渲染。
    • 如果在阈值范围内,正常显示。

下面是一个简化版的同步逻辑伪代码:

// 音频为主时钟的同步逻辑
double get_audio_clock() {
    // 返回当前音频播放的位置(单位:秒)
    return audio_device.getPlaybackPosition();
}

void sync_video_to_audio(VideoFrame frame) {
    double audio_clock = get_audio_clock();
    double video_pts = frame.pts;  // 视频帧的显示时间戳

    double delta = video_pts - audio_clock;

    if (delta > 0.04) {
        // 视频快了,等待
        usleep((int)(delta * 1000000));
    } else if (delta < -0.04) {
        // 视频慢了,丢帧
        skip_frame();
    } else {
        // 正常显示
        render_frame(frame);
    }
}

注意:阈值不能设得太小。人耳对音频抖动的容忍度很低,但人眼对视频轻微的快慢其实不太敏感。我一般设40-60ms。设成10ms的话,系统会频繁丢帧或等待,反而导致画面卡顿。

17.5 音画同步实战:Android MediaCodec + AudioTrack

在Android上做同步,核心是AudioTrack的时钟。AudioTrack可以告诉我们当前播放到了哪个位置,这个位置就是主时钟。

实战步骤:

  1. 初始化AudioTrack:设置音频参数,开始播放。
  2. 获取AudioTrack的播放位置:通过getPlaybackHeadPosition()获取已播放的采样数,换算成时间。
  3. 解码视频帧:从MediaCodec输出buffer拿到视频帧和它的PTS。
  4. 同步渲染:比较视频PTS和音频时钟,决定渲染时机。

关键代码片段:

// 获取音频时钟(单位:微秒)
long getAudioClockUs() {
    if (audioTrack == null || audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
        return 0;
    }
    long playedFrames = audioTrack.getPlaybackHeadPosition();
    long playedUs = (playedFrames * 1000000L) / sampleRate;
    return playedUs;
}

// 视频渲染线程
void renderLoop() {
    while (isPlaying) {
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        int outputIndex = videoDecoder.dequeueOutputBuffer(info, 10000);
        if (outputIndex >= 0) {
            ByteBuffer buffer = videoDecoder.getOutputBuffer(outputIndex);
            long videoPtsUs = info.presentationTimeUs;  // 视频PTS,单位微秒

            long audioClockUs = getAudioClockUs();
            long deltaUs = videoPtsUs - audioClockUs;

            if (deltaUs > 40000) {  // 视频快了40ms以上
                // 等待
                Thread.sleep((deltaUs - 20000) / 1000);  // 留20ms余量
            } else if (deltaUs < -40000) {  // 视频慢了40ms以上
                // 丢帧,不渲染
                videoDecoder.releaseOutputBuffer(outputIndex, false);
                continue;
            }

            // 渲染
            videoDecoder.releaseOutputBuffer(outputIndex, true);
            // 这里会触发SurfaceView或TextureView显示
        }
    }
}

避坑指南:我曾经遇到过一个问题——AudioTrack的getPlaybackHeadPosition()在刚开始播放时返回0,导致视频疯狂丢帧。后来发现,AudioTrack内部有缓冲区,播放头位置是滞后的。我的解决办法是:在音频开始播放后,先等缓冲区填满(大约100ms),再开始同步逻辑。

17.6 知识体系总览

下面这张图总结了音视频同步的核心脉络。你可以把它当作一个检查清单,做同步时对照着看。

音视频同步知识体系 音视频同步 同步策略 音频为主 视频为主 外部时钟 时间戳管理 PTS(显示时间) DTS(解码时间) 时间基转换 A/V同步算法 获取主时钟 计算PTS差值 丢帧/等待决策 实战:MediaCodec + AudioTrack ↓ 落地实现

17.7 总结与避坑

音视频同步,说白了就是“对齐时间轴”。你只要记住三点:

  • 选对主时钟:直播用音频,录播用视频,专业场景用外部时钟。
  • 管好时间戳:PTS是王道,别忘了B帧的DTS和PTS不一样。
  • 设好阈值:40-60ms是黄金区间,别太激进。

我曾经踩过的坑:有一次做多路直播合流,各路视频的PTS基准不一样。一路用毫秒,一路用微秒,还有一路直接用帧号当时间戳。结果合流后画面完全乱套。后来我强制所有输入流统一转成微秒时间基,才解决问题。所以,时间基统一是前提,别偷懒。

嗯,同步这块就讲到这里。你动手写代码时,建议先从音频为主策略开始,用AudioTrack的播放头位置做时钟。跑通了再考虑其他策略。毕竟,能跑起来的代码,比完美的理论有用得多。


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