Android --- IjkPlayer 的核心:音视频同步原理(十)

一般音视频同步有三种方法:视频同步音频、音频同步视频、外部时钟。IjkPlayer 没有实现外部时钟这种方式。所以只介绍视频同步音频和音频同步视频。

音视频同步的核心就是:比较两个的播放时间,然后判断一方是追赶还是等待另一方。所以我们先分析音频和视频更新播放时间的代码:


视频设置时间轴:

其中的vp->pts 为当前帧的显示时间,其他不重要。

set_clock_at:

重点:

  •   pts:最新视频帧的显示时间;(同步时比较的时间)
  •   last_updated :更新时间
  •   pts_drift: 视频帧的显示时间与更新时间差;

音频设置时间轴:

    if (!isnan(is->audio_clock)) {
        set_clock_at(&is->audclk, is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout), is->audio_clock_serial, ffp->audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }

也是调用set_clock_at:

重点:

  •   pts = is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout) 该帧音频播放完成时间 -  该帧还未播放时长 - 延迟时间(同步时比较的时间)
  •   last_updated = ffp->audio_callback_time / 1000000.0 获取需要播放音频数据的时间
  •   pts_drift = pts - last_updated

解释下pts的式子:

is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout)

  • is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate; 即该帧音频播放完成时间
  • audio_write_buf_size: 表示该帧还未发送给播放器的字节数;
  • bytes_per_sec:每秒钟消耗的音频字节数
  • (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec:该帧还未播放时长
  • SDL_AoutGetLatencySeconds(ffp->aout) :播放延迟时间,作者自己假设的值 =  4 * AudioTrack.getMinBufferSize() /  采样率
  • 播放延迟:https://blog.csdn.net/King1425/article/details/104901314

IjkPlayer中,因为每次发送给播放器的音频数据最多为256byte,如果该帧大于256byte,则会被拆分多次发送。这也是为啥有需要减去该帧还未播放时长的原因

由上面可以知道 pts = 该帧音频播放完成时间 -  该帧还未播放时长 - 延迟时间;


OK,知道了上面的知识后,就可以分析音视频同步了。先分析视频同步音频:

在ff_ffplay.c中的compute_target_delay函数中

static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is)
{
    double sync_threshold, diff = 0;

    // 默认是音频为参考时间轴。进入
    if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
    
        // 计算视频播放时间与参考时间轴(音频)的误差值----需要同步补偿的时间
        diff = get_clock(&is->vidclk) - get_master_clock(is);

        // 用当前帧与上一帧的显示间隔时间来确定一个合理的同步阀值,该值一定大于0。合理----不能太小,也不能太大
        sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));


        // 同步补偿时间还在能处理的范围
        if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
            // 由于sync_threshold一定大于0 =====》-sync_threshold <0
            // -0.12 < -0.1
            // diff <= -sync_threshold =====》diff <0 ,说明视频比音频慢,但是同步阀值不足以弥补该误差,缩短两帧显示间隔时间,即提前播放该帧,追赶音频
            if (diff <= -sync_threshold)
                delay = FFMAX(0, delay + diff);
            // diff >= sync_threshold>0 =====>视频比音频快。
            // delay > AV_SYNC_FRAMEDUP_THRESHOLD======>且两帧间隔时间足够长。
            // =======> 增加两帧显示间隔时间,即延迟播放该帧,等待音频
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                delay = delay + diff;
            // 视频比音频,适量增加两帧显示间隔时间,即延迟播放该帧,等待音频
            else if (diff >= sync_threshold)
                // 注意:乘2,多余的一个delay为适当的等待时间
                delay = 2 * delay;
        }
    }

    if (ffp) {
        // delay>0
        // 记录本次同步补偿的时间
        ffp->stat.avdelay = delay;
        // 记录需要同步补偿的总时间
        ffp->stat.avdiff  = diff;
    }
    // 返回同步补偿时间
    return delay;
}

上面代码就做了4几件事:

  1. 判断音视频的时间差是否在能处理的范围内???
  2. 视频比音频慢sync_threshold (同步阀值)以上,则delay = FFMAX(0, delay + diff);(diff < 0) 表示缩短相邻视频帧间的显示间隔时间,即加快播放当前帧。
  3. 视频比音频快sync_threshold以上,且相邻视频帧间的显示间隔足够长(上一帧会显示很久),则delay = delay + diff; 增加相邻视频帧间的显示间隔时间,即延迟播放当前帧。
  4. 视频比音频快sync_threshold以上,但相邻视频帧间的显示间隔比较短,则delay = 2 * delay; 适当的增加相邻视频帧间的显示间隔时间,即适当的延迟播放当前帧。多次调控。

