国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

FFmpeg5.0源碼閱讀——FFmpeg大體框架

這篇具有很好參考價值的文章主要介紹了FFmpeg5.0源碼閱讀——FFmpeg大體框架。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

??摘要:前一段時間熟悉了下FFmpeg主流程源碼實現(xiàn),對FFmpeg的整體框架有了個大概的認識,因此在此做一個筆記,希望以比較容易理解的文字描述FFmpeg本身的結(jié)構(gòu),加深對FFmpeg的框架進行梳理加深理解,如果文章中有紕漏或者錯誤歡迎指出。本文描述了FFmpeg編解碼框架的工程結(jié)構(gòu),基本構(gòu)成以及大體的調(diào)用流程。因為FFmpeg的濾鏡是相對獨立的一個模塊,因此在此不會進行描述。
??關(guān)鍵字:FFmpeg,Framework
??閱讀須知:閱讀本文前,你首先需要了解最基本的音視頻處理相關(guān)的知識,對于這些知識你至少需要最基本的了解,比如知道什么是容器,什么是編解碼器,以及大概的工作流程即可。
??FFmepg是一個用C語言實現(xiàn)的多媒體封裝、解封轉(zhuǎn)、編解碼開源框架,支持了多種IO協(xié)議操作,媒體封裝格式的封裝與解封裝以及編解碼格式編解碼器(包括硬解和軟解)。任何軟件都可以在FFmpeg的License范圍內(nèi)合理地基于FFmpeg進行開發(fā)。FFmpeg有兩種開源協(xié)議:

  • GPL,該協(xié)議是具有傳染性的,如果使用了GPL部分的代碼(FFmpeg可以配置是否開關(guān)這部分代碼)對應的軟件也必須開源否則有法律風險;
  • LGPL,允許以動態(tài)發(fā)布的形式使用,即將FFmpeg編譯為動態(tài)庫使用,但是修改到了FFmpeg部分的代碼,修改的部分也需要開源,一般商業(yè)軟件都會采用這種方式來進行商業(yè)軟件的開發(fā)。

??FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations.

1 FFmpeg工程

??本小節(jié)簡單描述下FFmpeg的工程結(jié)構(gòu)相關(guān)的內(nèi)容,以期對FFmpeg工程本身的基本構(gòu)成有一個基本的認識。

1.1 FFmpeg工程結(jié)構(gòu)

??FFmpeg本身的目錄結(jié)構(gòu)比較清晰,我們從目錄名稱中基本就能看出該目錄下可能包含哪些文件具體用來干什么。

  • .:當前目錄下存儲的是一些編譯和項目相關(guān)的配置文件,比如Makefile,License等;
  • compat:兼容文件;
  • doc:文檔,以及一些FFmpeg使用的示例,如果學習FFmpeg的話強烈建議閱讀示例;
  • ffbuild:編譯相關(guān)的一些文件,比如依賴選項等等;
  • fftools:可以編譯成可執(zhí)行文件的一些工具實現(xiàn),比如ffplay,ffmpeg,ffprobe等工具;
  • libavcodec:編解碼核心,編解碼相關(guān)的文件都存放在這里,比如h264dec.c等;
  • libavdevice:設備相關(guān),比如DShow等;
  • libavfilter:濾鏡特效處理;
  • libavformat:IO操作以及封裝格式的封裝和轉(zhuǎn)封裝等處理;
  • libavutil:工具庫,比如一些基本的字符串操作,圖像操作等;
  • libavpostproc:一些效果后處理相關(guān)的內(nèi)容,一般通過filter處理;
  • libswresample:音頻重采樣處理;
  • libswscale:視頻縮放、顏色空間轉(zhuǎn)換以及色調(diào)映射等;
  • presets:編解碼器的配置文件,參考FFmpeg-Present-files
  • tests:測試示例;
  • tools:一些簡單的工具。

2 FFmpeg架構(gòu)

2.1 FFmpeg的總體架構(gòu)

FFmpeg5.0源碼閱讀——FFmpeg大體框架,ffmpeg,音視頻,ffmpeg

??FFmpeg各個模塊是互相獨立的,都可以單獨使用,比如解封裝器只用來對媒體進行解封裝或者封裝拿到編碼器的裸流,或者編解碼器直接對裸流數(shù)據(jù)進行編解碼,亦或者使用工具集對已經(jīng)解碼完的數(shù)據(jù)盡興處理。
??編解碼模塊支持多種不同編解碼器,所有的編解碼器所使用的參數(shù)和當前編解碼器相關(guān)的Context都是使用AVCodecContext描述。而FFmpeg中每個具體的解碼器都有一個靜態(tài)的AVCodec描述當前解碼器如何解碼,這個是有一套統(tǒng)一的接口來定義的。上層拿到AVCodecContextAVCodec就可以初始化解碼器進行解碼了,只不過使用FFmpeg提供的解碼接口更加方便。FFmpeg并沒有硬件解碼器歸類的AVCodec下面,而是在其下層另外規(guī)定了一套AVHWAccel,通過AVCodec來描述該硬件解碼器。
??封裝和解封裝支持多種不同的媒體文件類型,F(xiàn)Fmpeg中講一個文件抽象為AVFormatContext,而內(nèi)部分別將輸入流和輸出流分別抽象為AVInputFormat,AVOutputFormatAVInputFormat,AVOutputFormat用來描述當前媒體文件的相關(guān)參數(shù)以及對媒體文件進行封裝和解封裝,而具體的操作通過AVIO來進行。AVIO抽象了具體的文件IO操作,類似編解碼器每種類型的輸入流都有各自的描述,封裝器和解封裝器同理。
??工具集也是獨立的,只是一些工具函數(shù)的集合。
??濾鏡用來對裸數(shù)據(jù)進行一些特效上的處理。(本文不會過多討論濾鏡)

2.2 代碼結(jié)構(gòu)

??FFmpeg內(nèi)有一系列的基礎組件,一部分是對一些native接口的封裝來保證對上層的接口的一致性,一部分是為了方便內(nèi)部的使用提供的基礎接口。比如av_malloc,av_mallocz系列就是對內(nèi)存分配接口的封裝以保證內(nèi)存對齊。
??FFmpeg中有比較多的基礎組件,有一些不了解也不影響我們使用FFmpeg,只需要在使用時去了解就可以,但是另一部分是必須了解的,比如AVOptionAVBuffer等。
AVOption
??AVOption是FFmpeg中設置參數(shù)的一個基本抽象結(jié)構(gòu)。因為FFmpeg是一個支持多種封裝解封裝器,編解碼器的框架,而不同的外部庫需要的參數(shù)各不相同,因此利用AVOption來封裝一個基本的key-value結(jié)構(gòu)來獲取和設置對應模塊的參數(shù)。AVOption本身就是一個key-value項,可以理解為C++中map中的項pair,而其中name就是key,default_val就是value。而在實際使用中所有的參數(shù)是存儲在AVClass中的AVOption數(shù)組中,而需要設置參數(shù)的模塊會在Context結(jié)構(gòu)體開頭設置一個AVClass的指針來表示當前模塊的參數(shù),F(xiàn)Fmpeg通過搜索該數(shù)組來獲取和設置對應模塊的參數(shù)(該列表的搜索是線性搜索的,由于一般參數(shù)不會太多,即便幾百個參數(shù)線性搜索也不會花費太多時間)。

