[ffmpeg] 定制濾波器


如果有定制ffmpeg濾波器的需求,有兩個結構體是必須要了解的:AVFilter、AVFilterPad,所定制的濾波器主要就是通過填充這兩個結構體來實現的。我們下面將詳細解析這兩個結構體,並通過對濾波器的初始化流程以及濾波流程進行分析,進一步加深對ffmpeg濾波框架的了解。

 

AVFilter

AVFilter就是一個濾波器的主體,其結構體定義如下:

typedef struct AVFilter {
    const char *name;
    const char *description;
    const AVFilterPad *inputs;
    const AVFilterPad *outputs;
    const AVClass *priv_class;
    int flags;
    int (*preinit)(AVFilterContext *ctx);
    int (*init)(AVFilterContext *ctx);
    int (*init_dict)(AVFilterContext *ctx, AVDictionary **options);
    void (*uninit)(AVFilterContext *ctx);
    int (*query_formats)(AVFilterContext *);
    int priv_size;      
    int flags_internal; 
    struct AVFilter *next;
    int (*process_command)(AVFilterContext *, const char *cmd, const char *arg, char *res, int res_len, int flags);
    int (*init_opaque)(AVFilterContext *ctx, void *opaque);
    int (*activate)(AVFilterContext *ctx);
} AVFilter;

其各個成員變量有如下含義:

name 濾波器名字。
description 濾波器的簡短介紹。
inputs 濾波器入口(AVFilterPad)列表。
outputs 濾波器出口(AVFilterPad)列表。
priv_class 主要用於維護用戶傳入的參數(AVOption)的結構體,一般來說用戶向濾波器傳入參數有兩個手段:在創建濾波器實例的時候傳入指定參數的字符串,或者在創建完成濾波器實例后通過av_opt_set之類的接口傳入字符串。
flags 濾波器標志。
preinit 濾波器預初始化函數。這個函數會在創建濾波器實例的開頭被調用。
init 濾波器自身的特制初始化函數。初始化,即avfilter_graph_create_filter,可以被分解成通用的初始化以及特制初始化。
通用初始化通常包含三個步驟:
  1. 創建用於存放濾波器實例的內存,進行一些初始化默認的賦值處理。
  2.把傳入的字符串解析進行解析得到字典的兩要素:參數名稱key,參數值val。
  3.通過priv_class所維護的AVOption,可以找到名為key的參數對應的內存位置(即濾波器實例的私有結構體priv中名稱為key的參數的位置),並把val寫入該位置當中即可完成參數設置。私有結構體priv中的參數就通常就是濾波器的實際參數,在進行濾波時會根據其中的參數進行濾波處理。
特制的初始化有很多不同的用途,比如檢查參數,如果檢查到所輸入的參數中缺少一些重要的參數,則可以返回負值來表示初始化錯誤。
init_dict 與上方init功能相同,不太常用。
uninit 如果在init函數出現錯誤則會調用uninit來做一些后續處理。
query_formats 為了進行濾波器之間的濾波格式協商,AVFilter的query_formats函數會去設置AVFilterLink上的in_formats/out_formats等,這是格式協商的第一步。
priv_size 濾波器實例的私有結構體(priv)的大小。我們前面也說了priv當中的參數就是濾波器的實際參數,而不同濾波器的參數不同,那么所占用的空間也不會一樣,因此在創建濾波器實例的時候會根據priv_size來開辟用於存放參數的空間。
flags_internal 濾波器內部標志。
next 在新版本ffmpeg中不會使用到這個next參數。
老版本的ffmpeg需要用avfilter_rigister來注冊濾波器(AVFilter),注冊的時候就會使用這個next參數,使得所有注冊了的濾波器形成一個濾波器鏈表,如果需要某個濾波器則可以從該鏈表中獲取。
新版本的ffmpeg使用的是列表(filter_list)來列出所有的濾波器(AVFilter),一般來說,如果想獲得濾波器,可以調用avfilter_get_by_name來輪詢列表獲得。
process_command 一般來說,濾波參數的設置有兩種方式:
  1. 在初始化時(avfilter_graph_create_filter),輸入參數字符串。
  2. 在初始化后,配置整個濾波圖前(avfilter_graph_config),調用av_opt_set之類的接口輸入參數。
