12.16 ffmpeg原理和架構

流媒體提供解決方案的跨平臺的C++開源項目,它實現了對標準流媒體傳輸是一個為流媒體提供解決方案的跨平臺的C++開源項目,它實現了對標準流媒體傳輸協議如RTP/RTCP、RTSP、SIP等的支持。Live555實現了對多種音視頻編碼格式的音視頻數據的流化、接收和處理等支持,包括MPEG、H.263+、DV、JPEG視頻和多種音頻編碼。

FFmpeg基本原理

文章最後由福利


FFmpeg是相當強大的多媒體編解碼框架,在深入分析其源代碼之前必須要有基本的多媒體基礎知識,否則其源代碼會非常晦澀難懂。本文將從介紹一些基本的多媒體只是,主要是為研讀ffmpeg源代碼做準備,比如一些編解碼部分,只有真正瞭解了多媒體處理的基本流程,研讀ffmpeg源代碼才能事半功倍。

下面分析一下多媒體中最基本最核心的視頻解碼過程,平常我們從網上下載一部電影或者一首歌曲,那麼相應的多媒體播放器為我們做好了一切工作,我們只用欣賞就ok了。目前幾乎所有的主流多媒體播放器都是基於開源多媒體框架ffmpeg來做的,可見ffmpeg的強大。下面是對一個媒體文件進行解碼的主要流程:

ffmpeg原理和架構

1. 解複用(Demux)

當我們打開一個多媒體文件之後,第一步就是解複用,稱之為Demux。為什麼需要這一步,這一步究竟是做什麼的?我們知道在一個多媒體文件中,既包括音頻也包括視頻,而且音頻和視頻都是分開進行壓縮的,因為音頻和視頻的壓縮算法不一樣,既然壓縮算法不一樣,那麼肯定解碼也不一樣,所以需要對音頻和視頻分別進行解碼。雖然音頻和視頻是分開進行壓縮的,但是為了傳輸過程的方便,將壓縮過的音頻和視頻捆綁在一起進行傳輸。所以我們解碼的第一步就是將這些綁在一起的音頻和視頻流分開來,也就是傳說中的解複用,所以一句話,解複用這一步就是將文件中捆綁在一起的音頻流和視頻流分開來以方便後面分別對它們進行解碼,下面是Demux之後的效果。


ffmpeg原理和架構


2. 解碼(Decode)

這一步不用多說,一個多媒體文件肯定是經過某種或幾種格式的壓縮的,也就是通常所說的視頻和音頻編碼,編碼是為了減少數據量,否則的話對我們的存儲設備是一個挑戰,如果是流媒體的話對網絡帶寬也是一個幾乎不可能完成的任務。所以我們必須對媒體信息進行儘可能的壓縮。

3. FFmpeg中解碼流程對應的API函數

瞭解了上面的一個媒體文件從打開到解碼的流程,就可以很輕鬆的閱讀ffmpeg代碼,ffmpeg的框架也基本是按照這個流程來的,但不是每個流程對應一個API,下面這副圖是我分析ffmpeg並根據自己的理解得到的ffmpeg解碼流程對應的API,我想這幅圖應該對理解ffmpeg和編解碼有一些幫助。


ffmpeg原理和架構

Ffmpeg中Demux這一步是通過avformat_open_input()這個api來做的,這個api讀出文件的頭部信息,並做demux,在此之後我們就可以讀取媒體文件中的音頻和視頻流,然後通過av_read_frame()從音頻和視頻流中讀取出基本數據流packet,然後將packet送到avcodec_decode_video2()和相對應的api進行解碼。

ffmpeg架構和解碼流程

一,ffmpeg架構

1. 簡介

FFmpeg是一個集錄制、轉換、音/視頻編碼解碼功能為一體的完整的開源解決方案。FFmpeg的

開發是基於Linux操作系統,但是可以在大多數操作系統中編譯和使用。FFmpeg支持MPEG、

DivX、MPEG4、AC3、DV、FLV等40多種編碼,AVI、MPEG、OGG、Matroska、ASF等90多種解碼.

TCPMP, VLC, MPlayer等開源播放器都用到了FFmpeg。

FFmpeg主目錄下主要有libavcodec、libavformat和libavutil等子目錄。其中libavcodec用

於存放各個encode/decode模塊,libavformat用於存放muxer/demuxer模塊,libavutil用於

存放內存操作等輔助性模塊。

以flash movie的flv文件格式為例, muxer/demuxer的flvenc.c和flvdec.c文件在