typedef struct AVClass {
    const char* class_name;                            //AVClass所屬類的名稱
    const char* (*item_name)(void* ctx);               //獲取AVClass所屬類名稱的函數(shù)指針,有些實現(xiàn)會直接返回AVClass->class_name
    const struct AVOption *option;                     //當前類的參數(shù),沒有就置為NULL
    int version;                                       //當前字段創(chuàng)建的版本,可用于版本控制,This is used to allow fields to be added without requiring major version bumps everywhere.
    int log_level_offset_offset;                       //AVClass所屬結(jié)構(gòu)體中l(wèi)og_level_offset相對于其首地址的偏移,0表示沒有該成員
    int parent_log_context_offset;                     //當前Context中存儲parent context的偏移量
    void* (*child_next)(void *obj, void *prev);        //AVOptions中下一個可用的參數(shù)
    AVClassCategory category;                          //當前類的類別
    AVClassCategory (*get_category)(void* ctx);        //獲取當前Context類別的函數(shù)指針      
    //查詢對應選項的范圍,雖然定義了但是FFmpeg源碼中好像沒有API用到
    int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags);
    /**
     * Iterate over the AVClasses corresponding to potential AVOptions-enabled children.
     * @param iter pointer to opaque iteration state. The caller must initialize *iter to NULL before the first call.
     * @return AVClass for the next AVOptions-enabled child or NULL if there are no more such children.
     * @note The difference between child_next and this is that child_next iterates over _already existing_ objects, while child_class_iterate iterates over _all possible_ children.
    const struct AVClass* (*child_class_iterate)(void **iter);
} AVClass;

??FFmpeg中Context中的一部分參數(shù)是自身持有的也可以通過AVOption來設置,其基本的原理就是通過對應成員的固定偏移來讀寫。

typedef struct AVFormatContext {
    //A class for logging and @ref avoptions. Set by avformat_alloc_context(). Exports (de)muxer private options if they exist.
    const AVClass *av_class;
    //省略大部分代碼
}AVFormatContext;

AVBuffer
??AVBuffer,AVBufferPool是FFmpeg比較簡單的一種基于引用技術(shù)實現(xiàn)的FIFO內(nèi)存池
。AVBufferPool是一個以單鏈表形式實現(xiàn)的棧式內(nèi)存池。其基本過程就是如果鏈表非空則出棧頭結(jié)點,否則申請內(nèi)存時就創(chuàng)建一個AVBufferRef返回給用戶,用戶釋放時就會將節(jié)點入棧到頭結(jié)點,并且申請和釋放內(nèi)存是線程安全的。AVBufferPool就是一個空閑鏈表棧,通過指定對應的AVBufferRef的釋放函數(shù)為pool_release_buffer來對內(nèi)存進行管理。
??對于一個剛初始化的內(nèi)存池,連續(xù)申請兩個Buffer就是下面這種狀態(tài):
FFmpeg5.0源碼閱讀——FFmpeg大體框架,ffmpeg,音視頻,ffmpeg
連續(xù)申請3個buffer,再釋放2個就是下面這種狀態(tài)(紅色為鏈表的連接線):
FFmpeg5.0源碼閱讀——FFmpeg大體框架,ffmpeg,音視頻,ffmpeg

??其他一些需要詳細注意的就是FFmpeg中存儲數(shù)據(jù)的AVFrameAVPacket,分別是用來存儲裸數(shù)據(jù)和編碼的數(shù)據(jù)流。
??FFmpeg雖然是用C語言寫的但是其基本的實現(xiàn)思想是按照OOP的思想實現(xiàn)的,每個具體的格式都有自己的Context和描述類然后通過函數(shù)指針來描述具體實例的實際實現(xiàn),也就是上面描述的Context->Context->Context->....>Implementation這種形式,為了對當前處理的對象統(tǒng)一抽象就會有一個Context來描述。而每個Context都有一個AVClassopaue來描述當前結(jié)構(gòu)的參數(shù)和獨有的一些數(shù)據(jù),通過這種方式保持了接口的統(tǒng)一的同時,又能兼顧差異性。一般的Context接口如下:

struct ***Context{
    const AVClass *av_class;
    //省略部分可能的成員
    void *private_data;
    //省略部分可能的成員
}

??FFmpeg雖然是用C語言寫的但是其基本的實現(xiàn)思想是按照OOP的思想實現(xiàn)的,每個具體的格式都有自己的Context和描述類然后通過函數(shù)指針來描述具體實例的實際實現(xiàn),也就是上面描述的Context->Context->Context->....>Implementation這種形式,為了對當前處理的對象統(tǒng)一抽象就會有一個Context來描述。而每個Context都有一個AVClassopaue來描述當前結(jié)構(gòu)的參數(shù)和獨有的一些數(shù)據(jù),通過這種方式保持了接口的統(tǒng)一的同時,又能兼顧差異性。

FFmpeg5.0源碼閱讀——FFmpeg大體框架,ffmpeg,音視頻,ffmpeg

2.3 調(diào)用流程

??FFmpeg的核心就是封裝/解封裝和解碼那一套,下面的流程圖是一個大概,有一部分調(diào)用被省略了。
FFmpeg5.0源碼閱讀——FFmpeg大體框架,ffmpeg,音視頻,ffmpeg

3 Gif轉(zhuǎn)碼

??上面大概描述了下FFmpeg的框架結(jié)構(gòu)和基本的調(diào)用流程,但是介紹的比較粗糙,可能一個具體的例子更容易理解。因此下面會針對GIF圖像的轉(zhuǎn)碼流程進行比較詳細的流程跟蹤FFmpeg的詳細調(diào)用流程,以及數(shù)據(jù)處理。選擇GIF的原因是GIF圖像的格式和編解碼相比其他格式相對比較簡單,可以讓我們更加關(guān)注主要的流程而不是具體某個格式的解封裝或者解碼。當然下面也會涉及的GIF的封裝解封裝,編解碼過程,因此為了更加流暢的閱讀,最好提前了解下GIF文件格式和GIF編解碼。

3.1 大體流程

??總體的調(diào)用流程如下,一般的轉(zhuǎn)碼的基本流程:
FFmpeg5.0源碼閱讀——FFmpeg大體框架,ffmpeg,音視頻,ffmpeg

??一個流媒體文件的轉(zhuǎn)碼基本上包含了FFmpeg的主要內(nèi)容,從該過程入手我們能清晰的看到FFmpeg內(nèi)部的實現(xiàn)邏輯。首先有一個流媒體文件,比如Mp4,MKV等等,我們期望是將其編碼封裝為另一種格式比如HEVC/MP4等等。
??首先是一些環(huán)境的準備,比如打開媒體文件,這個時候FFmpeg會根據(jù)文件的流內(nèi)容探測當前文件可能是什么格式,來確定使用哪種解封裝器。然后打開解碼器和編碼器,解碼器的參數(shù)是通過第一步探測到的,而編碼器的參數(shù)需要根據(jù)你的需要設置。
??文件和解碼器已經(jīng)打開就可以開始解碼了。因為流信息是按照幀存儲的,因此需要不斷讀取一幀一幀的壓縮的流信息送給解碼器進行解碼。未壓縮的數(shù)據(jù)存儲在AVPacket中,而解壓完的裸數(shù)據(jù)存儲在AVFrame中。拿到裸數(shù)據(jù)后就可以將該數(shù)據(jù)發(fā)送給編碼器進行編碼,最后送到封裝器進行封裝存儲就得到了一個完整的流媒體文件。

3.2 初始化Context

??FFmpeg中一個AVFormatContext表示一個媒體文件的抽象,AVCodecContextt,AVCodec表示編解碼參數(shù)和編解碼器的抽象,因此分別初始過程需要初始化讀和寫文件的AVFormatContext,編碼和解碼的``AVCodecContext````,以及打開編解碼器。

3.2.1 解封裝AVFormatContext初始化

??解封裝的AVFormatContextFFmpeg內(nèi)部會自動探測,不需要我們指定。該初始化過程主要涉及兩個對外的API:avformat_open_input,avformat_find_stream_info,前者用來打開文件,后者用來進行流媒體信息探測。

3.2.1.1 avformat_open_input

avformat_open_input
??avformat_open_input會打開文件句柄,探測當前文件的媒體格式,讀取基本的流媒體格式信息。
??avformat_open_input首先會在堆上分配一個AVFormatContext(下面稱之為媒體句柄)并將用戶自定義個一些options拷貝到該Context中。
??此時的媒體句柄只是一個帶有輸入?yún)?shù)和文件路徑的空殼,需要進一步的確認具體的媒體格式。之后會調(diào)用av_probe_input_format2(記住這個API,這里如果探測失敗后續(xù)還會繼續(xù)調(diào)用),實際上內(nèi)部調(diào)用的是av_probe_input_format3對媒體文件探測檢測。探測的方式比較粗暴就是遍歷當前FFmpeg支持的所有媒體格式然后調(diào)用對應媒體格式的read_probe函數(shù)指針拿到一個分值,分值最高的那個就是當前媒體文件的格式。此時就會拿到對應文件的AVInputFormat賦值給媒體句柄中的iformat。偽代碼如下:

int maxscore = 0;
AVInputFormat *tmp, *ret;
while(ret in [FFmpeg 支持的格式列表]){
    int score = ret->read_probe();
    if(score > maxscore){
        tmp = ret;
        maxscore = score;
    }
}

return ret;

??因為上面的probe是第一次調(diào)用還沒有打開文件IO無法訪問文件數(shù)據(jù),因此大概率失敗,那為什么還要在打開文件IO前調(diào)用?因為對于一些設置了AVFMT_NONFILE的輸入比如DShow等就不需要打開文件IO進行。

??然后就是調(diào)用媒體句柄中的io_open函數(shù)指針打開流,該指針是在創(chuàng)建媒體句柄時設置的默認函數(shù)指針io_open_default。打開流是首先需要確認流的類型,基本過程和媒體探測流程差不多,根據(jù)文件名遍歷FFmpeg支持的所有流格式拿到當前格式的URLProtocol,比如本地文件就是ff_file_protocol,確定流類型后就可以調(diào)用具體的函數(shù)指針url_open打開媒體文件了。對于本地文件的話就是posix那套文件操作,比如open,lseek,fstat等,之后文件讀取也一樣。打開文件后的文件句柄并不是URLProtocol的成員,而是存儲在priv_data中,這也是FFmpeg中規(guī)避差異化的基本做法。
??通過上述的操作我們只是拿到了URLContext,還需要拿到AVIOContext。創(chuàng)建AVIOContext的過程比較簡單,就是堆上申請塊兒對應的內(nèi)存設置必要的參數(shù)然后返回。需要注意的是此時會申請一會兒緩沖區(qū),存放在VIOContext供后續(xù)讀寫文件使用。
??拿到AVIOContext后也就意味著IO已經(jīng)成功打開,如果此時發(fā)現(xiàn)媒體句柄中沒有iformat就會調(diào)用av_probe_input_buffer2再次探測。av_probe_input_buffer2內(nèi)部會不斷讀取文件內(nèi)容然后調(diào)用上面提到的APIav_probe_input_format2對文件內(nèi)容進行探測,直到確定媒體文件格式或者達到最大的probesize為止。
??GIF的read_probe比較簡單,就是讀取頭部的標記確認是否為GIF文件。

static int gif_probe(const AVProbeData *p){
    /* check magick */
    if (memcmp(p->buf, gif87a_sig, 6) && memcmp(p->buf, gif89a_sig, 6))
        return 0;

    /* width or height contains zero? */
    if (!AV_RL16(&p->buf[6]) || !AV_RL16(&p->buf[8]))
        return 0;

    return AVPROBE_SCORE_MAX;
}

