引言
在前面的Demo中,我们已经分别在独立的线程中实现了对视频的解码渲染和音频的解码播放功能
(oceans.mp4)
不过随着播放的持续进行,可以发现播放的画面和声音会逐渐的对不上,存在严重的音画不同步问题,而精确的音频和视频同步,是媒体播放的关键性能衡量指标之一,所以这篇文章我们就来简单的聊聊音画同步的那些事
Demo中一直使用的oceans.mp4可能不是很容易区分音画不同步问题,除非是真的特别严重的时候,在网上找了一个可以用来测试音画是否同步的视频,也上传到工程中的assets目录中了,感兴趣的小伙伴可以自己在MainActivity中改下播放的file
(av_sync_test.mp4)
音画同步定义
音画同步是指播放器正在渲染的每一帧画面和正在播放的每一段声音都能严格对应起来,不存在视觉和听觉可以分辨出来的差异
视觉和听觉可以分辨的差异标准可以参考ITU-R BT.1359标准
从上图可以看到,我们并不是真的需要音频、视频帧的时间严格匹配,只需要在合理的区间内相互追赶就行,所以说音视频的同步是动态的、是暂时的,不同步则是常态
为什么要做音画同步
音视频文件在解复用阶段后,音频/视频独立解码、独立播放,理论上来说按照视频的帧率、音频采样率进行播放的话音画是同步的
这里以Demo工程中的av_sync_test.mp4为例
一个视频帧的播放时长为1000ms / 25 = 40ms,一个AAC音频帧的播放时长为1024 / 44100 * 1000ms ≈ 23.22ms,理想情况下音视频完全同步,播放过程如下:
不过实际上受限于各种原因,音画总是不同步的,可能的原因如下:
音画同步的三种策略
音视频编码的时候引入了显示时间戳pts的概念:
参考时钟的选择一般来说有三种:
视频同步到音频:以音频的播放速度为基准来同步视频
音频同步到视频:以视频的播放速度为基准来同步音频
音视频同步到外部时钟:以外部时钟为基准,视频和音频的播放速度都以该时钟为标准
这三种是最基本的同步策略,考虑到人对声音的敏感度要强于画面,频繁调节音频会带来较差的感官体验,另一方面是音频数据在确定采样率、采样位数、声道数等参数时播放时间就很容易计算且能准确计算,而视频数据不行,所以一般播放器都会默认以音频时钟为参考时钟,视频同步到音频上。ffplay,exoplayer都是如此
音画同步的关键在于计算视频和音频时间的diff和计算最终的delay,在ffplay.c源码中通过如下函数计算
static double compute_target_delay(double delay, VideoState *is)
{
double sync_threshold, diff = 0;
/* update delay to follow master synchronisation source */
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
/* if video is slave, we try to correct big delays by
duplicating or deleting a frame */
diff = get_clock(&is->vidclk) - get_master_clock(is);
/* skip or repeat frame. We take into account the
delay to compute the threshold. I still don't know
if it is the best guess */
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
if (diff <= -sync_threshold)
delay = FFMAX(0, delay + diff);
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
delay = delay + diff;
else if (diff >= sync_threshold)
delay = 2 * delay;
}
}
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
delay, -diff);
return delay;
}
这里我们先不深入计算细节,只需要把握主体思路即可
diff的计算参考网上总结的一张图:
回到Android端,要实现音画同步一个可参考源码的例子是exoplayer
这里说说AudioTrack来播放音频pcm数据,要计算audio playback position主要有的两种api:
AudioTrack#getTimestamp() (api level 19+)
返回的AudioTimestamp实例中将填入一个以帧为单位,以及呈现该帧的估计时间
该接口的注意事项:
AudioTrack#getPlaybackHeadPosition() (api level 3+)
返回当前播放的头位置(以帧为单位)
计算最新的音频时间戳
/** The number of microseconds in one second. */
public static final long MICROS_PER_SECOND = 1000000L;
private long framesToDurationUs(long frameCount) {
return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
}
long timestamp = framesToDurationUs(audioTrack.getPlaybackHeadPosition());
考虑底层的音频延迟(包括混音器的延迟、音频硬件驱动程序的延迟等)和AudioTrack缓冲区引入的延迟
Method getLatencyMethod;
if (Util.SDK_INT >= 18) {
try {
getLatencyMethod =
android.media.AudioTrack.class.getMethod("getLatency", (Class < ? > []) null);
} catch (NoSuchMethodException e) {
//不能保证此方法存在。不进行任何操作。
}
}
long bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
int audioLatencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - bufferSizeUs;
结合上述两个部分,计算音频管道渲染的上一时间戳的最终值为:
int latestAudioFrameTimestamp = framesToDurationUs(audioTrack.getPlaybackHeadPosition() - audioLatencyUs;
exoplayer中对拿到的playbackHeadPostion还做了平滑处理,实现细节可以查看:
AudioTrackPostionTracker#getCurrentPostionUs
~~END~~