前言

眾所周知,FFmpeg 在解碼的時候,無論輸入檔案是 MP4 檔案還是 FLV 檔案,或者其它檔案格式,都能正確解封裝、解碼,而程式碼不需要針對不同的格式做出任何改變,這是面向物件中很常見的多型特性,但 FFmpeg 是用 C 語言編寫的,那麼它是如何使用 C 語言實現了多型特性的呢?

要解決這個問題,首先需要從函式 av_register_all 說起。

av_register_all

av_register_all 是幾乎所有 FFmpeg 程式中第一個被呼叫的函式,用於註冊在編譯 FFmpeg 時設定了 --enable 選項的封裝器、解封裝器、編碼HX器、解碼HX器等。原始碼如下:

#define REGISTER_MUXER(X, x)                                            \
    {                                                                   \
        extern AVOutputFormat ff_##x##_muxer;                           \
        if (CONFIG_##X##_MUXER)                                         \
            av_register_output_format(&ff_##x##_muxer);                 \
    }

#define REGISTER_DEMUXER(X, x)                                          \
    {                                                                   \
        extern AVInputFormat ff_##x##_demuxer;                          \
        if (CONFIG_##X##_DEMUXER)                                       \
            av_register_input_format(&ff_##x##_demuxer);                \
    }

#define REGISTER_MUXDEMUX(X, x) REGISTER_MUXER(X, x); REGISTER_DEMUXER(X, x)

static void register_all(void)
{
    // 註冊編解碼HX器
    avcodec_register_all();

    // 註冊封裝器、解封裝器
    /* (de)muxers */
    REGISTER_MUXER   (A64,              a64);
    REGISTER_DEMUXER (AA,               aa);
    REGISTER_DEMUXER (AAC,              aac);
    REGISTER_MUXDEMUX(AC3,              ac3);
    REGISTER_MUXDEMUX(FLV,              flv);
    REGISTER_MUXDEMUX(GIF,              gif);
    ...

    /* image demuxers */
    REGISTER_DEMUXER (IMAGE_BMP_PIPE,        image_bmp_pipe);
    REGISTER_DEMUXER (IMAGE_JPEG_PIPE,       image_jpeg_pipe);
    REGISTER_DEMUXER (IMAGE_SVG_PIPE,        image_svg_pipe);
    REGISTER_DEMUXER (IMAGE_WEBP_PIPE,       image_webp_pipe);
    REGISTER_DEMUXER (IMAGE_PNG_PIPE,        image_png_pipe);
    ...

    /* external libraries */
    REGISTER_MUXER   (CHROMAPRINT,      chromaprint);
    ...
}

void av_register_all(void)
{
    static AVOnce control = AV_ONCE_INIT;

    ff_thread_once(&control, register_all);
}

define 裡的 ## 用於拼接兩個字串,比如 REGISTER_DEMUXER(AAC, aac) ,它等效於:

extern AVInputFormat ff_aac_demuxer;
if(CONFIG_AAC_DEMUXER) av_register_input_format(&ff_aac_demuxer);

可以看出,編譯 ffmpeg 時類似於 "--enable-muxer=xxx" 這樣的選項在此時發揮了作用,它決定是否註冊某個格式對應的(解)封裝器,以便之後處理該格式的時候找到這個(解)封裝器。

av_register_input_format

av_register_input_format、av_register_output_format 原始碼如下:

/** head of registered input format linked list */
static AVInputFormat *first_iformat = NULL;
/** head of registered output format linked list */
static AVOutputFormat *first_oformat = NULL;

static AVInputFormat **last_iformat = &first_iformat;
static AVOutputFormat **last_oformat = &first_oformat;

void av_register_input_format(AVInputFormat *format)
{
    AVInputFormat **p = last_iformat;

    // Note, format could be added after the first 2 checks but that implies that *p is no longer NULL
    while(p != &format->next && !format->next && avpriv_atomic_ptr_cas((void * volatile *)p, NULL, format))
        p = &(*p)->next;

    if (!format->next)
        last_iformat = &format->next;
}

void av_register_output_format(AVOutputFormat *format)
{
    AVOutputFormat **p = last_oformat;

    // Note, format could be added after the first 2 checks but that implies that *p is no longer NULL
    while(p != &format->next && !format->next && avpriv_atomic_ptr_cas((void * volatile *)p, NULL, format))
        p = &(*p)->next;

    if (!format->next)
        last_oformat = &format->next;
}

從程式碼中可以看到,這兩個註冊方法會把指定的 AVInputFormat、AVOutputFormat 加到連結串列的尾部。

AVInputFormat

接著看 AVInputFormat 的定義:

typedef struct AVInputFormat {
    /**
     * A comma separated list of short names for the format. New names
     * may be appended with a minor bump.
     */
    const char *name;
    
    /**
     * Descriptive name for the format, meant to be more human-readable
     * than name. You should use the NULL_IF_CONFIG_SMALL() macro
     * to define it.
     */
    const char *long_name;
    
    /**
     * Can use flags: AVFMT_NOFILE, AVFMT_NEEDNUMBER, AVFMT_SHOW_IDS,
     * AVFMT_GENERIC_INDEX, AVFMT_TS_DISCONT, AVFMT_NOBINSEARCH,
     * AVFMT_NOGENSEARCH, AVFMT_NO_BYTE_SEEK, AVFMT_SEEK_TO_PTS.
     */
    int flags;

    /**
     * If extensions are defined, then no probe is done. You should
     * usually not use extension format guessing because it is not
     * reliable enough
     */
    const char *extensions;
    ...

    /**
     * Tell if a given file has a chance of being parsed as this format.
     * The buffer provided is guaranteed to be AVPROBE_PADDING_SIZE bytes
     * big so you do not have to check for that unless you need more.
     */
    int (*read_probe)(AVProbeData *);

    /**
     * Read the format header and initialize the AVFormatContext
     * structure. Return 0 if OK. 'avformat_new_stream' should be
     * called to create new streams.
     */
    int (*read_header)(struct AVFormatContext *);

    /**
     * Read one packet and put it in 'pkt'. pts and flags are also
     * set. 'avformat_new_stream' can be called only if the flag
     * AVFMTCTX_NOHEADER is used and only in the calling thread (not in a
     * background thread).
     * @return 0 on success, < 0 on error.
     *         When returning an error, pkt must not have been allocated
     *         or must be freed before returning
     */
    int (*read_packet)(struct AVFormatContext *, AVPacket *pkt);

    ...
} AVInputFormat;

可以看到,這個結構體除了 name 等變數外,還具備 read_probe、read_header 等函式指標。

以前面提到的 ff_aac_demuxer 為例,這裡看一下它的實現:

AVInputFormat ff_aac_demuxer = {
    // 名稱
    .name         = "aac",
    .long_name    = NULL_IF_CONFIG_SMALL("raw ADTS AAC (Advanced Audio Coding)"),
    // 把函式指標指向能夠處理 aac 格式的函式實現
    .read_probe   = adts_aac_probe,
    .read_header  = adts_aac_read_header,
    .read_packet  = adts_aac_read_packet,
    .flags        = AVFMT_GENERIC_INDEX,
    .extensions   = "aac",
    .mime_type    = "audio/aac,audio/aacp,audio/x-aac",
    .raw_codec_id = AV_CODEC_ID_AAC,
};

總結

根據以上程式碼的分析,此時我們就能得出問題的答案了:

FFmpeg 之所以能夠作為一個平臺,無論是封裝、解封裝,還是編碼、解碼,在處理對應格式的檔案/資料時,都能找到對應的庫來實現,而不需要修改程式碼,主要就是通過結構體 + 函式指標實現的。具體實現方式是:首先設計一個結構體,然後建立該結構體的多個物件,每個物件都有著自己的成員屬性及函式實現。這樣就使得 FFmpeg 具備了類似於面向物件程式設計中的多型的效果。

PS:avcodec_register_all 也是一樣的,有興趣的可以看看 AVCodec 的宣告以及 ff_libx264_encoder 等編解碼HX器的實現。



作者:zouzhiheng
連結:https://www.jianshu.com/p/c12e6888de10
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授