日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

ffplay.c学习-1-框架及数据结构

發布時間:2024/4/11 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ffplay.c学习-1-框架及数据结构 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

ffplay.c學習-1-框架及數據結構


目錄

  • ffplay.c的意義
  • FFplay框架分析
  • 數據結構分析
  • struct VideoState 播放器封裝
  • struct Clock 時鐘封裝
  • struct MyAVPacketList和PacketQueue隊列
  • packet_queue_init()
  • packet_queue_destroy()
  • packet_queue_start()
  • packet_queue_abort()
  • packet_queue_put()
  • packet_queue_get()
  • packet_queue_put_nullpacket()
  • packet_queue_flush()
  • PacketQueue總結
  • struct Frame 和 FrameQueue隊列
  • frame_queue_init() 初始化
  • frame_queue_destory()銷毀
  • frame_queue_peek_writable()獲取可寫Frame
  • frame_queue_push()?隊列
  • frame_queue_peek_writable獲取可寫Frame指針
  • frame_queue_peek_readable() 獲取可讀Fram
  • frame_queue_next()出隊列
  • frame_queue_nb_remaining()獲取隊列的size
  • frame_queue_peek()獲取當前幀
  • frame_queue_peek_next()獲取下?幀
  • frame_queue_peek_last()獲取上?幀
  • struct AudioParams ?頻參數
  • struct Decoder解碼器封裝

  • 1. ffplay.c的意義

  • ffplay.c是FFmpeg源碼?帶的播放器,調?FFmpeg和SDL API實現?個?常有?的播放器。
  • 例如嗶哩嗶哩著名開源項?ijkplayer也是基于ffplay.c進??次開發。
  • ffplay實現了播放器的主體功能,掌握其原理對于我們獨?開發播放器?常有幫助。

  • 2. FFplay框架分析

    1. 播放器初始化

  • 初始化packet queue
  • 初始化frame queue
  • 初始化clock
  • 創建數據讀取線程
  • 2. 線程的劃分

  • 數據讀取線程
  • 打開媒體?件
  • 打開對應碼流的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主線程進?播放控制)
  • 3. packet隊列的設計

  • 線程安全,?持互斥、等待、喚醒
  • 緩存數據??
  • 緩存包數
  • 隊列播放可持續時間
  • 進隊列/出隊列等
  • 4. frame隊列的設計

  • 線程安全,?持互斥、等待、喚醒
  • 緩存幀數
  • ?持讀取數據?不出隊列
  • 進隊列/出隊列等
  • 5. ?視頻同步

  • ?頻同步
  • 視頻同步
  • 外部時鐘同步
  • 6. ?頻處理

  • ?量調節
  • 靜?
  • 重采樣
  • 7. 視頻處理

  • 圖像格式轉換YUV->RGB等
  • 圖像縮放1280720->800480等
  • 8. 播放器控制

  • 播放
  • 暫停
  • 停?
  • 快進/快退
  • 逐幀
  • 靜?

  • 3. 數據結構分析

    1. 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; // 隊列附件,用于mp3等專輯封面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 computationdouble audio_diff_avg_coef;double audio_diff_threshold;int audio_diff_avg_count;// endAVStream *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_sizeint 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; // 音頻TextureSDL_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; // 一幀最大間隔. above this, we consider the jump a timestamp discontinuitystruct 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;SDL_cond *continue_read_thread; // 當讀取數據隊列滿了后進入休眠時,可以通過該condition喚醒讀線程 } VideoState;

    2. struct Clock 時鐘封裝

    // 這里講的系統時鐘 是通過av_gettime_relative()獲取到的時鐘,單位為微妙 typedef struct Clock {double pts; // 時鐘基礎, 當前幀(待播放)顯示時間戳,播放后,當前幀變成上一幀// 當前pts與當前系統時鐘的差值, audio、video對于該值是獨立的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 serialint paused; // = 1 說明是暫停狀態// 指向packet_serialint *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */ } Clock;

    3. 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; // 播放序列號,和MyAVPacketList的serial作用相同,但改變的時序稍微有點不同SDL_mutex *mutex; // 用于維持PacketQueue的多線程安全(SDL_mutex可以按pthread_mutex_t理解)SDL_cond *cond; // 用于讀、寫線程相互通知(SDL_cond可以按pthread_cond_t理解) } PacketQueue;
    1. 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; }
    2. 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); }
    3. 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_pktSDL_UnlockMutex(q->mutex); }
  • flush_pkt定義是 static AVPacket flush_pkt; ,是?個特殊的packet,主要?來作為?連續的兩端數據的“分界”標記:
  • 插? flush_pkt 觸發PacketQueue其對應的serial,加1操作
  • 觸發解碼器清空?身緩存 avcodec_flush_buffers(),以備新序列的數據進?新解碼
  • 4. 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); }
    5. 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++;printf("q->serial = %d\n", 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進?分析。
  • 6. packet_queue_get()
  • 從隊列中取?個節點:
  • /* return < 0 if aborted, 0 if no packet and > 0 if packet. */ /*** @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; //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); //釋放節點內存,只是釋放節點,而不是釋放AVPacketret = 1;break;} else if (!block) { //隊列中沒有數據,且非阻塞調用ret = 0;break;} else { //隊列中沒有數據,且阻塞調用//這里沒有break。for循環的另一個作用是在條件變量滿足后重復上述代碼取出節點SDL_CondWait(q->cond, q->mutex);}}SDL_UnlockMutex(q->mutex); // 釋放鎖return ret; }
    7. 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); }
    8. 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); }
    9. PacketQueue總結
  • 前?我們分析了PacketQueue的實現和主要的操作?法,現在總結下兩個關鍵的點:
  • 1. PacketQueue的內存管理

  • MyAVPacketList的內存是完全由PacketQueue維護的,在put的時候malloc,在get的時候free。
  • AVPacket分兩塊:
  • ?部分是AVPacket結構體的內存,這部分從MyAVPacketList的定義可以看出是和MyAVPacketList共存亡的。
  • 另?部分是AVPacket字段指向的內存,這部分?般通過 av_packet_unref 函數釋放。?般情況下,是在get后由調?者負責? av_packet_unref 函數釋放。特殊的情況是當碰到packet_queue_flush 或put失敗時,這時需要隊列??處理。
  • 2. serial的變化過程:

  • 如上圖所示,左邊是隊頭,右邊是隊尾,從左往右標注了4個節點的serial,以及放?對應節點時queue的serial。
  • 可以看到放?flush_pkt的時候后,serial增加了1.
  • 假設,現在要從隊頭取出?個節點,那么取出的節點是serial 1,?PacketQueue?身的queue已經增?到了2
  • 3. PacketQueue設計思路:
  • 設計?個多線程安全的隊列,保存AVPacket,同時統計隊列內已緩存的數據??。(這個統計數據會?來后續設置要緩存的數據量)
  • 引?serial的概念,區別前后數據包是否連續,主要應?于seek操作
  • 設計了兩類特殊的packet——flush_pkt和nullpkt(類似?于多線程編程的事件模型——往隊列中放?flush事件、放?null事件),我們在?頻輸出、視頻輸出、播放控制等模塊時也會繼續對flush_pkt和nullpkt的作?展開分析。
  • 4. struct Frame 和 FrameQueue隊列

    1. Frame
    /* Common struct for handling all types of decoded data and allocated render buffers. */ // 用于緩存解碼后的數據 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; // 圖像的寬高比(16:9,4:3...),如果未知或未指定則為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變量)。
  • 2. FrameQueue
    /* 這是一個循環隊列,windex是指其中的首元素,rindex是指其中的尾部元素. */ 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時使?
  • 1. frame_queue_init() 初始化
    /* 初始化FrameQueue,視頻和音頻keep_last設置為1,字幕設置為0 */ 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)。
  • 2. 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_unref_item(vp) 釋放本隊列對vp->frame中AVBuffer的引?
  • av_frame_free(&vp->frame) 釋放vp->frame對象本身
  • 3. 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]; }
    4. 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); // 當_readable在等待時則可以喚醒SDL_UnlockMutex(f->mutex); }
  • FrameQueue寫隊列的步驟和PacketQueue不同,分了3步進?:
  • 調?frame_queue_peek_writable獲取可寫的Frame,如果隊列已滿則等待
  • 獲取到Frame后,設置Frame的成員變量
  • 再調?frame_queue_push更新隊列的寫索引,真正將Frame?隊列
  • Frame *frame_queue_peek_writable(FrameQueue *f); // 獲取可寫幀 void frame_queue_push(FrameQueue *f); // 更新寫索引
  • 通過實例看?下寫隊列的?法:
  • 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);av_frame_move_ref(vp->frame, src_frame); // 將src中所有數據拷貝到dst中,并復位src。frame_queue_push(&is->pictq); // 更新寫索引位置return 0; }
  • 上??段代碼是視頻解碼線程向視頻frame_queue中寫??幀的代碼,步驟如下:
  • frame_queue_peek_writable(&is->pictq) 向隊列尾部申請?個可寫的幀空間,若隊列已滿?空間可寫,則等待(由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。
  • frame_queue_push(&is->pictq) 此步僅將frame_queue中的寫索引加1,實際的數據寫?在此步之前已經完成
  • 5. 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可寫喚醒,還是其他喚醒。
  • 6. frame_queue_peek_readable() 獲取可讀Fram
    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]; }
    7. frame_queue_next()出隊列
  • 寫隊列中,應?程序寫??個新幀后通??偸菍懰饕?。?讀隊列中,“讀取”和“更新讀索引(同時刪除舊幀)”?者是獨?的,可以只讀取?不更新讀索引,也可以只更新讀索引(只刪除)?不讀取(只有更新讀索引的時候才真正釋放對應的Frame數據)。?且讀隊列引?了是否保留已顯示的最后?幀的機制,導致讀隊列?寫隊列要復雜很多。
  • 讀隊列和寫隊列步驟是類似的,基本步驟如下:
  • 調?frame_queue_peek_readable獲取可讀Frame;
  • 如果需要更新讀索引(出隊列該節點)則調?frame_queue_peek_next;
  • 讀隊列涉及如下函數
  • 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)
  • 通過實例看?下讀隊列的?法:
  • if (frame_queue_nb_remaining(&is->pictq) == 0) {// 所有幀已顯示// nothing to do, no picture to display in the queue// 什么都不做,隊列中沒有圖像可顯示} else {double last_duration, duration, delay;Frame *vp, *lastvp;/* dequeue the picture */// 從隊列取出上一個Framelastvp = frame_queue_peek_last(&is->pictq);// 上一幀:上次在顯示的幀vp = frame_queue_peek(&is->pictq); // 讀取待顯示的幀// lastvp 上一幀(正在顯示的幀)// vp 等待顯示的幀if (vp->serial != is->videoq.serial) {// 如果不是最新的播放序列,則將其從隊列,以盡快讀取最新的播放序列的Frameframe_queue_next(&is->pictq);goto retry;}.......
  • 上??段代碼是視頻播放線程從視頻frame_queue中讀取視頻幀進?顯示的基本步驟,其他代碼已省略,只保留了讀隊列部分。
  • 記lastvp為上?次已播放的幀,vp為本次待播放的幀,下圖中?框中的數字表示顯示序列中幀的序號:
  • 在啟?keep_last機制后,rindex_shown值總是為1,rindex_shown確保了最后播放的?幀總保留在隊列中。
  • 假設某次進? video_refresh() 的時刻為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
  • 8. 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; // 注意這里為什么要減去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; // 第一次進來沒有更新,對應的frame就沒有釋放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); }
  • 主要步驟:

  • 在啟?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() 的區別僅僅是多了不可讀時等待的操作。
  • 9. 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]; }
    10. frame_queue_peek_next()獲取下?幀
    /* 獲取當前Frame的下一Frame, 此時要確保queue里面至少有2個Frame */ // 不管你什么時候調用,返回來肯定不是 NULL static Frame *frame_queue_peek_next(FrameQueue *f) {return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size]; }
    11. 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]; // 這時候才有意義 }

    5. 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,2 channel,16bit,則一秒48000*2*16/8=192000 } AudioParams;

    6. 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;

    總結

    以上是生活随笔為你收集整理的ffplay.c学习-1-框架及数据结构的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。