美文网首页
音视频流媒体开发【二十九】ffplay播放器-音视频同步

音视频流媒体开发【二十九】ffplay播放器-音视频同步

作者: AlanGe | 来源:发表于2023-03-19 20:19 被阅读0次

音视频流媒体开发-目录

12 以⾳频为基准

⾳频主流程

ffplay默认也是采⽤的这种同步策略。

此时⾳频的时钟设置在sdl_audio_callback:

audio_callback_time = av_gettime_relative();

..................

/* Let's assume the audio driver that is used by SDL has two periods. */
if (!isnan(is->audio_clock)) {
    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);
}
⾳频时钟的维护

我们先来is->audio_clock是在audio_decode_frame赋值:
is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;

从这⾥可以看出来,这⾥的时间戳是audio_buf结束位置的时间戳,⽽不是audio_buf起始位置的时间戳,所以当audio_buf有剩余时(剩余的⻓度记录在audio_write_buf_size),那实际数据的pts就变成is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec,即是

再考虑到,实质上audio_hw_buf_size*2这些数据实际都没有播放出去,所以就有is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec。

再加上我们在SDL回调进⾏填充



时,实际上



是有开始被播放,所以我们这⾥采⽤的相对时间是,刚回调产⽣的,就是内部

在播放的时候,那相对时间实际也在⾛。


image.png

最终
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);

视频主流程

ffplay中将视频同步到⾳频的主要⽅案是,如果视频播放过快,则重复播放上⼀帧,以等待⾳频;如果视频播放过慢,则丢帧追赶⾳频。

这⼀部分的逻辑实现在视频输出函数 video_refresh 中,分析代码前,我们先来回顾下这个函数的流程图:

在这个流程中,“计算上⼀帧显示时⻓”这⼀步骤⾄关重要。先来看下代码:

static void video_refresh(void *opaque, double *remaining_time)
{
    ...
    /* compute nominal last_duration */
    //lastvp上⼀帧,vp当前帧 ,nextvp下⼀帧

    //last_duration 计算上⼀帧应显示的时⻓
    last_duration = vp_duration(is, lastvp, vp);

    // 经过compute_target_delay⽅法,计算出待显示帧vp需要等待的时间
    // 如果以video同步,则delay直接等于last_duration。
    // 如果以audio或外部时钟同步,则需要⽐对主时钟调整待显示帧vp要等待的时间。
    delay = compute_target_delay(last_duration, is);

    time= av_gettime_relative()/1000000.0;
    // is->frame_timer 实际上就是上⼀帧lastvp的播放时间,
    // is->frame_timer + delay 是待显示帧vp该播放的时间
    if (time < is->frame_timer + delay) { //判断是否继续显示上⼀帧
        // 当前系统时刻还未到达上⼀帧的结束时刻,那么还应该继续显示上⼀帧。
        // 计算出最⼩等待时间
        *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
        goto display;
    }

    // ⾛到这⼀步,说明已经到了或过了该显示的时间,待显示帧vp的状态变更为当前要显示的帧

    is->frame_timer += delay; // 更新当前帧播放的时间
    if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) {
        is->frame_timer = time; //如果和系统时间差距太⼤,就纠正为系统时间
    }
    SDL_LockMutex(is->pictq.mutex);
    if (!isnan(vp->pts))
    update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新video时钟
    SDL_UnlockMutex(is->pictq.mutex);
    //丢帧逻辑
    if (frame_queue_nb_remaining(&is->pictq) > 1) {//有nextvp才会检测是否该丢帧
        Frame *nextvp = frame_queue_peek_next(&is->pictq);
        duration = vp_duration(is, vp, nextvp);
        if(!is->step // ⾮逐帧模式才检测是否需要丢帧 is->step==1 为逐帧播放
           && (framedrop>0 || // cpu解帧过慢
           (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) // ⾮视频同步⽅式
           && time > is->frame_timer + duration // 确实落后了⼀帧数据) {
            printf("%s(%d) dif:%lfs, drop frame\n", __FUNCTION__, __LINE__,(is->frame_timer + duration) - time);
            is->frame_drops_late++; // 统计丢帧情况
            frame_queue_next(&is->pictq); // 这⾥实现真正的丢帧
            //(这⾥不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)
            goto retry; //回到函数开始位置,继续重试
        }
    }
    ...
}