但是你会发现还有两种情况并没有处理 :

             视频比音频慢,但小于sync_threshold

             视频比音频快,但小于sync_threshold

为啥了这两种情况不同处理了???这里我给出自己的猜测:

   视频时间轴中 pts = 最新视频帧的显示时间;其下一次pts的更新时间是下一帧的显示时间

   音频时间轴中的 pts =  该帧音频播放完成时间 -  该帧还未播放时长 - 延迟时间;每向播放器发送256字节音频数据,更新一次pts 

   所以 两个时间轴中的 pts 相减的极值 =  相邻视频帧的显示时间间隔。

   而 sync_threshold是用相邻帧的显示间隔时间来确定一个合理的同步阀值。

   所以小于sync_threshold的情况,可能是时间轴更新不及时引起的,不需要处理。

上面处理了音视频的时间差,在能处理的范围内,如果不再能处理的范围内,是如何处理的那???丢帧,如下:

// 如果还有两帧以上没展示,进入
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);
          goto retry;
    }
}

OK,视频同步音频就介绍完了,详细的代码请看:Android --- IjkPlayer 阅读native层源码之如何刷新视频的播放界面(七)


音频同步视频:

在ff_ffplay.c中的synchronize_audio函数中:

static int synchronize_audio(VideoState *is, int nb_samples)
{
    int wanted_nb_samples = nb_samples;

    // 如果不是视频同步音频,进入
    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);

        // diff!=NAN 且差值大小在可接受范围
        if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {

            // 将音视频时间差放入权重池
            is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;
            // 如果参与计算的帧数小于AUDIO_DIFF_AVG_NB=20,样本不足,不计算
            if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
                is->audio_diff_avg_count++;
            } else {
                // 计算平均的时间差
                avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
                // 如果平均时间差大于了该帧两倍采样时间(audio_diff_threshold)
                if (fabs(avg_diff) >= is->audio_diff_threshold) {
                    //计算期望的播放samples数量,公式:时间*频率=采集的sample数
                    wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);
                    // 计算最小的音频采集速度。能获得的最少的sample数量
                    min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100));
                    // 计算最大的音频采集速度。能获得的最多的sample数量
                    max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100));
                    // 保证期望输出的sample数量不要变化太大
                    wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);
                }
            }
        } else {
            // 音视频时间差超过了可控范围,清空权重池
            is->audio_diff_avg_count = 0;
            is->audio_diff_cum       = 0;
        }
    }

    return wanted_nb_samples;
}

上面写了详细的注释,但还是多说几句:

  • a. 首先降低原来的所有时间差的权重-------audio_diff_cum*audio_diff_avg_coef;
  • b. 接着将当前帧的时间差加入a中,产生新的audio_diff_cum;
  • c. 再计算出平均的时间差 = audio_diff_cum * (1.0 - audio_diff_avg_coef),
  • d. 然后计算出添加或者减少的sample数量并与当前帧相加得到期望输出的sample数;
  • e. 最后如果平均的时间差大于播放器的延迟时间(上面提到的SDL_AoutGetLatencySeconds),就会改变该的输出sample数,并保证期望输出的sample数量变化在SAMPLE_CORRECTION_PERCENT_MAX范围内。

计算好了该帧需要播放的样本sample数量(wanted_nb_samples),将其设置给重采样上下文,然后通过重采样将该帧播放的样本sample数改变为wanted_nb_samples,由公式:播放的样本sample数 /  采样率 = 该帧的播放时间。 因为采样率没变,所以该帧的播放时间和播放的样本sample数成正相关。从而达到音频去同步视频的目的。如下图,设置重采样上下文:

OK,音频同步视频也将完了,如果你不懂具体音频流程请看:Android --- IjkPlayer 阅读native层源码之解码成功后的音频数据如何发送回Android播放(九)


虽然写完了这篇博客,但是发现里面有很多变量没有解释,要读懂,你可能还需要联系我前几篇博客的内容阅读。

最后说一点:IjkPlayer 默认使用的是视频同步音频,原因在于人对声音的敏感度要强于视频,频繁调节音频会带来较差的观感体验。并且音频的播放时钟为线性增长。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值