1. 程式人生 > >轉:C語言的面向物件設計-對X264/FFMPEG架構探討

轉:C語言的面向物件設計-對X264/FFMPEG架構探討

https://www.cnblogs.com/xkfz007/articles/2616075.html

本文貢獻給ZSVC開源社群(https://sourceforge.net/projects/zsvc/),他們是來自於中國各高校的年輕學子,是滿懷激情與夢想的人,他們將用自己的勤勞與智慧在世界開源軟體領域為中國留下腳步,該社群提供大量視訊編解碼和影象處理的學習實踐機會,讓廣大參與者能夠體驗理論轉變為實際的過程。歡迎任何感興趣的朋友聯絡他們。

正文

類似題目的文章已經不新鮮了,這裡,我僅僅總結自己的一些程式碼經驗,結合兩款在視訊開發領域比較常用的開源軟體探討C語言的應用問題。

1.為什麼要用C語言

曾幾何時,我也不熟悉C,最早接觸C的是在大學四年級,當時已經學過pascal,過二級也是pascal。接著走上了Delphi的路,多方便的軟體,寫寫畫畫,程式就出來了,本科的畢業設計就是這樣出來的MIS,在當時還很時髦的花哨了一陣,弄了個優秀論文。當有一天,看到別人的程式碼,一行行的,整齊的縮排,而略為難理解的*號,一下被這種語言難住了。於是拿著國內最流行的譚浩強的C書一陣狂讀,雲裡霧裡的。等到了國外,依然要跟那些金髮碧眼的朋友一起讀C語言,但是是為了解決數值運算問題,一面拼命理解難懂的外文,一面偷偷在下面看譚C,所作的作業也是求4階矩陣乘法等等,終於有些實戰機會了,當然也很氣憤那除錯的黑呼呼GCC介面,想著弄個Delphi的美麗除錯介面該多好啊。越往後走,接觸的C程式碼越多,認識些C的程式設計師也越多,終於發現那些自詡厲害的"核心"程式設計師,原來是用C的。記得一次專案的機會,接觸一個在矽谷摸爬了20多年的美國架構師,每次開專案會議都把電腦帶上,一面討論一面敲打些什麼,會議結束,程式碼架構已成。不是流程圖,不是虛擬碼,不是文字,是真真實實的C程式碼。

直到今天,C語言雖然不是使用人數最多的語言了,但是C沒有老去,在很多的核心繫統程式碼裡,依然跑的是設計精美的C,絕大多數的嵌入式開發核心庫軟體是C開發的,多數標準演算法是基於標準C設計的。C語言以其簡潔,靈活和效能優越,依然在核心軟體設計師心目中有不可動搖的地位。

2.為什麼要面向物件

面向物件是一種設計方法,剛開始學習C++的時候,接觸了面向物件的方法,終於對以前寫Delphi系統是託動的圖圖框框的控制元件有了更深入的認識。 及至以後要用Java,更是覺得面向物件的方便,容易理解分析問題。以至於到了現在,每當要寫軟體原型,我的第一反應就是用寫字板來寫Java,然後用javac來命令列編譯,古老的列印除錯。這樣的開發方式好處多多,首先JAVA的庫比較穩定單一,不如C++的龐大繁雜,也沒有記憶體管理的頭痛事務,更重要的是能夠充分發揮面向物件分析,使得自己在單位時間專注做好一個類,最後把這若干的類串起來,程式碼已成。重構,設計模式都不過是日後的優化提煉,沒有必要硬性引入。

面向過程往往被認為是一種嚴格的自頂向下,逐步細分的設計方式,按部就班的大規模設計分解成小的具體實現。而面向物件是基於物件模型對問題域進行描述,更加接近於人們對客觀世界的認識過程。在一般的軟體工程教案中,羅列了如下面向物件的好處:(1)模組化 (2)抽象(3)資訊隱藏(4)弱耦合(5)強內聚(6) 可重用。而這些好處來自於運用面向物件的三個基本方法:封裝、繼承和多型。在實際的軟體工程專案中,正是因為面向物件的這些特性,這種分析方法受到廣泛歡 迎並且繼續保持發展,最近的J2EE專案層出不窮的框架思想正是最好的例子,無論是物件注入還是POJO都給面向物件方法增添更多新的活力。

從【1】 中可以看到面向過程和麵向物件對同一專案分析的簡單舉例,從而得到結論,面向物件是以功能來劃分問題,而不是步驟。在【2】的系列文章中,作者分析瞭如何 用C語言的結構體和函式指標模擬封裝,繼承和多型,並用簡單的實驗分析了效能損失,結果是C語言模擬類的損失可以忽略不計。基於標準C面向物件的程式碼示例 可以從LINUX,GTK等原始碼中看到,本文僅僅分析FFMPEG和X264的基本架構。面向物件是一種高效的分析設計方法,而C語言沒有直接支援面向對 象的語法,用C來模仿C++是沒有必要的,在考慮用C語言構建大型專案的時候,利用面向物件設計,並且適當的構造C語法支援這樣的設計思想是需要的。

3. FFMPEG架構分析

FFMPEG是目前被應用最廣泛的編解碼軟體庫,支援多種流行的編解碼器,它是C語言實現的,不僅被整合到各種PC軟體,也經常被移植到多種嵌入式裝置中。使用面向物件的辦法來設想這樣一個編解碼庫,首先讓人想到的是構造各種編解碼器的類,然後對於它們的抽象基類確定執行資料流的規則,根據演算法轉換 輸入輸出物件。

在實際的程式碼,將這些編解碼器分成encoder/decoder,muxer/demuxer和device三種物件,分別對應於編解碼,輸入輸出格式和裝置。在main函式的開始,就是初始化這三類物件。在avcodec_register_all中,很多編解碼器被註冊,包括視訊的H.264 解碼器和X264編碼器等,

REGISTER_DECODER (H264, h264);
REGISTER_ENCODER (LIBX264, libx264);

找到相關的巨集程式碼如下

#define REGISTER_ENCODER(X,x) { \
          extern AVCodec x##_encoder; \
          if(CONFIG_##X##_ENCODER)  avcodec_register(&x##_encoder); }
#define REGISTER_DECODER(X,x) { \
          extern AVCodec x##_decoder; \
          if(CONFIG_##X##_DECODER)  avcodec_register(&x##_decoder); }

這樣就實際在程式碼中根據CONFIG_##X##_ENCODER這樣的編譯選項來註冊libx264_encoder和 h264_decoder,註冊的過程發生在avcodec_register(AVCodec *codec)函式中,實際上就是向全域性連結串列first_avcodec中加入libx264_encoder、h264_decoder特定的編解碼 器,輸入引數AVCodec是一個結構體,可以理解為編解碼器的基類,其中不僅包含了名稱,id等屬性,而且包含了如下函式指標,讓每個具體的編解碼器擴 展類實現。

    int (*init)(AVCodecContext *);
    int (*encode)(AVCodecContext *, uint8_t *buf, int buf_size, void *data);
    int (*close)(AVCodecContext *);
    int (*decode)(AVCodecContext *, void *outdata, int *outdata_size,
                  const uint8_t *buf, int buf_size);
    void (*flush)(AVCodecContext *);

繼續追蹤libx264,也就是X264的靜態編碼庫,它在FFMPEG編譯的時候被引入作為H.264編碼器。在libx264.c中有如下程式碼

AVCodec libx264_encoder = {
    .name = "libx264",
    .type = CODEC_TYPE_VIDEO,
    .id = CODEC_ID_H264,
    .priv_data_size = sizeof(X264Context),
    .init = X264_init,
    .encode = X264_frame,
    .close = X264_close,
    .capabilities = CODEC_CAP_DELAY,
    .pix_fmts = (enum PixelFormat[]) { PIX_FMT_YUV420P, PIX_FMT_NONE },
    .long_name = NULL_IF_CONFIG_SMALL("libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"),
};

這裡具體對來自AVCodec得屬性和方法賦值。其中
    .init = X264_init,
    .encode = X264_frame,
    .close = X264_close,
將函式指標指向了具體函式,這三個函式將使用libx264靜態庫中提供的API,也就是X264的主要介面函式進行具體實現。pix_fmts定義了所支援的輸入格式,這裡4:2:0
PIX_FMT_YUV420P,   ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)

上面看到的X264Context封裝了X264所需要的上下文管理資料,
typedef struct X264Context {
    x264_param_t params;
    x264_t *enc;
    x264_picture_t pic;
    AVFrame out_pic;
} X264Context;
它 屬於結構體AVCodecContext的void *priv_data變數,定義了每種編解碼器私有的上下文屬性,AVCodecContext也類似上下文基類一樣,還提供其他表示螢幕解析率、量化範 圍等的上下文屬性和rtp_callback等函式指標供編解碼使用。

回到main函式,可以看到完成了各類編解碼器,輸入輸出格式和設備註冊以後,將進行上下文初始化和編解碼引數讀入,然後呼叫av_encode()函式進行具體的編解碼工作。根據該函式的註釋一路檢視其過程:

1. 輸入輸出流初始化。
2. 根據輸入輸出流確定需要的編解碼器,並初始化。
3. 寫輸出檔案的各部分

重點關注一下step2和3,看看怎麼利用前面分析的編解碼器基類來實現多型。大概檢視一下這段程式碼的關係,發現在FFMPEG裡,可以用類圖來表示大概的編解碼器組合。

可以參考【3】來了解這些結構的含義(見附錄)。在這裡會呼叫一系列來自utils.c的函式,這裡的avcodec_open()函式,在開啟編解碼器都會呼叫到,它將執行如下程式碼:
    avctx->codec = codec;
    avctx->codec_id = codec->id;
    avctx->frame_number = 0;
    if(avctx->codec->init){
        ret = avctx->codec->init(avctx);
進行具體適配的編解碼器初始化,而這裡的avctx->codec->init(avctx)就是呼叫AVCodec中函式指標定義的具體初始化函式,例如X264_init。

在 avcodec_encode_video()和avcodec_encode_audio()被output_packet()呼叫進行音視訊編碼,將 同樣利用函式指標avctx->codec->encode()呼叫適配編碼器的編碼函式,如X264_frame進行具體工作。

從上面的分析,我們可以看到FFMPEG怎麼利用面向物件來抽象編解碼器行為,通過組合和繼承關係具體化每個編解碼器實體。設想要在FFMPEG中加入新的解碼器H265,要做的事情如下:
1. 在config編譯配置中加入CONFIG_H265_DECODER
2. 利用巨集註冊H265解碼器
3. 定義AVCodec 265_decoder變數,初始化屬性和函式指標
4. 利用解碼器API具體化265_decoder的init等函式指標

完成以上步驟,就可以把新的解碼器放入FFMPEG,外部的匹配和執行規則由基類的多型實現了。

4. X264架構分析

X264 是一款從2004年有法國大學生髮起的開源H.264編碼器,對PC進行彙編級程式碼優化,捨棄了片組和多參考幀等效能效率比不高的功能來提高編碼效率,它 被FFMPEG作為引入的.264編碼庫,也被移植到很多DSP嵌入平臺。前面第三節已經對FFMPEG中的X264進行舉例分析,這裡將繼續結合 X264框架加深相關內容的瞭解。

檢視程式碼前,還是思考一下對於一款具體的編碼器,怎麼面向物件分析呢?對熵編碼部分對不同演算法的抽象,還有幀內或幀間編碼各種估計演算法的抽象,都可以作為類來構建。

在X264中,我們看到的對外API和上下文變數都宣告在X264.h中,API函式中,關於輔助功能的函式在common.c中定義
void x264_picture_alloc( x264_picture_t *pic, int i_csp, int i_width, int i_height );
void x264_picture_clean( x264_picture_t *pic );
int x264_nal_encode( void *, int *, int b_annexeb, x264_nal_t *nal );
而編碼功能函式定義在encoder.c
x264_t *x264_encoder_open   ( x264_param_t * );
int     x264_encoder_reconfig( x264_t *, x264_param_t * );
int     x264_encoder_headers( x264_t *, x264_nal_t **, int * );
int     x264_encoder_encode ( x264_t *, x264_nal_t **, int *, x264_picture_t *, x264_picture_t * );
void    x264_encoder_close  ( x264_t * );
在x264.c檔案中,有程式的main函式,可以看作做API使用的例子,它也是通過呼叫X264.h中的API和上下文變數來實現實際功能。

X264最重要的記錄上下文資料的結構體x264_t定義在common.h中,它包含了從執行緒控制變數到具體的SPS、PPS、量化矩陣、cabac上下文等所有的H.264編碼相關變數。其中包含如下的結構體
    x264_predict_t      predict_16x16[4+3];
    x264_predict_t      predict_8x8c[4+3];
    x264_predict8x8_t   predict_8x8[9+3];
    x264_predict_t      predict_4x4[9+3];
    x264_predict_8x8_filter_t predict_8x8_filter;

    x264_pixel_function_t pixf;
    x264_mc_functions_t   mc;
    x264_dct_function_t   dctf;
    x264_zigzag_function_t zigzagf;
    x264_quant_function_t quantf;
    x264_deblock_function_t loopf;
跟 蹤檢視可以看到它們或是一個函式指標,或是由函式指標組成的結構,這樣的用法很想面向物件中的interface介面宣告。這些函式指標將在 x264_encoder_open()函式中被初始化,這裡的初始化首先根據CPU的不同提供不同的函式實現程式碼段,很多與可能是彙編實現,以提高程式碼 執行效率。其次把功能相似的函式集中管理,例如類似intra16的4種和intra4的九種預測函式都被用函式指標陣列管理起來。

x264_encoder_encode() 是負責編碼的主要函式,而其內包含的x264_slice_write()負責片層一下的具體編碼,包括了幀內和幀間巨集塊編碼。在這裡,cabac和 cavlc的行為是根據h->param.b_cabac來區別的,分別執行x264_macroblock_write_cabac()和 x264_macroblock_write_cavlc()來寫碼流,在這一部分,功能函式按檔案定義歸類,基本按照編碼流程圖執行,看起來更像面向過 程的寫法,在已經初始化了具體的函式指標,程式就一直按編碼過程的邏輯實現。如果從整體架構來看,x264利用這種類似介面的形式實現了弱耦合和可重用, 利用x264_t這個貫穿始終的上下文,實現資訊封裝和多型。

本文大概分析了FFMPEG/X264的程式碼架構,重點探討用C語言來實現面向物件編碼,雖不至於強行向C++靠攏,但是也各有實現特色,保證實用性。值得規劃C語言軟體專案所借鑑。 

【參考文獻】

1."用例子說明面向物件和麵向過程的區別"
2. liyuming1978,"liyuming1978的專欄"
3. "FFMpeg框架程式碼閱讀"

附錄:節選自【3】

3. 當前muxer/demuxer的匹配
在FFmpeg的檔案轉換過程中,首先要做的就是根據傳入檔案和傳出檔案的字尾名[FIXME]匹配
合適的demuxer和muxer。匹配上的demuxer和muxer都儲存在如下所示,定義在ffmpeg.c裡的
全域性變數file_iformat和file_oformat中:
    static AVInputFormat *file_iformat;
    static AVOutputFormat *file_oformat;

3.1 demuxer匹配
在libavformat\utils.c中的static AVInputFormat *av_probe_input_format2(
AVProbeData *pd, int is_opened, int *score_max)函式用途是根據傳入的probe data資料
,依次呼叫每個demuxer的read_probe介面,來進行該demuxer是否和傳入的檔案內容匹配的
判斷。其呼叫順序如下:
void parse_options(int argc, char **argv, const OptionDef *options,
          void (* parse_arg_function)(const char *));
    static void opt_input_file(const char *filename)
        int av_open_input_file(…… )
            AVInputFormat *av_probe_input_format(AVProbeData *pd,
                                int is_opened)
                static AVInputFormat *av_probe_input_format2(……)
opt_input_file函式是在儲存在const OptionDef options[]陣列中,用於
void parse_options(int argc, char **argv, const OptionDef *options)中解析argv裡的
"-i" 引數,也就是輸入檔名時呼叫的。

3.2 muxer匹配
與demuxer的匹配不同,muxer的匹配是呼叫guess_format函式,根據main() 函式的argv裡的
輸出檔案字尾名來進行的。
void parse_options(int argc, char **argv, const OptionDef *options,
          void (* parse_arg_function)(const char *));
    void parse_arg_file(const char *filename)
        static void opt_output_file(const char *filename)
            AVOutputFormat *guess_format(const char *short_name,
                            const char *filename,
                            const char *mime_type)
3.3 當前encoder/decoder的匹配
在main()函式中除了解析傳入引數並初始化demuxer與muxer的parse_options( )函式以外,
其他的功能都是在av_encode( )函式裡完成的。
在libavcodec\utils.c中有如下二個函式:
    AVCodec *avcodec_find_encoder(enum CodecID id)
    AVCodec *avcodec_find_decoder(enum CodecID id)
他們的功能就是根據傳入的CodecID,找到匹配的encoder和decoder。

在av_encode( )函式的開頭,首先初始化各個AVInputStream和AVOutputStream,然後分別調
用上述二個函式,並將匹配上的encoder與decoder分別儲存在:
AVInputStream->AVStream *st->AVCodecContext *codec->struct AVCodec *codec與
AVOutputStream->AVStream *st->AVCodecContext *codec->struct AVCodec *codec變數。

4. 其他主要資料結構
4.1 AVFormatContext
AVFormatContext是FFMpeg格式轉換過程中實現輸入和輸出功能、儲存相關資料的主要結構。
每一個輸入和輸出檔案,都在如下定義的指標陣列全域性變數中有對應的實體。
    static AVFormatContext *output_files[MAX_FILES];
    static AVFormatContext *input_files[MAX_FILES];
對於輸入和輸出,因為共用的是同一個結構體,所以需要分別對該結構中如下定義的iformat
或oformat成員賦值。
    struct AVInputFormat *iformat;
    struct AVOutputFormat *oformat;
對一個AVFormatContext來說,這二個成員不能同時有值,即一個AVFormatContext不能同時
含有demuxer和muxer。在main( )函式開頭的parse_options( )函式中找到了匹配的muxer和
demuxer之後,根據傳入的argv引數,初始化每個輸入和輸出的AVFormatContext結構,並保
存在相應的output_files和input_files指標陣列中。在av_encode( )函式中,output_files
和input_files是作為函式引數傳入後,在其他地方就沒有用到了。

4.2 AVCodecContext
儲存AVCodec指標和與codec相關資料,如video的width、height,audio的sample rate等。
AVCodecContext中的codec_type,codec_id二個變數對於encoder/decoder的匹配來說,最為
重要。
    enum CodecType codec_type;    /* see CODEC_TYPE_xxx */
    enum CodecID codec_id;        /* see CODEC_ID_xxx */

如上所示,codec_type儲存的是CODEC_TYPE_VIDEO,CODEC_TYPE_AUDIO等媒體型別,
codec_id儲存的是CODEC_ID_FLV1,CODEC_ID_VP6F等編碼方式。

以支援flv格式為例,在前述的av_open_input_file(…… ) 函式中,匹配到正確的
AVInputFormat demuxer後,通過av_open_input_stream( )函式中呼叫AVInputFormat的
read_header介面來執行flvdec.c中的flv_read_header( )函式。在flv_read_header( )函式
內,根據檔案頭中的資料,建立相應的視訊或音訊AVStream,並設定AVStream中
AVCodecContext的正確的codec_type值。codec_id值是在解碼過程中flv_read_packet( )函
數執行時根據每一個packet頭中的資料來設定的。

4.3 AVStream
AVStream結構儲存與資料流相關的編解碼器,資料段等資訊。比較重要的有如下二個成員:
    AVCodecContext *codec; /**< codec context */
    void *priv_data;
其中codec指標儲存的就是上節所述的encoder或decoder結構。priv_data指標儲存的是和具
體編解碼流相關的資料,如下程式碼所示,在ASF的解碼過程中,priv_data儲存的就是
ASFStream結構的資料。
    AVStream *st;
    ASFStream *asf_st; 
    … …
    st->priv_data = asf_st;

4.4 AVInputStream/ AVOutputStream
根據輸入和輸出流的不同,前述的AVStream結構都是封裝在AVInputStream和AVOutputStream
結構中,在av_encode( )函式中使用。AVInputStream中還儲存的有與時間有關的資訊。
AVOutputStream中還儲存有與音視訊同步等相關的資訊。

4.5 AVPacket
AVPacket結構定義如下,其是用於儲存讀取的packet資料。
typedef struct AVPacket {
    int64_t pts;            ///< presentation time stamp in time_base units
    int64_t dts;            ///< decompression time stamp in time_base units
    uint8_t *data;
    int  size;
    int  stream_index;
    int  flags;
    int  duration;        ///< presentation duration in time_base units (0 if not available)
    void (*destruct)(struct AVPacket *);
    void *priv;
    int64_t pos;          ///< byte position in stream, -1 if unknown
} AVPacket;

在av_encode()函式中,呼叫AVInputFormat的
(*read_packet)(struct AVFormatContext *, AVPacket *pkt)介面,讀取輸入檔案的一幀數
據儲存在當前輸入AVFormatContext的AVPacket成員中。