ffplay整體框架

才高八斗船帆ub 發佈 2022-12-08T10:43:30.110033+00:00

雖說ffplay是一個簡單的播放器,但是其實內部一點也不簡單,其實筆者也不知道說它簡單的理由是什麼,是因為它只有一個點c文件?

前言

雖說ffplay是一個簡單的播放器,但是其實內部一點也不簡單,其實筆者也不知道說它簡單的理由是什麼,是因為它只有一個點c文件???

ffplay內部細節繁多,想要深入分析不單單要掌握音視頻的相關概念,還要掌握多線程等相關知識,但是不得不說ffplay確實是學習的播放器開發的一個最佳例子。

建議想要學習ffplay的童鞋們集成後邊閱讀邊增加注釋,多閱讀幾次,相信你每次閱讀都會有不同的理解與收穫...

本文使用的ffplay.c的版本是搭配FFmpeg5.0的版本。

ffplay代碼大致架構

關於fplay的架構很難三言兩語說得清楚,而且本人對它的理解也不是很深,加上行筆比較囉嗦,可能就更加描述不清了,因此筆者製作了一張圖,這張圖描述了ffplay的三大子線程工作內容,當然如果加上字幕的話有四個子線程。

圖導出的可能有點模糊,再加上上傳圖床後不知道有沒有更加模糊了,想要高清大圖的可以後台留言,加v信索取。

上圖只是簡單地畫出了ffplay的大體架構,不同版本的代碼API名稱可能有點不同,但是大致思路肯定是一致的。這個圖也忽略了一些細節,比如解碼線程與讀取線程的同步處理等。

ffplay關鍵數據結構

1、VideoState

VideoState可以說是ffplay的內部管家了,幾乎能拿到ffplay內部的所有變量以及相關狀態。下面是筆者加了注釋的的VideoState源碼:


【騰訊文檔】FFmpegWebRTCRTMPrtspHLSRTP播放器-音視頻流媒體高級開發-資料領取FFmpegWebRTCRTMPRTSPHLSRTP鎾斁鍣�-闊寵棰戞祦濯掍綋楂樼駭寮€鍙�-璧勬枡棰嗗彇


/**
 * 統領全局的結構體,幾乎能拿到ffplay內部所有的變量
 */
