ffplay---源码分析(十一):同步到外部时钟

        前面我们分析了音视频同步中的两种策略:视频同步到音频,以及音频同步到视频。接下来要分析的是第三种,音频和视频都同步到外部时钟。

 

回顾

先回顾下前面两种同步策略。

视频同步到音频主要由函数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_delaysynchronize_audio,就会发现进行校正的必要条件之一是!isnan(diff),也就是diff值是合法数值,这在第一帧的音频或视频显示前是不成立的,也就无需做同步校正。在第一帧视频或音频显示后,此时extclk得到对时,接下来就可以进入正常的同步“循环”了。

 

至此,同步到外部时钟的同步策略分析完了,简单总结下:

  1. 该策略“复用”了前两种策略的代码,代码上几乎等效于前两种策略的叠加

  2. 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);
}

有空再来分析。

发布了178 篇原创文章 · 获赞 89 · 访问量 18万+

你可能感兴趣的:(ffplay---源码分析(十一):同步到外部时钟)