ffplay是FFmpeg工程自帶的簡單播放器,使用FFmpeg提供的解碼器和SDL庫進行視頻播放。本文基於FFmpeg工程4.1版本進行分析,其中ffplay源碼清單以下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.chtml
在嘗試分析源碼前,可先閱讀以下參考文章做爲鋪墊:
[1]. 雷霄驊,視音頻編解碼技術零基礎學習方法
[2]. 視頻編解碼基礎概念
[3]. 色彩空間與像素格式
[4]. 音頻參數解析
[5]. FFmpeg基礎概念git
「ffplay源碼分析」系列文章以下:
[1]. ffplay源碼分析1-概述
[2]. ffplay源碼分析2-數據結構
[3]. ffplay源碼分析3-代碼框架
[4]. ffplay源碼分析4-音視頻同步
[5]. ffplay源碼分析5-圖像格式轉換
[6]. ffplay源碼分析6-音頻重採樣
[7]. ffplay源碼分析7-播放控制github
幾個關鍵的數據結構以下:數組
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;
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;
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。緩存
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。下面先講一下環形緩衝區的基本原理,其示意圖以下:
環形緩衝區的一個元素被用掉後,其他元素不須要移動其存儲位置。相反,一個非環形緩衝區在用掉一個元素後,其他元素須要向前搬移。換句話說,環形緩衝區適合實現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稱做寫指針或寫索引,叫法用混用的狀況,不做文字上的嚴格區分。ide
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對象自己源碼分析
寫隊列的步驟是:
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); }
向隊列尾部壓入一幀,只更新計數與寫指針,所以調用此函數前應將幀數據寫入隊列相應位置
寫隊列中,應用程序寫入一個新幀後一般老是將寫指針加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
變量值)。
在啓用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_last
和FrameQueue.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->rindex
和f->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),只讀取不刪除。