这段代码的逻辑在上述流程图中有包含。主要思路就是⼀开始提到的:

  • 如果视频播放过快,则重复播放上⼀帧,以等待⾳频;
  • 如果视频播放过慢,则丢帧追赶⾳频。实现的⽅式是,参考audio clock,计算上⼀帧(在屏幕上的那个画⾯)还应显示多久(含帧本身时⻓),然后与系统时刻对⽐,是否该显示下⼀帧了。

这⾥与系统时刻的对⽐,引⼊了另⼀个概念——frame_timer。可以理解为帧显示时刻,如更新前,是上⼀帧lastvp的显示时刻;对于更新后( is->frame_timer += delay ),则为当前帧vp显示时刻。

上⼀帧显示时刻加上delay(还应显示多久(含帧本身时⻓))即为上⼀帧应结束显示的时刻。具体原理看如下示意图:

这⾥给出了3种情况的示意图:

  • time1:系统时刻⼩于lastvp结束显示的时刻(frame_timer+dealy),即虚线圆圈位置。此时应该继续显示lastvp
  • time2:系统时刻⼤于lastvp的结束显示时刻,但⼩于vp的结束显示时刻(vp的显示时间开始于虚线圆圈,结束于⿊⾊圆圈)。此时既不重复显示lastvp,也不丢弃vp,即应显示vp
  • time3:系统时刻⼤于vp结束显示时刻(⿊⾊圆圈位置,也是nextvp预计的开始显示时刻)。此时应该丢弃vp。

delay的计算

那么接下来就要看最关键的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 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;
}

compute_target_delay 返回的值越⼤,画⾯越慢(上⼀帧持续的时间就越⻓了)。

这段代码中最难理解的是sync_threshold,sync_threshold值范围:
FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay)),其中delay为传⼊的上⼀帧播放需要持续的时间(本质是帧持续时间 frame duration),即是分以下3种情况:

  1. delay >AV_SYNC_THRESHOLD_MAX=0.1秒,则sync_threshold = 0.1秒
  2. delay <AV_SYNC_THRESHOLD_MIN=0.04秒,则sync_threshold = 0.04秒
  3. AV_SYNC_THRESHOLD_MIN = 0.0.4秒 <= delay <= AV_SYNC_THRESHOLD_MAX=0.1秒,则sync_threshold为delay本身。

从这⾥分析也可以看出来,sync_threshold 最⼤值为0.1秒,最⼩值为0.04秒。这⾥说明⼀个说明问题呢?

  • 同步精度最好的范围是:-0.0.4秒~+0.04秒;
  • 同步精度最差的范围是:-0.1秒~+0.1秒;

和具体视频的帧率有关系,delay帧间隔(frame duration)落在0.04~0.1秒时,则同步精度为正负1帧。

画个图帮助理解:

图中:

  • 坐标轴是diff值⼤⼩,diff为0表示video clock与audio clock完全相同,完美同步。
  • 坐标轴下⽅⾊块,表示要返回的值,⾊块值的delay指传⼊参数,结合上⼀节代码,即lastvp的显示时⻓(frame duration)。

从图上可以看出来sync_threshold是建⽴⼀块区域,在这块区域内⽆需调整lastvp的显示时⻓,直接返回delay即可。也就是在这块区域内认为是准同步的(sync_threshold也是最⼤允许同步误差)。

