3個重點,20個函式分析,淺析FFmpeg轉碼過程

柒柒愛程式設計發表於2022-01-12

寫在前面

最近在做和轉碼有關的專案,接觸到ffmpeg這個神器。從一開始簡單的寫指令碼直接呼叫ffmpeg的可執行檔案做些轉碼的工作,到後來需要寫程式呼叫ffmpeg的API。雖然上網搜了別人的demo稍微改改順利完成了工作,但是對於ffmpeg這個黑盒子,還是有些好奇心和擔心(專案中使用不了解的程式碼總是不那麼放心),於是抽空翻了翻ffmpeg的原始碼,整理成文章給大家分享分享。

由於我並非做音訊出身,對於音訊一竅不通。ffmpeg整個也非常龐大,所以這篇文章從ffmpeg提供的轉碼的demo開始,側重於講清楚整個輸入->轉碼->輸出的流程,並學習ffmpeg如何做到通用和可擴充套件性。

注:本文基於ffmpeg提供的transcode_aac.c樣例。

三個重點

轉碼的過程是怎麼樣的?簡單來說就是從輸入讀取資料,解析原來的資料格式,轉成目標資料格式,再將最終資料輸出。這裡就涉及到三個資料輸入和輸出方式資料的編碼方式資料的容器格式(容器是用來區分不同檔案的資料型別的,而編碼格式則由音視訊的壓縮演算法決定,一般所說的檔案格式或者字尾名指的就是檔案的容器。對於一種容器,可以包含不同編碼格式的一種視訊和音訊)。

ffmpeg是一個非常非常通用的工具,支援非常廣的資料輸入和輸出,包括:hls流,檔案,記憶體等,支援各類資料編碼格式,包括:aac,mp3等等,同時支援多種容器格式,包括ts,aac等。另外ffmpeg是通過C語言實現的,如果是C++,我們可以通過繼承和多型來實現。定義一個IO的基類,一個Format的基類和一個Codec的基類,具體的輸入輸出協議繼承IO基類實現各自的輸入輸出方法,具體的容器格式繼承Format基類,具體的編碼格式繼承Codec基類。這篇文章也會簡單講解ffmpeg如何用C語言實現類似C++的繼承和多型。

基本資料結構

ffmpeg轉碼中最基本的結構為AVFormatContext和AVCodecContext。AVCodecContext負責編碼,AVFormatContext負責IO和容器格式。

我從AVFormatContext類抽離出三個基本的成員iformat,oformat,pb。分別屬於AVInputFormat,AVOutputFormat,AVIOContext類。iformat為輸入的資料格式,oformat為輸出的資料格式,pb則負責輸入輸出。

img

我把這三個類的定義抽離了出來簡化了下,可以看出AVInputFormat宣告瞭read_packet方法,AVOutputFormat宣告瞭write_packet方法,AVIOContext宣告瞭read_packet, write_packet方法。同時AVInputFormat和AVOutputFormat還有一個成員變數name用以標識該格式的字尾名。

img

下一節我們會看到Input/OutputForm的read/write packet方法和IOContext的關係。

輸入函式呼叫圖

下面是初始化輸入的整個過程的函式呼叫圖。

img

首先從呼叫open_input_file開始,首先解析輸入的protocol。avio_open2函式會呼叫一系列helper函式(ffurl_open,ffio_fdopen)分析輸入的協議,設定AVFormatContext的pb變數的read_packet方法。而av_probe_input_buffer2函式則會分析輸入檔案的格式(從檔名解析或輸入資料做判斷),設定AVFormatContext的iformat的read_packet方法。

img

兩個read_packet有什麼關係呢?第二個函式呼叫圖可以看出,iformat的read_packet最終會呼叫pb的read_packet方法。意思就是資料本身由pb的read_packet方法來讀取,而iformat則會在輸入的資料上做些格式相關的解析操作(比如解析輸入資料的頭部,提取出輸入資料中真正的音訊/視訊資料,再加以轉碼)。

IO相關程式碼

直接看上面的圖不太直觀,這一節我把原始碼中各個步驟截圖下來進行分析。

