ffplay數據結構分析

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個步驟:

  1. 隊列操作:出隊列操作; nb_packets相應-1; duration 的也相應減少, size也相應占?的字節
    ??(pkt1->pkt.size + sizeof(*pkt1))
  2. 給輸出參數賦值:就是MyAVPacketList的成員傳遞給輸出參數pkt和serial
  3. 釋放節點內存:釋放放?隊列時申請的節點內存(注意是節點內存?不是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設計思路:

  1. 設計?個多線程安全的隊列,保存AVPacket,同時統計隊列內已緩存的數據??。(這個統計數據會?來后續設置要緩存的數據量)
  2. 引?serial的概念,區別前后數據包是否連續,主要應?于seek操作。
  3. 設計了兩類特殊的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);
}

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

  1. <font style="color:rgb(38,38,38);">frame_queue_unref_item(vp)</font>釋放本隊列對vp->frame中AVBuffer的引?
  2. <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步進?:

  1. 調?frame_queue_peek_writable獲取可寫的Frame,如果隊列已滿則等待
  2. 獲取到Frame后,設置Frame的成員變量
  3. 再調?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中寫??幀的代碼,步驟如下:

  1. <font style="color:rgb(38,38,38);">frame_queue_peek_writable(&is->pictq) </font>向隊列尾部申請?個可寫的幀空間,若隊列已滿 ?空間可寫,則等待(由SDL_cond *cond控制,由frame_queue_next或frame_queue_signal觸發喚醒)
  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. <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數據)。?且讀隊列引?了是否保留已顯示的最后?幀的機制,導致讀隊列?寫隊列要復雜很多。

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

  1. 調?frame_queue_peek_readable獲取可讀Frame;
  2. 如果需要更新讀索引(出隊列該節點)則調?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時刻,讀隊列的步驟如下:

  1. rindex表示上?次播放的幀lastvp,本次調? video_refresh() 中,lastvp會被刪除,rindex會加 1,即是當調?frame_queue_next刪除的是lastvp,?不是當前的vp,當前的vp轉為lastvp。
  2. rindex+rindex_shown表示本次待播放的幀vp,本次調? video_refresh() 中,vp會被讀出播放 圖中已播放的幀是灰??框,本次待播放的幀是紅??框,其他未播放的幀是綠??框,隊列中空位置為???框。
  3. 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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/922385.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/922385.shtml
英文地址,請注明出處:http://en.pswp.cn/news/922385.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

OpenCV:銀行卡號識別

目錄 一、項目原理與核心技術 二、環境準備與工具包導入 1. 環境依賴 2. 工具包導入 三、自定義工具類 myutils.py 實現 四、主程序核心流程&#xff08;銀行卡識別.py&#xff09; 1. 命令行參數設置 2. 銀行卡類型映射 3. 輔助函數&#xff1a;圖像展示 五、步驟 1…

基于spark的澳洲光伏發電站選址預測

