作者:波哥
心理分析:音视频同步本身比较难,一般使用ijkplayer 第三方做音视频同步。不排除有视频直播 视频通话需要用音视频同步,可以从三种 音频为准 视频为准 自定义时钟为准三种方式实现音视频同步
求职者:如果被问到 放正心态,能回答多少是多少。如果你看了这篇文章肯定是可以回答上的
音视频的直播系统是一个复杂的工程系统,要做到非常低延迟的直播,需要复杂的系统工程优化和对各组件非常熟悉的掌握。下面整理几个简单常用的调优技巧:
ffplay中将视频同步到音频的主要方案是,如果视频播放过快,则重复播放上一帧,以等待音频;如果视频播放过慢,则丢帧追赶音频。
这一部分的逻辑实现在视频输出函数video_refresh
中,分析代码前,我们先来回顾下这个函数的流程图:
在这个流程中,“计算上一帧显示时长”这一步骤至关重要。先来看下代码:
static void video_refresh(void *opaque, double *remaining_time)
{//……//lastvp上一帧,vp当前帧 ,nextvp下一帧last_duration = vp_duration(is, lastvp, vp);//计算上一帧的持续时长delay = compute_target_delay(last_duration, is);//参考audio clock计算上一帧真正的持续时长time= av_gettime_relative()/1000000.0;//取系统时刻if (time < is->frame_timer + delay) {//如果上一帧显示时长未满,重复显示上一帧*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);goto display;}is->frame_timer += delay;//frame_timer更新为上一帧结束时刻,也是当前帧开始时刻if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)is->frame_timer = time;//如果与系统时间的偏离太大,则修正为系统时间//更新video clock//视频同步音频时没作用SDL_LockMutex(is->pictq.mutex);if (!isnan(vp->pts))update_video_pts(is, vp->pts, vp->pos, vp->serial);SDL_UnlockMutex(is->pictq.mutex);//……//丢帧逻辑if (frame_queue_nb_remaining(&is->pictq) > 1) {Frame *nextvp = frame_queue_peek_next(&is->pictq);duration = vp_duration(is, vp, nextvp);//当前帧显示时长if(time > is->frame_timer + duration){//如果系统时间已经大于当前帧,则丢弃当前帧is->frame_drops_late++;frame_queue_next(&is->pictq);goto retry;//回到函数开始位置,继续重试(这里不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)}}
}
这段代码的逻辑在上述流程图中有包含。主要思路就是一开始提到的如果视频播放过快,则重复播放上一帧,以等待音频;如果视频播放过慢,则丢帧追赶音频。实现的方式是,参考audio clock,计算上一帧(在屏幕上的那个画面)还应显示多久(含帧本身时长),然后与系统时刻对比,是否该显示下一帧了。
这里与系统时刻的对比,引入了另一个概念——frame_timer。可以理解为帧显示时刻,如更新前,是上一帧的显示时刻;对于更新后(is->frame_timer += delay
),则为当前帧显示时刻。
上一帧显示时刻加上delay(还应显示多久(含帧本身时长))即为上一帧应结束显示的时刻。具体原理看如下示意图:
这里给出了3种情况的示意图:
那么接下来就要看最关键的lastvp的显示时长delay是如何计算的。
这在函数compute_target_delay中实现:
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 byduplicating or deleting a frame */diff = get_clock(&is->vidclk) - get_master_clock(is);/* skip or repeat frame. We take into account thedelay to compute the threshold. I still don't knowif 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;
}
上面代码中的注释全部是源码的注释,代码不长,注释占了快一半,可见这段代码重要性。
这段代码中最难理解的是sync_threshold,画个图帮助理解:
图中坐标轴是diff值大小,diff为0表示video clock与audio clock完全相同,完美同步。图纸下方色块,表示要返回的值,色块值的delay指传入参数,结合上一节代码,即lastvp的显示时长。
从图上可以看出来sync_threshold是建立一块区域,在这块区域内无需调整lastvp的显示时长,直接返回delay即可。也就是在这块区域内认为是准同步的。
如果小于-sync_threshold,那就是视频播放较慢,需要适当丢帧。具体是返回一个最大为0的值。根据前面frame_timer的图,至少应更新画面为vp。
如果大于sync_threshold,那么视频播放太快,需要适当重复显示lastvp。具体是返回2倍的delay,也就是2倍的lastvp显示时长,也就是让lastvp再显示一帧。
如果不仅大于sync_threshold,而且超过了AV_SYNC_FRAMEDUP_THRESHOLD,那么返回delay+diff,由具体diff决定还要显示多久(这里不是很明白代码意图,按我理解,统一处理为返回2*delay,或者delay+diff即可,没有区分的必要)
至此,基本上分析完了视频同步音频的过程,简单总结下:
在面试过程中感觉对音视频NDK底层这一块的知识点掌握好,于是针对这块知识点进行了梳理整理,并将面试中遇到的一些问题,也在其中找到了想要的答案,想了解恶补音视频开发这块知识的小伙伴可以参考这《音视频开发核心笔记》:https://qr18.cn/Ei3VPD