libavformat目錄下,encode/decode的mpegvideo.c和h263de.c在libavcodec目錄下。

2. muxer/demuxer與encoder/decoder定義與初始化

muxer/demuxer和encoder/decoder在FFmpeg中的實現代碼裡,有許多相同的地方,而二者最

大的差別是muxer 和demuxer分別是不同的結構AVOutputFormat與AVInputFormat,而encoder

和decoder都是用的AVCodec 結構。

muxer/demuxer和encoder/decoder在FFmpeg中相同的地方有:

二者都是在main()開始的av_register_all()函數內初始化的

二者都是以鏈表的形式保存在全局變量中的

muxer/demuxer是分別保存在全局變量AVOutputFormat *first_oformat與

AVInputFormat *first_iformat中的。

encoder/decoder都是保存在全局變量AVCodec *first_avcodec中的。

二者都用函數指針的方式作為開放的公共接口

demuxer開放的接口有:


ffmpeg原理和架構


ffmpeg原理和架構


ffmpeg原理和架構


仍以flv文件為例來說明muxer/demuxer的初始化。

在libavformat\\allformats.c文件的av_register_all(void)函數中,通過執行

REGISTER_MUXDEMUX(FLV, flv);

將支持flv 格式的flv_muxer與flv_demuxer變量分別註冊到全局變量first_oformat與first_iformat鏈表的最後位置。

其中flv_muxer在libavformat\\flvenc.c中定義如下:

AVOutputFormat flv_muxer = {


"flv",

"flv format",

"video/x-flv",

"flv",

sizeof(FLVContext),

#ifdef CONFIG_LIBMP3LAME

CODEC_ID_MP3,

#else // CONFIG_LIBMP3LAME

CODEC_ID_NONE,

CODEC_ID_FLV1,

flv_write_header,

flv_write_packet,

flv_write_trailer,

.codec_tag= (const AVCodecTag*[]){flv_video_codec_ids, flv_audio_codec_ids, 0},

}

AVOutputFormat結構的定義如下:

typedef struct AVOutputFormat {

const char *name;

const char *long_name;

const char *mime_type;

const char *extensions;

int priv_data_size;

enum CodecID audio_codec;

enum CodecID video_codec;

int (*write_header)(struct AVFormatContext *);

int (*write_packet)(struct AVFormatContext *, AVPacket *pkt);

int (*write_trailer)(struct AVFormatContext *);

int flags;

int (*set_parameters)(struct AVFormatContext *, AVFormatParameters *);

int (*interleave_packet)(struct AVFormatContext *, AVPacket *out, AVPacket *in, int flush);


const struct AVCodecTag **codec_tag;

struct AVOutputFormat *next;

} AVOutputFormat;


ffmpeg原理和架構

AVInputFormat flv_demuxer = {

"flv",

"flv format",

0,

flv_probe,

flv_read_header,

flv_read_packet,

flv_read_close,

flv_read_seek,

.extensions = "flv",

.value = CODEC_ID_FLV1,

};

在上述av_register_all(void)函數中通過執行libavcodec\\allcodecs.c文件裡的

avcodec_register_all(void)函數來初始化全部的encoder/decoder。


ffmpeg原理和架構

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裡的

輸出文件後綴名來進行的。


ffmpeg原理和架構

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;

enum CodecID codec_id;

如上所示,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;

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成員中。

---------------------------------------------------------------------

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

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

REGISTER_DECODER (H264, h264);

REGISTER_ENCODER (LIBX264, libx264);

找到相關的宏代碼如下


ffmpeg原理和架構

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


ffmpeg原理和架構

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


ffmpeg原理和架構

它 屬於結構體AVCodecContext的void *priv_data變量,定義了每種編解碼器私有的上下文屬性,AVCodecContext也類似上下文基類一樣,還提供其他表示屏幕解析率、量化範圍等的上下文屬性和rtp_callback等函數指針供編解碼使用。

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

1. 輸入輸出流初始化。

2. 根據輸入輸出流確定需要的編解碼器,並初始化。

3. 寫輸出文件的各部分

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

可以參考【3】來了解這些結構的含義(見附錄)。在這裡會調用一系列來自utils.c的函數,這裡的avcodec_open()函數,在打開編解碼器都會調用到,它將運行如下代碼:


ffmpeg原理和架構

進行具體適配的編解碼器初始化,而這裡的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語言軟件項目所借鑑。

資料內容包括:C/C++,Linux,golang,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,嵌入式 等。。

。後臺私信;資料;兩個字可以免費領取


分享到:


相關文章: