ijkplayer 源碼解析5(音視頻同步)

之前的文章,已經(jīng)把播放器的讀線程、音頻解碼線程、視頻解碼線程,視頻渲染線程都講了一遍,現(xiàn)在到了播放器實(shí)現(xiàn)最復(fù)雜的功能之一,就是音視頻同步;

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

  • 1. 以音頻時(shí)鐘為主時(shí)鐘,默認(rèn)方式
  • 1. 以視頻時(shí)鐘為主時(shí)鐘
  • 3. 以外部時(shí)鐘為主時(shí)鐘
    因?yàn)槿说亩鋵?duì)音頻特別敏感,所以以音頻為主時(shí)鐘的最為常用;
    ijkplayer中以視頻時(shí)鐘為主時(shí)鐘的只有在視頻流內(nèi)不存在音頻流的情況下才會(huì)啟用,以外部時(shí)鐘為主時(shí)鐘的情況,目前沒看到該邏輯;

以下文章將會(huì)從2個(gè)部分介紹音視頻同步;
1.音視頻同步基礎(chǔ)
2.音頻為主時(shí)鐘同步邏輯分析

1.音視頻同步基礎(chǔ)

以一個(gè)xxx.mp4文件為例,音頻流的格式是 采樣率48000,采樣深度16bitplaner模式,視頻流的格式是 1920x108025FPS;
音頻舉例以AAC格式來說,AVFrame里面有1024個(gè)音頻樣本,那么一幀的播放時(shí)間是 0.0213s;
視頻幀率是25FPS,播放一幀視頻的時(shí)間是0.04s

音視頻同步流程圖.jpg

以上的流程圖是,當(dāng)12:00:00 時(shí)播放 文件,此時(shí)音頻和視頻都是播放第一幀,按照預(yù)定的時(shí)間,12:00:120的時(shí)刻應(yīng)該播放 視頻第4幀,音頻的第7幀;

1.1 何為音視頻不同步

假如手機(jī)的打開了某個(gè)軟件,導(dǎo)致視頻播放線程被卡住了,導(dǎo)致線程調(diào)度不及時(shí),從而導(dǎo)致視頻第4幀12:00:140才開始播放,音頻播放線程一切正常的話,是不是音視頻就不同步了?;
那如果在上述情況下,音頻播放線程也被卡住了, 導(dǎo)致12:00:140正好播放的是音頻第7幀視頻第4幀,是不是就音視頻能正常同步?;

1.2 視頻落后于音頻

假設(shè)一個(gè)場景,系統(tǒng)卡頓,導(dǎo)致 視頻第4幀12:00:150的時(shí)刻才播放,但是音頻播放卡頓沒那么嚴(yán)重,音頻幀第7幀12:00:153 的時(shí)刻才可以播放,那是不是意味著 視頻比音頻慢了 0.03 s?,這樣的表述是錯(cuò)誤的?;
因?yàn)橐纛l幀 應(yīng)該在14:00:126 的時(shí)刻播放第7幀,但是 實(shí)際上是12:00:153的時(shí)刻才可以播放,音頻幀也慢了0.027s;
預(yù)定的時(shí)間可以消除,所以正確的計(jì)算如下:

視頻pts - 預(yù)定時(shí)間 = 0.03
音頻pts - 預(yù)定時(shí)間 = 0.027
視頻pts - 音頻pts = 0.003

這就是以音頻為主時(shí)鐘的邏輯,拉長或者縮短視頻幀的播放時(shí)長,或者丟棄視頻幀。

以視頻為主時(shí)鐘,就是拉長或者縮短音頻幀的播放時(shí)長,但是不會(huì)丟棄音頻幀。音頻幀連續(xù)性太長,丟幀很容易被耳朵發(fā)現(xiàn)。

以上的舉例,只是為了說明,參考不同的時(shí)鐘,應(yīng)該如何操作音/視頻-幀,從而實(shí)現(xiàn)同步操作;

2.音頻為主時(shí)鐘同步邏輯分析

ijkplayer 默認(rèn)以音頻為主時(shí)鐘,視頻播放過程中,當(dāng)視頻幀晚于預(yù)定播放時(shí)間則丟棄,當(dāng)視頻幀遭遇預(yù)定播放時(shí)間,則重復(fù)播放上一幀 或 增加待顯示幀的播放時(shí)間

默認(rèn)以音頻主時(shí)鐘

 ffp->av_sync_type           = AV_SYNC_AUDIO_MASTER;

當(dāng)視頻流不存在音頻,且存在視頻流的情況下,以視頻為主時(shí)鐘

if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
    stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
} else {
    /// 音頻不存在的情況下,以視頻時(shí)鐘為主時(shí)鐘
    ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
    is->av_sync_type  = ffp->av_sync_type;
}

代碼中,音視頻的同步,主要在video_refresh 函數(shù)中實(shí)現(xiàn);而在上一篇文章ijkplayer 源碼解析4(視頻解碼+渲染) 中,已經(jīng)對(duì)video_refresh()的代碼做了分析,且有源碼注釋,這里還需要再對(duì)代碼調(diào)用到的相關(guān)函數(shù)進(jìn)行更深層的分析;

下面來分析一下 compute_target_delay()函數(shù),如下:

當(dāng)以音頻時(shí)鐘為主時(shí)鐘的情況,就會(huì)進(jìn)入到if 條件內(nèi),變量 diff 代表視頻時(shí)鐘和主時(shí)鐘的時(shí)間差,
當(dāng)diff >0 的情況,代表視頻時(shí)鐘比音頻時(shí)鐘;
當(dāng)diff <0的情況,代表視頻始終比音頻時(shí)鐘;

同步閾值sync_threshold也是一個(gè)很重要的參數(shù), 用來根據(jù)不同的FPS調(diào)整閥值大小

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 之間選出一個(gè)值作為 同步閾值。
對(duì)于1/12幀的視頻,delay是 0.082,所以sync_threshold 等于 0.082,等于一幀的播放時(shí)長。
對(duì)于 1/24幀的視頻,delay是 0.041,所以sync_threshold 等于 0.041,等于一幀的播放時(shí)長。
對(duì)于 1/48幀的視頻,delay是 0.0205,所以sync_threshold 等于 0.04,約等于兩幀的播放時(shí)長。

compute_target_delay()函數(shù)代碼注釋如下:

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

    /// 以音頻時(shí)鐘為主時(shí)鐘的情況
    if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
        
        //獲取當(dāng)前播放的音頻和視頻 進(jìn)度 的差值
        diff = get_clock(&is->vidclk) - get_master_clock(is);

        // 計(jì)算同步閥值 (這個(gè)會(huì)單獨(dú)介紹)
        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 為負(fù)值代表 視頻比音頻慢
                delay = FFMAX(0, delay + diff);
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                /// diff為正數(shù),且延遲超過 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;
}

當(dāng)播放的時(shí)候,通過compute_target_delay()函數(shù),對(duì)比當(dāng)前播放進(jìn)度,決定當(dāng)前幀是立即丟棄還是,延遲播放,視頻刷新代碼代碼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) {
        ///以音頻時(shí)鐘為主時(shí)鐘的情況
        time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + ffp->rdftspeed < time) {
            /// 符合立即刷新的情況,且  is->last_vis_time +0.02 仍然小于當(dāng)前時(shí)間
            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) {
            /// 隊(duì)列中沒有視頻幀 什么也不做
        } else {
            double last_duration, duration, delay;
            Frame *vp, *lastvp;

            /* dequeue the picture */
            /// 獲取上一幀
            lastvp = frame_queue_peek_last(&is->pictq);
            /// 獲取準(zhǔn)備播放的當(dāng)前幀
            vp = frame_queue_peek(&is->pictq);

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

            if (lastvp->serial != vp->serial)
                /// 當(dāng)seek或者快進(jìn)、快退的情況重新賦值 frame_time 時(shí)間
                is->frame_timer = av_gettime_relative() / 1000000.0;

            if (is->paused)
                /// 暫停的情況重復(fù)顯示上一幀
                goto display;

            /* compute nominal last_duration */
            /// last_duration 表示上一幀播放時(shí)間
            last_duration = vp_duration(is, lastvp, vp);
            /// delay 表示當(dāng)前幀需要播放的時(shí)間
            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 不準(zhǔn)的情況下更新
                is->frame_timer = time;
            if (time < is->frame_timer + delay) {
                /// 即將播放的幀+播放時(shí)長 大于 當(dāng)前時(shí)間,則可以播放,跳轉(zhuǎn)到display播放
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }
            /// 更新 is->frame_timer時(shí)間
            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))
                /// 更新視頻時(shí)鐘
                update_video_pts(is, vp->pts, vp->pos, vp->serial);
            SDL_UnlockMutex(is->pictq.mutex);

            if (frame_queue_nb_remaining(&is->pictq) > 1) {
                /// 隊(duì)列視頻幀>1 的情況 當(dāng)前幀展示時(shí)間 大于當(dāng)前時(shí)間,則丟掉該幀
                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);
            /// 設(shè)置立即刷新
            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 默認(rèn)為 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 默認(rèn)為 0,這部分邏輯不分析
        省略部分代碼...
        
    }
}

2.1 解碼視頻幀丟幀的情況

在解碼視頻幀的時(shí)候,如果發(fā)現(xiàn)解碼后的視頻幀已經(jīng)晚于當(dāng)前播放時(shí)間,則丟棄
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)
        /// 獲取當(dāng)前展示幀 的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) {
                /// 獲取當(dāng)前展示幀 于系統(tǒng)時(shí)間的 差值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 &&           /// 序列號(hào)一致(同一個(gè)播放序列)
                    is->videoq.nb_packets) {                                /// 視頻隊(duì)列還有其它視頻幀
                    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;
}

至此,音視頻同步的邏輯已經(jīng)講解完畢了,看官老爺們,參照源碼,一行不落的閱讀,方能修成正果??;

`

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容