typedef struct VideoState {
    SDL_Thread *read_tid;  // 資源讀取線程
    const AVInputFormat *iformat; // 解封裝輸入格式
    int abort_request; // 是否退出
    int force_refresh;  // =1時需要刷新畫面,請求立即刷新畫面的意思
    int paused;  // =1時暫停,=0時播放
    int last_paused; // 存取了paused狀態,是因為多線程???
    int queue_attachments_req;  // 隊列附件請求,比如一些mp3是帶有專輯封面的
    int seek_req;  // 表示是否進行了seek請求
    int seek_flags;  // seek類型,有些封裝格式是按照字節seek,有些是按照播放時間seek
    int64_t seek_pos; // seek位置,當前位置+增量
    int64_t seek_rel; // seek增量
    int read_pause_return; // rtsp等流協議控制???
    AVFormatContext *ic; // 解封裝上下文
    int realtime; // =1為實時流  直播還是點播?

    Clock audclk; // 音頻時鐘 每個流都需要有自己獨立的時鐘,然後在同步時拿自己的時鐘去與別人的比較
    Clock vidclk; // 視頻時鐘
    Clock extclk; // 外部時鐘

    FrameQueue pictq; // 視頻幀隊列
    FrameQueue subpq; // 字幕
    FrameQueue sampq; // 音頻幀隊列

    Decoder auddec; // 音頻解碼器
    Decoder viddec; // 視頻解碼器
    Decoder subdec; // 字幕解碼器

    int audio_stream; // 音頻流索引

    int av_sync_type; // 音視頻同步的類型,以音頻為基準?是視頻為基準?以外部時鐘為基準?

    double audio_clock; // 當前音頻幀的PTS+當前幀Duration
    int audio_clock_serial; // 當前音頻幀的播放序列,seek可改變此值
    double audio_diff_cum; /* used for AV difference average computation */
    double audio_diff_avg_coef;
    double audio_diff_threshold;
    int audio_diff_avg_count;
    AVStream *audio_st; // 音頻流
    PacketQueue audioq; // 音頻包隊列
    int audio_hw_buf_size; // 下面個是SDL播放相關
    uint8_t *audio_buf;
    uint8_t *audio_buf1;
    unsigned int audio_buf_size; /* in bytes */
    unsigned int audio_buf1_size;
    int audio_buf_index; /* in bytes */
    int audio_write_buf_size;
    int audio_volume;
    int muted;
    struct AudioParams audio_src;
#if CONFIG_AVFILTER
    struct AudioParams audio_filter_src;
#endif
    struct AudioParams audio_tgt;
    struct SwrContext *swr_ctx;
    int Frame_drops_early;
    int frame_drops_late;

    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;

    double frame_timer; // 最後一幀播放的時刻
    double frame_last_returned_time; //上一次返回時間
    double frame_last_filter_delay;
    int video_stream; // 視頻流索引
    AVStream *video_st; // 視頻流
    PacketQueue videoq; // 視頻包隊列
    double max_frame_duration;    // 一幀最大的間隔,音視頻同步時使用  // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
    struct SwsContext *img_convert_ctx; // 視頻尺寸變化上下文
    struct SwsContext *sub_convert_ctx;
    int eof; // 文件是否讀取結束

    char *filename;
    int width, height, xleft, ytop;
    int step; // =1 步進播放模式,暫停狀態下seek需要更新seek成功的哪一幀 =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 video chain
    AVFilterContext *in_audio_filter;   // the first filter in the audio chain
    AVFilterContext *out_audio_filter;  // the last filter in the audio chain
    AVFilterGraph *agraph;              // audio filter graph
#endif

    int last_video_stream, last_audio_stream, last_subtitle_stream;

    SDL_cond *continue_read_thread;
} VideoState;

關於SDL相關的,以及一些filter相關的筆者忽略了,因為從本質上講它們不屬於ffplay的核心內容。

2、 MyAVPacketList

MyAVPacketList是表示待解碼數據包的數據內容。

// 未解碼的包隊列節點數據、增加了一個播放序列 serial 欄位而已
typedef struct MyAVPacketList {
    AVPacket *pkt;
    int serial;
} MyAVPacketList;

在新版本中筆者感覺這個結構體有些雞肋,因為它是搭配PacketQueue使用的,但是PacketQueue又沒有直接使用它,而是拷貝了它的數據...

3、PacketQueue

PacketQueue表示待解碼包的隊列,就是從流文件中讀取到的數據包,然後將它們放入到這個容器中去,然後等待解碼線程獲取進行消費,涉及到線程安全、互斥量、條件變量等。

// 未解碼數據包隊列
typedef struct PacketQueue {
    AVFifoBuffer *pkt_list; // 包內容,先進先出隊列
    int nb_packets; // 隊列長度,隊列有幾個包
    int size; // 隊列所有元素大小之和
    int64_t duration; // 隊列所有包所持續播放的時間之和
    int abort_request; // 是否退出了
    int serial; // 播放序列
    SDL_mutex *mutex; // 同步互斥量
    SDL_cond *cond; // 條件變量
} PacketQueue;

4、Clock

時鐘是ffplay中一個很重要的概念,沒了它音視頻同步就沒法做。音頻和視頻都有各自的時鐘,在同步的時候以參考系看自己到底是快了還是慢了及時作出校正。

這個看不懂不要緊,後面在音視頻同步中我們再詳細介紹下這個結構體。

