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

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

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

  • 1. 以音频时钟为主时钟,默认方式
  • 1. 以视频时钟为主时钟
  • 3. 以外部时钟为主时钟
    因为人的耳朵对音频特别敏感,所以以音频为主时钟的最为常用;
    ijkplayer中以视频时钟为主时钟的只有在视频流内不存在音频流的情况下才会启用,以外部时钟为主时钟的情况,目前没看到该逻辑;

以下文章将会从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;
}

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

`

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容