iOS Developer

ijkplayer 源码解析5(音视频同步)

2023-02-14  本文已影响0人  pengxiaochao

之前的文章,已经把播放器的读线程音频解码线程视频解码线程视频渲染线程都讲了一遍,现在到了播放器实现最复杂的功能之一,就是音视频同步

ijkplayer 支持 3种同步方式,如下:

以下文章将会从2个部分介绍音视频同步;
1.音视频同步基础
2.音频为主时钟同步逻辑分析

1.音视频同步基础

以一个xxx.mp4文件为例,音频流的格式是 采样率48000采样深度16bitplaner模式,视频流的格式是 1920x108025FPS
音频举例以AAC格式来说,AVFrame里面有1024个音频样本,那么一帧的播放时间是 0.0213s;
视频帧率是25FPS,播放一帧视频的时间是0.04s

音视频同步流程图.jpg

以上的流程图是,当12:00:00 时播放 文件,此时音频和视频都是播放第一帧,按照预定的时间,12:00:120的时刻应该播放 视频第4帧音频的第7帧

1.1 何为音视频不同步

假如手机的打开了某个软件,导致视频播放线程被卡住了,导致线程调度不及时,从而导致视频第4帧12:00:140才开始播放,音频播放线程一切正常的话,是不是音视频就不同步了✅;
那如果在上述情况下,音频播放线程也被卡住了, 导致12:00:140正好播放的是音频第7帧视频第4帧,是不是就音视频能正常同步✅;

1.2 视频落后于音频

假设一个场景,系统卡顿,导致 视频第4帧12:00:150的时刻才播放,但是音频播放卡顿没那么严重,音频帧第7帧12:00:153 的时刻才可以播放,那是不是意味着 视频比音频慢了 0.03 s?,这样的表述是错误的❌;
因为音频帧 应该在14:00:126 的时刻播放第7帧,但是 实际上是12:00:153的时刻才可以播放,音频帧也慢了0.027s
预定的时间可以消除,所以正确的计算如下:

视频pts - 预定时间 = 0.03
音频pts - 预定时间 = 0.027
视频pts - 音频pts = 0.003

这就是以音频为主时钟的逻辑,拉长或者缩短视频帧的播放时长,或者丢弃视频帧。

以视频为主时钟,就是拉长或者缩短音频帧的播放时长,但是不会丢弃音频帧。音频帧连续性太长,丢帧很容易被耳朵发现。

以上的举例,只是为了说明,参考不同的时钟,应该如何操作音/视频-帧,从而实现同步操作;

2.音频为主时钟同步逻辑分析

ijkplayer 默认以音频为主时钟,视频播放过程中,当视频帧晚于预定播放时间则丢弃,当视频帧遭遇预定播放时间,则重复播放上一帧 或 增加待显示帧的播放时间

默认以音频主时钟

 ffp->av_sync_type           = AV_SYNC_AUDIO_MASTER;

当视频流不存在音频,且存在视频流的情况下,以视频为主时钟

if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
    stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
} else {
    /// 音频不存在的情况下,以视频时钟为主时钟
    ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
    is->av_sync_type  = ffp->av_sync_type;
}

代码中,音视频的同步,主要在video_refresh 函数中实现;而在上一篇文章ijkplayer 源码解析4(视频解码+渲染) 中,已经对video_refresh()的代码做了分析,且有源码注释,这里还需要再对代码调用到的相关函数进行更深层的分析;

下面来分析一下 compute_target_delay()函数,如下:

当以音频时钟为主时钟的情况,就会进入到if 条件内,变量 diff 代表视频时钟和主时钟的时间差,
diff >0 的情况,代表视频时钟比音频时钟
diff <0的情况,代表视频始终比音频时钟

同步阈值sync_threshold也是一个很重要的参数, 用来根据不同的FPS调整阀值大小

sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));

1,AV_SYNC_THRESHOLD_MIN,最小的同步阈值,值为0.04,单位是
2,AV_SYNC_THRESHOLD_MAX,最大的同步阈值,值为0.1,单位是
上面的代码,就是从0.04 ~ 0.1 之间选出一个值作为 同步阈值。
对于1/12帧的视频,delay是 0.082,所以sync_threshold 等于 0.082,等于一帧的播放时长。
对于 1/24帧的视频,delay是 0.041,所以sync_threshold 等于 0.041,等于一帧的播放时长。
对于 1/48帧的视频,delay是 0.0205,所以sync_threshold 等于 0.04,约等于两帧的播放时长。

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

        // 计算同步阀值 (这个会单独介绍)
        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)
                /// diff 为负值代表 视频比音频慢
                delay = FFMAX(0, delay + diff);
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                /// diff为正数,且延迟超过 AV_SYNC_FRAMEDUP_THRESHOLD阀值 累加
                delay = delay + diff;
            else if (diff >= sync_threshold)
                /// 超过阀值的情况,加倍
                delay = 2 * delay;
        }
    }

    if (ffp) {
        ffp->stat.avdelay = delay;
        ffp->stat.avdiff  = diff;
    }
#ifdef FFP_SHOW_AUDIO_DELAY
    av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
            delay, -diff);
#endif

    return delay;
}

当播放的时候,通过compute_target_delay()函数,对比当前播放进度,决定当前帧是立即丢弃还是,延迟播放,视频刷新代码代码video_refresh()逻辑和注释如下⬇️;

static void video_refresh(FFPlayer *opaque, double *remaining_time)
{
    FFPlayer *ffp = opaque;
    VideoState *is = ffp->is;
    double time;

    Frame *sp, *sp2;

    if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
        check_external_clock_speed(is);

    if (!ffp->display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
        ///以音频时钟为主时钟的情况
        time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + ffp->rdftspeed < time) {
            /// 符合立即刷新的情况,且  is->last_vis_time +0.02 仍然小于当前时间
            video_display2(ffp);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + ffp->rdftspeed - time);
    }
    /// 判断视频流存在
    if (is->video_st) {
retry:
        if (frame_queue_nb_remaining(&is->pictq) == 0) {
            /// 队列中没有视频帧 什么也不做
        } else {
            double last_duration, duration, delay;
            Frame *vp, *lastvp;

            /* dequeue the picture */
            /// 获取上一帧
            lastvp = frame_queue_peek_last(&is->pictq);
            /// 获取准备播放的当前帧
            vp = frame_queue_peek(&is->pictq);

            if (vp->serial != is->videoq.serial) {
                frame_queue_next(&is->pictq);
                goto retry;
            }

            if (lastvp->serial != vp->serial)
                /// 当seek或者快进、快退的情况重新赋值 frame_time 时间
                is->frame_timer = av_gettime_relative() / 1000000.0;

            if (is->paused)
                /// 暂停的情况重复显示上一帧
                goto display;

            /* compute nominal last_duration */
            /// last_duration 表示上一帧播放时间
            last_duration = vp_duration(is, lastvp, vp);
            /// delay 表示当前帧需要播放的时间
            delay = compute_target_delay(ffp, last_duration, is);

            time= av_gettime_relative()/1000000.0;
            if (isnan(is->frame_timer) || time < is->frame_timer)
                /// is->frame_timer 不准的情况下更新
                is->frame_timer = time;
            if (time < is->frame_timer + delay) {
                /// 即将播放的帧+播放时长 大于 当前时间,则可以播放,跳转到display播放
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }
            /// 更新 is->frame_timer时间
            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);
            SDL_UnlockMutex(is->pictq.mutex);

            if (frame_queue_nb_remaining(&is->pictq) > 1) {
                /// 队列视频帧>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;
                }
            }

            if (is->subtitle_st) {
                /// 视频字幕流的情况 暂不分析
                while (frame_queue_nb_remaining(&is->subpq) > 0) {
                    sp = frame_queue_peek(&is->subpq);

                    if (frame_queue_nb_remaining(&is->subpq) > 1)
                        sp2 = frame_queue_peek_next(&is->subpq);
                    else
                        sp2 = NULL;

                    if (sp->serial != is->subtitleq.serial
                            || (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
                            || (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
                    {
                        if (sp->uploaded) {
                            ffp_notify_msg4(ffp, FFP_MSG_TIMED_TEXT, 0, 0, "", 1);
                        }
                        frame_queue_next(&is->subpq);
                    } else {
                        break;
                    }
                }
            }
            /// Video Frame queue 索引+1
            frame_queue_next(&is->pictq);
            /// 设置立即刷新
            is->force_refresh = 1;

            SDL_LockMutex(ffp->is->play_mutex);
            if (is->step) {
                is->step = 0;
                if (!is->paused)
                    stream_update_pause_l(ffp);
            }
            SDL_UnlockMutex(ffp->is->play_mutex);
        }
display:
        /* display picture */
        /// 渲染开启、force_refresh ==1 、show_mode 默认为 SHOW_MODE_VIDEO 的情况渲染
        if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display2(ffp);
    }
    is->force_refresh = 0;
    if (ffp->show_status) {
        /// show_status 默认为 0,这部分逻辑不分析
        省略部分代码...
        
    }
}

2.1 解码视频帧丢帧的情况

在解码视频帧的时候,如果发现解码后的视频帧已经晚于当前播放时间,则丢弃
get_video_frame()逻辑如下,代码注释;

static int get_video_frame(FFPlayer *ffp, AVFrame *frame)
{
    VideoState *is = ffp->is;
    int got_picture;

    ffp_video_statistic_l(ffp);
    /// 解码
    if ((got_picture = decoder_decode_frame(ffp, &is->viddec, frame, NULL)) < 0)
        return -1;

    if (got_picture) {
        double dpts = NAN;

        if (frame->pts != AV_NOPTS_VALUE)
        /// 获取当前展示帧 的PTS
            dpts = av_q2d(is->video_st->time_base) * frame->pts;

        frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);

        if (ffp->framedrop>0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
            ffp->stat.decode_frame_count++;
            if (frame->pts != AV_NOPTS_VALUE) {
                /// 获取当前展示帧 于系统时间的 差值diff
                double diff = dpts - get_master_clock(is);
                if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&     /// 差值小于100
                    diff - is->frame_last_filter_delay < 0 &&               /// 视频比音频慢的条件
                    is->viddec.pkt_serial == is->vidclk.serial &&           /// 序列号一致(同一个播放序列)
                    is->videoq.nb_packets) {                                /// 视频队列还有其它视频帧
                    is->frame_drops_early++;                                
                    is->continuous_frame_drops_early++;
                    if (is->continuous_frame_drops_early > ffp->framedrop) {
                        is->continuous_frame_drops_early = 0;
                    } else {
                        ffp->stat.drop_frame_count++;
                        ffp->stat.drop_frame_rate = (float)(ffp->stat.drop_frame_count) / (float)(ffp->stat.decode_frame_count);
                        /// 丢弃该帧
                        av_frame_unref(frame);  
                        got_picture = 0;
                    }
                }
            }
        }
    }

    return got_picture;
}

至此,音视频同步的逻辑已经讲解完毕了,看官老爷们,参照源码,一行不落的阅读,方能修成正果🙏;

`

上一篇下一篇

猜你喜欢

热点阅读