struct VideoState 播放器封裝
typedef struct VideoState {SDL_Thread *read_tid; // 讀線程句柄AVInputFormat *iformat; // 指向demuxerint 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 masterdouble audio_clock; // 當前音頻幀的PTS+當前幀Durationint audio_clock_serial; // 播放序列,seek可改變此值// 以下4個參數 非audio master同步方式使用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音頻緩沖區大小(字節為單位)/*指向待播放的一幀音頻數據,指向的數據區將被拷入SDL音頻緩沖區。如果經過重采樣則指向audio_buf1否則指向frame中的音頻*/uint8_t *audio_buf; // 指向需要重采樣的數據uint8_t *audio_buf1; // 指向重采樣后的數據unsigned int audio_buf_size; // 待播放的一幀音頻數據(audio_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_AVFILTERstruct AudioParams audio_filter_src;
#endifstruct AudioParams audio_tgt; // SDL支持的音頻參數,重采樣轉換:audio_src->audio_tgtstruct SwrContext *swr_ctx; // 音頻重采樣contextint 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; // 視頻packet隊列double max_frame_duration; // 一幀最大間隔struct SwsContext *img_convert_ctx; // 視頻尺寸格式變換struct SwsContext *sub_convert_ctx; // 字幕尺寸格式變換int eof; // 是否讀取結束char *filename; // 文件名int width, height, xleft, ytop; // 寬 高 x起始坐標 y起始坐標int step; // =1步進播放模式 =0其他模式#if CONFIG_AVFILTERint vfilter_idx;AVFilterContext *in_video_filter; // the first filter in the video chainAVFilterContext *out_video_filter; // the last filter in the video chainAVFilterContext *in_audio_filter; // the first filter in the audio chainAVFilterContext *out_audio_filter; // the last filter in the audio chainAVFilterGraph *agraph; // audio filter graph
#endif// 保存最近的相應audio video subtitle流的steam indexint last_video_stream, last_audio_stream, last_subtitle_stream;// 當讀取數據隊列滿了后進入休眠時,可以通過該condition喚醒讀現成SDL_cond *continue_read_thread;
} VideoState;
struct Clock 時鐘封裝
typedef struct Clock {/* 時鐘基礎,當前幀(待播放)顯示時間戳,播放后,當前幀變成上一幀 */double pts; /* 當前pts與當前系統時鐘的差值,audio video 對于該值是獨立的 */double pts_drift;// 當前時鐘(如視頻時鐘)最后一次更新時間,也可稱當前時鐘時間double last_updated;double speed; // 時鐘速度控制,用于控制播放速度int serial; /* clock is based on a packet with this serial */int paused; // =1 說明是暫停狀態// 指向packet_serialint *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
struct MyAVPacketList 和 PacketQueue 隊列
ffplay 用 PacketQueue 保存解封裝后的數據,即保存 AVPacket
ffplay 首先定義了一個結構體 MyAVPacketList
:
typedef struct MyAVPacketList {AVPacket pkt; // 解封裝后的數據struct MyAVPacketList *next; // 下一個節點int serial; // 播放序列
} 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; // 播放序列號和上文作用相同SDL_mutex *mutex; // 用于維持PacketQueue的多線程安全SDL_cond *cond; // 用于讀,寫線程互相通知
} PacketQueue;
該結構體內定義了“隊列”?身的屬性。上?的注釋對每個字段作了簡單的介紹,這?也看到了serial字段, MyAVPacketList的serial字段的賦值來?PacketQueue的serial,每個PacketQueue的serial是獨?的。
接下來我們也從隊列的操作函數具體分析各字段的含義:
<font style="color:rgb(38,38,38);">PacketQueue</font>
操作提供以下方法:
- 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; // 在packete_queue_start和packet_queue_abort時修改該值return 0;
}
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;pkt1->pkt = *pkt; // 拷貝AVPacket(淺拷貝,AVPacket.data等內存并沒有拷貝)pkt1->next = NULL;if (pkt == &flush_pkt) // 如果放入的是flush_pkt,需要增加隊列的播放序列號,區分不同兩段數據q->serial++;pkt1->serial = q->serial; // 用隊列序列號標記節點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 主要完成三件事:
- 計算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*/
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;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; // 時長扣掉一個節點*pkt = pkt1->pkt;if (serial)*serial = pkt1->serial;av_free(pkt1); // 釋放節點內存,只釋放節點,不釋放AVPacketret = 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個步驟:
- 隊列操作:出隊列操作; nb_packets相應-1; duration 的也相應減少, size也相應占?的字節
??(pkt1->pkt.size + sizeof(*pkt1)) - 給輸出參數賦值:就是MyAVPacketList的成員傳遞給輸出參數pkt和serial
- 釋放節點內存:釋放放?隊列時申請的節點內存(注意是節點內存?不是AVPacket的數據的內存)
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之前緩存的節點數據,以便插?新節點數據
static void packet_queue_flush(PacketQueue *q)
{MyAVPacketList *pkt, *pkt1;SDL_LockMutex(q->mutex);for (pkt = q->first_pkt; pkt; pkt = pkt1) {pkt1 = pkt->next;av_packet_unref(&pkt->pkt);av_freep(&pkt);}q->last_pkt = NULL;q->first_pkt = NULL;q->nb_packets = 0;q->size = 0;q->duration = 0;SDL_UnlockMutex(q->mutex);
}
函數主體的for循環是隊列遍歷,遍歷過程釋放節點和AVPacket(AVpacket對應的數據也被釋放掉)。最后
將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設計思路:
- 設計?個多線程安全的隊列,保存AVPacket,同時統計隊列內已緩存的數據??。(這個統計數據會?來后續設置要緩存的數據量)
- 引?serial的概念,區別前后數據包是否連續,主要應?于seek操作。
- 設計了兩類特殊的packet——flush_pkt和nullpkt(類似?于多線程編程的事件模型——往隊列中放? flush事件、放?null事件),我們在?頻輸出、視頻輸出、播放控制等模塊時也會繼續對flush_pkt和 nullpkt的作?展開分析。
struct Frame 和 FrameQueue 隊列
struct 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 AVSampleFormatAVRational sar; // 圖像寬高比,默認0/1int 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變量)。
struct FrameQueue
typedef struct FrameQueue {Frame queue[FRAME_QUEUE_SIZE]; // FRAME_QUEUE_SIZE最大sizeint 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(<font style="color:rgb(38,38,38);">f->queue[i].frame</font>
)分配內
存,注意只是分配Frame對象本身,?不關注Frame中的數據緩沖區。Frame中的數據緩沖區是
AVBuffer,使?引?計數機制。
<font style="color:rgb(38,38,38);">f->max_size</font>
是隊列的??,此處值為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))
<font style="color:rgb(38,38,38);">f->keep_last</font>
是隊列中是否保留最后?次播放的幀的標志。<font style="color:rgb(38,38,38);">f->keep_last = !!keep_last</font>
是將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->frameav_frame_free(&vp->frame);}SDL_DestroyMutex(f->mutex);SDL_DestroyCond(f->cond);
}
隊列銷毀函數對隊列中的每個節點作了如下處理:
<font style="color:rgb(38,38,38);">frame_queue_unref_item(vp)</font>
釋放本隊列對vp->frame中AVBuffer的引?<font style="color:rgb(38,38,38);">av_frame_free(&vp->frame)</font>
釋放vp->frame對象本身
frame_queue_peek_writable()獲取可寫 Frame
frame_queue_push()入隊列
FrameQueue寫隊列的步驟和PacketQueue不同,分了3步進?:
- 調?frame_queue_peek_writable獲取可寫的Frame,如果隊列已滿則等待
- 獲取到Frame后,設置Frame的成員變量
- 再調?frame_queue_push更新隊列的寫索引,真正將Frame?隊列
// 獲取可寫幀
/*
向隊列尾部申請?個可寫的幀空間,若?空間可寫,則等待。
這?最需要體會到的是abort_request的使?,在等待時如果播放器需要退出則將abort_request = 1,那
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];
}
// 更新寫索引
static void frame_queue_push(FrameQueue *f)
{if (++f->windex == f->max_size)f->windex = 0;SDL_LockMutex(f->mutex); // 當frame_queue_peek_readable等待時可以喚醒f->size++;SDL_CondSignal(f->cond);SDL_UnlockMutex(f->mutex);
}
/*
向隊列尾部壓??幀,只更新計數與寫指針,因此調?此函數前應將幀數據寫?隊列相應位
置。SDL_CondSignal(f->cond);可以喚醒讀frame_queue_peek_readable。
*/
通過具體場景看寫隊列的用法:
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);
#endif// frame_queue_peek_writable監測隊列是否有可寫空間if (!(vp = frame_queue_peek_writable(&is->pictq)))return -1; // 隊列滿了返回-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中寫??幀的代碼,步驟如下:
<font style="color:rgb(38,38,38);">frame_queue_peek_writable(&is->pictq) </font>
向隊列尾部申請?個可寫的幀空間,若隊列已滿 ?空間可寫,則等待(由SDL_cond *cond控制,由frame_queue_next或frame_queue_signal觸發喚醒)- 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。
<font style="color:rgb(38,38,38);">frame_queue_push(&is->pictq)</font>
此步僅將frame_queue中的寫索引加1,實際的數據寫?在此步之前已經完成。
frame_queue_peek_readable()獲取可讀 Frame
frame_queue_next()出隊列
寫隊列中,應?程序寫??個新幀后通常總是將寫索引加1。?讀隊列中,“讀取”和“更新讀索引(同時刪除 舊幀)”?者是獨?的,可以只讀取?不更新讀索引,也可以只更新讀索引(只刪除)?不讀取(只有更新讀索引的時候才真正釋放對應的Frame數據)。?且讀隊列引?了是否保留已顯示的最后?幀的機制,導致讀隊列?寫隊列要復雜很多。
讀隊列和寫隊列步驟是類似的,基本步驟如下:
- 調?frame_queue_peek_readable獲取可讀Frame;
- 如果需要更新讀索引(出隊列該節點)則調?frame_queue_peek_next;
讀隊列涉及如下函數:
// 獲取可讀Framezhizhen(讀空則等待)
static Frame *frame_queue_peek_readable(FrameQueue *f);
// 獲取當前Frame指針
static Frame *frame_queue_peek(FrameQueue *f);
// 獲取下一個Frame指針
static Frame *frame_queue_peek_next(FrameQueue *f);
// 獲取上一個Frame指針
static Frame *frame_queue_peek_last(FrameQueue *f);
// 更新讀索引,刪除舊frame
static void frame_queue_next(FrameQueue *f);
通過實例看一下讀隊列的用法:
static void video_refresh(void *opaque, double *remaining_time)
{if (frame_queue_nb_remaining(&is->pictq) == 0) /* 所有幀已顯示 */ {} else {double last_duration, duration, delay;Frame *vp, *lastvp;// 上一幀:上一個已顯示的幀lastvp = frame_queue_peek_last(&is->pictq);// 當前幀:當前待顯示的幀vp = frame_queue_peek(&is->pictq);if (vp->serial != is->videoq.serial) {// 出隊列,更新rindexframe_queue_next(&is->pictq);goto retry;}}
}
記lastvp為上?次已播放的幀,vp為本次待播放的幀,下圖中?框中的數字表示顯示序列中幀 的序號:
在啟?keep_last機制后,rindex_shown值總是為1,rindex_shown確保了最后播放的?幀總保留在隊列中。
假設某次進?<font style="color:rgb(38,38,38);">video_refresh()</font>
的時刻為T0,下次進?的時刻為T1。在T0時刻,讀隊列的步驟如下:
- rindex表示上?次播放的幀lastvp,本次調? video_refresh() 中,lastvp會被刪除,rindex會加 1,即是當調?frame_queue_next刪除的是lastvp,?不是當前的vp,當前的vp轉為lastvp。
- rindex+rindex_shown表示本次待播放的幀vp,本次調? video_refresh() 中,vp會被讀出播放 圖中已播放的幀是灰??框,本次待播放的幀是紅??框,其他未播放的幀是綠??框,隊列中空位置為???框。
- rindex+rindex_shown+1表示下?幀nextvp
frame_queue_nb_remaining()獲取 size
static int frame_queue_nb_remaining(FrameQueue *f)
{return f->size - f->rindex_shown;
}
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()獲取下一幀
// 使用時需要確保queue里面至少兩個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()獲取上一幀
// 當rindex_shown=0時,和frame_queue_peek效果一樣,獲取當前幀
// 當rindex_show=1時,讀取上一個
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_FMT_S16表示16bit交錯排列int frame_size; // 一個采樣單元占用的字節數,比如2通道則左右各采樣一次合成一個采樣單元int bytes_per_sec; // ?秒時間的字節數,?如采樣率48Khz,2channel,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;
參考資料:https://github.com/0voice