同步判断结果:
  • diff <= -sync_threshold:如果⼩于-sync_threshold,那就是视频播放较慢,需要适当丢帧。具体是返回⼀个最⼤为0的值。根据前⾯frame_timer的图,⾄少应更新画⾯为vp。
  • diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD:如果不仅⼤于sync_threshold,⽽且超过了AV_SYNC_FRAMEDUP_THRESHOLD,那么返回delay+diff,由具体diff决定还要显示多久(这⾥不是很明⽩代码意图,按我理解,统⼀处理为返回2*delay,或者delay+diff即可,没有区分的必要)
    • 此逻辑帧间隔delay > AV_SYNC_FRAMEDUP_THRESHOLD =0.1秒,此时sync_threshold =0.1秒,那delay + diff > 0.1 + diff >= 0.1 + 0.1 = 0.2秒。
  • diff >= sync_threshold:如果⼤于sync_threshold,那么视频播放太快,需要适当重复显示lastvp。具体是返回2*delay,也就是2倍的lastvp显示时⻓,也就是让lastvp再显示⼀帧。
    • 此逻辑⼀定是 delay <= 0.1时秒,2*delay <= 0.2秒
  • -sync_threshold <diff < +sync_threshold:允许误差内,按frame duration去显示视频,即返回delay。

⽐如写这段代码的作也表示: I still don't know if it is the best guess

⾄此,基本上分析完了视频同步⾳频的过程,简单总结下:

  • 基本策略是:如果视频播放过快,则重复播放上⼀帧,以等待⾳频;如果视频播放过慢,则丢帧追赶⾳频。
  • 这⼀策略的实现⽅式是:引⼊frame_timer概念,标记帧的显示时刻和应结束显示的时刻,再与系统时刻对⽐,决定重复还是丢帧。
  • lastvp的应结束显示的时刻,除了考虑这⼀帧本身的显示时⻓,还应考虑了video clock与audio clock的差值。
  • 并不是每时每刻都在同步,⽽是有⼀个“准同步”的差值区域。

13 以视频为基准

媒体流⾥⾯只有视频成分,这个时候才会⽤以视频为基准。

在“视频同步⾳频”的策略中,我们是通过丢帧或重复显示的⽅法来达到追赶或等待⾳频时钟的⽬的,但在“⾳频同步视频”时,却不能这样简单处理。

在⾳频输出时,最⼩单位是“样本”。⾳频⼀般以数字采样值保存,⼀般常⽤的采样频率有44.1K,48K等,也就是每秒钟有44100或48000个样本。视频输出中与“样本”概念最为接近的画⾯帧,如⼀个24fps(frame per second)的视频,⼀秒钟有24个画⾯输出,这⾥的⼀个画⾯和⾳频中的⼀个样本是等效的。可以想⻅,如果对⾳频使⽤⼀样的丢帧(丢样本)和重复显示⽅案,是不科学的。(⾳频的连续性远⾼于视频,通过重复⼏百个样本或者丢弃⼏百个样本来达到同步,会在听觉有很明显的不连贯)

⾳频本质上来讲:就是做重采样补偿,⾳频慢了,重采样后的样本就⽐正常的减少,以赶紧播放下⼀帧;⾳频快了,重采样后的样本就⽐正常的增加,从⽽播放慢⼀些。

视频主流程

video_refresh()-> update_video_pts() 按照着视频帧间隔去播放,并实时地重新矫正video时钟。

重点主要在audio的播放。

⾳频主流程

在分析具体的补偿⽅法的之前,先回顾下⾳频输出的流程。

在《⾳频输出》章节我们分析过,⾳频输出的主要模型是:

在 audio_buf 缓冲不⾜时, audio_decode_frame 会从FrameQueue中取出数据放⼊ audio_buf .audio_decode_frame 函数有⾳视频同步相关的控制代码:

//为了⽅便阅读,以下代码经过简化,只保留⾳视频同步相关代码
static int audio_decode_frame(VideoState *is)
{
    //1. 根据与vidoe clock的差值,计算应该输出的样本数
    wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
    //2. 判断是否需要重采样:如果要输出的样本数与frame的样本数不相等,也就是需要适当缩减或添加样本了
    if (wanted_nb_samples != af->frame->nb_samples && !is->swr_ctx){
        //创建重采样ctx
        is->swr_ctx = swr_alloc_set_opts(NULL,
                                        is->audio_tgt.channel_layout,
                                        is->audio_tgt.fmt, is->audio_tgt.freq,
                                        dec_channel_layout,
                                        af->frame->format, af->frame->sample_rate,0, NULL);
        if (!is->swr_ctx || swr_init(is->swr_ctx) < 0) {
            return -1;
        }
    }
    //3. 重采样,利⽤重采样库进⾏样本的插⼊或剔除
    if (is->swr_ctx) {
        uint8_t **out = &is->audio_buf1;
        int out_count = (int64_t)wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate + 256;
        if (wanted_nb_samples != af->frame->nb_samples) {
            if (swr_set_compensation(is->swr_ctx,
                                    (wanted_nb_samples - af->frame->nb_samples) * is->audio_tgt.freq / af->frame->sample_rate,
                                    wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate) < 0) {
                return -1;
            }
        }
        len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);
        is->audio_buf = is->audio_buf1;
        resampled_data_size = len2 * is->audio_tgt.channels * av_get_bytes_per_sample(is->audio_tgt.fmt);
    }
    else {//如果重采样ctx没有初始化过,说明⽆需做同步(这⾥不考虑⾳频源格式设备不⽀持的情况)
        is->audio_buf = af->frame->data[0];
        resampled_data_size = data_size;
    }
    return resampled_data_size;
}

主要分3个步骤:

  1. 根据与vidoe clock的差值,计算应该输出的样本数。由函数 synchronize_audio 完成:
    a. ⾳频慢了则样本数减少
    b. ⾳频快了则样本数增加
  2. 判断是否需要重采样:如果要输出的样本数与frame的样本数不相等,也就是需要适当减少或增加样本。
  3. 重采样——利⽤重采样库进⾏样本的插⼊或剔除

可以看到,与视频的处理略有不同,视频的同步控制主要体现在上⼀帧显示时⻓的控制,即对frame_timer的控制;⽽⾳频是直接体现在输出样本上的控制。

前⾯提到如果单纯判断某个时刻应该重复样本或丢弃样本,然后对输出⾳频进⾏修改,⼈⽿会很容易感知到这⼀不连贯,体验不好。

这⾥的处理⽅式是利⽤重采样库进⾏平滑地样本剔除或添加。即在获知要调整的⽬标样本数wanted_nb_samples 后,通过 swr_set_compensation 和 swr_convert 的函数组合完成”重采样“。

需要注意的是,因为增加或删除了样本,样本总数发⽣了变化,⽽采样率不变,那么假设原先1s的声⾳将被以⼤于1s或⼩于1s的时⻓进⾏播放,这会导致声⾳整体频率被拉低或拉⾼。直观感受,就是声⾳变粗或变尖了。ffplay也考虑到了这点影响,其做法是设定⼀个最⼤、最⼩调整范围,避免⼤幅度的⾳调变化。

synchronize_audio

在了解了整体流程后, 就来看下关键函数: synchronize_audiosynchronize_audio 负责根据与video clock的差值计算出合适的⽬标样本数,通过样本数控制⾳频输出速度。

现在让我们看看当 N 组⾳频采样已经不同步的情况。⽽这些⾳频采样不同步的程度也有很⼤的不同,所以我们要取平均值来衡量每个采样的不同步情况。⽐如,第⼀次调⽤时显示我们不同步了 40ms,下⼀次是50ms,等等。但是我们不会采取简单的平均计算,因为最近的值⽐之前的值更重要也更有意义,这时候我们会使⽤⼀个⼩数系数 audio_diff_cum,并对不同步的延时求和:is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;。当我们找到平均差异值时,我们就简单的计算 avg_diff= is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);。我们代码如下:

