ffplay播放器
1 ffplay.c的意義
ffplay.c是FFmpeg源碼?帶的播放器,調?FFmpeg和SDL API實現?個?常有?的播放器。 例如嗶哩嗶哩著名開源項?ijkplayer也是基于ffplay.c進??次開發。
ffplay實現了播放器的主體功能,掌握其原理對于我們獨?開發播放器?常有幫助。
2 FFplay框架分析
播放器初始化
- 初始化packet queue
- 初始化frame queue
- 初始化clock
- 創建數據讀取線程
線程的劃分
- 數據讀取線程
- 打開媒體?件
- 打開對應碼流的decoder以及初始化對應的audio、video、subtitle輸出
- 創建decoder線程,audio、video和subtitle的解碼線程獨?
- 調?av_read_frame讀取packet,并根據steam_index放?不同stream對應的packet隊列
- ?頻解碼
- 從packet queue讀取packet,解出frame后放?frame queue
- 視頻解碼
- 從packet queue讀取packet,解出frame后放?frame queue
- 字幕解碼
- 從packet queue讀取packet,解出frame后放?frame queue
- ?頻播放(或者回調函數)
- 從frame queue讀取frame進?播放
- 視頻播放(ffplay?前是在main主線程進?視頻播放)
- 從frame queue讀取frame進?播放 字幕播放(ffplay?前是在main主線程進?字幕播放) 從frame queue讀取frame進?播放
- 控制響應(播放/暫停/快進/快退等)(ffplay?前是在main主線程進?播放控制)
packet隊列的設計
- 線程安全,?持互斥、等待、喚醒
- 緩存數據??
- 緩存包數
- 隊列播放可持續時間
- 進隊列/出隊列等
frame隊列的設計
- 線程安全,?持互斥、等待、喚醒
- 緩存幀數
- ?持讀取數據?不出隊列
- 進隊列/出隊列等
?視頻同步
- ?量調節
- 靜?
- 重采樣
視頻處理
- 圖像格式轉換YUV->RGB等
- 圖像縮放1280720->800480等
播放器控制
- 播放
- 暫停
- 停?
- 快進/快退
- 逐幀靜?
3數據結構分析
struct VideoState 播放器封裝
typedef struct VideoState { SDL_Thread *read_tid; // 讀線程句柄 AVInputFormat *iformat; // 指向demuxer int abort_request; // =1時請求退出播放 int force_refresh; // =1時需要刷新畫?,請求?即刷新畫?的意思 int paused; // =1時暫停,=0時播放 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; // iformat的上下? int realtime; // =1為實時流 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; // ?視頻同步類型, 默認audio master double audio_clock; // 當前?頻幀的PTS+當前幀Du ration int audio_clock_serial; // 播放序列,seek可改變此值 // 以下4個參數 ?audio master同步?式使? double audio_diff_cum; // used for AV differen ce average computation double audio_diff_avg_coef; double audio_diff_threshold; int audio_diff_avg_count; // end 4142 AVStream *audio_st; // ?頻流 PacketQueue audioq; // ?頻packet隊列 int audio_hw_buf_size; // SDL?頻緩沖區的??(字節 為單位) // 指向待播放的?幀?頻數據,指向的數據區將被拷?SDL?頻緩沖區。若經過重采樣 則指向audio_buf1, // 否則指向frame中的?頻 uint8_t *audio_buf; // 指向需要重采樣的數據 uint8_t *audio_buf1; // 指向重采樣后的數據 unsigned int audio_buf_size; // 待播放的?幀?頻數據(aud io_buf指向)的?? unsigned int audio_buf1_size; // 申請到的?頻緩沖區audio_ buf1的實際尺? int audio_buf_index; // 更新拷?位置 當前?頻幀中 已拷?SDL?頻緩沖區 // 的位置索引(指向第?個待拷 ?字節) // 當前?頻幀中尚未拷?SDL?頻緩沖區的數據量: // audio_buf_size = audio_buf_index + audio_write_buf_size int audio_write_buf_size; int audio_volume; // ?量 int muted; // =1靜?,=0則正常 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 { 68 SHOW_MODE_NONE = -1, SHOW_MODE_VIDEO = 0, SHOW_MODE_WAVES, SHOW_MODE_RDFT, SHOW_MODE_NB 69 } show_mode; 7071 // ?頻波形顯示使? 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; 92 double frame_last_filter_delay; 9394 int video_stream; // 視頻流索引 AVStream *video_st; // 視頻流 PacketQueue videoq; // 視頻隊列 double max_frame_duration; // ?幀最?間隔. above this, we co nsider the jump a timestamp discontinuity 98 struct SwsContext *img_convert_ctx; // 視頻尺?格式變換 struct SwsContext *sub_convert_ctx; // 字幕尺?格式變換 100 int eof; // 是否讀取結束 char *filename; // ?件名 int width, height, xleft, ytop; // 寬、?,x起始坐標,y起始坐標 104 int step; // =1 步進播放模式, =0 其他模式 #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 v ideo chain AVFilterContext *in_audio_filter; // the first filter in the audio chain AVFilterContext *out_audio_filter; // the last filter in the a udio chain AVFilterGraph *agraph; // audio filter graph 113 #endif // 保留最近的相應audio、video、subtitle流的steam index int last_video_stream, last_audio_stream, last_subtitle_stream; SDL_cond *continue_read_thread; // 當讀取數據隊列滿了后進?休眠時,可 以通過該condition喚醒讀線程 } VideoState;struct Clock 時鐘封裝
typedef struct Clock { double pts; // 時鐘基礎, 當前幀(待播放)顯示時間戳,播放后, 當前幀變成上?幀 // 當前pts與當前系統時鐘的差值, audio、video對于該值是獨?的 double pts_drift; // clock base minus time at which we upd ated the clock // 當前時鐘(如視頻時鐘)最后?次更新時間,也可稱當前時鐘時間 double last_updated; // 最后?次更新的系統時鐘 double speed; // 時鐘速度控制,?于控制播放速度 // 播放序列,所謂播放序列就是?段連續的播放動作,?個seek操作會啟動?段新的播 放序列 int serial; // clock is based on a packet with this serial int paused; // = 1 說明是暫停狀態 // 指向packet_serial int *queue_serial; /* pointer to the current packet queue s erial, used for obsolete clock detection */ } Clock;struct MyAVPacketList和PacketQueue隊列
ffplay?PacketQueue保存解封裝后的數據,即保存AVPacket。
ffplay?先定義了?個結構體 MyAVPacketList :
可以理解為是隊列的?個節點。可以通過其 next 字段訪問下?個節點。
serial字段主要?于標記當前節點的播放序列號,ffplay中多處?到serial的概念,主要?來區分是否連續 數據,每做?次seek,該serial都會做+1的遞增,以區分不同的播放序列。serial字段在我們ffplay的分析 中應??常?泛,謹記他是?來區分數據否連續先。
接著定義另?個結構體PacketQueue:
typedef struct PacketQueue { MyAVPacketList *first_pkt, *last_pkt; // 隊?,隊尾指針 int nb_packets; // 包數量,也就是隊列元素數量 int size; // 隊列所有元素的數據??總和 int64_t duration; // 隊列所有元素的數據播放持續時間 int abort_request; // ?戶退出請求標志 int serial; // 播放序列號,和MyAVPacketList的serial作?相 同 SDL_mutex *mutex; // ?于維持PacketQueue的多線程安全(SDL_mutex 可以按pthread_mutex_t理解) SDL_cond *cond; // ?于讀、寫線程相互通知(SDL_cond可以按pthrea d_cond_t理解) } PacketQueue;該結構體內定義了“隊列”?身的屬性。上?的注釋對每個字段作了簡單的介紹,這?也看到了serial字段, **MyAVPacketList的serial字段的賦值來?PacketQueue的serial,**每個PacketQueue的serial是獨?的。
?頻、視頻、字幕流都有??獨?的PacketQueue
接下來我們也從隊列的操作函數具體分析各個字段的含義。
PacketQueue 操作提供以下?法:
- packet_queue_init:初始化
- packet_queue_destroy:銷毀
- packet_queue_start:啟?
- packet_queue_abort:中?
- packet_queue_get:獲取?個節點
- packet_queue_put:存??個節點
- packet_queue_put_nullpacket:存??個空節點
- packet_queue_flush:清除隊列內所有的節點
packet_queue_init()
初始化?于初始各個字段的值,并創建mutex和cond:
/* packet queue handling */ static int packet_queue_init(PacketQueue *q) {memset(q, 0, sizeof(PacketQueue));q->mutex = SDL_CreateMutex();if (!q->mutex) {av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());return AVERROR(ENOMEM);}q->cond = SDL_CreateCond();if (!q->cond) {av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());return AVERROR(ENOMEM);}q->abort_request = 1;return 0; }packet_queue_destroy()
相應的,packet_queue_destroy()銷毀過程負責清理mutex和cond:
static void packet_queue_destroy(PacketQueue *q) {packet_queue_flush(q);//先清除所有的節點SDL_DestroyMutex(q->mutex);SDL_DestroyCond(q->cond); }packet_queue_start()
啟動隊列
static void packet_queue_start(PacketQueue *q) {SDL_LockMutex(q->mutex);q->abort_request = 0;packet_queue_put_private(q, &flush_pkt);//這?放?了?個flush_pkt, 問題:?的是什么SDL_UnlockMutex(q->mutex); }flush_pkt定義是 static AVPacket flush_pkt; ,是?個特殊的packet,主要?來作為?連續的兩 端數據的“分界”標記:
- 插? flush_pkt 觸發PacketQueue其對應的serial,加1操作
- 觸發解碼器清空?身緩存 avcodec_flush_buffers(),以備新序列的數據進?新解碼
packet_queue_abort()
中?隊列:
static void packet_queue_abort(PacketQueue *q) {SDL_LockMutex(q->mutex);q->abort_request = 1;// 請求退出SDL_CondSignal(q->cond);//釋放?個條件信號SDL_UnlockMutex(q->mutex); }這?SDL_CondSignal的作?在于確保當前等待該條件的線程能被激活并繼續執?退出流程,并喚醒者會 檢測abort_request標志確定??的退出流程
packet_queue_put()
讀、寫是PacketQueue的主要?法。 先看寫——往隊列中放??個節點:
static int packet_queue_put(PacketQueue *q, AVPacket *pkt) {int ret;SDL_LockMutex(q->mutex);ret = packet_queue_put_private(q, pkt);//主要實現SDL_UnlockMutex(q->mutex);if (pkt != &flush_pkt && ret < 0)av_packet_unref(pkt);//放?失敗,釋放AVPacketreturn ret; }主要實現在函數 packet_queue_put_private ,這?需要注意的是如果插?失敗,則需要釋放 AVPacket。
我們再分析packet_queue_put_private:
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt) {MyAVPacketList *pkt1;if (q->abort_request)//如果已中?,則放?失敗return -1;pkt1 = av_malloc(sizeof(MyAVPacketList));if (!pkt1)//內存不?,則放?失敗return -1;// 沒有做引?計數,那這?也說明av_read_frame不會釋放替?戶釋放buffer。pkt1->pkt = *pkt; //拷?AVPacket(淺拷?,AVPacket.data等內存并沒有拷?)pkt1->next = NULL;if (pkt == &flush_pkt)//如果放?的是flush_pkt,需要增加隊列的播放序列 號,以區分不連續的兩段數據q->serial++;pkt1->serial = q->serial;//?隊列序列號標記節點/* 隊列操作:如果last_pkt為空,說明隊列是空的,新增節點為隊頭; * 否則,隊列有數據,則讓原隊尾的next為新增節點。 最后將隊尾指向新增節點 */if (!q->last_pkt)q->first_pkt = pkt1;elseq->last_pkt->next = pkt1;q->last_pkt = pkt1;//隊列屬性操作:增加節點數、cache??、cache總時?, ?來控制隊列的??q->nb_packets++;q->size += pkt1->pkt.size + sizeof(*pkt1);q->duration += pkt1->pkt.duration;/* XXX: should duplicate packet data in DV case *///發出信號,表明當前隊列中有數據了,通知等待中的讀線程可以取數據了SDL_CondSignal(q->cond);return 0; }對于packet_queue_put_private主要完成3件事
- 計算serial。serial標記了這個節點內的數據是何時的。?般情況下新增節點與上?個節點的serial是? 樣的,但當隊列中加??個flush_pkt后,后續節點的serial會?之前?1,?來區別不同播放序列的 packet.
- 節點?隊列操作。
- 隊列屬性操作。更新隊列中節點的數?、占?字節數(含AVPacket.data的??)及其時?。主要?來 控制Packet隊列的??,我們PacketQueue鏈表式的隊列,在內存充?的條件下我們可以?限put? packet,如果我們要控制隊列??,則需要通過其變量size、duration、nb_packets三者單?或者綜 合去約束隊列的節點的數量,具體在read_thread進?分析。
packet_queue_get()
從隊列中取?個節點:
/** * @brief packet_queue_get * @param q 隊列 * @param pkt 輸出參數,即MyAVPacketList.pkt * @param block 調?者是否需要在沒節點可取的情況下阻塞等待 * @param serial 輸出參數,即MyAVPacketList.serial * @return <0: aborted; =0: no packet; >0: has packet *//* return < 0 if aborted, 0 if no packet and > 0 if packet. */ static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial) {MyAVPacketList *pkt1;int ret;SDL_LockMutex(q->mutex);// 加鎖for (;;) {if (q->abort_request) {ret = -1;break;}pkt1 = q->first_pkt;//MyAVPacketList *pkt1; 從隊頭拿數據if (pkt1) {//隊列中有數據q->first_pkt = pkt1->next;//隊頭移到第?個節點if (!q->first_pkt)q->last_pkt = NULL;q->nb_packets--;//節點數減1q->size -= pkt1->pkt.size + sizeof(*pkt1);//cache??扣 除?個節點q->duration -= pkt1->pkt.duration;//返回AVPacket,這?發??次AVPacket結構體拷?,AVPacket的data 只拷?了指針*pkt = pkt1->pkt;if (serial) //如果需要輸出serial,把serial輸出*serial = pkt1->serial;av_free(pkt1);//釋放節點內存,只是釋放節點,?不是釋放AVPa cketret = 1;break;} else if (!block) {//隊列中沒有數據,且?阻塞調?ret = 0;break;} else {//隊列中沒有數據,且阻塞調?//這?沒有break。for循環的另?個作?是在條件變量滿?后重復上述代碼取出節點SDL_CondWait(q->cond, q->mutex);}}SDL_UnlockMutex(q->mutex);// 釋放鎖return ret; }該函數整體流程:
- 加鎖
- 進?for循環,如果需要退出for循環,則break;當沒有數據可讀且block為1時則等待
- ret = -1 終?獲取packet
- ret = 0 沒有讀取到packet
- ret = 1 獲取到了packet
- 釋放鎖
如果有取到數據,主要分3個步驟:
packet_queue_put_nullpacket()
放?“空包”(nullpacket)。放?空包意味著流的結束,?般在媒體數據讀取完成的時候放?空包。放? 空包,?的是為了沖刷解碼器,將編碼器??所有frame都讀取出來:
static int packet_queue_put_nullpacket(PacketQueue *q, int stream_index) {AVPacket pkt1, *pkt = &pkt1;av_init_packet(pkt);pkt->data = NULL;pkt->size = 0;pkt->stream_index = stream_index;return packet_queue_put(q, pkt); }?件數據讀取完畢后刷?空包。
packet_queue_flush()
packet_queue_flush?于將packet隊列中的所有節點清除,包括節點對應的AVPacket。?如?于退出播 放和seek播放:
- 退出播放,則要清空packet queue的節點
- seek播放,要清空seek之前緩存的節點數據,以便插?新節點數據
函數主體的for循環是隊列遍歷,遍歷過程釋放節點和AVPacket(AVpacket對應的數據也被釋放掉)。最后 將PacketQueue的屬性恢復為空隊列狀態。
PacketQueue總結
前?我們分析了PacketQueue的實現和主要的操作?法,現在總結下兩個關鍵的點:
第?,PacketQueue的內存管理
MyAVPacketList的內存是完全由PacketQueue維護的,在put的時候malloc,在get的時候free。 AVPacket分兩塊:
- ?部分是AVPacket結構體的內存,這部分從MyAVPacketList的定義可以看出是和MyAVPacketList 共存亡的。
- 另?部分是AVPacket字段指向的內存,這部分?般通過 av_packet_unref 函數釋放。?般情況 下,是在get后由調?者負責? av_packet_unref 函數釋放。特殊的情況是當碰到 packet_queue_flush 或put失敗時,這時需要隊列??處理。
第?,serial的變化過程:
如上圖所示,左邊是隊頭,右邊是隊尾,從左往右標注了4個節點的serial,以及放?對應節點時queue的 serial。
可以看到放?flush_pkt的時候后,serial增加了1.
假設,現在要從隊頭取出?個節點,那么取出的節點是serial 1,?PacketQueue?身的queue已經增?到 了2。
PacketQueue設計思路:
struct Frame 和 FrameQueue隊列
1 Frame
typedef struct Frame { AVFrame *frame; // 指向數據幀 AVSubtitle sub; // ?于字幕 int serial; // 播放序列,在seek的操作時serial會變化 double pts; // 時間戳,單位為秒 double duration; // 該幀持續時間,單位為秒 int64_t pos; // 該幀在輸??件中的字節位置 int width; // 圖像寬度 int height; // 圖像?讀 int format; // 對于圖像為(enum AVPixelFormat), // 對于聲?則為(enum AVSampleFormat) AVRational sar; // 圖像的寬??,如果未知或未指定則為0/1 int uploaded; // ?來記錄該幀是否已經顯示過? int flip_v; // =1則旋轉180, = 0則正常播放 } Frame;真正存儲解碼后?視頻數據的結構體為AVFrame ,存儲字幕則使?AVSubtitle,該Frame的設計是為了? 頻、視頻、字幕幀通?,所以Frame結構體的設計類似AVFrame,部分成員變量只對不同類型有作?,? 如sar只對視頻有作?。
??也包含了serial播放序列(每次seek時都切換serial),sar(圖像的寬??(16:9,4:3…),該值來 ?AVFrame結構體的sample_aspect_ratio變量)。
2 FrameQueue
typedef struct FrameQueue { Frame queue[FRAME_QUEUE_SIZE]; // FRAME_QUEUE_SIZE 最? size, 數字太?時會占??量的內存,需要注意該值的設置 int rindex; // 讀索引。待播放時讀取此幀進?播 放,播放后此幀成為上?幀 int windex; // 寫索引 int size; // 當前總幀數 int max_size; // 可存儲最?幀數 int keep_last; // = 1說明要在隊列??保持最后? 幀的數據不釋放,只在銷毀隊列的時候才將其真正釋放 int rindex_shown; // 初始化為0,配合keep_last=1 使? SDL_mutex *mutex; // 互斥量 SDL_cond *cond; // 條件變量 PacketQueue *pktq; // 數據包緩沖隊列 } FrameQueue;FrameQueue是?個環形緩沖區(ring buffer),是?數組實現的?個FIFO。數組?式的環形緩沖區適合于 事先明確了緩沖區的最?容量的情形。
ffplay中創建了三個frame_queue:?頻frame_queue,視頻frame_queue,字幕frame_queue。每? 個frame_queue?個寫端?個讀端,寫端位于解碼線程,讀端位于播放線程。
FrameQueue的設計?如PacketQueue復雜,引?了讀取節點但節點不出隊列的操作、讀取下?節點也不 出隊列等等的操作,FrameQueue操作提供以下?法:
- frame_queue_unref_item:釋放Frame??的AVFrame和 AVSubtitle
- frame_queue_init:初始化隊列
- frame_queue_destory:銷毀隊列
- frame_queue_signal:發送喚醒信號
- frame_queue_peek:獲取當前Frame,調?之前先調?
- frame_queue_nb_remaining確保有frame可 讀
- frame_queue_peek_next:獲取當前Frame的下?Frame,調?之前先調?
- frame_queue_nb_remaining確保?少有2 Frame在隊列
- frame_queue_peek_last:獲取上?Frame
- frame_queue_peek_writable:獲取?個可寫Frame,可以以阻塞或?阻塞?式進?
- frame_queue_peek_readable:獲取?個可讀Frame,可以以阻塞或?阻塞?式進?
- frame_queue_push:更新寫索引,此時Frame才真正?隊列,隊列節點Frame個數加1
- frame_queue_next:更新讀索引,此時Frame才真正出隊列,隊列節點Frame個數減1,內部調?
- frame_queue_unref_item是否對應的AVFrame和AVSubtitle
- frame_queue_nb_remaining:獲取隊列Frame節點個數
- frame_queue_last_pos:獲取最近播放Frame對應數據在媒體?件的位置,主要在seek時使?
frame_queue_init() 初始化
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()))// 分配AVFrame結 構體return AVERROR(ENOMEM);return 0; }隊列初始化函數確定了隊列??,將為隊列中每?個節點的frame( f->queue[i].frame )分配內 存,注意只是分配Frame對象本身,?不關注Frame中的數據緩沖區。Frame中的數據緩沖區是 AVBuffer,使?引?計數機制。
f->max_size 是隊列的??,此處值為16(由FRAME_QUEUE_SIZE定義),實際分配的時候視 頻為3,?頻為9,字幕為16,因為這?存儲的是解碼后的數據,不宜設置過?,?如視頻當為 1080p時,如果為YUV420p格式,?幀就有3110400字節。
- #define VIDEO_PICTURE_QUEUE_SIZE 3 // 圖像幀緩存數量
- #define SUBPICTURE_QUEUE_SIZE 16 // 字幕幀緩存數量
- #define SAMPLE_QUEUE_SIZE 9 // 采樣幀緩存數量
- #define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE,
- FFMAX(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE))
f->keep_last 是隊列中是否保留最后?次播放的幀的標志。
f->keep_last = !!keep_last 是將int取值的keep_last轉換為boot取值(0或1)。
frame_queue_destory()銷毀
static void frame_queue_destory(FrameQueue *f) {int i;for (i = 0; i < f->max_size; i++) {Frame *vp = &f->queue[i];// 釋放對vp->frame中的數據緩沖區的引?,注意不是釋放frame對象本身frame_queue_unref_item(vp);// 釋放vp->frame對象av_frame_free(&vp->frame);}SDL_DestroyMutex(f->mutex);SDL_DestroyCond(f->cond); }隊列銷毀函數對隊列中的每個節點作了如下處理:
frame_queue_peek_writable()獲取可寫
Frame frame_queue_push()?隊列
FrameQueue寫隊列的步驟和PacketQueue不同,分了3步進?:
通過實例看?下寫隊列的?法:
// video AVFrame ?隊列 static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial) {Frame *vp;#if defined(DEBUG_SYNC)printf("frame_type=%c pts=%0.3f\n",av_get_picture_type_char(src_frame->pict_type), pts); #endifif (!(vp = frame_queue_peek_writable(&is->pictq)))// 檢測隊列是否 有可寫空間return -1;// Frame隊列滿了則返回-1// 執?到這步說已經獲取到了可寫?的Framevp->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);// 將src中所有數據拷?到dst 中,并復位srcav_frame_move_ref(vp->frame, src_frame);frame_queue_push(&is->pictq);// 更新寫索引位置return 0; }上??段代碼是視頻解碼線程向視頻frame_queue中寫??幀的代碼,步驟如下:
frame_queue_peek_writable獲取可寫Frame指針
// 獲取可寫指針 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]; }向隊列尾部申請?個可寫的幀空間,若?空間可寫,則等待。
這?最需要體會到的是abort_request的使?,在等待時如果播放器需要退出則將abort_request = 1,那 frame_queue_peek_writable函數可以知道是正常frame可寫喚醒,還是其他喚醒。
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);// 當frame_queue_peek_readable在等待時 則可以喚醒SDL_UnlockMutex(f->mutex); }向隊列尾部壓??幀,只更新計數與寫指針,因此調?此函數前應將幀數據寫?隊列相應位 置。SDL_CondSignal(f->cond);可以喚醒讀frame_queue_peek_readable。
frame_queue_peek_readable() 獲取可讀
Frame frame_queue_next()出隊列
寫隊列中,應?程序寫??個新幀后通常總是將寫索引加1。?讀隊列中,“讀取”和“更新讀索引(同時刪除 舊幀)”?者是獨?的,可以只讀取?不更新讀索引,也可以只更新讀索引(只刪除)?不讀取(只有更新讀索 引的時候才真正釋放對應的Frame數據)。?且讀隊列引?了是否保留已顯示的最后?幀的機制,導致讀 隊列?寫隊列要復雜很多。
讀隊列和寫隊列步驟是類似的,基本步驟如下:
讀隊列涉及如下函數:
Frame *frame_queue_peek_readable(FrameQueue *f); // 獲取可讀Frame指 針(若讀空則等待) Frame *frame_queue_peek(FrameQueue *f); // 獲取當前Frame指針 Frame *frame_queue_peek_next(FrameQueue *f); // 獲取下?Frame指針 Frame *frame_queue_peek_last(FrameQueue *f); // 獲取上?Frame指針 void frame_queue_next(FrameQueue *f); // 更新讀索引(同時刪除 舊frame)通過實例看?下讀隊列的?法:
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中讀取視頻幀進?顯示的基本步驟,其他 代碼已省略,只保留了讀隊列部分。 記lastvp為上?次已播放的幀,vp為本次待播放的幀,下圖中?框中的數字表示顯示序列中幀 的序號:
在啟?keep_last機制后,rindex_shown值總是為1,rindex_shown確保了最后播放的?幀總保留在隊列 中。
假設某次進? video_refresh() 的時刻為T0,下次進?的時刻為T1。在T0時刻,讀隊列的步驟如下:
frame_queue_nb_remaining()獲取隊列的size
/* 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的值 就是0或1,不存在變為2,3等的可能。在計算隊列當前Frame數量是不包含lastvp。
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。
我們具體分析下 frame_queue_next() 函數:
/* 釋放當前frame,并更新讀索引rindex,* 當keep_last為1, rindex_show為0時不去更新rindex,也不釋放當前frame */ static void frame_queue_next(FrameQueue *f) {if (f->keep_last && !f->rindex_shown) {f->rindex_shown = 1;// 第?次調?置為1return;}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); }主要步驟:
- 在啟?keeplast時,如果rindex_shown為0則將其設置為1,并返回。此時并不會更新讀索引。也就是 說keeplast機制實質上也會占?著隊列Frame的size,當調?frame_queue_nb_remaining()獲取size 時并不能將其計算?size;
- 釋放Frame對應的數據(?如AVFrame的數據),但不釋放Frame本身
- 更新讀索引
- 釋放喚醒信號,以喚醒正在等待寫?的線程。
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()獲取當前幀
/* 獲取隊列當前Frame, 在調?該函數前先調?frame_queue_nb_remaining確保有frame 可讀 */ static Frame *frame_queue_peek(FrameQueue *f) {return &f->queue[(f->rindex + f->rindex_shown) % f->max_size]; }frame_queue_peek_next()獲取下?幀
/* 獲取當前Frame的下?Frame, 此時要確保queue???少有2個Frame */ static Frame *frame_queue_peek_next(FrameQueue *f) {return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size]; }frame_queue_peek_last()獲取上?幀
/* 獲取last Frame: * 當rindex_shown=0時,和frame_queue_peek效果?樣 * 當rindex_shown=1時,讀取的是已經顯示過的frame */ static Frame *frame_queue_peek_last(FrameQueue *f) {return &f->queue[f->rindex]; }struct AudioParams ?頻參數
typedef struct AudioParams {int freq;// 采樣率int channels;// 通道數int64_t channel_layout;// 通道布局,?如2.1聲道,5.1聲道等enum AVSampleFormat fmt;// ?頻采樣格式,?如AV_SAMPLE_FM T_S16表示為有符號16bit深度,交錯排列模式int frame_size;// ?個采樣單元占?的字節數(?如2通道時,則左右通道各采樣?次合成?個采樣單元)int bytes_per_sec;// ?秒時間的字節數,?如采樣率48Khz,2 channel,16bit,則?秒48000*2*16/8=192000 } AudioParams;struct Decoder解碼器封裝
typedef struct Decoder {AVPacket pkt;PacketQueue *queue;// 數據包隊列AVCodecContext *avctx;// 解碼器上下?int pkt_serial;// 包序列int finished;// =0,解碼器處于?作狀態;=?0,解碼器處于 空閑狀態 int packet_pending;// =0,解碼器處于異常狀態,需要考慮重置解碼 器;=1,解碼器處于正常狀態SDL_cond *empty_queue_cond;// 檢查到packet隊列空時發送 signal緩 存read_thread讀取數據 int64_t start_pts;// 初始化時是stream的start timeAVRational start_pts_tb;// 初始化時是stream的time_baseint64_t next_pts;// 記錄最近?次解碼后的frame的pts,當 解出來的部分幀沒有有效的pts時則使?next_pts進?推算AVRational next_pts_tb;// next_pts的單位SDL_Thread *decoder_tid;// 線程句柄 } Decoder;總結
- 上一篇: 经过了多种方法的尝试,终于找到Quart
- 下一篇: python语言程序设计教程答案赵璐_p