??到目前為止我們只是打開了IO,確認了媒體類型,但是媒體的基本信息比如寬高等還不清楚,剩下的工作就是調(diào)用iformat->read_header讀取一些基本的信息寫入到媒體句柄中。至此,媒體流打開的工作就已經(jīng)結(jié)束了。

gif_read_header
??下面通過詳細的注釋描述讀取header的過程:

static int gif_read_header(AVFormatContext *s)
{
    GIFDemuxContext *gdc = s->priv_data;
    AVIOContext     *pb  = s->pb;
    AVStream        *st;
    int type, width, height, ret, n, flags;
    int64_t nb_frames = 0, duration = 0;
    if ((ret = resync(pb)) < 0)         //跳過開頭89a和87a的標識符
        return ret;
    gdc->delay  = gdc->default_delay;
    width  = avio_rl16(pb);             //gif中寬高存儲在開頭,且分別占2個字節(jié)
    height = avio_rl16(pb);
    flags = avio_r8(pb);                //讀取標志位
    avio_skip(pb, 1);                   //背景色索引,目前不需要就跳過
    n      = avio_r8(pb);               //像素比
    if (width == 0 || height == 0)
        return AVERROR_INVALIDDATA;
    st = avformat_new_stream(s, NULL);  //動態(tài)圖一定只有一個視頻流,這里只需要創(chuàng)建一個即可
    if (!st) return AVERROR(ENOMEM);
    if (flags & 0x80)                   //跳過全局顏色表,全局顏色表只有在解碼時有用
        avio_skip(pb, 3 * (1 << ((flags & 0x07) + 1)));
    while ((type = avio_r8(pb)) != GIF_TRAILER) {           //每個block都有各自的標識符,這里判斷是否到達結(jié)尾
        if (avio_feof(pb)) break;
        if (type == GIF_EXTENSION_INTRODUCER) {             //0x21
            int subtype = avio_r8(pb);
            if (subtype == GIF_COM_EXT_LABEL) {             //Comment Extension
                AVBPrint bp;
                int block_size;
                av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED);
                while ((block_size = avio_r8(pb)) != 0) {
                    avio_read_to_bprint(pb, &bp, block_size);
                }
                av_dict_set(&s->metadata, "comment", bp.str, 0);
                av_bprint_finalize(&bp, NULL);
            } else if (subtype == GIF_GCE_EXT_LABEL) {  //Graphic Control Extension描述每一幀圖像的內(nèi)容
                int block_size = avio_r8(pb);
                if (block_size == 4) {
                    int delay;
                    avio_skip(pb, 1);
                    delay = avio_rl16(pb);              //求delay總和得到gif的時長
                    if (delay < gdc->min_delay)
                        delay = gdc->default_delay;
                    delay = FFMIN(delay, gdc->max_delay);
                    duration += delay;
                    avio_skip(pb, 1);
                } else {
                    avio_skip(pb, block_size);
                }
                gif_skip_subblocks(pb);
            } else {
                gif_skip_subblocks(pb);
            }
        } else if (type == GIF_IMAGE_SEPARATOR) {       //Image Descriptor描述當前block的基本寬高等
            avio_skip(pb, 8);
            flags = avio_r8(pb);
            if (flags & 0x80)                           //跳過局部顏色表
                avio_skip(pb, 3 * (1 << ((flags & 0x07) + 1)));
            avio_skip(pb, 1);
            gif_skip_subblocks(pb);
            nb_frames++;                                //統(tǒng)計幀的數(shù)量
        } else {
            break;
        }
    }

    /* GIF format operates with time in "hundredths of second",
     * therefore timebase is 1/100 */
    avpriv_set_pts_info(st, 64, 1, 100);
    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
    st->codecpar->codec_id   = AV_CODEC_ID_GIF;
    st->codecpar->width      = width;
    st->codecpar->height     = height;
    st->start_time           = 0;
    st->duration             = duration;
    st->nb_frames            = nb_frames;
    if (n) {//計算寬高比
        st->codecpar->sample_aspect_ratio.num = n + 15;
        st->codecpar->sample_aspect_ratio.den = 64;
    }
    /* jump to start because gif decoder needs header data too */
    if (avio_seek(pb, 0, SEEK_SET) != 0)
        return AVERROR(EIO);

    return 0;
}