// 時鐘/同步時鐘
typedef struct Clock {
    double pts;       // 當前正在播放的幀的pts    /* clock base */
    double pts_drift;   // 當前的pts與系統時間的差值  保持設置pts時候的差值,後面就可以利用這個差值推算下一個pts播放的時間點
    double last_updated; // 最後一次更新時鐘的時間,應該是一個系統時間吧?
    double speed;  // 播放速度控制
    int serial;     // 播放序列      /* clock is based on a packet with this serial */
    int paused;  // 是否暫停
    int *queue_serial;   // 隊列的播放序列 PacketQueue中的 serial /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

5、Frame

Frame表示解碼後的數據幀,它內部封裝了ffmpeg的結構體AVFrame。

/**
 * 解碼後的數據幀
 */
typedef struct Frame {
    AVFrame *frame;
    AVSubtitle sub; // 字幕
    int serial;   // 播放序列
    double pts;    // 當前幀指向的pts 時間戳,單位為秒       /* presentation timestamp for the frame */
    double duration;  // 當前幀所持續的時間    /* estimated duration of the frame */
    int64_t pos;      // 該幀在文件中的字節位置    /* byte position of the frame in the input file */
    int width;     // 寬度,感覺和*frame裡面的冗餘了....
    int height;
    int format;   // 圖像或聲音格式
    AVRational sar; // 圖像寬高比 如果未知或未指定則為0/1
    int uploaded;  // 用來記錄該幀是否已經顯示過?
    int flip_v;   // =1則旋轉180, = 0則正常播放
} Frame;

6、FrameQueue

FrameQueue解釋解碼後的待播放的數據幀隊列,它是環形的隊列,通過索引進行操作內部元素,涉及到線程安全、互斥量、條件變量等。

/**
 * 幀隊列,環形隊列,播放時並不會刪除,移動索引指針
 */
typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex; // 讀索引
    int windex; // 寫索引
    int size; // 隊列中總幀數量
    int max_size; // 隊列最大的容量
    int keep_last; // 是否保持最後一幀不釋放,= 1說明要在隊列裡面保持最後一幀的數據不釋放,只在銷毀隊列的時候才將其真正釋放
    int rindex_shown; // 初始化為0,配合keep_last=1使用,要麼為0要麼為1
    SDL_mutex *mutex;
    SDL_cond *cond;
    PacketQueue *pktq; // 包隊列,為了獲取包裡面的播放序列???
} FrameQueue;

7、Decoder

Decoder是ffplay封裝的解碼器結構體,其實內部是調用了ffmpeg的API進行解碼音頻、視頻、字幕等,內部封裝了子線程。

/**
 *視頻、音頻、字幕解碼器
 */
typedef struct Decoder {
    AVPacket *pkt; // 緩衝包
    PacketQueue *queue; // 待解碼隊列
    AVCodecContext *avctx; // 解碼器上下文
    int pkt_serial; // 解碼序列
    int finished;
    int packet_pending; // 是否有緩衝包,就是是否有緩衝的*pkt變量需要處理
    SDL_cond *empty_queue_cond;
    int64_t start_pts;  // 初始化時是stream的start time
    AVRational start_pts_tb; // 初始化時是stream的time_base
    int64_t next_pts; //  記錄最近一次解碼後的frame的pts,當解出來的部分幀沒有有效的pts時則使用next_pts進行推算
    AVRational next_pts_tb; // next_pts的單位
    SDL_Thread *decoder_tid; // 解碼線程
} Decoder;

關於ffpaly的整體架構今天就先介紹到這裡,關於ffplay內部的其他細節問題,我們將在後面繼續介紹,敬請關注。

播放序列

在上面的數據結構的介紹中我們看到幾乎每個結構體都含有一個類似於serial這樣的播放序列的欄位,那麼這個播放序列的欄位是幹啥用的呢?

所謂的播放序列是指一個連續的播放過程,比如播放了一個媒體流一直沒有進行操作就是一個播放序列,一旦操作了播放位置就是另外一個播放序列了,比如說開始播放的時候播放序列是0,進行了seek 操作之後播放序列就不是0了,會變成1,每次seek之後播放序列都會加1,然後在讀取線程、解碼線程、SDL顯示過程中都進行當前的數據包、數據幀是否是屬於最新的播放序列的內容的判斷,如果不屬於的話將被直接丟棄掉, 如果是屬於當前播放序列的話就進行入隊或解碼或播放等相關操作。

關鍵字: