ffplay源碼分析4-音視頻同步

本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-nbmsavzf-ha.htmlhtml

ffplay是FFmpeg工程自帶的簡單播放器,使用FFmpeg提供的解碼器和SDL庫進行視頻播放。本文基於FFmpeg工程4.1版本進行分析,其中ffplay源碼清單以下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.cgit

在嘗試分析源碼前,可先閱讀以下參考文章做爲鋪墊:
[1]. 雷霄驊,視音頻編解碼技術零基礎學習方法
[2]. 視頻編解碼基礎概念
[3]. 色彩空間與像素格式
[4]. 音頻參數解析
[5]. FFmpeg基礎概念github

「ffplay源碼分析」系列文章以下:
[1]. ffplay源碼分析1-概述
[2]. ffplay源碼分析2-數據結構
[3]. ffplay源碼分析3-代碼框架
[4]. ffplay源碼分析4-音視頻同步
[5]. ffplay源碼分析5-圖像格式轉換
[6]. ffplay源碼分析6-音頻重採樣
[7]. ffplay源碼分析7-播放控制緩存

4. 音視頻同步

音視頻同步的目的是爲了使播放的聲音和顯示的畫面保持一致。視頻按幀播放,圖像顯示設備每次顯示一幀畫面,視頻播放速度由幀率肯定,幀率指示每秒顯示多少幀;音頻按採樣點播放,聲音播放設備每次播放一個採樣點,聲音播放速度由採樣率肯定,採樣率指示每秒播放多少個採樣點。若是僅僅是視頻按幀率播放,音頻按採樣率播放,兩者沒有同步機制,即便最初音視頻是基本同步的,隨着時間的流逝,音視頻會逐漸失去同步,而且不一樣步的現象會愈來愈嚴重。這是由於:1、播放時間難以精確控制,2、異常及偏差會隨時間累積。因此,必需要採用必定的同步策略,不斷對音視頻的時間差做校訂,使圖像顯示與聲音播放整體保持一致。網絡

咱們以一個44.1KHz的AAC音頻流和25FPS的H264視頻流爲例,來看一下理想狀況下音視頻的同步過程:
一個AAC音頻frame每一個聲道包含1024個採樣點(也多是2048,參「FFmpeg關於nb_smples,frame_size以及profile的解釋」),則一個frame的播放時長(duration)爲:(1024/44100)×1000ms = 23.22ms;一個H264視頻frame播放時長(duration)爲:1000ms/25 = 40ms。聲卡雖然是以音頻採樣點爲播放單位,但一般咱們每次往聲卡緩衝區送一個音頻frame,每送一個音頻frame更新一下音頻的播放時刻,即每隔一個音頻frame時長更新一下音頻時鐘,實際上ffplay就是這麼作的。咱們暫且把一個音頻時鐘更新點記做其播放點,理想狀況下,音視頻徹底同步,音視頻播放過程以下圖所示:數據結構

音視頻同步理想狀況

音視頻同步的方式基本是肯定一個時鐘(音頻時鐘、視頻時鐘、外部時鐘)做爲主時鐘,非主時鐘的音頻或視頻時鐘爲從時鐘。在播放過程當中,主時鐘做爲同步基準,不斷判斷從時鐘與主時鐘的差別,調節從時鐘,使從時鐘追趕(落後時)或等待(超前時)主時鐘。按照主時鐘的不一樣種類,能夠將音視頻同步模式分爲以下三種:
音頻同步到視頻,視頻時鐘做爲主時鐘。
視頻同步到音頻,音頻時鐘做爲主時鐘。
音視頻同步到外部時鐘,外部時鐘做爲主時鐘。
ffplay中同步模式的定義以下:框架

enum {
    AV_SYNC_AUDIO_MASTER, /* default choice */
    AV_SYNC_VIDEO_MASTER,
    AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */
};

4.1 time_base