??read_header執(zhí)行完就拿到了流的基本信息,下面最后一步就是校正一些參數(shù),然后調(diào)用update_stream_avctx將部分參數(shù)拷貝給解碼器的Context等。

3.2.1.2 avformat_find_stream_info

??avformat_open_input之后能夠拿到基本的流信息但是具體的流信息,但是媒體文件Header中存儲的數(shù)據(jù)可能和幀中實際的信息不一致,因此需要通過解封裝解碼獲取具體的幀信息來矯正。實際探測信息時回嘗試解碼一部分幀來獲取信息,因此avformat_find_stream_info可能比較耗時。
avformat_find_stream_info
??通過解碼流獲取詳細的流信息,但是到底探測多少內(nèi)容?下面是FFmpeg中決定探測多少流內(nèi)容的閾值,可以看到如果沒有設置的話這里會使用一些經(jīng)驗值。閾值是探測流的時長,而不是文件大小,流越大解碼耗時越久就會越慢。
??這個函數(shù)非常長大概500行,在詳細了解具體實現(xiàn)前,下面是大概調(diào)用流程的偽代碼:

int avformat_find_stream_info(){
    從已有的AVFormatContext和AVStream中獲取和探測流相關(guān)的參數(shù)
    for(int i = 0 -> number of streams){
        初始化AVCodecParser
        拷貝參數(shù)到AVCodecContext
        find_decoder()
        avcodec_open2()
        設置一些解碼相關(guān)的參數(shù)
    }

    for(;;){
        for(int i = 0 -> number of streams){
            分析出一些探測時長的參數(shù)
        }

        if(readsize > probesize) break;
        read_frame_internal()
        avpriv_packet_list_put();
        if(has_extradata){
            extract_extradata();
        }
        try_decode_frame();
    }

    if(flush_codecs) flush_codecs();
    for(int i = 0 -> number of streams){
        計算幀率
    }
    for(int i = 0 -> number of streams){
        add_coded_side_data();
    }
}

??上面是基本的調(diào)用流程,下面一個一個流程詳細說明:
探測參數(shù)設置
??首先便是從已經(jīng)創(chuàng)建的Context中獲取解封裝相關(guān)的參數(shù),比如需要探測的碼流時長等等。

    int64_t max_analyze_duration = ic->max_analyze_duration;
    max_stream_analyze_duration = max_analyze_duration;
    max_subtitle_analyze_duration = max_analyze_duration;
    if (!max_analyze_duration) {
        max_stream_analyze_duration =
        max_analyze_duration        = 5*AV_TIME_BASE;
        max_subtitle_analyze_duration = 30*AV_TIME_BASE;
        if (!strcmp(ic->iformat->name, "flv"))
            max_stream_analyze_duration = 90*AV_TIME_BASE;
        if (!strcmp(ic->iformat->name, "mpeg") || !strcmp(ic->iformat->name, "mpegts"))
            max_stream_analyze_duration = 7*AV_TIME_BASE;
    }

嘗試解碼
??探測流是因為需要部分解碼因此FFmpeg需要初始化解碼器對一部分幀解碼從解碼的幀中獲取流數(shù)據(jù)。解碼的過程就是FFmpeg的基本流程,先準備解碼器,然后調(diào)用avcodec_open2打開解碼器。環(huán)境準備好之后調(diào)用read_frame_internal逐幀讀取壓縮的數(shù)據(jù)AVPacket然后調(diào)用try_decode_frame送給解碼器解碼。然后根據(jù)當前解碼的幀參數(shù),更新當前解碼器的AVCodecContext以及AVStream中的參數(shù)。

3.2.2 封裝AVFormatContext初始化

??封裝時會調(diào)用avformat_alloc_output_context2初始化一個用于寫文件的AVFormatContext。創(chuàng)建AVFormatContext主要是兩部分,首先在堆上分配AVFormatContext設置默認的參數(shù),然后調(diào)用oformat = av_guess_format(NULL, filename, NULL);獲取寫文件的AVOutpuFormat(對比輸入時需要AVInputFormat)。av_guess_format內(nèi)部通過遍歷FFmpeg支持的所有格式的AVOutputFormat對比擴展名得到一個分數(shù)取分數(shù)最高的格式作為當前文件的格式。

    while ((fmt = av_muxer_iterate(&i))) {
        score = 0;
        if (fmt->name && short_name && av_match_name(short_name, fmt->name))
            score += 100;
        if (fmt->mime_type && mime_type && !strcmp(fmt->mime_type, mime_type))
            score += 10;
        if (filename && fmt->extensions &&
            av_match_ext(filename, fmt->extensions)) {
            score += 5;
        }
        if (score > score_max) {
            score_max = score;
            fmt_found = fmt;
        }
    }

3.2.3 打開解碼器

??打開解碼器的基本流程比較簡單,初始化Paser,將流中的參數(shù)拷貝給AVCodecContext,然后是根據(jù)解碼器ID查找解碼器,最后就是直接調(diào)用avcodec_open2打開解碼器了。
??初始化paser就是遍歷FFmpeg支持的paser的靜態(tài)數(shù)組,找到后存儲到FFStream中。GIF的paser就是ff_gif_parser。
??搜索解碼器會調(diào)用avcodec_find_decoder遍歷當前FFmpeg中支持的解碼器類型,直到找到相同解碼器ID的AVCodec解碼器。
??準備好解碼器和Context就會打開解碼器。打開解碼器前為了保證線程安全會鎖住解碼器,鎖解碼器的鎖是一個全局靜態(tài)鎖static AVMutex codec_mutex = AV_MUTEX_INITIALIZER。打開解碼器具體的內(nèi)容就是設置解碼器相關(guān)的參數(shù),分配一些解碼過程需要用到的內(nèi)部變量比如AVCodecInternal等。初始化codec時會創(chuàng)建一個AVCodecDescriptorcodec描述,這個也是從一個內(nèi)部的全局表格codec_descriptors中搜索得到的。之后會根據(jù)當前codec的類型分別調(diào)用ff_encode_preinitff_decode_preinit做一些基本的初始化,這里面也是對當前codec的一些基本參數(shù)設置和一些和codec本身相關(guān)的對象的創(chuàng)建。
??線程初始化。ff_thread_init用于初始化codec運行時的解碼線程內(nèi)部會創(chuàng)建多個線程的context并初始化,初始化最終調(diào)用的是pthread_***_init接口進行初始化。解碼線程的運行任務為frame_worker_thread。

err = init_pthread(fctx, thread_ctx_offsets);
if (err < 0) {
    free_pthread(fctx, thread_ctx_offsets);
    av_freep(&avctx->internal->thread_ctx);
    return err;
}

fctx->async_lock = 1;
fctx->delaying = 1;

if (codec->type == AVMEDIA_TYPE_VIDEO)
    avctx->delay = src->thread_count - 1;

fctx->threads = av_mallocz_array(thread_count, sizeof(PerThreadContext));
if (!fctx->threads) {
    err = AVERROR(ENOMEM);
    goto error;
}

for (; i < thread_count; ) {
    PerThreadContext *p  = &fctx->threads[i];
    int first = !i;

    err = init_thread(p, &i, fctx, avctx, src, codec, first);
    if (err < 0)
        goto error;
}

??最后調(diào)用AVCodec的初始化函數(shù)指針初始化解碼器,完成后解鎖。下面是GIF初始化解碼器的實現(xiàn),主要就是設置當前解碼的參數(shù)和分配解碼器需要用到的緩存以及打開lzw解碼器(其實就是分配一個LZWState并且設置參數(shù))。

