在第六篇:视频渲染的学习过程中,我了解到video_refresh中所做的事情,并涉及到一些音视频同步的内容,这一篇将深入学习这一部分内容。以下是video_refresh这个函数的流程图:
说到同步,就要有一个参考对象,这里称为参考时钟源,选择参考时钟源有多种方式:(注意时间戳和时间的区分,需要转换)
- 视频时间戳作为参考时钟源
- 音频时间戳作为参考时钟源
- 外部时间作为参考时钟源
通常在存在音频的情况下,优先选择音频作为主时钟源,ijkplayer在默认情况下也是使用音频作为参考时钟源。
video_refresh
// 上一帧
lastvp = frame_queue_peek_last(&is->pictq);
// 当前帧
vp = frame_queue_peek(&is->pictq);
......
/* compute nominal last_duration */
// 根据当前帧和上一帧的pts,计算出来上一帧的显示时长
last_duration = vp_duration(is, lastvp, vp);
// 计算显示当前帧需要等待的时间(即上一帧真正的持续时长)
delay = compute_target_delay(ffp, last_duration, is)
//取系统时刻
time= av_gettime_relative()/1000000.0;
//如果上一帧显示时长未满,重复显示上一帧
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
//frame_timer更新为上一帧结束时刻,也是当前帧开始时刻
is->frame_timer += delay;
//如果与系统时间的偏离太大,则修正为系统时间
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
......
//丢帧逻辑
if (frame_queue_nb_remaining(&is->pictq) > 1) {
Frame *nextvp = frame_queue_peek_next(&is->pictq);
duration = vp_duration(is, vp, nextvp);
if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) {
frame_queue_next(&is->pictq);
//回到函数开始位置,继续重试(这里不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)
goto retry;
}
}
ffplay中将视频同步到音频的主要方案是,如果视频播放过快,则重复播放上一帧,以等待音频;如果视频播放过慢,则丢帧追赶音频。
理解上图之前,我们先清楚几个概念:
- lastvp:上一帧
- vp:当前帧
- nextvp:下一帧
- frame_timer:帧显示的时刻
- delay:帧还应显示多久(包含帧本身显示时长)
- frame_timer += delay:frame_timer更新为上一帧结束时刻,也是当前帧开始显示时刻
示意图中给出了三种情况:
time1:系统时刻小于lastvp结束显示的时刻(frame_timer+dealy),即虚线圆圈位置。此时应该继续显示lastvp
time2:系统时刻大于lastvp的结束显示时刻,但小于vp的结束显示时刻(vp的显示时间开始于虚线圆圈,结束于黑色圆圈)。此时既不重复显示lastvp,也不丢弃vp,即应显示vp
time3:系统时刻大于vp结束显示时刻(黑色圆圈位置,也是nextvp预计的开始显示时刻)。此时应该丢弃vp
compute_target_delay
static double compute_target_delay(FFPlayer *ffp, 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));
/* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
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;
}
}
......
return delay;
}
image.png
从图上可以看出来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决定还要显示多久。
网友评论