為了保證濾波器正常運行,在濾波的過程中一般是不會對濾波參數進行修改的。當然,在濾波過程中調用av_opt_set之類的函數是可以修改濾波參數,但是並不能保證濾波器會按照我們預想地那樣運行。因為如果按照前面的兩種方式設置濾波參數,后面可能還會執行AVFilterPad的config_props操作,而在濾波過程中通過av_opt_set之類的函數去設置濾波參數時是不會再回去繼續執行這一步的。
不過現實當中確實存在在濾波過程中修改濾波參數的需求,比如說播放音樂時可以調整EQ。此時就可以通過實現process_command這個函數來實現濾波過程中的各種變化。
使用avfilter_graph_send_command就能觸發所指定的濾波器調用其process_command函數。
init_opaque 與init功能相同,不常用。
activate 濾波函數。濾波函數有兩種實現方式,一種是通過activate來實現,另一種是后面會說到的AVFilterPad中的filter_frame以及request_frame函數。如果是采用activate的方式,就需要在activate內實現以下流程:
  1. 獲取前面的濾波器實例輸出的幀。具體操作就是調用ff_inlink_consume_frame來從inlink獲取前面濾波器實例輸出的幀。
        如果所需要的幀未准備好,則需要通知相應的濾波器實例,表明當前濾波器需要幀。具體操作就是調用ff_inlink_request_frame來設置inlink上的frame_wanted_out,該變量就是用於表明inlink的目標濾波器實例,即當前濾波器實例需要前一個濾波器實例輸出幀。
        如果所需要的幀已准備好,就可以執行濾波操作。
  2. 向后面的濾波器實例輸出濾波完成的幀。具體操作就是調用ff_filter_frame來向outlink輸出幀。

 

 

AVFilterPad

AVFilterPad是濾波器的出口或者入口,其結構定義如下:

struct AVFilterPad {
    const char *name;
    enum AVMediaType type;
    AVFrame *(*get_video_buffer)(AVFilterLink *link, int w, int h);
    AVFrame *(*get_audio_buffer)(AVFilterLink *link, int nb_samples);
    int (*filter_frame)(AVFilterLink *link, AVFrame *frame);
    int (*poll_frame)(AVFilterLink *link);
    int (*request_frame)(AVFilterLink *link);
    int (*config_props)(AVFilterLink *link);
    int needs_fifo;
    int needs_writable;
};

各成員變量具有如下含義:

name 出/入口(input/output pads)名字。
type 支持的幀類型:AVMEDIA_TYPE_VIDEO/AVMEDIA_TYPE_AUDIO。
get_video_buffer
(input pads only)
提供用於寫入視頻圖像的buffer,一般是向前一個濾波器實例提供。
一個濾波器在濾波過程中,可能需要額外的buffer來進行濾波處理,比如scale或者aresample這種格式轉換濾波器,在進行濾波處理時,有輸入幀作為源材料,輸入幀有確實存在的buffer,而為了進行輸出,我們需要額外的buffer來存放格式轉換后的幀。所需的buffer除了指定的寬與高之外,還有像素格式,這三點是影響buffer大小的因素,像素格式就是輸出鏈上的格式(link->format)。
如果一個濾波器實例需要buffer,可以通過ff_get_video_buffer(outlink, w, h)來調用下一個濾波器對應AVFilterPad上的get_video_buffer函數。不過一般來說,是不需要AVFilterPad去實現get_video_buffer這個函數的,因為如果AVFilterPad不實現這個函數,則會調用默認的ff_default_get_video_buffer,該函數會根據輸入的w,h以及link的format來提供buffer。
※ffmpeg中僅有幾個filter實現了get_video_buffer(vflip,swapuv等),不過其內部也是調用了ff_default_get_video_buffer,並且其它部分的代碼看起來並沒有起到什么實際作用。
get_audio_buffer
(input pads only)
含義同上,提供給上一個濾波器實例調用。
調用接口為ff_get_audio_buffer(outlink, nb_samples),如果沒有實現該函數,則會默認調用到函數ff_default_get_audio_buffer,buffer的大小受到輸入參數的nb_samples以及link->channels,link->format的影響。
一般來說不需要濾波器實現get_audio_buffer函數。
※ffmpeg中並沒有實現了get_audio_buffer的濾波器。
filter_frame
(input pads only)
filter_frame是最常見的濾波實現函數。如果AVFilter沒有實現activate函數,則會調用默認的activate函數ff_filter_activate_default,該函數最終會調用到filter_frame來提供濾波的實現。
filter_frame的輸入參數中包括濾波實例的inlink以及從inlink上提取的frame,一般來說filter_frame會對該frame進行濾波處理,然后調用ff_filter_frame向outlink輸出濾波后的幀。
poll_frame
(output pads only)
設定上poll_frame是用於查看前一個濾波器實例的request_frame能返回多少幀,不過實際上應該是沒有用到這個函數的地方。
request_frame
(output pads only)
request_frame其實也是一個用於產生幀的濾波函數,不過觀察request_frame的參數可以發現該函數並沒有frame作為輸入參數,這表明了request_frame有特定的應用場景:
1. 如果一個濾波器是源濾波器,僅需要輸出幀,則可以在request_frame內生成幀,然后調用ff_filter_frame把幀輸出到outlink。ffmpeg中源濾波器的源文件都帶有src關鍵字,如buffersrc以及vsrc/asrc為開頭的濾波器。
2. 如果一個濾波器希望在EOF后繼續輸出幀,則可以用request_frame調用ff_filter_frame來進行輸出。
config_props config_props的調用發生在query_formats之后,此時濾波格式的協調已經完成,也就已經確定了濾波器實例的輸入以及輸出格式(inlink->format/outlink->format)。如果某些設置需要使用到這些輸入輸出格式,就可以在config_props中進行設置。如aresampe在config_props中就利用協調完成的format、channel_layout、sample_rate來進行重采樣的參數設置。
needs_fifo
(input pad only)
表明只有當濾波器實例主動請求幀(調用ff_inlink_request frame或者ff_request_frame)的時候,前一個濾波器實例才會向當前濾波器實例輸出幀(ff_filter_frame)。
如果needs_fifo為1,會自動在當前濾波器實例與前一個濾波器實例之間插入一個名為fifo的濾波器,該濾波器實現了上述功能。
needs_writable
(input pad only)
表明濾波器需要對pad對應的link所輸入的frame進行寫入。如進行字幕渲染的ass濾波器就需要對輸入的視頻幀進行寫入。

 

 

初始化流程

首先是avfilter_graph_create_filter,即創建濾波器實例。如前面所說,這個函數會在最開頭調用濾波器的preinit函數,然后創建濾波器實例並做一些簡單的初始化,解析輸入的字符串,最后調用濾波器的init函數。

image

在構建好一整個AVFilterGraph后,就可以調用avfilter_graph_config來做graph最后的配置。

image

其中graph_insert_fifos中就會對設定了needs_fifo=1的input pad所在的link插入名為fifo的濾波器。

            if (!link->dstpad->needs_fifo)
                continue;

            fifo = f->inputs[j]->type == AVMEDIA_TYPE_VIDEO ?
                   avfilter_get_by_name("fifo") :
                   avfilter_get_by_name("afifo");

            snprintf(name, sizeof(name), "auto_fifo_%d", fifo_count++);

            ret = avfilter_graph_create_filter(&fifo_ctx, fifo, name, NULL,
                                               NULL, graph);

            ret = avfilter_insert_filter(link, fifo_ctx, 0, 0);

graph_config_formats中如前一篇文章所說,就是對整個graph中濾波格式進行協商,協商過后可以確定所有link上的格式。

graph_config_links主要目的就是調用pad中的config_props,那么config_props就能根據前面協商得到的link格式做進一步的操作。

 

 

濾波流程

一般來說,用戶會按照如下方式調用濾波API來進行濾波處理:

    ret = av_buffersrc_add_frame(in_filter, pFrame);

    while((ret = av_buffersink_get_frame(out_filter, pFrame))>=0){
        //TODO
    }

向graph輸入幀

濾波的流程都是首先調用av_buffersrc_add_frame,從向buffersrc輸入幀開始的

image

可以看到調用了buffersrc的request_frame函數,該函數最后用ff_filter_frame向outlink輸出幀。、

 

從graph提取幀

然后調用av_buffersink_get_frame,嘗試獲得buffersink輸出的幀,如果返回值大於0則表明得到了一幀,正常情況下如果無法獲得幀通常會返回EAGAIN,這表明要求用戶向buffersrc輸入更多的幀。

av_buffersink_get_frame會向下調用到get_frame_internal,該函數主要作用就是調用濾波器進行濾波,並返回濾波完成的幀。函數實現如下:

static int get_frame_internal(AVFilterContext *ctx, AVFrame *frame, int flags, int samples)
{
    BufferSinkContext *buf = ctx->priv;
    AVFilterLink *inlink = ctx->inputs[0];
    int status, ret;
    AVFrame *cur_frame;
    int64_t pts;

    if (buf->peeked_frame)
        return return_or_keep_frame(buf, frame, buf->peeked_frame, flags);

    while (1) {
        ret = samples ? ff_inlink_consume_samples(inlink, samples, samples, &cur_frame) :
                        ff_inlink_consume_frame(inlink, &cur_frame);
        if (ret < 0) {
            return ret;
        } else if (ret) {
            /* TODO return the frame instead of copying it */
            return return_or_keep_frame(buf, frame, cur_frame, flags);
        } else if (ff_inlink_acknowledge_status(inlink, &status, &pts)) {
            return status;
        } else if ((flags & AV_BUFFERSINK_FLAG_NO_REQUEST)) {
            return AVERROR(EAGAIN);
        } else if (inlink->frame_wanted_out) {
            ret = ff_filter_graph_run_once(ctx->graph);
            if (ret < 0)
                return ret;
        } else {
            ff_inlink_request_frame(inlink);
        }
    }
}

該函數有如下實現邏輯:

  1. 調用ff_inlink_consume_frame,看是否可以獲得濾波完成的幀,如果得到了濾波后的幀,則調用return_or_keep_frame進行返回。
  2. 調用ff_inlink_acknowledge_status,查看濾波過程中是否出了差錯,或者是否到了EOF,是則返回錯誤。
  3. frame_wanted_out用於表明當前濾波器是否已經獲得了前一個濾波器輸出的一幀,等於1則表示未獲得,那么需要調用ff_filter_graph_run_once;等於0則表示已獲得一幀,或者是第一次調用該函數。

跳出get_frame_internal里面的while循環只有下面幾種情況:

  1. ff_inlink_consume_frame返回值不為0,表明可以返回濾波后的幀。
  2. ff_inlink_acknowledge_status返回值不為0,表明濾波器運行過程中出錯。
  3. ff_filter_graph_run_once返回值小於0,一般情況下為EAGAIN,表明濾波器需要輸入更多的幀作為原料。