static av_cold int gif_decode_init(AVCodecContext *avctx){
    GifState *s = avctx->priv_data;

    s->avctx = avctx;

    avctx->pix_fmt = AV_PIX_FMT_RGB32;
    s->frame = av_frame_alloc();
    if (!s->frame)
        return AVERROR(ENOMEM);
    ff_lzw_decode_open(&s->lzw);
    if (!s->lzw)
        return AVERROR(ENOMEM);
    return 0;
}

3.2.4 打開編碼器

??打開編碼器和打開解碼器都是調(diào)用avcodec_open2基本流程差不多,區(qū)別是解碼器的參數(shù)是通過探測得來的,而編碼器的參數(shù)需要用戶自己設置。編碼器初始化時調(diào)用的函數(shù)指針為gif_encode_init,線程初始化調(diào)用的是ff_frame_thread_encoder_init,線程運行的任務是worker。
??gif_encode_init只是創(chuàng)建內(nèi)部使用的一些變量并做參數(shù)檢查。

static av_cold int gif_encode_init(AVCodecContext *avctx){
    GIFContext *s = avctx->priv_data;

    if (avctx->width > 65535 || avctx->height > 65535) {
        av_log(avctx, AV_LOG_ERROR, "GIF does not support resolutions above 65535x65535\n");
        return AVERROR(EINVAL);
    }

    s->transparent_index = -1;

    s->lzw = av_mallocz(ff_lzw_encode_state_size);
    s->buf_size = avctx->width*avctx->height*2 + 1000;
    s->buf = av_malloc(s->buf_size);
    s->tmpl = av_malloc(avctx->width);
    if (!s->tmpl || !s->buf || !s->lzw)
        return AVERROR(ENOMEM);

    if (avpriv_set_systematic_pal2(s->palette, avctx->pix_fmt) < 0)
        av_assert0(avctx->pix_fmt == AV_PIX_FMT_PAL8);

    return 0;
}

3.2 解封裝

??av_read_frame用于從已經(jīng)打開的文件中讀取未經(jīng)過解碼的碼流AVPacket,對于視頻幀就是一幀的壓縮幀,對于音頻幀如果音頻是固定大小的話則可以是多幀,否則也是一幀。av_read_frame內(nèi)部讀取碼流時調(diào)用avpriv_packet_list_getav_read_frame_internal。
??avpriv_packet_list_get比較簡單就是從當前媒體的PackList中取出一幀。av_read_frame的函數(shù)實現(xiàn)比較長,其大致流程為:

  1. 調(diào)用ff_read_packet讀取一幀碼流;
  2. 如果1步驟失敗則調(diào)用parse_packet刷新解析器,否則繼續(xù)到步驟3;
  3. 如果當前context需要更新解碼器context,則將internal的解碼器context更新到stream的解碼器context;
  4. 如果成功拿到預期的幀則下一步,否則跳轉(zhuǎn)到步驟1;
  5. 后續(xù)的工作就是解析元數(shù)據(jù),計算需要丟棄的數(shù)據(jù)大小等。

??ff_read_packet會先檢查緩沖區(qū)是否有幀沒有的話就會調(diào)用s->iformat->read_packet即對應個是的解析碼流的函數(shù)進行解碼。

??GIF圖解封裝就是調(diào)用的gif_read_packet,解封裝首先就是跳過圖像中的頭信息,比如Image Descriptor等。然后不斷遍歷內(nèi)部的流尋找一幀圖像的Block,找到后根據(jù)當前Block的size讀取數(shù)據(jù)組裝一個AVPacket,設置AVPacket的參數(shù),然后更新GIFDemuxContext中存儲的當前解封裝讀取到的位置,dt
等參數(shù)返回幀。

3.3 解碼

??avcodec_send_packet首先是檢查解碼器的合法性以及數(shù)據(jù)是否為空,如果輸入數(shù)據(jù)和Context符合要求就會刪除AVcodecContext->internal->buffer_pkt中緩存的一幀碼流數(shù)據(jù),將輸入的Packet拷貝到該buffer上。av_bsf_send_packet只是拷貝增加輸入的Packet引用計數(shù)到AVBSFInternal->buffer_pkt,最后如果緩存的buffer_frame是空的就會調(diào)用decode_receive_frame_internal解碼幀,該過程根據(jù)配置項可謂同步也可為異步。

3.3.1 decode_receive_frame_internal

??decode_receive_frame_internal內(nèi)就是真正的調(diào)用解碼流程,如果解碼器的receive_frame函數(shù)指針不為空就直接調(diào)用解碼器的receive_frame進行解碼該過程是同步的。否則就會調(diào)用decode_simple_receive_frame進行解碼。解碼完成后需要根據(jù)解碼的數(shù)據(jù)和當前解碼器Context的一些pts相關(guān)的值計算當前幀的具體pts和dts,另外如果有指定FrameDecodeData還會調(diào)用后處理流程fdd->post_process進行解碼。

3.3.2 decode_simple_receive_frame

??decode_simple_receive_frame主要是調(diào)用decode_simple_internal進行解碼。這里使用的Packet就是前面存儲在AVBSFInternal中的buffer_pkt。然后就是實際調(diào)用解碼的流程,如果沒有配置解碼線程就直接調(diào)用每個解碼器對應的函數(shù)指針的avctx->codec->decode直接同步拿到幀。否則就會調(diào)用ff_thread_decode_frame進行多線程解碼。
??FFmpeg中每種格式,解碼器等都有自己的描述結(jié)構(gòu),比如下面是gif的解碼器描述。

static const AVClass decoder_class = {
    .class_name = "gif decoder",
    .item_name  = av_default_item_name,
    .option     = options,
    .version    = LIBAVUTIL_VERSION_INT,
    .category   = AV_CLASS_CATEGORY_DECODER,
};

const AVCodec ff_gif_decoder = {
    .name           = "gif",
    .long_name      = NULL_IF_CONFIG_SMALL("GIF (Graphics Interchange Format)"),
    .type           = AVMEDIA_TYPE_VIDEO,
    .id             = AV_CODEC_ID_GIF,
    .priv_data_size = sizeof(GifState),
    .init           = gif_decode_init,
    .close          = gif_decode_close,
    .decode         = gif_decode_frame,
    .capabilities   = AV_CODEC_CAP_DR1,
    .caps_internal  = FF_CODEC_CAP_INIT_THREADSAFE |
                      FF_CODEC_CAP_INIT_CLEANUP,
    .priv_class     = &decoder_class,
};

??ff_thread_decode_frame內(nèi)都是通過鎖和條件變量進行同步的。首先根據(jù)當前的狀態(tài)獲取一個解碼線程的Context,然后將當前的Packet提交到該線程上,提交就是將一幀數(shù)據(jù)增加引用讓解碼Context的avpkt也占用輸入幀的引用計數(shù),提交完成就會發(fā)送信號通知在等待的解碼線程啟動。
??解碼線程起始在avcodec_open2的時候就已經(jīng)創(chuàng)建好了,在wait數(shù)據(jù)。具體的執(zhí)行函數(shù)就是frame_worker_thread,該函數(shù)內(nèi)就是調(diào)用codec->decode進行解碼解碼完成后就會發(fā)送通知到ff_thread_decode_frame中取解碼完的幀。令條件if (!p->avctx->thread_safe_callbacks && ( p->avctx->get_format != avcodec_default_get_format || p->avctx->get_buffer2 != avcodec_default_get_buffer2))為A,如果A為true則當前線程是會被阻塞的,完全就是同步運行,否則就是多線程的。

if (!p->avctx->thread_safe_callbacks && (
         p->avctx->get_format != avcodec_default_get_format ||
         p->avctx->get_buffer2 != avcodec_default_get_buffer2)) {
        while (atomic_load(&p->state) != STATE_SETUP_FINISHED && atomic_load(&p->state) != STATE_INPUT_READY) {
            int call_done = 1;
            pthread_mutex_lock(&p->progress_mutex);
            while (atomic_load(&p->state) == STATE_SETTING_UP)
                pthread_cond_wait(&p->progress_cond, &p->progress_mutex);

            switch (atomic_load_explicit(&p->state, memory_order_acquire)) {
            case STATE_GET_BUFFER:
                p->result = ff_get_buffer(p->avctx, p->requested_frame, p->requested_flags);
                break;
            case STATE_GET_FORMAT:
                p->result_format = ff_get_format(p->avctx, p->available_formats);
                break;
            default:
                call_done = 0;
                break;
            }
            if (call_done) {
                atomic_store(&p->state, STATE_SETTING_UP);
                pthread_cond_signal(&p->progress_cond);
            }
            pthread_mutex_unlock(&p->progress_mutex);
        }
    }

3.3.2 avcodec_receive_frame

??avcodec_receive_frame比較簡單先檢查buffer_frame有沒有數(shù)據(jù),有的話就直接返回,沒有即調(diào)用decode_receive_frame_internal進行解碼。

3.3.3 gif_decode_frame

??gif_decode_frame中會將碼流送給解碼器進行解碼然后將得到的數(shù)據(jù)填充到AVFrame返回給上層。代碼中前面一大段都是讀取當前Block的圖像信息比如Image Header這些,實際進行解碼的是gif_parse_next_image,其內(nèi)部會根據(jù)當前block的類型調(diào)用具體的解碼函數(shù),比如圖像就是調(diào)用gif_read_image進行解碼。

static int gif_parse_next_image(GifState *s, AVFrame *frame){
    while (bytestream2_get_bytes_left(&s->gb) > 0) {
        int code = bytestream2_get_byte(&s->gb);
        int ret;

        av_log(s->avctx, AV_LOG_DEBUG, "code=%02x '%c'\n", code, code);

        switch (code) {
        case GIF_IMAGE_SEPARATOR:
            return gif_read_image(s, frame);
        case GIF_EXTENSION_INTRODUCER:
            if ((ret = gif_read_extension(s)) < 0)
                return ret;
            break;
        case GIF_TRAILER:
            /* end of image */
            return AVERROR_EOF;
        default:
            /* erroneous block label */
            return AVERROR_INVALIDDATA;
        }
    }
    return AVERROR_EOF;
}

??gif_read_image解碼過程中首先就是解析當前幀的局部顏色表以及GIF的存儲模式,如果沒有的話就使用全局的顏色表。參數(shù)解析完后直接調(diào)用ff_lzw_decode解碼讀取到的LZW編碼流,最后將索引映射根據(jù)顏色表映射會幀圖像。

3.4 編碼

??avcodec_send_frame用于在編碼時將一幀raw數(shù)據(jù)發(fā)送給編碼器,其基本的調(diào)用流程比較簡單,主要工作就是將輸入的數(shù)據(jù)ref到Internal Frame上。
??avcodec_send_frame首先檢查當前的codec是不是編碼器且是否打開,并且檢查codec中的buffer是否有數(shù)據(jù)沒有,有的話就意味著上一幀的數(shù)據(jù)還沒處理完需要等待這一幀處理完才能繼續(xù)發(fā)送。

    if (!avcodec_is_open(avctx) || !av_codec_is_encoder(avctx->codec))
        return AVERROR(EINVAL);

    if (avci->draining)
        return AVERROR_EOF;

    if (avci->buffer_frame->data[0])
        return AVERROR(EAGAIN);

??然后是根據(jù)輸入的frame是否為空來設置標志位,如果為空就表示是最后一幀數(shù)據(jù)后續(xù)的數(shù)據(jù)就無效了。能夠看到在最后如果codec中的packet buffer是空的就會嘗試獲取一幀packet。

    if (!frame) {
        avci->draining = 1;
    } else {
        ret = encode_send_frame_internal(avctx, frame);
        if (ret < 0)
            return ret;
    }

    if (!avci->buffer_pkt->data && !avci->buffer_pkt->side_data) {
        ret = encode_receive_packet_internal(avctx, avci->buffer_pkt);
        if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF)
            return ret;
    }

??encode_send_frame_internal比較簡單,主要就是針對音頻數(shù)據(jù)進行參數(shù)檢查并對數(shù)據(jù)進行填充,最后調(diào)用av_frame_ref將輸入的數(shù)據(jù)的引用計數(shù)+1、

3.4.1 avcodec_receive_packet

3.4.1.1 基本流程

??首先是檢查當前codec是否為編碼器并且是否打開,如果是就繼續(xù)。然后檢查codec中的packet buffer是否有數(shù)據(jù)有的話就直接返回了,不然就會調(diào)用encode_receive_packet_internal。

int attribute_align_arg avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt){
    AVCodecInternal *avci = avctx->internal;
    int ret;

    av_packet_unref(avpkt);

    if (!avcodec_is_open(avctx) || !av_codec_is_encoder(avctx->codec))
        return AVERROR(EINVAL);

    if (avci->buffer_pkt->data || avci->buffer_pkt->side_data) {
        av_packet_move_ref(avpkt, avci->buffer_pkt);
    } else {
        ret = encode_receive_packet_internal(avctx, avpkt);
        if (ret < 0)
            return ret;
    }

    return 0;
}

??encode_receive_packet_internal首先就是參數(shù)檢查,然后根據(jù)codec的函數(shù)指針設置看調(diào)用哪個流程獲取編碼流。encode_simple_receive_packet就是個while循環(huán)調(diào)用encode_simple_internal直到獲取編碼數(shù)據(jù)或者出錯為止。

    if (avctx->codec->receive_packet) {
        ret = avctx->codec->receive_packet(avctx, avpkt);
        if (ret < 0)
            av_packet_unref(avpkt);
        else
            // Encoders must always return ref-counted buffers.
            // Side-data only packets have no data and can be not ref-counted.
            av_assert0(!avpkt->data || avpkt->buf);
    } else
        ret = encode_simple_receive_packet(avctx, avpkt);

??encode_simple_internal除了前面一大坨參數(shù)檢查,主要救贖下面這塊兒,看是利用多線程編碼還是利用codec的encode接口編碼。

    if (CONFIG_FRAME_THREAD_ENCODER &&
        avci->frame_thread_encoder && (avctx->active_thread_type & FF_THREAD_FRAME))
        /* This might modify frame, but it doesn't matter, because
         * the frame properties used below are not used for video
         * (due to the delay inherent in frame threaded encoding, it makes
         *  no sense to use the properties of the current frame anyway). */
        ret = ff_thread_video_encode_frame(avctx, avpkt, frame, &got_packet);
    else {
        ret = avctx->codec->encode2(avctx, avpkt, frame, &got_packet);
        if (avctx->codec->type == AVMEDIA_TYPE_VIDEO && !ret && got_packet &&
            !(avctx->codec->capabilities & AV_CODEC_CAP_DELAY))
            avpkt->pts = avpkt->dts = frame->pts;
    }
3.4.1.2 多線程

??編碼的線程和解碼的線程一樣都是在avcodec_open2時創(chuàng)建的,編碼是調(diào)用ff_frame_thread_encoder_init創(chuàng)建的,其中主要就是調(diào)用pthread的接口創(chuàng)建線程和相關(guān)的參數(shù),可以看到其工作的函數(shù)為static void * attribute_align_arg worker(void *v),編碼過程中有多個線程每個線程都運行一個worker任務,通過信號量來進行消息的同步。該任務中最終會調(diào)用avctx->codec->encode2對數(shù)據(jù)進行編碼。而所有的數(shù)據(jù)交互都是通過ThreadContext進行的,無論是輸入數(shù)還是輸出的數(shù)據(jù)還是消息同步都是通過該Context進行的。

typedef struct{
    AVCodecContext *parent_avctx;
    pthread_mutex_t buffer_mutex;

    pthread_mutex_t task_fifo_mutex; /* Used to guard (next_)task_index */
    pthread_cond_t task_fifo_cond;

    unsigned max_tasks;
    Task tasks[BUFFER_SIZE];
    pthread_mutex_t finished_task_mutex; /* Guards tasks[i].finished */
    pthread_cond_t finished_task_cond;

    unsigned next_task_index;
    unsigned task_index;
    unsigned finished_task_index;

    pthread_t worker[MAX_THREADS];
    atomic_int exit;
} ThreadContext;

??當數(shù)據(jù)到達時主線程會先拷貝數(shù)據(jù)然后發(fā)送信號量signal給任務線程,任務線程拿到消息后編碼完成后給主線程發(fā)信號finish,主線程取走數(shù)據(jù)。

3.4.2 gif_encode_frame

??GIF編碼調(diào)用的是gif_encode_frame,內(nèi)部實際調(diào)用的gif_image_write_image。GIF編碼和解碼的流程基本相反,除了寫B(tài)lock的流信息外,先將當前圖像的顏色映射根據(jù)顏色表映射到具體的索引,然后調(diào)用ff_lzw_encode對流進行編碼。

        for (y = 0; y < height; y++) {
            memcpy(s->tmpl, ptr, width);
            for (x = 0; x < width; x++)
                if (ref[x] == ptr[x])
                    s->tmpl[x] = trans;
            len += ff_lzw_encode(s->lzw, s->tmpl, width);
            ptr += linesize;
            ref += ref_linesize;

3.5 封裝

3.5.1 avformat_write_header

??avformat_write_header比較簡單直接調(diào)用的對應格式的write_header的函數(shù)指針。GIF的write_header做的事情比較少。

static int gif_write_header(AVFormatContext *s){
    if (s->nb_streams != 1 ||
        s->streams[0]->codecpar->codec_type != AVMEDIA_TYPE_VIDEO ||
        s->streams[0]->codecpar->codec_id   != AV_CODEC_ID_GIF) {
        av_log(s, AV_LOG_ERROR,
               "GIF muxer supports only a single video GIF stream.\n");
        return AVERROR(EINVAL);
    }

    avpriv_set_pts_info(s->streams[0], 64, 1, 100);

    return 0;
}

3.5.2 av_interleaved_write_frame

??首先就是根據(jù)輸入數(shù)據(jù)是否為空選擇調(diào)用的函數(shù),如果為空就會調(diào)用interleaved_write_packet刷新數(shù)據(jù),否則調(diào)用write_packets_common寫數(shù)據(jù)。
??write_packets_common中,check_packet檢查輸入的數(shù)據(jù)和期望寫入的媒體流是否能夠?qū)ι稀?code>prepare_input_packet對輸入數(shù)據(jù)進行修正,如果pts和dts其中之一為NOPTS則設置為對方的值,以及如果設置了is_intra_only則每一幀都會設置標志位AV_PKT_FLAG_KEY。而check_bitstream就是調(diào)用s->oformat->check_bitstream檢查流是否符合對應的格式。最后才是調(diào)用write_packet_common進行寫數(shù)據(jù)。如果有設置filter的話就調(diào)用write_packets_from_bsfs處理。

??write_packet_common會根據(jù)輸入的參數(shù)是否需要交織存儲來調(diào)用具體的函數(shù)寫packet。非交織的情況下就會調(diào)用write_packet,該函數(shù)內(nèi)部實際調(diào)用的s->oformat->write_packets->oformat->write_uncoded_frame寫文件,后者處理裸流。
??interleaved_write_packet內(nèi),如果AVOuputFormat設置了對應的函數(shù)指針則直接調(diào)用s->oformat->interleave_packet寫文件,否則就用FFmpeg提供的ff_interleave_packet_per_dts。我們重點看下這個函數(shù)實現(xiàn)。

3.5.2.1 ff_interleave_packet_per_dts

??ff_interleave_packet_per_dts只是針對當前的兩個流的packet的時間戳進行比較避免在文件存儲過程中距離太遠導致解封轉(zhuǎn)時要頻繁seek文件。最終封裝文件寫入到磁盤還是需要write_packet。該函數(shù)首先將送入的pkt插入到緩存隊列中,然后在從當前緩存隊列中選出一幀返回調(diào)用write_packet進行寫入。
??在看ff_interleave_add_packet函數(shù)的實現(xiàn)之前,我們先簡單看下幀比較函數(shù)interleave_compare_dts的實現(xiàn),該函數(shù)用來比較兩個packet的dts。如果非音頻流就是調(diào)用的av_compare_ts進行比較,否則會根據(jù)當前音頻流是否有preload去除preload的偏移:

int preload  = st ->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;
int preload2 = st2->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;
if (preload != preload2) {
    int64_t ts, ts2;
    preload  *= s->audio_preload;
    preload2 *= s->audio_preload;
    //preload不同時需要減掉preload的偏移
    ts = av_rescale_q(pkt ->dts, st ->time_base, AV_TIME_BASE_Q) - preload;
    ts2= av_rescale_q(next->dts, st2->time_base, AV_TIME_BASE_Q) - preload2;
    if (ts == ts2) {
        ts  = ((uint64_t)pkt ->dts*st ->time_base.num*AV_TIME_BASE - (uint64_t)preload *st ->time_base.den)*st2->time_base.den
            - ((uint64_t)next->dts*st2->time_base.num*AV_TIME_BASE - (uint64_t)preload2*st2->time_base.den)*st ->time_base.den;
        ts2 = 0;
    }
    comp = (ts2 > ts) - (ts2 < ts);
}

??重點就是下面的代碼,從當前buffer中找到當前幀的插入位置然后插入到packet的鏈表中。

if (st->internal->last_in_packet_buffer) {
    next_point = &(st->internal->last_in_packet_buffer->next);
} else {
    next_point = &s->internal->packet_buffer;
}
//省略部分代碼.......
if (*next_point) {
    if (chunked && !(pkt->flags & CHUNK_START))
        goto next_non_null;

    if (compare(s, &s->internal->packet_buffer_end->pkt, pkt)) {
        while (   *next_point
                && ((chunked && !((*next_point)->pkt.flags&CHUNK_START))
                    || !compare(s, &(*next_point)->pkt, pkt)))
            next_point = &(*next_point)->next;
        if (*next_point)
            goto next_non_null;
    } else {
        next_point = &(s->internal->packet_buffer_end->next);
    }
}

??插入成功后回到ff_interleave_packet_per_dts中,從當前的packet鏈表的頭結(jié)點拿到一陣返回給write_packet寫入。

3.5.2.2 gif_write_packet

??gif_write_packet比較簡單就是根據(jù)當前的流信息寫GCE等基本的BLOCK信息。

        /* "NETSCAPE EXTENSION" for looped animation GIF */
        if (gif->loop >= 0) {
            avio_w8(pb, GIF_EXTENSION_INTRODUCER); /* GIF Extension code */
            avio_w8(pb, GIF_APP_EXT_LABEL); /* Application Extension Label */
            avio_w8(pb, 0x0b); /* Length of Application Block */
            avio_write(pb, "NETSCAPE2.0", sizeof("NETSCAPE2.0") - 1);
            avio_w8(pb, 0x03); /* Length of Data Sub-Block */
            avio_w8(pb, 0x01);
            avio_wl16(pb, (uint16_t)gif->loop);
            avio_w8(pb, 0x00); /* Data Sub-block Terminator */
        }

        delay_pos = gif_parse_packet(s, pkt->data + off, pkt->size - off);
        if (delay_pos > 0 && delay_pos < pkt->size - off - 2) {
            avio_write(pb, pkt->data + off, delay_pos);
            avio_wl16(pb, gif_get_delay(gif, pkt, new_pkt));
            avio_write(pb, pkt->data + off + delay_pos + 2, pkt->size - off - delay_pos - 2);
        } else {
            avio_write(pb, pkt->data + off, pkt->size - off);
        }

