作者: 葉餘 來源: https://www.cnblogs.com/leisure_chn/p/10301253.html 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源碼分析”系列文章如下: ffplay源碼分析1-概述 ffplay源碼分析2-資料結構 ffplay源碼分析3-代碼架構 ffplay源碼分析4-音視訊同步 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。下面先講一下環形緩沖區的基本原理,其示意圖如下:

環形緩沖區的一個元素被用掉後,其餘元素不需要移動其存儲位置。相反,一個非環形緩沖區在用掉一個元素後,其餘元素需要向前搬移。換句話說,環形緩沖區适合實作FIFO,而非環形緩沖區适合實作LIFO。環形緩沖區适合于事先明确了緩沖區的最大容量的情形。擴充一個環形緩沖區的容量,需要搬移其中的資料。是以一個緩沖區如果需要經常調整其容量,用連結清單實作更為合适。
環形緩沖區使用中要避免讀空和寫滿,但空和滿狀态下讀指針和寫指針均相等,是以其實作中的關鍵點就是如何區分出空和滿。有多種政策可以用來區分空和滿的标志:
- 總是保持一個存儲單元為空:“讀指針”“寫指針”時為空,“讀指針”“寫指針+1”時為滿;
- 使用有效資料計數:每次讀寫都更新資料計數,計數等于0時為空,等于BUF_SIZE時為滿;
- 記錄最後一次操作:用一個标志記錄最後一次是讀還是寫,在“讀指針”==“寫指針”時若最後一次是寫,則為滿狀态;若最後一次是讀,則為空狀态。
可以看到,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);
}
隊列銷毀函數對隊列中的每個節點作了如下處理:
-
釋放本隊列對vp->frame中AVBuffer的引用frame_queue_unref_item(vp)
-
釋放vp->frame對象本身av_frame_free(&vp->frame)
2.4.2 寫隊列
寫隊列的步驟是:
- 擷取寫指針(若寫滿則等待);
- 将元素寫入隊列;
-
更新寫指針。
寫隊列涉及下列兩個函數:
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中寫入一幀的代碼,步驟如下:
-
向隊列尾部申請一個可寫的幀空間,若隊列已滿無空間可寫,則等待frame_queue_peek_writable(&is->pictq)
-
av_frame_move_ref(vp->frame, src_frame)
将src_frame中所有資料拷貝到vp->
frame并複位src_frame,vp->
frame中AVBuffer使用引用計數機制,不會執行AVBuffer的拷貝動作,僅是修改指針指向值。為避免記憶體洩漏,在
之前應先調用av_frame_move_ref(dst, src)
,這裡沒有調用,是因為frame_queue在删除一個節點時,已經釋放了frame及frame中的AVBuffer。av_frame_unref(dst)
-
此步僅将frame_queue中的寫指針加1,實際的資料寫入在此步之前已經完成。frame_queue_push(&is->pictq)
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。而讀隊列中,“讀取”和“更新讀指針(同時删除舊幀)”二者是獨立的,可以隻讀取而不更新讀指針,也可以隻更新讀指針(隻删除)而不讀取。而且讀隊列引入了是否保留已顯示的最後一幀的機制,導緻讀隊列比寫隊列要複雜很多。
讀隊列和寫隊列步驟是類似的,基本步驟如下:
- 擷取讀指針(若讀空則等待);
- 讀取一個節點;
-
更新寫指針(同時删除舊節點)。
寫隊列涉及如下函數:
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時刻,讀隊列的步驟如下:
- rindex(圖中ri)表示上一次播放的幀lastvp,本次調用
中,lastvp會被删除,rindex會加1video_refresh()
- 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),隻讀取不删除。
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。