25、MediaCodec与音频同步:音频时钟基准、音视频时间戳对齐、A/V同步算法、唇音同步
做视频播放,最怕什么?画面和声音对不上。
我见过太多项目,解码跑得飞快,渲染也流畅,但用户一看就说「嘴型对不上」。说白了,音视频同步才是播放器体验的生死线。今天我们就来聊聊,怎么用MediaCodec把音频和视频「绑」在一起。
音频时钟基准:谁才是时间的主人?
同步的核心,是选一个「主时钟」。我个人习惯用音频时钟作为基准。为什么?因为人耳对声音的抖动比眼睛对画面的抖动敏感得多。你想想看,画面卡一下还能忍,声音一卡一卡的,那体验直接崩了。
音频时钟基准,说白了就是音频渲染的进度。在Android上,AudioTrack会告诉我们当前写入了多少数据,播放了多久。这个时间就是我们的「锚点」。
核心公式:
音频当前时间 = 已写入的采样帧数 / 采样率
或者更精确一点:
音频当前时间 = AudioTrack.getPlaybackHeadPosition() / 采样率
嗯,这里要注意:getPlaybackHeadPosition()返回的是帧数,不是字节数。如果你用的是16位立体声,一帧就是4个字节。我刚开始做的时候就在这里栽过跟头,算出来的时间总是翻倍,画面和声音差了十万八千里。
音视频时间戳对齐:MediaCodec的「时间护照」
MediaCodec在解码每一帧时,都会带上一个时间戳——presentationTimeUs。这个时间戳就是帧的「护照」,告诉渲染器:我应该在什么时候显示。
但问题来了:视频的时间戳和音频的时间戳,是各自独立的。视频帧的时间戳来自视频文件的PTS(显示时间戳),音频帧的时间戳来自音频文件的PTS。它们不在同一个坐标系里。
怎么办?对齐。
我建议的做法是:
- 找到第一个视频帧和第一个音频帧的时间戳
- 计算它们的差值,作为初始偏移
- 后续所有帧都基于这个偏移进行校准
避坑指南:
我曾经遇到过一个MP4文件,视频第一帧的时间戳是0,音频第一帧的时间戳是1024。如果不做对齐,音频会一直比视频快1024个采样点(约23ms)。虽然人眼可能看不出来,但专业测试设备一测就露馅了。
A/V同步算法:三种策略,一个目标
对齐之后,就是实时的同步控制。常用的策略有三种:
| 策略 | 原理 | 适用场景 |
|---|---|---|
| 音频为主 | 视频追赶音频时钟 | 大多数播放器 |
| 视频为主 | 音频追赶视频时钟 | 直播、低延迟场景 |
| 外部时钟 | 两者都追赶系统时钟 | 多路同步播放 |
我个人最常用的是「音频为主」策略。具体做法是:
- 每次视频帧解码完成后,计算它的显示时间 vs 音频当前时间
- 如果视频快了,就延迟渲染(sleep一下)
- 如果视频慢了,就丢帧(skip掉一些帧)
丢帧的阈值怎么定?我一般设两个值:
- 如果视频落后超过40ms,丢一帧
- 如果视频落后超过100ms,丢两帧
- 如果视频落后超过200ms,直接跳到音频当前时间对应的关键帧
注意:丢帧不能乱丢。只能丢非关键帧(P帧/B帧),关键帧(I帧)绝对不能丢,否则画面会花掉。我曾经在测试时忘了这个规则,结果画面全是马赛克,被测试同学追着骂了一周。
唇音同步:从「差不多」到「精准」
唇音同步,说白了就是让说话的口型和声音完全对上。这个要求比普通音视频同步高得多。普通同步允许±40ms的误差,但唇音同步要求±10ms以内。
怎么做到?
第一,时间戳精度要够。MediaCodec的时间戳单位是微秒(µs),理论上够用了。但实际中,很多编码器的时间戳精度只有毫秒级。我建议在解码后,用音频时钟重新校准一下视频帧的时间戳。
第二,渲染延迟要可控。视频渲染有延迟,音频输出也有延迟。这两个延迟必须测量出来,并在同步计算中补偿。我习惯在初始化时做一次「握手」测试:
// 伪代码:测量渲染延迟
long startTime = System.nanoTime();
videoSurface.renderFrame(frame);
long endTime = System.nanoTime();
long renderLatency = (endTime - startTime) / 1000; // 转微秒
第三,缓冲区要足够小。缓冲区越大,延迟越高。但缓冲区太小,又容易卡顿。这是个trade-off。我一般把音频缓冲区设为512帧(约12ms@44.1kHz),视频缓冲区设为2帧。
我的经验值:
音频缓冲区:512帧(12ms)
视频缓冲区:2帧(33ms@30fps)
同步误差:±5ms以内
SVG:音视频同步核心流程
这张图展示了整个同步流程。音频路径负责提供时钟基准,视频路径负责提供帧时间,两者在同步算法模块中交汇,最终输出到渲染器。
嗯,最后说一句:音视频同步没有银弹。不同的场景、不同的设备、不同的码流,都需要微调参数。我建议你在开发时,把同步参数做成可配置的,方便线上调试。
调试小技巧:
在屏幕上叠加显示音频时钟和视频时钟的差值,用颜色表示同步状态:绿色(正常)、黄色(轻微偏差)、红色(严重不同步)。这样一眼就能看出问题。