基于spark的澳洲光伏發電站選址預測項目概況 [&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;] 點這里,查看所有項目 [&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x…

Kibana 雙棧網絡(Dual-Stack)支持能力評估

#作者&#xff1a;Unstopabler 文章目錄一&#xff0e;測試目標二&#xff0e;測試環境三&#xff0e;Kibana1、查詢 Kibana pod信息2、查詢Kibana service信息3、Kibana service 設置四&#xff0e;驗證測試1、Kibana 監聽參數設置2、Kibana節點IPv4狀態檢查3、Kibana節點IPv6…

標準CAN幀介紹

標準CAN幀介紹標準CAN&#xff08;Controller Area Network&#xff09;結構1.幀起始&#xff08;SOF-Start Of Frame&#xff09;2.仲裁段&#xff08;Arbitration Field&#xff09;3.控制段&#xff08;Control Field&#xff09;4.數據段&#xff08;Data Field&#xff09…

easyPoi實現動表頭Excel的導入和導出

easyPoi實現動表頭Excel的導入和導出 Maven依賴 !-- EasyPoi 核心依賴 --><dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-base</artifactId><version>4.4.0</version></dependency><!-- EasyPoi Web…

瘋狂星期四文案網第67天運營日記

網站運營第67天&#xff0c;點擊觀站&#xff1a; 瘋狂星期四 crazy-thursday.com 全網最全的瘋狂星期四文案網站 運營報告 今日訪問量 今日搜索引擎收錄情況

CAS理解

CAS&#xff08;Compare And Swap&#xff09; 是非阻塞同步的實現原理&#xff0c;它是CPU硬件層面的一種指令&#xff1b; CAS制定操作包含三個參數 內存值&#xff08;內存地址&#xff09;v預期值E新增值N 當CAS指令執行時&#xff0c;當且僅當預期值E和內存值V相同時&…

【SQL】指定日期的產品價格

目錄 題目 分析 代碼 題目 產品數據表: Products ------------------------ | Column Name | Type | ------------------------ | product_id | int | | new_price | int | | change_date | date | ------------------------ (product_id, chang…

《突破Unity+騰訊云聯機瓶頸:多人游戲同步延遲與數據安全的雙維度優化》

在Unity開發的多人聯機游戲中&#xff0c;騰訊云的云服務器&#xff08;CVM&#xff09;、游戲多媒體引擎&#xff08;GME&#xff09;與云數據庫&#xff08;CDB&#xff09;共同構成了聯機體驗的核心支撐。但隨著玩家并發量提升與游戲玩法復雜度增加&#xff0c;“實時同步延…

BisenetV1/2網絡以及模型推理轉換

BisenetV1/2網絡以及模型推理轉換 文章目錄BisenetV1/2網絡以及模型推理轉換1 BiSenetV11.1 Contex Path1.2 Spatial Path1.3 ARM1.4 FFM1.5 backbone2 模型推理代碼流程分析2.1 加載模型2.2 模型推理① 轉換張量② 輸入尺寸調整③ 模型推理④ 輸出尺寸還原⑤ 類別預測⑥ 保存繪…

Android開發-文本輸入

一、EditText 基礎&#xff1a;不僅僅是輸入框EditText 是 TextView 的子類&#xff0c;允許用戶輸入和編輯文本。1. 基本布局<EditTextandroid:id"id/et_username"android:layout_width"match_parent"android:layout_height"wrap_content"an…

數據化存儲菜單,國際化方案

djangoclass Menu(models.Model):parent_id models.BigIntegerField(default0, verbose_name父菜單ID)name models.CharField(max_length50, verbose_name菜單名稱)icon models.CharField(max_length50, blankTrue, nullTrue, verbose_name菜單圖標)path models.CharField(…

SQL-用戶管理與操作權限

在 SQL 中&#xff0c;用戶管理和權限操作是數據庫安全管理的核心組成部分&#xff0c;用于控制 “誰能訪問數據庫” 以及 “能對數據庫做什么”。它們共同保障數據庫的安全性、完整性和合規性。一、用戶管理&#xff1a;控制 “誰能訪問數據庫”用戶管理是指對數據庫用戶的創建…

計算機視覺案例分享之答題卡識別

目錄 一、基本流程 二、代碼實現 1. 導入工具包和定義常量 2. 輔助函數定義 2.1 坐標點排序函數 2.2 透視變換函數 2.3 輪廓排序函數 2.4 圖像顯示函數 3. 主程序處理流程 3.1 圖像預處理 3.2 輪廓檢測與透視變換 3.3 閾值處理與選項檢測 3.4 答案識別與評分 我們…

Java面試問題記錄(四)

四、設計模式1、設計模式6大原則1&#xff09;單一職責(一個類和方法只做一件事)、2&#xff09;里氏替換(多態&#xff0c;子類可擴展父類)、3&#xff09;依賴倒置(細節依賴抽象&#xff0c;下層依賴上層)、4&#xff09;接口隔離(建立單一接口)、迪米特原則(最少知道&#x…

高等教育學

高等教育學第一章 高等教育與高等教育學第二章 高等教育發展史2-1西方高等教育發展史2-2中國高等教育發展史第三章 高等教育理念3.1-王一軍-高等教育理念的構成要素3.2-王一軍-高等教育理念的主要流派第四章 高等學校教育4.1 高等學校教育制度4.2-陳何芳-高等教育辦學體制 &…

unordered_map使用MFC的CString作為鍵值遇到C2056和C2064錯誤

文章目錄unordered_map使用MFC的CString作為鍵值遇到C2056和C2064錯誤問題出現的背景解決方案總結unordered_map使用MFC的CString作為鍵值遇到C2056和C2064錯誤 問題出現的背景 在我的一個老工程項目中&#xff0c;使用C的std::unordered_map時&#xff0c;使用了MFC的CStrin…

Maven 本地倉庫的 settings.xml 文件

本地倉庫目錄位置&#xff1a;C:/用戶/用戶名/.m2/repository 需要修改配置&#xff0c;具體的修改方法請看 ↓↓↓ 2024版 IDEA 用 Maven 創建 java 項目&#xff08;Maven 安裝和配置&#xff09; <?xml version"1.0" encoding"UTF-8"?><!…

vue動畫內置組件

文章目錄vue動畫的官方類名EnterLeaveTransition組件注意事項觸發實例TransitionGroup組件注意事項觸發機制實例拓展vue動畫的官方類名 如下來自vue官方文檔&#xff0c;提供了dom元素&#xff0c;插入Enter和刪除Leave的類名 Enter v-enter-from&#xff1a;進入動畫的起始…

軟考中級信息安全與病毒防護知識點

### 一、核心知識點梳理這部分內容可以大致分為三個方面&#xff1a;**信息安全基本概念**、**加解密技術** 和 **惡意代碼&#xff08;病毒&#xff09;防護**。#### 1. 信息安全的基本目標&#xff08;CIA三元組&#xff09; 這是所有信息安全問題的基石&#xff0c;必須熟練…