static int synchronize_audio(VideoState *is, int nb_samples)
{
    int wanted_nb_samples = nb_samples;
    /* if not master, then we try to remove or add samples to correct the clock */
    if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER) {
        double diff, avg_diff;
        int min_nb_samples, max_nb_samples;
        diff = get_clock(&is->audclk) - get_master_clock(is);
        if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
            is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;
            if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
                /* not enough measures to have a correct estimate */
                is->audio_diff_avg_count++;
            } else {
                /* estimate the A-V difference */
                avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
                if (fabs(avg_diff) >= is->audio_diff_threshold) {
                    wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);
                    min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100));
                    max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100));
                    wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);
                }
                av_log(NULL, AV_LOG_TRACE, "diff=%f adiff=%f sample_diff=%d apts=%0.3f %f\n",diff, avg_diff, wanted_nb_samples - nb_samples,is->audio_clock, is->audio_diff_threshold);
            }
        } else {
            /* too big difference : may be initial PTS errors, so
            reset A-V filter */
            is->audio_diff_avg_count = 0;
            is->audio_diff_cum = 0;
        }
    }
    return wanted_nb_samples;
}

和 compute_target_delay ⼀样,这个函数的源码注释也是ffplay⾥算多的。

这⾥⾸先得先理解⼀个”神奇的算法“。这⾥有⼀组变量 audio_diff_avg_coef 、audio_diff_avg_count 、 audio_diff_cum 、 avg_diff .我们会发现在开始播放的AUDIO_DIFF_AVG_NB(20)个帧内,都是在通过公式 is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum; 计算累加值 audio_diff_cum 。按注释的意思是为了得到⼀个准确的估计值。接着在后⾯计算与主时钟的差值时,并不是直接求当前时刻的差值,⽽是根据累加值计算⼀个平均值: avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef); ,然后通过这个均值进⾏校正。

翻阅了⼀些资料,这个公式的⽬的应该是为了让越靠近当前时刻的diff值在平均值中的权重越⼤,不过还没找到对应的数学公式及含义。

继续看在计算得到 avg_diff 后,如何确定要输出的样本数:

1 wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);
2 min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX)/ 100));
3 max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX)/ 100));
4 wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);

时间差值乘以采样率可以得到⽤于补偿的样本数,加之原样本数,即应输出样本数。另外考虑到上⼀节提到的⾳频⾳调变化问题,这⾥限制了调节范围在正负10%以内。

所以如果⾳视频不同步的差值较⼤,并不会⽴即完全同步,最多只调节当前帧样本数的10%,剩余会在下次调节时继续校正。

最后,是与视频同步⾳频时类似地,有⼀个准同步的区间,在这个区间内不去做同步校正,其⼤⼩是audio_diff_threshold:

is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;

即⾳频输出设备内缓冲的⾳频时⻓。

以上,就是⾳频去同步视频时的主要逻辑。简单总结如下:

  1. ⾳频追赶、等待视频采样的⽅法是直接调整输出样本数量
  2. 调整输出样本时为避免听觉上不连贯的体验,使⽤了重采样库进⾏⾳频的剔除和添加
  3. 计算校正后输出的样本数量,使⽤了⼀个”神奇的公式“,其意义和含义还有待进⼀步确认

swr_set_compensation

/**
* Activate resampling compensation ("soft" compensation). This function is
* internally called when needed in swr_next_pts().
*
* @param[in,out] s allocated Swr context. If it is not initialized,
* or SWR_FLAG_RESAMPLE is not set, swr_init() is
* called with the flag set.
* @param[in] sample_delta delta in PTS per sample
* @param[in] compensation_distance number of samples to compensate for
* @return >= 0 on success, AVERROR error codes if:
*     @li @c s is NULL,
*     @li @c compensation_distance is less than 0,
*     @li @c compensation_distance is 0 but sample_delta is not,
*     @li compensation unsupported by resampler, or
*     @li swr_init() fails when called.
*/
int swr_set_compensation(struct SwrContext *s, int sample_delta, int compensation_distance);

激活重采样补偿(“软”补偿)。

在swr_next_pts()中需要时,内部调⽤此函数。

参数:
s:分配Swr上下⽂。 如果未初始化,或未设置SWR_FLAG_RESAMPLE,则会使⽤标志集调⽤swr_init()。
sample_delta:每个样本PTS的delta
compensation_distance:要补偿的样品数量

返回:> = 0成功,AVERROR错误代码如果:
1、s为null
2、compensation_distance⼩于0,
3、compensation_distance是0,但是sample_delta不是,
4、补偿不⽀持重采样器,或
5、调⽤时,swr_init()失败。

14 以外部时钟为基准

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

在seek的时候体验⾮常差,没有必要选择这种同步⽅式。

回顾

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

视频同步到⾳频主要由函数 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库重采样输出。

1 static int synchronize_audio(VideoState *is, int nb_samples)
2 {
3 //C. 只要主时钟不是audio,就需要作同步校正
4 if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER) {
5 diff = get_clock(&is->audclk) - get_master_clock(is);
6 }
7 return wanted_nb_samples;
8 }
9 static int audio_decode_frame(VideoState *is)
10 {
11 wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
12 return resampled_data_size;
13 }
14 static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
15 {
16 audio_size = audio_decode_frame(is);
17
17 //D. 更新audclk,同时更新extclk
18 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);
19 sync_clock_to_slave(&is->extclk, &is->audclk);
20 }

会找到和“视频同步⾳频”类似的的两点:
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

1 static void sync_clock_to_slave(Clock *c, Clock *slave)
2 {
3 double clock = get_clock(c);
4 double slave_clock = get_clock(slave);
5 if (!isnan(slave_clock) && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD))
6 set_clock(c, slave_clock, slave->serial);
7 }

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得到对时,接下来就可以进⼊正常的同步“循环”了。

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

  1. 该策略“复⽤”了前两种策略的代码,代码上⼏乎等效于前两种策略的叠加
  2. extclk的对时依赖于已同步的audio或video的Clock

相关文章

  • FFmpeg命令行工具ffplay

    插播下音画同步的知识: 其中的音频为基准进行音视频同步: ffplay设置音视频同步方式: 播放封装好的音视频: ...

  • 视频推流

    直播技术? 姿势:摄像头采集,音视频编解码,流媒体协议,音视频流推送到流媒体服务器,流媒体网络分发,用户播放器,音...

  • 执行编译FFmpeg库

    FFmpeg工具 FFmpeg FFplay FFprobe FFmpeg开发库 Libavcodec 音视频编解...

  • ffmpeg中的时间戳与时间基

    前言 在开发多媒体播放器或直播系统时,音视频的同步是非常关键且复杂的点。要想把音视频同步搞明白,我们必须要了解一些...

  • FFmpeg中的时间戳与时间基

    简介 在开发多媒体播放器或直播系统时,音视频的同步是非常关键且复杂的点。要想把音视频同步搞明白,我们必须要了解一些...

  • ffmpeg 2.3版本, 关于ffplay音视频同步的分析

    最近学习播放器的一些东西,所以接触了ffmpeg,看源码的过程中,就想了解一下ffplay是怎么处理音视频同步的,...

  • 29_SDL 多线程与锁机制

    一、简介 为什么要用多线程?在音视频领域主要是实现音视频同步。实现了音视频同步,我们的播放器就基本上合格了。多线程...

  • 使用RTMP流媒体服务器

    常用音视频工具: ffmpeg ffplay flashplayer 所谓流媒体,它不是一个固定大小的视频文件,而...

  • Ffmpeg音视频常用命令

    播放器架构 同时还有音视频同步,这个是很重要的。 渲染流程 1、FFmpeg常用命令实战 FFmpeg音视频处理流...

  • 【iOS】AVPlayer 播放音视频

    1、常见的音视频播放器 iOS开发中不可避免地会遇到音视频播放方面的需求。 常用的音频播放器有 AVAudioPl...

网友评论

      本文标题:音视频流媒体开发【二十九】ffplay播放器-音视频同步

      本文链接:https://www.haomeiwen.com/subject/xudrrdtx.html