轉碼開始步驟,呼叫open_input_file函式,傳入檔名。

img

avformat_open_input函式會呼叫init_input()來處理輸入檔案。

img

init_input函式主要做兩個事情,一是解析輸入協議(如何讀取資料?hls流?檔案?記憶體?),二是解析輸入資料的格式(輸入資料為aac?ts?m4a?)

img

avio_open2函式首先呼叫ffurl_open函式,根據檔名來推斷所屬的輸入協議(URLProtocol)。之後再呼叫ffio_fdopen設定pb的read_packet方法。

img

img

img

上面幾段程式碼的邏輯為:根據檔名查詢對應的URLProtocol->把該URLProtocol賦值給URLContext的prot成員變數->建立AVIOContext例項,賦值給AVFormatContext的pb成員變數。

img

img

這裡設定了AVIOContext例項的read_packet為ffurl_read方法。

img

ffurl_read方法其實就是呼叫URLContext的prot(上面賦值的)的url_read方法。通過函式指標去呼叫具體的URLContext物件的prot成員變數的url_read方法。

img

接下來看看解析輸入資料格式的程式碼。av_probe_input_buffer2函式呼叫av_probe_input_format2函式來推斷資料資料的格式。從之前的圖我們知道*fmt其實就是&s->iformat。因此這裡設定了AVFormatContext的iformat成員變數。

img

至此AVFormatContext物件的iformat和pb成員變數就設定好了。接下來看看如何讀取輸入開始轉碼。

av_read_frame函式呼叫read_frame_internal函式開始讀取資料。

img

read_frame_internal會呼叫ff_read_packet,後者最終呼叫的是iformat成員變數的read_packet方法。

img

img

拿aac舉例,aac的read_packet方法實際上是ff_raw_read_partial_packet函式。

img

ff_raw_read_partial_packet會呼叫ffio_read_partial,後者最終呼叫的是AVFormatContext的pb成員變數的read_packet方法。而我們知道pb成員的read_packet其實就是ffurl_read,也就是具體輸入URLProtocl的read_packet方法。

img

img

至此已經走完了整個輸入的流程,輸出也是類似的程式碼,這裡就不再贅述。

轉碼函式呼叫圖

上面關於IO的介紹我從輸入的角度進行分析。接下來的轉碼過程我則從輸出的角度進行分析。下圖是轉碼過程的函式呼叫圖(做了簡化)。load_encode_and_write呼叫encode_audio_frame, encode_audio_frame呼叫avcodec_encode_audio2來做實際的編碼工作,最後呼叫av_write_frame將編碼完的資料寫入輸出。

img

轉碼相關程式碼

首先需要設定輸出目標編碼格式,下面的程式碼為設定編碼格式(aac)的片段:

img

在這裡設定了output_codec_context(AVCodecContext類物件)之後,從前面的函式呼叫圖,我們知道是avcodec_encode_audio2函式執行的轉碼過程:

img

這裡看到呼叫了avctx(AVCodecContext類物件)的codec(AVCodec類物件)成員變數的encode2方法去做編碼操作。

轉碼這裡專業性比較強,我並沒有細讀,因此這裡簡單帶過。

總結

可以看出ffmpeg大量使用函式指標來實現類似C++的繼承/多型的效果。並且ffmpeg具有非常好的擴充套件性。如果我需要自定義一個新的輸入協議,只需要自己定義一個新的URLProtocol物件,實現read_packet方法即可。如果需要自定義一個新的容器格式,只需要定義一個新的AVInputFormat物件,實現read_packet方法即可。如果需要自定義一個新的編碼格式,只需要定義一個新的AVCodec物件,實現encode2方法即可。真是非常讚的程式碼架構設計!

本文涉及的資料全部打包放到我Github倉:

GitHub:2022年,最新 ffmpeg 資料整理,專案(除錯可用),命令手冊,文章,編解碼論文,視訊講解,面試題全套資料

有需要的可以前去下載,或者覺得還不錯,請給我Star,感謝支援!

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章