如果是第一次調用av_buffersink_get_frame,當進入該函數時,正常情況下會按照如下步驟執行:

  1. 由於是第一次調用,因此frame_wanted_out為0,那么第一次循環會去執行ff_inlink_request_frame,這個函數會把frame_wanted_out設置為1,表明當前濾波器未獲得前一個濾波器輸出的幀。
  2. 然后下一次循環時由於frame_wanted_out為1,會去調用ff_filter_graph_run_once。
  3. 通常來說,經過幾次循環調用ff_filter_graph_run_once后,會得到濾波后的幀,此時ff_inlink_consume_frame會返回1,因此就能調用return_or_keep_frame來向用戶返回濾波后的幀了。
  4. 如果需要輸入更多的幀才能繼續進行濾波,那么會從ff_filter_graph_run_once返回EAGAIN。

 

激活濾波器

我們前面提到的ff_filter_graph_run_once就是查找已經就緒(ready)的濾波器實例,並對該濾波器進行激活。所得到的濾波器實例會滿足兩個條件:

  1. 濾波器的ready值(優先級)更高的會被選上
  2. 在此基礎上,因為查找順序是按照用戶調用avfilter_graph_create_filter的順序,因此最先創建的濾波器實例會被選中。
AVFilterContext *avfilter_graph_alloc_filter(AVFilterGraph *graph,
                                             const AVFilter *filter,
                                             const char *name)
{
    s = ff_filter_alloc(filter, name);
    graph->filters[graph->nb_filters++] = s;
}

int ff_filter_graph_run_once(AVFilterGraph *graph)
{
    filter = graph->filters[0];
    for (i = 1; i < graph->nb_filters; i++)
        if (graph->filters[i]->ready > filter->ready)
            filter = graph->filters[i];
    if (!filter->ready)
        return AVERROR(EAGAIN);
    return ff_filter_activate(filter);
}

 

選出優先級最高的濾波器實例后,首先把該濾波器就緒狀態清零,然后開始執行激活操作:如果該濾波器實現了activate函數,則調用該函數,否則調用默認的激活函數ff_filter_activate_default。

int ff_filter_activate(AVFilterContext *filter)
{
    filter->ready = 0;
    ret = filter->filter->activate ? filter->filter->activate(filter) :
          ff_filter_activate_default(filter);
}

如果調用的是默認的激活函數,則會按照以下優先級進行激活處理:

static int ff_filter_activate_default(AVFilterContext *filter)
{
    unsigned i;

    for (i = 0; i < filter->nb_inputs; i++) {
        if (samples_ready(filter->inputs[i], filter->inputs[i]->min_samples)) {
            return ff_filter_frame_to_filter(filter->inputs[i]);
        }
    }
    for (i = 0; i < filter->nb_inputs; i++) {
        if (filter->inputs[i]->status_in && !filter->inputs[i]->status_out) {
            av_assert1(!ff_framequeue_queued_frames(&filter->inputs[i]->fifo));
            return forward_status_change(filter, filter->inputs[i]);
        }
    }
    for (i = 0; i < filter->nb_outputs; i++) {
        if (filter->outputs[i]->frame_wanted_out &&
            !filter->outputs[i]->frame_blocked_in) {
            return ff_request_frame_to_filter(filter->outputs[i]);
        }
    }
    return FFERROR_NOT_READY;
}

可以發現共有三種激活處理方式,分別為:

  • 從上往下傳遞濾波完成的幀。
  • 從上往下傳遞錯誤代碼。
  • 從下往上傳遞幀的請求。

前面的方式優先級更高。下面我們會對這三種方式進行較為詳細的分析

ff_filter_frame_to_filter

觸發條件是:當前濾波器實例的input link上的samples_ready。這samples_ready表明了前面的濾波器實例已經執行ff_filter_frame向當前濾波器實例的input link輸出了幀。