3.5.3 av_write_trailer

??av_write_trailer就做了兩件是刷新緩沖區(qū)和寫尾。GIF寫尾調(diào)用的static int gif_write_trailer(AVFormatContext *s)

static int gif_write_trailer(AVFormatContext *s){
    GIFContext *gif = s->priv_data;
    AVIOContext *pb = s->pb;

    if (!gif->prev_pkt)
        return AVERROR(EINVAL);

    gif_write_packet(s, NULL);

    if (!gif->have_end)
        avio_w8(pb, GIF_TRAILER);
    av_packet_free(&gif->prev_pkt);

    return 0;
}

3.6 銷毀

??完成任務后調(diào)用各自的現(xiàn)場清理函數(shù)。文章來源地址http://www.zghlxwxcb.cn/news/detail-681515.html

到了這里,關(guān)于FFmpeg5.0源碼閱讀——FFmpeg大體框架的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權(quán),不承擔相關(guān)法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務器費用

相關(guān)文章

  • FFmpeg5.0源碼閱讀—— av_read_frame

    FFmpeg5.0源碼閱讀—— av_read_frame

    ?? 摘要 :本文主要描述了FFmpeg中用于打開編解碼器接口 av_read_frame 的具體調(diào)用流程,詳細描述了該接口被調(diào)用時所作的具體工作。 ?? : ffmpeg 、 av_read_frame ?? 讀者須知 :讀者需要了解FFmpeg的基本使用流程,以及一些FFmpeg的基本常識,了解FFmpegIO相關(guān)的內(nèi)容,

    2024年02月16日
    瀏覽(19)
  • FFmpeg5.0源碼閱讀——av_interleaved_write_frame

    FFmpeg5.0源碼閱讀——av_interleaved_write_frame

    ?? 摘要 :本文主要詳細描述FFmpeg中封裝時寫packet到媒體文件的函數(shù) av_interleaved_write_frame 的實現(xiàn)。 ?? : av_interleaved_write_frame ?? 讀者須知 :讀者需要熟悉ffmpeg的基本使用。 ?? av_interleaved_write_frame 的基本調(diào)用流程圖如下。 ??首先就是根據(jù)輸入數(shù)據(jù)是否為空

    2024年02月14日
    瀏覽(15)
  • 音視頻八股文(6)-- ffmpeg大體介紹和內(nèi)存模型

    音視頻八股文(6)-- ffmpeg大體介紹和內(nèi)存模型

    ? 容器/文件(Conainer/File):即特定格式的多媒體文件, 比如mp4、flv、mkv等。 ? 媒體流(Stream):表示時間軸上的一段連續(xù)數(shù)據(jù),如一 段聲音數(shù)據(jù)、一段視頻數(shù)據(jù)或一段字幕數(shù)據(jù),可以是壓縮 的,也可以是非壓縮的,壓縮的數(shù)據(jù)需要關(guān)聯(lián)特定的編解 碼器(有些碼流音頻

    2023年04月27日
    瀏覽(24)
  • FFmpeg5.0源碼閱讀—— avcodec_send_frame && avcodec_receive_packet

    FFmpeg5.0源碼閱讀—— avcodec_send_frame && avcodec_receive_packet

    ?? 摘要 :本文主要描述了FFmpeg中用于編碼的接口的具體調(diào)用流程,詳細描述了該接口被調(diào)用時所作的具體工作。 ?? : ffmpeg 、 avcodec_send_frame 、 avcodec_receive_packet ?? 讀者須知 :讀者需要了解FFmpeg的基本使用流程,以及一些FFmpeg的基本常識,了解FFmpegIO相關(guān)的內(nèi)

    2024年02月16日
    瀏覽(22)
  • 【Android音視頻】MacOS上FFmpeg5.0.1編譯

    【Android音視頻】MacOS上FFmpeg5.0.1編譯

    1. FFmpeg官網(wǎng)下載鏈接(推薦下載release的版本): Download FFmpeg http://ffmpeg.org/download.html#releases ?盡情去下載并開始編譯吧 2. 下載壓縮包,解壓至自己想要的文件路徑下即可。個人習慣用全英文路徑,避免出現(xiàn)奇怪的問題。 3. Android Studio請預先下載好。點擊AS右上角“SDK Manager”

    2024年02月02日
    瀏覽(29)
  • 【音視頻處理】基礎框架介紹,F(xiàn)Fmpeg、GStreamer、OpenCV、OpenGL

    【音視頻處理】基礎框架介紹,F(xiàn)Fmpeg、GStreamer、OpenCV、OpenGL

    大家好,歡迎來到停止重構(gòu)的頻道。? 本期我們介紹 音視頻處理的基礎框架 。 包括FFmpeg、GStreamer、OpenCV、OpenGL 。 我們按這樣的分類介紹 : 1、編解碼處理:FFmpeg、GStreamer 2、圖像分析:OpenCV 3、復雜圖像生成:OpenGL 首先是編解碼處理的基礎框架,這類基礎框架的 應用場景

    2024年02月08日
    瀏覽(49)
  • FFmpeg——開源的開源的跨平臺音視頻處理框架簡介

    FFmpeg——開源的開源的跨平臺音視頻處理框架簡介

    引言: ????????FFmpeg是一個開源的跨平臺音視頻處理框架,可以處理多種音視頻格式。它由Fabrice Bellard于2000年創(chuàng)建,最初是一個只包括解碼器的項目。后來,很多開發(fā)者參與其中,為FFmpeg增加了多種新的功能,例如編碼器、過濾器、muxer、demuxer等等,使它成為了一個完整

    2024年03月23日
    瀏覽(91)
  • QtAV:基于Qt和FFmpeg的跨平臺高性能音視頻播放框架

    QtAV:基于Qt和FFmpeg的跨平臺高性能音視頻播放框架

    目錄 一.簡介 1.特性 2.支持的平臺 3.簡單易用的接口 二.編譯 1.下載依賴包 2.開始編譯 2.1克隆 2.2修改配置文件 2.3編譯 三.試用 官網(wǎng)地址:http://www.qtav.org/ Github地址:https://github.com/wang-bin/QtAV ●支持大部分播放功能 ●播放、暫停、播放速度、快進快退、字幕、音量、聲道、音

    2024年01月22日
    瀏覽(668)
  • FFmpeg源碼分析:avcodec_send_frame()和avcodec_receive_packet()音視頻編碼

    FFmpeg源碼分析:avcodec_send_frame()和avcodec_receive_packet()音視頻編碼

    FFmpeg在libavcodec模塊,舊版本提供avcodec_encode_video2()作為視頻編碼函數(shù),avcodec_encode_audio2()作為音頻編碼函數(shù)。在FFmpeg 3.1版本新增avcodec_send_frame()與avcodec_receive_packet()作為音視頻編碼函數(shù)。后來,在3.4版本把avcodec_encode_video2()和avcodec_encode_audio2()標記為過時API。 在上一篇文章介紹

    2023年04月11日
    瀏覽(22)
  • FFmpeg源碼分析:avcodec_send_packet()與avcodec_receive_frame()音視頻解碼

    FFmpeg源碼分析:avcodec_send_packet()與avcodec_receive_frame()音視頻解碼

    FFmpeg在libavcodec模塊,舊版本提供avcodec_decode_video2()作為視頻解碼函數(shù),avcodec_decode_audio4()作為音頻解碼函數(shù)。在FFmpeg 3.1版本新增avcodec_send_packet()與avcodec_receive_frame()作為音視頻解碼函數(shù)。后來,在3.4版本把avcodec_decode_video2()和avcodec_decode_audio4()標記為過時API。版本變更描述如下

    2024年02月03日
    瀏覽(95)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包