time_base是PTS和DTS的時間單位,也稱時間基。不一樣的封裝格式time_base不同,轉碼過程當中的不一樣階段time_base也不同。以mpegts封裝格式爲例,假設視頻幀率爲25FPS。編碼數據包packet(數據結構AVPacket)的time_base爲AVRational{1,90000},這個是容器層的time_base,定義在AVStream結構體中。原始數據幀frame(數據結構AVFrame)的time_base爲AVRational{1,25},這個是視頻層的time_base,是幀率的倒數,定義在AVCodecContext結構體中。time_base的類型是AVRational,表示一個分數,例如AVRational{1,25}表示值爲1/25(單位是秒)。ide

typedef struct AVStream {
    ......
    
    /**
     * This is the fundamental unit of time (in seconds) in terms
     * of which frame timestamps are represented.
     *
     * decoding: set by libavformat
     * encoding: May be set by the caller before avformat_write_header() to
     *           provide a hint to the muxer about the desired timebase. In
     *           avformat_write_header(), the muxer will overwrite this field
     *           with the timebase that will actually be used for the timestamps
     *           written into the file (which may or may not be related to the
     *           user-provided one, depending on the format).
     */
    AVRational time_base;
    
    ......
}

typedef struct AVCodecContext {
    ......
    
    /**
     * This is the fundamental unit of time (in seconds) in terms
     * of which frame timestamps are represented. For fixed-fps content,
     * timebase should be 1/framerate and timestamp increments should be
     * identically 1.
     * This often, but not always is the inverse of the frame rate or field rate
     * for video. 1/time_base is not the average frame rate if the frame rate is not
     * constant.
     *
     * Like containers, elementary streams also can store timestamps, 1/time_base
     * is the unit in which these timestamps are specified.
     * As example of such codec time base see ISO/IEC 14496-2:2001(E)
     * vop_time_increment_resolution and fixed_vop_rate
     * (fixed_vop_rate == 0 implies that it is different from the framerate)
     *
     * - encoding: MUST be set by user.
     * - decoding: the use of this field for decoding is deprecated.
     *             Use framerate instead.
     */
    AVRational time_base;
    
    ......
}

/**
 * Rational number (pair of numerator and denominator).
 */
typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

time_base是一個分數,av_q2d(time_base)則可將分數轉換爲對應的double類型數。所以有以下計算:函數

AVStream *st;
double duration_of_stream = st->duration * av_q2d(st->time_base);   // 視頻流播放時長
double pts_of_frame = frame->pts * av_q2d(st->time_base);           // 視頻幀顯示時間戳

4.2 PTS/DTS/解碼過程

DTS(Decoding Time Stamp, 解碼時間戳),表示壓縮幀的解碼時間。
PTS(Presentation Time Stamp, 顯示時間戳),表示將壓縮幀解碼後獲得的原始幀的顯示時間。
音頻中DTS和PTS是相同的。視頻中因爲B幀須要雙向預測,B幀依賴於其前和其後的幀,所以含B幀的視頻解碼順序與顯示順序不一樣,即DTS與PTS不一樣。固然,不含B幀的視頻,其DTS和PTS是相同的。下圖以一個開放式GOP示意圖爲例,說明視頻流的解碼順序和顯示順序
圖4 解碼和顯示順序
採集順序指圖像傳感器採集原始信號獲得圖像幀的順序。
編碼順序指編碼器編碼後圖像幀的順序。存儲到磁盤的本地視頻文件中圖像幀的順序與編碼順序相同。
傳輸順序指編碼後的流在網絡中傳輸過程當中圖像幀的順序。
解碼順序指解碼器解碼圖像幀的順序。
顯示順序指圖像幀在顯示器上顯示的順序。
採集順序與顯示順序相同。編碼順序、傳輸順序和解碼順序相同。
以圖中「B[1]」幀爲例進行說明,「B[1]」幀解碼時須要參考「I[0]」幀和「P[3]」幀,所以「P[3]」幀必須比「B[1]」幀先解碼。這就致使瞭解碼順序和顯示順序的不一致,後顯示的幀須要先解碼。oop

上述內容可參考「視頻編解碼基礎概念」。

理解了含B幀視頻流解碼順序與顯示順序的不一樣,才容易理解解碼函數decoder_decode_frame()中對視頻解碼的處理:
avcodec_send_packet()按解碼順序發送packet。
avcodec_receive_frame()按顯示順序輸出frame。
這個過程由解碼器處理,不須要用戶程序費心。
decoder_decode_frame()是很是核心的一個函數,代碼自己並不難理解。decoder_decode_frame()是一個通用函數,能夠解碼音頻幀、視頻幀和字幕幀,本節着重關注視頻幀解碼過程。音頻幀解碼過程在註釋中。

// 從packet_queue中取一個packet,解碼生成frame
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);

    for (;;) {
        AVPacket pkt;

        // 本函數被各解碼線程(音頻、視頻、字幕)首次調用時,d->pkt_serial等於-1,d->queue->serial等於1
        if (d->queue->serial == d->pkt_serial) {
            do {
                if (d->queue->abort_request)
                    return -1;

                // 3. 從解碼器接收frame
                switch (d->avctx->codec_type) {
                    case AVMEDIA_TYPE_VIDEO:
                        // 3.1 一個視頻packet含一個視頻frame
                        //     解碼器緩存必定數量的packet後,纔有解碼後的frame輸出
                        //     frame輸出順序是按pts的順序,如IBBPBBP
                        //     frame->pkt_pos變量是此frame對應的packet在視頻文件中的偏移地址,值同pkt.pos
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {
                            if (decoder_reorder_pts == -1) {
                                frame->pts = frame->best_effort_timestamp;
                            } else if (!decoder_reorder_pts) {
                                frame->pts = frame->pkt_dts;
                            }
                        }
                        break;
                    case AVMEDIA_TYPE_AUDIO:
                        // 3.2 一個音頻packet含多個音頻frame,每次avcodec_receive_frame()返回一個frame,此函數返回。
                        // 下次進來此函數,繼續獲取一個frame,直到avcodec_receive_frame()返回AVERROR(EAGAIN),
                        // 表示解碼器須要填入新的音頻packet
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {
                            AVRational tb = (AVRational){1, frame->sample_rate};
                            if (frame->pts != AV_NOPTS_VALUE)
                                frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
                            else if (d->next_pts != AV_NOPTS_VALUE)
                                frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
                            if (frame->pts != AV_NOPTS_VALUE) {
                                d->next_pts = frame->pts + frame->nb_samples;
                                d->next_pts_tb = tb;
                            }
                        }
                        break;
                }
                if (ret == AVERROR_EOF) {
                    d->finished = d->pkt_serial;
                    avcodec_flush_buffers(d->avctx);
                    return 0;
                }
                if (ret >= 0)
                    return 1;   // 成功解碼獲得一個視頻幀或一個音頻幀,則返回
            } while (ret != AVERROR(EAGAIN));
        }

        do {
            if (d->queue->nb_packets == 0)  // packet_queue爲空則等待
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {        // 有未處理的packet則先處理
                av_packet_move_ref(&pkt, &d->pkt);
                d->packet_pending = 0;
            } else {
                // 1. 取出一個packet。使用pkt對應的serial賦值給d->pkt_serial
                if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                    return -1;
            }
        } while (d->queue->serial != d->pkt_serial);

        // packet_queue中第一個老是flush_pkt。每次seek操做會插入flush_pkt,更新serial,開啓新的播放序列
        if (pkt.data == flush_pkt.data) {
            // 復位解碼器內部狀態/刷新內部緩衝區。當seek操做或切換流時應調用此函數。
            avcodec_flush_buffers(d->avctx);
            d->finished = 0;
            d->next_pts = d->start_pts;
            d->next_pts_tb = d->start_pts_tb;
        } else {
            if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
                int got_frame = 0;
                ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);
                if (ret < 0) {
                    ret = AVERROR(EAGAIN);
                } else {
                    if (got_frame && !pkt.data) {
                       d->packet_pending = 1;
                       av_packet_move_ref(&d->pkt, &pkt);
                    }
                    ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);
                }
            } else {
                // 2. 將packet發送給解碼器
                //    發送packet的順序是按dts遞增的順序,如IPBBPBB
                //    pkt.pos變量能夠標識當前packet在視頻文件中的地址偏移
                if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
                    av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
                    d->packet_pending = 1;
                    av_packet_move_ref(&d->pkt, &pkt);
                }
            }
            av_packet_unref(&pkt);
        }
    }
}