有了上述條件,那么為了完成當前濾波器實例的濾波操作,接下來會執行:

  1. 從input link上提取出幀。
  2. 調用當前濾波器的filter_frame函數來執行濾波操作。
  3. 一般來說濾波處理完成過后也會調用ff_filter_frame來把濾波完成的幀輸出到output link。
  4. ff_filter_frame接下來會把下一個濾波器實例設為就緒狀態。
  5. 如果在執行filter_frame時出錯,沒能完成濾波處理,則會把錯誤代碼寫入input link的status_out,表明該link無法順利輸出frame到當前filter。
  6. 如果執行filter_frame成功,考慮到前一個濾波器實例可能輸出了不止一幀,因此再次把當前濾波器實例設為就緒狀態,如果前一個濾波器確實輸出了多個幀,這部操作會使得這多個幀都被當前濾波器實例濾波完成后才會執行下一個濾波器實例的濾波處理。

image

 

foward_status_change

觸發條件是:當前濾波器實例的input link上有status_in && !status_out。status_in不為0表明調用前一個濾波器實例的request_frame時出現了錯誤,status_in的值就是錯誤代碼,我們需要向后面的濾波器實例傳播這個錯誤代碼。

forward_status_change是傳播錯誤代碼的入口,傳播錯誤代碼的很大一步分的實現都在ff_request_frame函數內。基於上述條件,即調用前一個濾波器實例的request_frame時出現了錯誤。

  1. forward_status_change會調用當前濾波器實例的request_frame來傳輸錯誤代碼,request_frame一般內部都會調用到ff_request_farme函數。
  2. 錯誤代碼status_in會導致ff_request_frame走入錯誤處理分支。
  3. 把status_out的值設置為status_in。
  4. 那么返回值也會是該錯誤代碼,這用於表示當前request_frame也出現了錯誤。
  5. 因此在返回的時候會把下一個濾波器實例的input link(即當前濾波器實例的output link)上的status_in設置為錯誤代碼。
  6. 把frame_wanted_out設置為0用於表示當前濾波器實例的錯誤處理已完成,避免再次調用當前濾波器實例的request_frame函數做重復的錯誤處理。
  7. 把下一個濾波器實例設置為就緒狀態。

image

 

ff_request_frame_to_filter

觸發條件是:當前濾波器的output link上有frame_wanted_out以及!frame_blocked_in。其中frame_blocked_in用於防止重復對同一個link執行request frame操作,重要的是frame_wanted_out。我們前面也說過,如果一個link上的frame_wanted_out=1,表明該link的dst要求src輸出幀,具體一點,就是這會導致執行src的request_frame函數。那么這里的output link上的frame_wanted_out=1就會導致:

  1. 當前的濾波器實例的request_frame被執行。
  2. request_frame內一般會調用ff_request_frame函數。一般情況下,該函數會把input link的frame_wanted_out設置為1。
  3. 然后把前一個濾波器實例設為就緒狀態。
  4. 如果request_frame出錯的話,就是request_frame沒能處理完成,則會把錯誤代碼寫入outlink的status_in,表明該link無法順序求得當前濾波器實例的幀。

image

 

注意:上面所描述的三種處理方式只是比較常見方式。在實際應用中,有些濾波器在filter_frame內調用ff_request_frame,也有些濾波器在request_frame內調用ff_filter_frame的,而且實現了activate函數的濾波器並不會執行上述流程,如果全部都列出來就過於冗余了,具體問題具體分析是很重要的。

 

此外,我們可以看到在執行ff_filter_frame時會把ready值設置為300;在執行ff_request_frame時會吧ready值設置為100;在保存錯誤代碼時會把ready值設置為200。這表明了優先進行濾波處理,其次是錯誤處理,最后才是幀請求。

濾波的激活處理可以按照如下進行總結:

  1. 優先執行濾波處理,如果前面的濾波器實例輸出幀,當前的濾波器實例就可以執行濾波處理,並把濾波后得到的幀向后傳遞。
  2. 如果在濾波時缺乏幀作為原料,則向前要求前面的濾波器實例輸出幀,這個要求會一直往前發送,直到有濾波器實例可以輸出幀。
  3. 如果在濾波或者請求幀的過程中出錯了,就把錯誤往后傳遞,典型的如EOF就是這種傳遞方式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM