前面我们分析了音视频同步中的两种策略:视频同步到音频,以及音频同步到视频。接下来要分析的是第三种,音频和视频都同步到外部时钟。
回顾
先回顾下前面两种同步策略。
视频同步到音频主要由函数compute_target_delay
计算出lastvp应显示时长,并通过frame_timer对比系统时间控制输出,最后在video_refresh
中更新了video clock(vidclk)。
static double compute_target_delay(double delay, VideoState *is) { //A. 只要主时钟不是video,就需要作同步校正 if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) { diff = get_clock(&is->vidclk) - get_master_clock(is); } return delay; } static void video_refresh(void *opaque, double *remaining_time) { delay = compute_target_delay(last_duration, is); if (time < is->frame_timer + delay) { goto display; } //B. 更新vidclk,同时更新extclk update_video_pts(is, vp->pts, vp->pos, vp->serial); } static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) { set_clock(&is->vidclk, pts, serial); sync_clock_to_slave(&is->extclk, &is->vidclk); }
注意这里的两点:
A. 只要主时钟不是video,就需要作同步校正
B. 更新vidclk,同时更新extclk
再看音频同步到视频。主要由函数synchronize_audio
计算校正后应输出的样本数,然后通过libswresample库重采样输出。
static int synchronize_audio(VideoState *is, int nb_samples) { //C. 只要主时钟不是audio,就需要作同步校正 if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER) { diff = get_clock(&is->audclk) - get_master_clock(is); } return wanted_nb_samples; } static int audio_decode_frame(VideoState *is) { wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples); return resampled_data_size; } static void sdl_audio_callback(void *opaque, Uint8 *stream, int len) { audio_size = audio_decode_frame(is); //D. 更新audclk,同时更新extclk set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0); sync_clock_to_slave(&is->extclk, &is->audclk); }
会找到和“视频同步音频”类似的的两点:
C. 只要主时钟不是audio,就需要作同步校正
D. 更新audclk,同时更新extclk
分析
我们知道通过sync选项可以选择同步策略,分别可以选择audio/video/ext,选择不同选项的效果是:
audio:视频同步到音频。上一节中的A被触发,video输出需要作同步,同步的参考(get_master_clock)是audclk.
video:音频同步到视频。上一节中的C被触发,audio输出需要作同步,同步的参考是vidclk。
ext:视频和音频都同步到外部时钟,上一节中的A和C都被触发,同步的参考是extclk
不论选择的是哪一个选项,B和D始终都有执行。
所以外部时钟为主的同步策略是这样的:video输出和audio输出时都需要作校正,校正的方法是参考extclk计算diff值。其余部分参考“视频同步到音频”和“音频同步到视频”这两节的分析即可。
另一个问题是外部时钟(extclk)是如何对时的?在音视频同步基础概念中我们分析过Clock是需要一直对时以保持pts_drift估算出来的pts不会偏差太远,并且get_clock
的返回值实际是这一Clock对应的流的pts。这两点对于extclk来说都是问题。
答案就在前面的B和D步骤中。
对于audclk和vidclk,都是每次在“显示”时用显示的那一帧的pts去对时set_clock_at/set_clock
.顺带地,会执行sync_clock_to_slave(&is->extclk, &is->audclk);//&is->vidclk
static void sync_clock_to_slave(Clock *c, Clock *slave) { double clock = get_clock(c); double slave_clock = get_clock(slave); if (!isnan(slave_clock) && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD)) set_clock(c, slave_clock, slave->serial); }
sync_clock_to_slave
的意思是用从时钟的pts和serial对主时钟对时。
而之所以可以这样做的原因是,在更新audclk和vidclk的时候,音频或视频已经同步到了外部时钟,此时取它们的值来反过来对外部时钟对时可以认为是准确的。
也许你会发现,不对,被兜了一圈!这是一个先有鸡还是先有蛋的问题。既然要把video和audio同步到extclk,我们用的extclk校正video和audio,得到更新后的audclk和vidclk,却又反过来用audclk和vidclk去对时extclk。分明就是蛋要鸡来生,鸡要蛋来敷嘛。
幸运的是,这个问题对于开天辟地,扮演上帝角色的代码而言并不难,ffplay说先有蛋。如果有仔细阅读过compute_target_delay
和synchronize_audio
,就会发现进行校正的必要条件之一是!isnan(diff)
,也就是diff值是合法数值,这在第一帧的音频或视频显示前是不成立的,也就无需做同步校正。在第一帧视频或音频显示后,此时extclk得到对时,接下来就可以进入正常的同步“循环”了。
至此,同步到外部时钟的同步策略分析完了,简单总结下:
该策略“复用”了前两种策略的代码,代码上几乎等效于前两种策略的叠加
extclk的对时依赖于已同步的audio或video的Clock
PS:外部时钟同步策略中其实还有一个小分支没分析,即这段代码:
static void video_refresh(void *opaque, double *remaining_time) { if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime) check_external_clock_speed(is); }
有空再来分析。