本函數實現以下功能:
[1]. 從視頻packet隊列中取一個packet
[2]. 將取得的packet發送給解碼器
[3]. 從解碼器接收解碼後的frame,此frame做爲函數的輸出參數供上級函數處理

注意以下幾點:
[1]. 含B幀的視頻文件,其視頻幀存儲順序與顯示順序不一樣
[2]. 解碼器的輸入是packet隊列,視頻幀解碼順序與存儲順序相同,是按dts遞增的順序。dts是解碼時間戳,所以存儲順序解碼順序都是dts遞增的順序。avcodec_send_packet()就是將視頻文件中的packet序列依次發送給解碼器。發送packet的順序如IPBBPBB。
[3]. 解碼器的輸出是frame隊列,frame輸出順序是按pts遞增的順序。pts是解碼時間戳。pts與dts不一致的問題由解碼器進行了處理,用戶程序沒必要關心。從解碼器接收frame的順序如IBBPBBP。
[4]. 解碼器中會緩存必定數量的幀,一個新的解碼動做啓動後,向解碼器送入好幾個packet解碼器纔會輸出第一個packet,這比較容易理解,由於解碼時幀之間有信賴關係,例如IPB三個幀被送入解碼器後,B幀解碼須要依賴I幀和P幀,所在在B幀輸出前,I幀和P幀必須存在於解碼器中而不能刪除。理解了這一點,後面視頻frame隊列中對視頻幀的顯示和刪除機制才容易理解。
[5]. 解碼器中緩存的幀能夠經過沖洗(flush)解碼器取出。沖洗(flush)解碼器的方法就是調用avcodec_send_packet(..., NULL),而後屢次調用avcodec_receive_frame()將緩存幀取盡。緩存幀取完後,avcodec_receive_frame()返回AVERROR_EOF。ffplay中,是經過向解碼器發送flush_pkt(實際爲NULL),每次seek操做都會向解碼器發送flush_pkt。

如何肯定解碼器的輸出frame與輸入packet的對應關係呢?能夠對比frame->pkt_pos和pkt.pos的值,這兩個值表示packet在視頻文件中的偏移地址,若是這兩個變量值相等,表示此frame來自此packet。調試跟蹤這兩個變量值,即能發現解碼器輸入幀與輸出幀的關係。爲簡便,就不貼圖了。

4.3 視頻同步到音頻

視頻同步到音頻是ffplay的默認同步方式。在視頻播放線程中實現。視頻播放函數video_refresh()實現了視頻顯示(包含同步控制),是很是核心的一個函數,理解起來也有些難度。這個函數的調用過程以下:

main() -->
event_loop() -->
refresh_loop_wait_event() -->
video_refresh()

函數實現以下:

/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
    VideoState *is = opaque;
    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 (!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 + rdftspeed < time) {
            video_display(is);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
    }

    // 視頻播放
    if (is->video_st) {
retry:
        if (frame_queue_nb_remaining(&is->pictq) == 0) {    // 全部幀已顯示
            // nothing to do, no picture to display in the queue
        } 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;
            }

            // lastvp和vp不是同一播放序列(一個seek會開始一個新播放序列),將frame_timer更新爲當前時間
            if (lastvp->serial != vp->serial)
                is->frame_timer = av_gettime_relative() / 1000000.0;

            // 暫停處理:不停播放上一幀圖像
            if (is->paused)
                goto display;

            /* compute nominal last_duration */
            last_duration = vp_duration(is, lastvp, vp);        // 上一幀播放時長:vp->pts - lastvp->pts
            delay = compute_target_delay(last_duration, is);    // 根據視頻時鐘和同步時鐘的差值,計算delay值

            time= av_gettime_relative()/1000000.0;
            // 當前幀播放時刻(is->frame_timer+delay)大於當前時刻(time),表示播放時刻未到
            if (time < is->frame_timer + delay) {
                // 播放時刻未到,則更新刷新時間remaining_time爲當前時刻到下一播放時刻的時間差
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                // 播放時刻未到,則不更新rindex,把上一幀再lastvp再播放一遍
                goto display;
            }

            // 更新frame_timer值
            is->frame_timer += delay;
            // 校訂frame_timer值:若frame_timer落後於當前系統時間過久(超過最大同步域值),則更新爲當前系統時間
            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);             // 當前幀vp播放時長 = nextvp->pts - vp->pts
                // 1. 非步進模式;2. 丟幀策略生效;3. 當前幀vp未能及時播放,即下一幀播放時刻(is->frame_timer+duration)小於當前系統時刻(time)
                if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
                    is->frame_drops_late++;         // framedrop丟幀處理有兩處:1) packet入隊列前,2) frame未及時顯示(此處)
                    frame_queue_next(&is->pictq);   // 刪除上一幀已顯示幀,即刪除lastvp,讀指針加1(從lastvp更新到vp)
                    goto retry;
                }
            }

            // 字幕播放
            ......

            // 刪除當前讀指針元素,讀指針+1。若未丟幀,讀指針從lastvp更新到vp;如有丟幀,讀指針從vp更新到nextvp
            frame_queue_next(&is->pictq);
            is->force_refresh = 1;

            if (is->step && !is->paused)
                stream_toggle_pause(is);
        }
display:
        /* display picture */
        if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display(is);                      // 取出當前幀vp(如有丟幀是nextvp)進行播放
    }
    is->force_refresh = 0;
    if (show_status) {                              // 更新顯示播放狀態
        ......
    }
}

視頻同步到音頻的基本方法是:若是視頻超前音頻,則不進行播放,以等待音頻;若是視頻落後音頻,則丟棄當前幀直接播放下一幀,以追趕音頻。
此函數執行流程參考以下流程圖:
video_refresh()流程圖

步驟以下:
[1] 根據上一幀lastvp的播放時長duration,校訂等到delay值,duration是上一幀理想播放時長,delay是上一幀實際播放時長,根據delay值能夠計算獲得當前幀的播放時刻
[2] 若是當前幀vp播放時刻未到,則繼續顯示上一幀lastvp,並將延時值remaining_time做爲輸出參數供上級調用函數處理
[3] 若是當前幀vp播放時刻已到,則當即顯示當前幀,並更新讀指針

在video_refresh()函數中,調用了compute_target_delay()來根據視頻時鐘與主時鐘的差別來調節delay值,從而調節視頻幀播放的時刻。

// 根據視頻時鐘與同步時鐘(如音頻時鐘)的差值,校訂delay值,使視頻時鐘追趕或等待同步時鐘
// 輸入參數delay是上一幀播放時長,即上一幀播放後應延時多長時間後再播放當前幀,經過調節此值來調節當前幀播放快慢
// 返回值delay是將輸入參數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 */
        // 視頻時鐘與同步時鐘(如音頻時鐘)的差別,時鐘值是上一幀pts值(實爲:上一幀pts + 上一幀至今流逝的時間差)
        diff = get_clock(&is->vidclk) - get_master_clock(is);
        // delay是上一幀播放時長:當前幀(待播放的幀)播放時間與上一幀播放時間差理論值
        // diff是視頻時鐘與同步時鐘的差值

        /* 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 */
        // 若delay < AV_SYNC_THRESHOLD_MIN,則同步域值爲AV_SYNC_THRESHOLD_MIN
        // 若delay > AV_SYNC_THRESHOLD_MAX,則同步域值爲AV_SYNC_THRESHOLD_MAX
        // 若AV_SYNC_THRESHOLD_MIN < delay < AV_SYNC_THRESHOLD_MAX,則同步域值爲delay
        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); // 當前幀播放時刻落後於同步時鐘(delay+diff<0)則delay=0(視頻追趕,當即播放),不然delay=delay+diff
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)  // 視頻時鐘超前於同步時鐘,且超過同步域值,但上一幀播放時長超長
                delay = delay + diff;           // 僅僅校訂爲delay=delay+diff,主要是AV_SYNC_FRAMEDUP_THRESHOLD參數的做用,不做同步補償
            else if (diff >= sync_threshold)    // 視頻時鐘超前於同步時鐘,且超過同步域值
                delay = 2 * delay;              // 視頻播放要放慢腳步,delay擴大至2倍
        }
    }

    av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
            delay, -diff);

    return delay;
}

compute_target_delay()的輸入參數delay是上一幀理想播放時長duration,返回值delay是經校訂後的上一幀實際播放時長。爲方便描述,下面咱們將輸入參數記做duration(對應函數的輸入參數delay),返回值記做delay(對應函數返回值delay)。
本函數實現功能以下:
[1] 計算視頻時鐘與音頻時鐘(主時鐘)的誤差diff,實際就是視頻上一幀pts減去音頻上一幀pts。所謂上一幀,就是已經播放的最後一幀,上一幀的pts能夠標識視頻流/音頻流的播放時刻(進度)。
[2] 計算同步域值sync_threshold,同步域值的做用是:若視頻時鐘與音頻時鐘差別值小於同步域值,則認爲音視頻是同步的,不校訂delay;若差別值大於同步域值,則認爲音視頻不一樣步,須要校訂delay值。
同步域值的計算方法以下:
若duration < AV_SYNC_THRESHOLD_MIN,則同步域值爲AV_SYNC_THRESHOLD_MIN
若duration > AV_SYNC_THRESHOLD_MAX,則同步域值爲AV_SYNC_THRESHOLD_MAX
若AV_SYNC_THRESHOLD_MIN < duration < AV_SYNC_THRESHOLD_MAX,則同步域值爲duration
[3] delay校訂策略以下:

a) 視頻時鐘落後於同步時鐘且落後值超過同步域值:
a1) 若當前幀播放時刻落後於同步時鐘(delay+diff<0)則delay=0(視頻追趕,當即播放);
a2) 不然delay=duration+diff

b) 視頻時鐘超前於同步時鐘且超過同步域值:
b1) 上一幀播放時長過長(超過最大值),僅校訂爲delay=duration+diff;
b2) 不然delay=duration×2,視頻播放放慢腳步,等待音頻

c) 視頻時鐘與音頻時鐘的差別在同步域值內,代表音視頻處於同步狀態,不校訂delay,則delay=duration

對上述視頻同步到音頻的過程做一個總結,參考下圖:

ffplay音視頻同步示意圖

圖中,小黑圓圈是表明幀的實際播放時刻,小紅圓圈表明幀的理論播放時刻,小綠方塊表示當前系統時間(當前時刻),小紅方塊表示位於不一樣區間的時間點,則當前時刻處於不一樣區間時,視頻同步策略爲:
[1] 當前時刻在T0位置,則重複播放上一幀,延時remaining_time後再播放當前幀
[2] 當前時刻在T1位置,則當即播放當前幀
[3] 當前時刻在T2位置,則忽略當前幀,當即顯示下一幀,加速視頻追趕
上述內容是爲了方便理解進行的簡單而形象的描述。實際過程要計算相關值,根據compute_target_delay()和video_refresh()中的策略來控制播放過程。

4.4 音頻同步到視頻

音頻同步到視頻的方式,在音頻播放線程中,實現代碼在audio_decode_frame()及synchronize_audio()中。
函數調用關係以下:

sdl_audio_callback() -->
audio_decode_frame() -->
synchronize_audio()

之後有時間再補充分析過程。

4.5 音視頻同步到外部時鐘

相關文章
相關標籤/搜索