ffplay源碼分析2-數據結構


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

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

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

2. 數據結構

幾個關鍵的數據結構如下:

2.1 struct VideoState

typedef struct VideoState {
    SDL_Thread *read_tid;           // demux解復用線程
    AVInputFormat *iformat;
    int abort_request;
    int force_refresh;
    int paused;
    int last_paused;
    int queue_attachments_req;
    int seek_req;                   // 標識一次SEEK請求
    int seek_flags;                 // SEEK標志,諸如AVSEEK_FLAG_BYTE等
    int64_t seek_pos;               // SEEK的目標位置(當前位置+增量)
    int64_t seek_rel;               // 本次SEEK的位置增量
    int read_pause_return;
    AVFormatContext *ic;
    int realtime;

    Clock audclk;                   // 音頻時鍾
    Clock vidclk;                   // 視頻時鍾
    Clock extclk;                   // 外部時鍾

    FrameQueue pictq;               // 視頻frame隊列
    FrameQueue subpq;               // 字幕frame隊列
    FrameQueue sampq;               // 音頻frame隊列

    Decoder auddec;                 // 音頻解碼器
    Decoder viddec;                 // 視頻解碼器
    Decoder subdec;                 // 字幕解碼器

    int audio_stream;               // 音頻流索引

    int av_sync_type;

    double audio_clock;             // 每個音頻幀更新一下此值,以pts形式表示
    int audio_clock_serial;         // 播放序列,seek可改變此值
    double audio_diff_cum; /* used for AV difference average computation */
    double audio_diff_avg_coef;
    double audio_diff_threshold;
    int audio_diff_avg_count;
    AVStream *audio_st;             // 音頻流
    PacketQueue audioq;             // 音頻packet隊列
    int audio_hw_buf_size;          // SDL音頻緩沖區大小(單位字節)
    uint8_t *audio_buf;             // 指向待播放的一幀音頻數據,指向的數據區將被拷入SDL音頻緩沖區。若經過重采樣則指向audio_buf1,否則指向frame中的音頻
    uint8_t *audio_buf1;            // 音頻重采樣的輸出緩沖區
    unsigned int audio_buf_size; /* in bytes */ // 待播放的一幀音頻數據(audio_buf指向)的大小
    unsigned int audio_buf1_size;   // 申請到的音頻緩沖區audio_buf1的實際尺寸
    int audio_buf_index; /* in bytes */ // 當前音頻幀中已拷入SDL音頻緩沖區的位置索引(指向第一個待拷貝字節)
    int audio_write_buf_size;       // 當前音頻幀中尚未拷入SDL音頻緩沖區的數據量,audio_buf_size = audio_buf_index + audio_write_buf_size
    int audio_volume;               // 音量
    int muted;                      // 靜音狀態
    struct AudioParams audio_src;   // 音頻frame的參數
#if CONFIG_AVFILTER
    struct AudioParams audio_filter_src;
#endif
    struct AudioParams audio_tgt;   // SDL支持的音頻參數,重采樣轉換:audio_src->audio_tgt
    struct SwrContext *swr_ctx;     // 音頻重采樣context
    int frame_drops_early;          // 丟棄視頻packet計數
    int frame_drops_late;           // 丟棄視頻frame計數

    enum ShowMode {
        SHOW_MODE_NONE = -1, SHOW_MODE_VIDEO = 0, SHOW_MODE_WAVES, SHOW_MODE_RDFT, SHOW_MODE_NB
    } show_mode;
    int16_t sample_array[SAMPLE_ARRAY_SIZE];
    int sample_array_index;
    int last_i_start;
    RDFTContext *rdft;
    int rdft_bits;
    FFTSample *rdft_data;
    int xpos;
    double last_vis_time;
    SDL_Texture *vis_texture;
    SDL_Texture *sub_texture;
    SDL_Texture *vid_texture;

    int subtitle_stream;                // 字幕流索引
    AVStream *subtitle_st;              // 字幕流
    PacketQueue subtitleq;              // 字幕packet隊列

    double frame_timer;                 // 記錄最后一幀播放的時刻
    double frame_last_returned_time;
    double frame_last_filter_delay;
    int video_stream;
    AVStream *video_st;                 // 視頻流
    PacketQueue videoq;                 // 視頻隊列
    double max_frame_duration;      // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
    struct SwsContext *img_convert_ctx;
    struct SwsContext *sub_convert_ctx;
    int eof;

    char *filename;
    int width, height, xleft, ytop;
    int step;

#if CONFIG_AVFILTER
    int vfilter_idx;
    AVFilterContext *in_video_filter;   // the first filter in the video chain
    AVFilterContext *out_video_filter;  // the last filter in the video chain
    AVFilterContext *in_audio_filter;   // the first filter in the audio chain
    AVFilterContext *out_audio_filter;  // the last filter in the audio chain
    AVFilterGraph *agraph;              // audio filter graph
#endif

    int last_video_stream, last_audio_stream, last_subtitle_stream;

    SDL_cond *continue_read_thread;
} VideoState;

2.2 struct Clock

typedef struct Clock {
    // 當前幀(待播放)顯示時間戳,播放后,當前幀變成上一幀
    double pts;           /* clock base */
    // 當前幀顯示時間戳與當前系統時鍾時間的差值
    double pts_drift;     /* clock base minus time at which we updated the clock */
    // 當前時鍾(如視頻時鍾)最后一次更新時間,也可稱當前時鍾時間
    double last_updated;
    // 時鍾速度控制,用於控制播放速度
    double speed;
    // 播放序列,所謂播放序列就是一段連續的播放動作,一個seek操作會啟動一段新的播放序列
    int serial;           /* clock is based on a packet with this serial */
    // 暫停標志
    int paused;
    // 指向packet_serial
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

2.3 struct PacketQueue

typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;
    int nb_packets;                 // 隊列中packet的數量
    int size;                       // 隊列所占內存空間大小
    int64_t duration;               // 隊列中所有packet總的播放時長
    int abort_request;
    int serial;                     // 播放序列,所謂播放序列就是一段連續的播放動作,一個seek操作會啟動一段新的播放序列
    SDL_mutex *mutex;
    SDL_cond *cond;
} PacketQueue;

棧(LIFO)是一種表,隊列(FIFO)也是一種表。數組是表的一種實現方式,鏈表也是表的一種實現方式,例如FIFO既可以用數組實現,也可以用鏈表實現。PacketQueue是用鏈表實現的一個FIFO。

2.4 struct FrameQueue

typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex;                     // 讀索引。待播放時讀取此幀進行播放,播放后此幀成為上一幀
    int windex;                     // 寫索引
    int size;                       // 總幀數
    int max_size;                   // 隊列可存儲最大幀數
    int keep_last;                  // 是否保留已播放的最后一幀使能標志
    int rindex_shown;               // 是否保留已播放的最后一幀實現手段
    SDL_mutex *mutex;
    SDL_cond *cond;
    PacketQueue *pktq;              // 指向對應的packet_queue
} FrameQueue;

FrameQueue是一個環形緩沖區(ring buffer),是用數組實現的一個FIFO。下面先講一下環形緩沖區的基本原理,其示意圖如下:
ring buffer示意圖
環形緩沖區的一個元素被用掉后,其余元素不需要移動其存儲位置。相反,一個非環形緩沖區在用掉一個元素后,其余元素需要向前搬移。換句話說,環形緩沖區適合實現FIFO,而非環形緩沖區適合實現LIFO。環形緩沖區適合於事先明確了緩沖區的最大容量的情形。擴展一個環形緩沖區的容量,需要搬移其中的數據。因此一個緩沖區如果需要經常調整其容量,用鏈表實現更為合適。

環形緩沖區使用中要避免讀空和寫滿,但空和滿狀態下讀指針和寫指針均相等,因此其實現中的關鍵點就是如何區分出空和滿。有多種策略可以用來區分空和滿的標志:

  1. 總是保持一個存儲單元為空:“讀指針”“寫指針”時為空,“讀指針”“寫指針+1”時為滿;
  2. 使用有效數據計數:每次讀寫都更新數據計數,計數等於0時為空,等於BUF_SIZE時為滿;
  3. 記錄最后一次操作:用一個標志記錄最后一次是讀還是寫,在“讀指針”==“寫指針”時若最后一次是寫,則為滿狀態;若最后一次是讀,則為空狀態。

可以看到,FrameQueue使用上述第2種方式,使用FrameQueue.size記錄環形緩沖區中元素數量,作為有效數據計數。
ffplay中創建了三個frame_queue:音頻frame_queue,視頻frame_queue,字幕frame_queue。每一個frame_queue一個寫端一個讀端,寫端位於解碼線程,讀端位於播放線程。
為了敘述方便,環形緩沖區的一個元素也稱作節點(或幀),將rindex稱作讀指針或讀索引,將windex稱作寫指針或寫索引,叫法用混用的情況,不作文字上的嚴格區分。

2.4.1 初始化與銷毀

static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
{
    int i;
    memset(f, 0, sizeof(FrameQueue));
    if (!(f->mutex = SDL_CreateMutex())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    if (!(f->cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    f->pktq = pktq;
    f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
    f->keep_last = !!keep_last;
    for (i = 0; i < f->max_size; i++)
        if (!(f->queue[i].frame = av_frame_alloc()))
            return AVERROR(ENOMEM);
    return 0;
}

隊列初始化函數確定了隊列大小,將為隊列中每一個節點的frame(f->queue[i].frame)分配內存,注意只是分配frame對象本身,而不關注frame中的數據緩沖區。frame中的數據緩沖區是AVBuffer,使用引用計數機制。
f->max_size是隊列的大小,此處值為16,細節不展開。
f->keep_last是隊列中是否保留最后一次播放的幀的標志。f->keep_last = !!keep_last是將int取值的keep_last轉換為boot取值(0或1)。

static void frame_queue_destory(FrameQueue *f)
{
    int i;
    for (i = 0; i < f->max_size; i++) {
        Frame *vp = &f->queue[i];
        frame_queue_unref_item(vp);     // 釋放對vp->frame中的數據緩沖區的引用,注意不是釋放frame對象本身
        av_frame_free(&vp->frame);      // 釋放vp->frame對象
    }
    SDL_DestroyMutex(f->mutex);
    SDL_DestroyCond(f->cond);
}

隊列銷毀函數對隊列中的每個節點作了如下處理:

  1. frame_queue_unref_item(vp)釋放本隊列對vp->frame中AVBuffer的引用
  2. av_frame_free(&vp->frame)釋放vp->frame對象本身

2.4.2 寫隊列

寫隊列的步驟是:

  1. 獲取寫指針(若寫滿則等待);
  2. 將元素寫入隊列;
  3. 更新寫指針。
    寫隊列涉及下列兩個函數:
frame_queue_peek_writable()     // 獲取寫指針
frame_queue_push()              // 更新寫指針

通過實例看一下寫隊列的用法:

static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    Frame *vp;

    if (!(vp = frame_queue_peek_writable(&is->pictq)))
        return -1;

    vp->sar = src_frame->sample_aspect_ratio;
    vp->uploaded = 0;

    vp->width = src_frame->width;
    vp->height = src_frame->height;
    vp->format = src_frame->format;

    vp->pts = pts;
    vp->duration = duration;
    vp->pos = pos;
    vp->serial = serial;

    set_default_window_size(vp->width, vp->height, vp->sar);

    av_frame_move_ref(vp->frame, src_frame);
    frame_queue_push(&is->pictq);
    return 0;
}

上面一段代碼是視頻解碼線程向視頻frame_queue中寫入一幀的代碼,步驟如下:

  1. frame_queue_peek_writable(&is->pictq)向隊列尾部申請一個可寫的幀空間,若隊列已滿無空間可寫,則等待
  2. av_frame_move_ref(vp->frame, src_frame)將src_frame中所有數據拷貝到vp->
    frame並復位src_frame,vp->
    frame中AVBuffer使用引用計數機制,不會執行AVBuffer的拷貝動作,僅是修改指針指向值。為避免內存泄漏,在av_frame_move_ref(dst, src)之前應先調用av_frame_unref(dst),這里沒有調用,是因為frame_queue在刪除一個節點時,已經釋放了frame及frame中的AVBuffer。
  3. frame_queue_push(&is->pictq)此步僅將frame_queue中的寫指針加1,實際的數據寫入在此步之前已經完成。

frame_queue寫操作相關函數實現如下:
frame_queue_peek_writable()

static Frame *frame_queue_peek_writable(FrameQueue *f)
{
    /* wait until we have space to put a new frame */
    SDL_LockMutex(f->mutex);
    while (f->size >= f->max_size &&
           !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[f->windex];
}

向隊列尾部申請一個可寫的幀空間,若無空間可寫,則等待

frame_queue_push()

static void frame_queue_push(FrameQueue *f)
{
    if (++f->windex == f->max_size)
        f->windex = 0;
    SDL_LockMutex(f->mutex);
    f->size++;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

向隊列尾部壓入一幀,只更新計數與寫指針,因此調用此函數前應將幀數據寫入隊列相應位置

2.4.3 讀隊列

寫隊列中,應用程序寫入一個新幀后通常總是將寫指針加1。而讀隊列中,“讀取”和“更新讀指針(同時刪除舊幀)”二者是獨立的,可以只讀取而不更新讀指針,也可以只更新讀指針(只刪除)而不讀取。而且讀隊列引入了是否保留已顯示的最后一幀的機制,導致讀隊列比寫隊列要復雜很多。

讀隊列和寫隊列步驟是類似的,基本步驟如下:

  1. 獲取讀指針(若讀空則等待);
  2. 讀取一個節點;
  3. 更新寫指針(同時刪除舊節點)。
    寫隊列涉及如下函數:
frame_queue_peek_readable()     // 獲取讀指針(若讀空則等待)
frame_queue_peek()              // 獲取當前節點指針
frame_queue_peek_next()         // 獲取下一節點指針
frame_queue_peek_last()         // 獲取上一節點指針
frame_queue_next()              // 更新讀指針(同時刪除舊節點)

通過實例看一下讀隊列的用法:

static void video_refresh(void *opaque, double *remaining_time)
{
    ......
    if (frame_queue_nb_remaining(&is->pictq) == 0) {    // 所有幀已顯示
        // nothing to do, no picture to display in the queue
    } else {
        Frame *vp, *lastvp;
        lastvp = frame_queue_peek_last(&is->pictq);     // 上一幀:上次已顯示的幀
        vp = frame_queue_peek(&is->pictq);              // 當前幀:當前待顯示的幀
        frame_queue_next(&is->pictq);                   // 刪除上一幀,並更新rindex
        video_display(is)-->video_image_display()-->frame_queue_peek_last();
    }
    ......
}

上面一段代碼是視頻播放線程從視頻frame_queue中讀取視頻幀進行顯示的基本步驟,其他代碼已省略,只保留了讀隊列部分。video_refresh()的實現詳情可參考第3節。
記lastvp為上一次已播放的幀,vp為本次待播放的幀,下圖中方框中的數字表示顯示序列中幀的序號(實際就是Frame.frame.display_picture_number變量值)。
frame_queue示意圖
在啟用keep_last機制后,rindex_shown值總是為1,rindex_shown確保了最后播放的一幀總保留在隊列中。
假設某次進入video_refresh()的時刻為T0,下次進入的時刻為T1。在T0時刻,讀隊列的步驟如下:

  1. rindex(圖中ri)表示上一次播放的幀lastvp,本次調用video_refresh()中,lastvp會被刪除,rindex會加1
  2. rindex+rindex_shown(圖中ris)表示本次待播放的幀vp,本次調用video_refresh()中,vp會被讀出播放
    圖中已播放的幀是灰色方框,本次待播放的幀是黑色方框,其他未播放的幀是綠色方框,隊列中空位置為白色方框。
    在之后的某一時刻TX,首先調用frame_queue_nb_remaining()判斷是否有幀未播放,若無待播放幀,函數video_refresh()直接返回,不往下執行。
/* return the number of undisplayed frames in the queue */
static int frame_queue_nb_remaining(FrameQueue *f)
{
    return f->size - f->rindex_shown;
}

rindex_shown為1時,隊列中總是保留了最后一幀lastvp(灰色方框)。按照這樣的設計思路,如果rindex_shown為2,隊列中就會保留最后2幀。
但keep_last機制有什么用途呢?希望知道的同學指點一下。
注意,在TX時刻,無新幀可顯示,保留的一幀是已經顯示過的。那么最后一幀什么時候被清掉呢?在播放結束或用戶中途取消播放時,會調用frame_queue_destory()清空播放隊列。

rindex_shown的引入增加了讀隊列操作的理解難度。大多數讀操作函數都會用到這個變量。
通過FrameQueue.keep_lastFrameQueue.rindex_shown兩個變量實現了保留最后一次播放幀的機制。
是否啟用keep_last機制是由全局變量keep_last值決定的,在隊列初始化函數frame_queue_init()中有f->keep_last = !!keep_last;,而在更新讀指針函數frame_queue_next()中如果啟用keep_last機制,則f->rindex_shown值為1。如果rindex_shown對理解代碼造成了困擾,可以先將全局變量keep_last值賦為0,這樣f->rindex_shown值為0,代碼看起來會清晰很多。理解了讀隊列的基本方法后,再看f->rindex_shown值為1時代碼是如何運行的。

先看frame_queue_next()函數:
frame_queue_next()

static void frame_queue_next(FrameQueue *f)
{
    if (f->keep_last && !f->rindex_shown) {
        f->rindex_shown = 1;
        return;
    }
    frame_queue_unref_item(&f->queue[f->rindex]);
    if (++f->rindex == f->max_size)
        f->rindex = 0;
    SDL_LockMutex(f->mutex);
    f->size--;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

三個動作:刪除rindex節點(lastvp),更新f->rindexf->size

frame_queue_peek_readable()

static Frame *frame_queue_peek_readable(FrameQueue *f)
{
    /* wait until we have a readable a new frame */
    SDL_LockMutex(f->mutex);
    while (f->size - f->rindex_shown <= 0 &&
           !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

從隊列頭部讀取一幀(vp),只讀取不刪除,若無幀可讀則等待。這個函數和frame_queue_peek()的區別僅僅是多了不可讀時等待的操作。

frame_queue_peek()

static Frame *frame_queue_peek(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

static Frame *frame_queue_peek_next(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
}

// 取出此幀進行播放,只讀取不刪除,不刪除是因為此幀需要緩存下來供下一次使用。播放后,此幀變為上一幀
static Frame *frame_queue_peek_last(FrameQueue *f)
{
    return &f->queue[f->rindex];
}

從隊列頭部讀取一幀(vp),只讀取不刪除。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM