FFmpeg原始幀處理-濾鏡API用法詳解

本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-vojnkisk-e.htmlhtml

在FFmpeg中,濾鏡(filter)處理的是未壓縮的原始音視頻數據(RGB/YUV視頻幀,PCM音頻幀等)。一個濾鏡的輸出能夠鏈接到另外一個濾鏡的輸入,多個濾鏡能夠鏈接起來,構成濾鏡鏈/濾鏡圖,各類濾鏡的組合爲FFmpeg提供了豐富的音視頻處理功能。linux

比較經常使用的濾鏡有:scale、trim、overlay、rotate、movie、yadif。scale濾鏡用於縮放,trim濾鏡用於幀級剪切,overlay濾鏡用於視頻疊加,rotate濾鏡實現旋轉,movie濾鏡能夠加載第三方的視頻,yadif濾鏡能夠去隔行。git

本文首先介紹濾鏡的原理,而後經過實例詳細介紹濾鏡API使用方法。github

1. 濾鏡的構成及命令行用法

本節內容節選自「FFmpeg使用基礎」,翻譯整理自《FFmpeg Basics》及官網文檔「Documentation-ffmpeg」。api

在多媒體處理中,術語濾鏡(filter)指的是修改未編碼的原始音視頻數據幀的一種軟件工具。濾鏡分爲音頻濾鏡和視頻濾鏡。FFmpeg提供了不少內置濾鏡,能夠用不少方式將這些濾鏡組合使用。經過一些複雜指令,能夠將解碼後的幀從一個濾鏡引向另外一個濾鏡。這簡化了媒體處理,由於有損編解碼器對媒體流進行屢次解碼和編碼操做會下降整體質量,而引入濾鏡後,不須要屢次解碼編碼操做,相關處理可使用多個濾鏡完成,而濾鏡處理的是原始數據,不會形成數據損傷。緩存

1.1 濾鏡的使用

FFmpeg的libavfilter庫提供了濾鏡API,支持多路輸入和多路輸出。數據結構

濾鏡(filter)的語法爲:
[input_link_lable1][input_link_lable2]... filter_name=parameters [output_link_lable1][output_link_lable12]...
上述語法中,輸入輸出都有鏈接標號(link lable),鏈接符號是可選項,輸入鏈接標號表示濾鏡的輸入,輸出鏈接標號表示濾鏡的輸出。鏈接標號一般用在濾鏡圖中,一般前一個濾鏡的輸出標號會做爲後一個濾鏡的輸入標號,經過同名的標號將濾鏡及濾鏡鏈鏈接起來。鏈接標號的用法參考1.3.2節示例。ide

示例1:
ffplay -f lavfi -i testsrc -vf transpose=1
「-vf」(同「-filter:v」)選項表示使用視頻濾鏡,「transpose=1」是濾鏡,此行命令表示使用transpose視頻濾鏡產生一個順時針旋轉90度的測試圖案svn

示例2:
ffmpeg -i input.mp3 -af atempo=0.8 output.mp3
「-af」(同「-filter:a」)選項表示使用音頻濾鏡,「atempo=0.8」是濾鏡,此行命令表示使用atempo音頻濾鏡將輸入音頻速率下降到80%後寫入輸出文件函數

注意:有些濾鏡只會修改幀屬性而不會修改幀內容。例如,fps濾鏡,setpts濾鏡等。

1.2 濾鏡鏈的使用

濾鏡鏈(filterchain)是以逗號分隔的濾鏡(filter)序列,語法以下:
filter1,fiter2,filter3,...,filterN-2,filterN-1,filterN
濾鏡鏈中若是有空格,須要將濾鏡鏈用雙引號括起來,由於命令行中空格是分隔參數用的。

示例1:
ffmpeg -i input.mpg -vf hqdn3d,pad=2*iw output.mp4
「hqdn3d,pad=2iw」是filterchain,第一個filter是「hqdn3d」(降噪);第二個filter是「pad=2iw」(將圖像寬度填充到輸入寬度的2倍)。此行命令表示,將輸入視頻經降噪處理後,再填充視頻寬度爲輸入寬度的2倍。

1.3 濾鏡圖的使用

濾鏡圖(filtergraph)一般是以分號分隔的濾鏡鏈(filterchain)序列。濾鏡圖分爲簡單濾鏡圖和複雜濾鏡圖。
濾鏡圖(filtergraph)的語法以下:
filter1;fiter2;filter3;...;filterN-2;filterN-1;filterN

1.3.1 簡單濾鏡圖

簡單濾鏡圖(filtergraph)只能處理單路輸入流和單路輸出流,並且要求輸入和輸出具備相同的流類型。
簡單濾鏡圖由-filter選項指定。簡單濾鏡圖示意圖以下:

_______        _____________________        ________
|       |      |                     |      |        |
| input | ---> | simple filter graph | ---> | output |
|_______|      |_____________________|      |________|

1.3.2 複雜濾鏡圖

複雜濾鏡圖(filtergraph)用於簡單濾鏡圖處理不了的場合。好比,多路輸入流和(或)多路輸出流,或者輸出流與輸入流類型不一樣。
有些特殊的濾鏡(filter)自己就屬於複雜濾鏡圖,用-filter_complex選項或-lavfi選項指定,如overlay濾鏡和amix濾鏡就是複雜濾鏡圖。overlay濾鏡有兩個視頻輸入和一個視頻輸出,將兩個輸入視頻混合在一塊兒。而amix濾鏡則是將兩個輸入音頻混合在一塊兒。
複雜濾鏡圖(filtergraph)示意圖以下:

_________
|         |
| input 0 |\                    __________
|_________| \                  |          |
             \   _________    /| output 0 |
              \ |         |  / |__________|
 _________     \| complex | /
|         |     |         |/
| input 1 |---->| filter  |\
|_________|     |         | \   __________
               /| graph   |  \ |          |
              / |         |   \| output 1 |
 _________   /  |_________|    |__________|
|         | /
| input 2 |/
|_________|

示例1:
ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT
上例中"split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2"是複雜濾鏡圖,由三個濾鏡鏈構成(分號分隔),第二個濾鏡鏈「[tmp] crop=iw:ih/2:0:0, vflip [flip]」由兩個濾鏡構成(逗號分隔)。第一個濾鏡鏈中:濾鏡split產生兩個輸出[main]和[tmp];第二個濾鏡鏈中:[tmp]做爲crop濾鏡的輸入,[flip]做爲vflip濾鏡的輸出,crop濾鏡輸出鏈接到vflip濾鏡的輸入;第三個濾鏡鏈中:[main]和[flip]做爲overlay濾鏡的輸入。整行命令實現的功能是:將輸入分隔爲兩路,其中一路通過裁剪和垂直翻轉後,再與另外一路混合,生成輸出文件。示意圖以下所示:

[main]
input --> split ---------------------> overlay --> output
            |                             ^
            |[tmp]                  [flip]|
            +-----> crop --> vflip -------+

1.3.3 濾鏡圖中的鏈接標號

在濾鏡圖中可使用鏈接標號(link lable),鏈接標號表示特定濾鏡/濾鏡鏈的輸入或輸出,參1.1節。

例如,咱們想要把一個通過降噪處理後的輸出文件與輸入原文件進行比較,若是不使用帶鏈接標號的濾鏡圖,咱們須要至少兩條命令:
ffmpeg -i input.mpg -vf hqdn3d,pad=2*iw output.mp4
ffmpeg -i output.mp4 -i input.mpg -filter_complex overlay=w compare.mp4

若是使用帶有鏈接標號的濾鏡圖,則一條命令就能夠了:
ffplay -i i.mpg -vf split[a][b];[a]pad=2*iw[A];[b]hqdn3d[B];[A][B]overlay=w

1.4 濾鏡使用總結

濾鏡(廣義)一般以濾鏡鏈(filterchain, 以逗號分隔的濾鏡序列)和濾鏡圖(filtergraph, 以分號分隔的濾鏡序列)的形式使用。濾鏡鏈由濾鏡構成,濾鏡圖由濾鏡鏈構成,這樣能夠提供複雜多樣的組合方式以應對不一樣的應用場景。
濾鏡(狹義)是濾鏡鏈的簡單特例,濾鏡鏈是濾鏡圖的簡單特例。注意這裏濾鏡(狹義)、濾鏡鏈、濾鏡圖之間不是繼承的關係,而是組合的關係,好比,一個濾鏡圖能夠只包含一個濾鏡鏈,而一個濾鏡鏈也能夠只包含一個濾鏡,這種特例狀況下,一個濾鏡圖僅由單個濾鏡構成。FFmpeg的命令行中,濾鏡(廣義)的出現形式有濾鏡(狹義)、濾鏡鏈、濾鏡圖三種形式,但濾鏡(狹義)和濾鏡鏈能夠看做是特殊的濾鏡圖,所以,爲了簡便,FFmpeg的命令行中濾鏡相關選項,只針對濾鏡圖(filtergraph)概念,分爲以下兩類:
針對簡單濾鏡圖的選項:「-vf」等同「-filter:v」,「-af」等同「-filter:a」
針對複雜濾鏡圖的選項:「-lavfi」等價「-filter_complex」

2. 濾鏡數據結構與API簡介

待補充

struct AVFilter

/**
 * Filter definition. This defines the pads a filter contains, and all the
 * callback functions used to interact with the filter.
 */
typedef struct AVFilter {
    const char *name;
    const char *description;
    const AVFilterPad *inputs;
    const AVFilterPad *outputs;
    const AVClass *priv_class;
    int flags;
    
    // private API
    ......
} AVFilter;

struct AVFilterContext

/** An instance of a filter */
struct AVFilterContext {
    const AVClass *av_class;        ///< needed for av_log() and filters common options

    const AVFilter *filter;         ///< the AVFilter of which this is an instance

    char *name;                     ///< name of this filter instance

    AVFilterPad   *input_pads;      ///< array of input pads
    AVFilterLink **inputs;          ///< array of pointers to input links
    unsigned    nb_inputs;          ///< number of input pads

    AVFilterPad   *output_pads;     ///< array of output pads
    AVFilterLink **outputs;         ///< array of pointers to output links
    unsigned    nb_outputs;         ///< number of output pads

    void *priv;                     ///< private data for use by the filter

    struct AVFilterGraph *graph;    ///< filtergraph this filter belongs to

    ......
};

struct AVFilterGraph

typedef struct AVFilterGraph {
    const AVClass *av_class;
    AVFilterContext **filters;
    unsigned nb_filters;

    ......
} AVFilterGraph;

struct AVFilterLink

/**
 * A link between two filters. This contains pointers to the source and
 * destination filters between which this link exists, and the indexes of
 * the pads involved. In addition, this link also contains the parameters
 * which have been negotiated and agreed upon between the filter, such as
 * image dimensions, format, etc.
 *
 * Applications must not normally access the link structure directly.
 * Use the buffersrc and buffersink API instead.
 * In the future, access to the header may be reserved for filters
 * implementation.
 */
struct AVFilterLink {
    AVFilterContext *src;       ///< source filter
    AVFilterPad *srcpad;        ///< output pad on the source filter

    AVFilterContext *dst;       ///< dest filter
    AVFilterPad *dstpad;        ///< input pad on the dest filter
    
    ......
}

struct AVFilterInOut

/**
 * A linked-list of the inputs/outputs of the filter chain.
 *
 * This is mainly useful for avfilter_graph_parse() / avfilter_graph_parse2(),
 * where it is used to communicate open (unlinked) inputs and outputs from and
 * to the caller.
 * This struct specifies, per each not connected pad contained in the graph, the
 * filter context and the pad index required for establishing a link.
 */
typedef struct AVFilterInOut {
    /** unique name for this input/output in the list */
    char *name;

    /** filter context associated to this input/output */
    AVFilterContext *filter_ctx;

    /** index of the filt_ctx pad to use for linking */
    int pad_idx;

    /** next input/input in the list, NULL if this is the last */
    struct AVFilterInOut *next;
} AVFilterInOut;

avfilter_graph_create_filter()

/**
 * Create and add a filter instance into an existing graph.
 * The filter instance is created from the filter filt and inited
 * with the parameters args and opaque.
 *
 * In case of success put in *filt_ctx the pointer to the created
 * filter instance, otherwise set *filt_ctx to NULL.
 *
 * @param name the instance name to give to the created filter instance
 * @param graph_ctx the filter graph
 * @return a negative AVERROR error code in case of failure, a non
 * negative value otherwise
 */
int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt,
                                 const char *name, const char *args, void *opaque,
                                 AVFilterGraph *graph_ctx);

avfilter_graph_parse_ptr()

/**
 * Add a graph described by a string to a graph.
 *
 * In the graph filters description, if the input label of the first
 * filter is not specified, "in" is assumed; if the output label of
 * the last filter is not specified, "out" is assumed.
 *
 * @param graph   the filter graph where to link the parsed graph context
 * @param filters string to be parsed
 * @param inputs  pointer to a linked list to the inputs of the graph, may be NULL.
 *                If non-NULL, *inputs is updated to contain the list of open inputs
 *                after the parsing, should be freed with avfilter_inout_free().
 * @param outputs pointer to a linked list to the outputs of the graph, may be NULL.
 *                If non-NULL, *outputs is updated to contain the list of open outputs
 *                after the parsing, should be freed with avfilter_inout_free().
 * @return non negative on success, a negative AVERROR code on error
 */
int avfilter_graph_parse_ptr(AVFilterGraph *graph, const char *filters,
                             AVFilterInOut **inputs, AVFilterInOut **outputs,
                             void *log_ctx);

avfilter_graph_config()

/**
 * Check validity and configure all the links and formats in the graph.
 *
 * @param graphctx the filter graph
 * @param log_ctx context used for logging
 * @return >= 0 in case of success, a negative AVERROR code otherwise
 */
int avfilter_graph_config(AVFilterGraph *graphctx, void *log_ctx);

av_buffersrc_add_frame_flags()

/**
 * Add a frame to the buffer source.
 *
 * By default, if the frame is reference-counted, this function will take
 * ownership of the reference(s) and reset the frame. This can be controlled
 * using the flags.
 *
 * If this function returns an error, the input frame is not touched.
 *
 * @param buffer_src  pointer to a buffer source context
 * @param frame       a frame, or NULL to mark EOF
 * @param flags       a combination of AV_BUFFERSRC_FLAG_*
 * @return            >= 0 in case of success, a negative AVERROR code
 *                    in case of failure
 */
av_warn_unused_result
int av_buffersrc_add_frame_flags(AVFilterContext *buffer_src,
                                 AVFrame *frame, int flags);

av_buffersink_get_frame()

/**
 * Get a frame with filtered data from sink and put it in frame.
 *
 * @param ctx pointer to a context of a buffersink or abuffersink AVFilter.
 * @param frame pointer to an allocated frame that will be filled with data.
 *              The data must be freed using av_frame_unref() / av_frame_free()
 *
 * @return
 *         - >= 0 if a frame was successfully returned.
 *         - AVERROR(EAGAIN) if no frames are available at this point; more
 *           input frames must be added to the filtergraph to get more output.
 *         - AVERROR_EOF if there will be no more output frames on this sink.
 *         - A different negative AVERROR code in other failure cases.
 */
int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame);

3. 濾鏡API使用方法

在代碼中使用濾鏡,主要分爲兩個步驟:
[1]. 濾鏡的初始化配置:根據濾鏡參數,配置生成濾鏡圖,此濾鏡圖供下一步驟使用
[2]. 使用濾鏡處理原始音視頻幀:向濾鏡圖提供輸入幀(AVFrame),從濾鏡圖取出經處理後的輸出幀(AVFrame)

1. init_filters()                   // 配置生成可用的濾鏡圖,由用戶編寫  
2. av_buffersrc_add_frame_flags()   // 向濾鏡圖提供輸入幀,API函數  
3. av_buffersink_get_frame()        // 從濾鏡圖取出處理後的輸出幀,API函數

本節節選的代碼示例選自:
https://github.com/leichn/exercises/blob/master/source/ffmpeg/ffmpeg_vfilter/video_filter.c

3.1 濾鏡配置

在代碼中,濾鏡配置比濾鏡使用複雜,濾鏡配置代碼以下:

// 功能:建立配置一個濾鏡圖,在後續濾鏡處理中,能夠往此濾鏡圖輸入數據並從濾鏡圖得到輸出數據
// filters_descr:輸入參數,形如「transpose=cclock,pad=iw+80:ih:40」
// @vfmt:輸入參數,描述提供給待生成濾鏡圖的視頻幀和格式
// @fctx:輸出參數,返回生成濾鏡圖的信息,供調用者使用
int init_filters(const char *filters_descr, const input_vfmt_t *vfmt, filter_ctx_t *fctx)
{
    int ret = 0;

    // 1. 配置濾鏡圖輸入端和輸出端 
    fctx->filter_graph = avfilter_graph_alloc();
    if (!fctx->filter_graph)
    {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    char args[512];
    char *p_args = NULL;
    if (vfmt != NULL)
    {
        /* buffer video source: the decoded frames from the decoder will be inserted here. */
        // args是buffersrc濾鏡的參數
        snprintf(args, sizeof(args),
                 "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
                 vfmt->width, vfmt->height, vfmt->pix_fmt, 
                 vfmt->time_base.num, vfmt->time_base.den, 
                 vfmt->sar.num, vfmt->sar.den);
        p_args = args;
    }
    ret = avfilter_graph_create_filter(&fctx->bufsrc_ctx, bufsrc, "in",
                                       p_args, NULL, fctx->filter_graph);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n");
        goto end;
    }

    const AVFilter *bufsink = avfilter_get_by_name("buffersink");
    ret = avfilter_graph_create_filter(&fctx->bufsink_ctx, bufsink, "out",
                                       NULL, NULL, fctx->filter_graph);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n");
        goto end;
    }

#if 0   // 由於後面顯示視頻幀時有sws_scale()進行圖像格式轉換,故此處不設置濾鏡輸出格式也可
    enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUYV422, AV_PIX_FMT_NONE };
    // 設置輸出像素格式爲pix_fmts[]中指定的格式(若是要用SDL顯示,則這些格式應是SDL支持格式)
    ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
                              AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
        goto end;
    }
#endif
    // 1. end


    // 2. 將filters_descr描述的濾鏡圖添加到filter_graph濾鏡圖中
    AVFilterInOut *outputs = avfilter_inout_alloc();
    outputs->name       = av_strdup("in");
    outputs->filter_ctx = fctx->bufsrc_ctx;
    outputs->pad_idx    = 0;
    outputs->next       = NULL;

    AVFilterInOut *inputs  = avfilter_inout_alloc();
    inputs->name       = av_strdup("out");
    inputs->filter_ctx = fctx->bufsink_ctx;
    inputs->pad_idx    = 0;
    inputs->next       = NULL;

    ret = avfilter_graph_parse_ptr(fctx->filter_graph, filters_descr,
                                   &inputs, &outputs, NULL);
    if (ret < 0)
    {
        goto end;
    }
    // 2. end

    // 3. 配置filtergraph濾鏡圖,創建濾鏡間的鏈接
    ret = avfilter_graph_config(fctx->filter_graph, NULL);
    if (ret < 0)
    {
        goto end;
    }
    // 3. end

end:
    avfilter_inout_free(&inputs);
    avfilter_inout_free(&outputs);

    return ret;
}

函數參數說明:

  • 輸入參數const char *filters_descr
    以字符串形式提供濾鏡選項,例如參數爲transpose=cclock,pad=iw+80:ih:40時,表示將視頻幀逆時針旋轉90度,而後在視頻左右各填充40像素的黑邊。

  • 輸入參數input_vfmt_t *vfmt
    用於描述提供給濾鏡圖的視頻幀和格式,在配置濾鏡圖中的第一個濾鏡buffer時須要爲濾鏡提供參數,就是從vfmt參數轉換獲得。
    input_vfmt_t爲自定義數據結構,定義以下:
typedef struct {
    int width;
    int height;
    enum AVPixelFormat pix_fmt;
    AVRational time_base;
    AVRational sar;
    AVRational frame_rate;
}   input_vfmt_t;
  • 輸出參數filter_ctx_t *fctx
    用於返回生成濾鏡圖的信息,供調用者使用。
    filter_ctx_t爲自定義數據結構,定義以下:
typedef struct {
    AVFilterContext *bufsink_ctx;
    AVFilterContext *bufsrc_ctx;
    AVFilterGraph   *filter_graph;
}   filter_ctx_t;

此結構中三個成員:bufsrc_ctx用於濾鏡圖的輸入,bufsink_ctx用於濾鏡圖的輸出,filter_graph用於銷燬濾鏡圖。
TODO: 一個濾鏡圖可能含多個濾鏡鏈,便可能有多個輸入節點(bufsrc_ctx)或多個輸出節點(bufsink_ctx),此數據結構應改進爲支持多輸入和多輸出

init_filters()函數實現的幾個步驟以下:

3.1.1 配置濾鏡圖輸入端和輸出端

buffer濾鏡和buffersink濾鏡是兩個特殊的視頻濾鏡,分別用於視頻濾鏡鏈的輸入端和輸出端。與之類似,abuffer濾鏡和abuffersink濾鏡是兩個特殊的音頻濾鏡,分別用於音頻濾鏡鏈的輸入端和輸出端。

一個濾鏡圖可能由多個濾鏡鏈構成,每一個濾鏡鏈的輸入節點就是buffer濾鏡,輸出節點是buffersink濾鏡,所以一個濾鏡圖可能有多個buffer濾鏡,也可能有多個buffersink濾鏡。應用程序經過訪問buffer濾鏡和buffersink濾鏡實現和濾鏡圖的數據交互。

buffer濾鏡
在命令行中輸入ffmpeg -h filter=buffer查看buffer濾鏡的幫助信息,以下:

$ ffmpeg -h filter=buffer
ffmpeg version 4.1 Copyright (c) 2000-2018 the FFmpeg developers
Filter buffer
  Buffer video frames, and make them accessible to the filterchain.
    Inputs:
        none (source filter)
    Outputs:
       #0: default (video)
buffer AVOptions:
  width             <int>        ..FV..... (from 0 to INT_MAX) (default 0)
  video_size        <image_size> ..FV.....
  height            <int>        ..FV..... (from 0 to INT_MAX) (default 0)
  pix_fmt           <pix_fmt>    ..FV..... (default none)
  sar               <rational>   ..FV..... sample aspect ratio (from 0 to DBL_MAX) (default 0/1)
  pixel_aspect      <rational>   ..FV..... sample aspect ratio (from 0 to DBL_MAX) (default 0/1)
  time_base         <rational>   ..FV..... (from 0 to DBL_MAX) (default 0/1)
  frame_rate        <rational>   ..FV..... (from 0 to DBL_MAX) (default 0/1)
  sws_param         <string>     ..FV.....

buffer濾鏡用做濾鏡鏈的輸入節點。buffer濾鏡緩衝視頻幀,濾鏡鏈能夠從buffer濾鏡中取得視頻幀數據。
在上述幫助信息中,Inputs和Outputs指濾鏡的輸入引腳和輸出引腳。buffer濾鏡是濾鏡鏈中的第一個濾鏡,所以只有輸出引腳而無輸入引腳。

濾鏡(AVFilter)須要經過濾鏡實例(AVFilterContext)引用,爲buffer濾鏡建立的濾鏡實例是fctx->bufsrc_ctx,用戶經過往fctx->bufsrc_ctx填入視頻幀來爲濾鏡鏈提供輸入。
爲buffer濾鏡建立濾鏡實例時須要提供參數,buffer濾鏡須要的參數在幫助信息中的「buffer AVOptions」部分列出,由vfmt輸入參數提供,代碼以下:

char args[512];
    char *p_args = NULL;
    if (vfmt != NULL)
    {
        // args是buffersrc濾鏡的參數
        snprintf(args, sizeof(args),
                 "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
                 vfmt->width, vfmt->height, vfmt->pix_fmt, 
                 vfmt->time_base.num, vfmt->time_base.den, 
                 vfmt->sar.num, vfmt->sar.den);
        p_args = args;
    }
    // buffer濾鏡:緩衝視頻幀,做爲濾鏡圖的輸入
    const AVFilter *bufsrc  = avfilter_get_by_name("buffer");
    // 爲buffersrc濾鏡建立濾鏡實例buffersrc_ctx,命名爲"in"
    // 將新建立的濾鏡實例buffersrc_ctx添加到濾鏡圖filter_graph中
    ret = avfilter_graph_create_filter(&fctx->bufsrc_ctx, bufsrc, "in",
                                       p_args, NULL, fctx->filter_graph);

buffersink濾鏡
在命令行中輸入ffmpeg -h filter=buffersink查看buffersink濾鏡的幫助信息,以下:

$  ffmpeg -h filter=buffersink
ffmpeg version 4.1 Copyright (c) 2000-2018 the FFmpeg developers
Filter buffersink
  Buffer video frames, and make them available to the end of the filter graph.
    Inputs:
       #0: default (video)
    Outputs:
        none (sink filter)
buffersink AVOptions:
  pix_fmts          <binary>     ..FV..... set the supported pixel formats

buffersink濾鏡用做濾鏡鏈的輸出節點。濾鏡鏈處理後的視頻幀能夠緩存到buffersink濾鏡中。
buffersink濾鏡是濾鏡鏈中的最後一個濾鏡,所以只有輸入引腳而無輸出引腳。

爲buffersink濾鏡建立的濾鏡實例是fctx->bufsink_ctx,用戶能夠從fctx->bufsink_ctx中讀視頻幀來得到濾鏡鏈的輸出。
經過幫助信息能夠看到,buffersink濾鏡參數只有一個「pix_fmt」,用於設置濾鏡鏈輸出幀的像素格式列表,這個像素格式有多種,以限制輸出幀格式不超過指定的範圍。

// buffersink濾鏡:緩衝視頻幀,做爲濾鏡圖的輸出
    const AVFilter *bufsink = avfilter_get_by_name("buffersink");
    // 爲buffersink濾鏡建立濾鏡實例buffersink_ctx,命名爲"out"
    // 將新建立的濾鏡實例buffersink_ctx添加到濾鏡圖filter_graph中
    ret = avfilter_graph_create_filter(&fctx->bufsink_ctx, bufsink, "out",
                                       NULL, NULL, fctx->filter_graph);

#if 0   // 由於後面顯示視頻幀時有sws_scale()進行圖像格式轉換,故此處不設置濾鏡輸出格式也可
    enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUYV422, AV_PIX_FMT_NONE };
    // 設置輸出像素格式爲pix_fmts[]中指定的格式(若是要用SDL顯示,則這些格式應是SDL支持格式)
    ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
                              AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
#endif

將buffer濾鏡和buffsink濾鏡添加進濾鏡圖中後,以下圖所示:
pic0

3.1.2 將filters_descr描述的濾鏡插入濾鏡圖中

解析濾鏡選項(filters_descr),將解析獲得的濾鏡插入第1步構造的濾鏡圖中,並與濾鏡圖輸入端和輸出端鏈接起來

// 設置濾鏡圖的端點,將filters_descr描述的濾鏡圖鏈接到此濾鏡圖
    // 兩個濾鏡圖的鏈接是經過端點(AVFilterInOut)鏈接完成的
    // 端點數據結構AVFilterInOut主要用於avfilter_graph_parse()系列函數

    // outputs變量意指buffersrc_ctx濾鏡的輸出引腳(output pad)
    // src緩衝區(buffersrc_ctx濾鏡)的輸出必須連到filters_descr中第一個
    // 濾鏡的輸入;filters_descr中第一個濾鏡的輸入標號未指定,故默認爲
    // "in",此處將buffersrc_ctx的輸出標號也設爲"in",就實現了同標號相連
    AVFilterInOut *outputs = avfilter_inout_alloc();
    outputs->name       = av_strdup("in");
    outputs->filter_ctx = fctx->bufsrc_ctx;
    outputs->pad_idx    = 0;
    outputs->next       = NULL;

    // inputs變量意指buffersink_ctx濾鏡的輸入引腳(input pad)
    // sink緩衝區(buffersink_ctx濾鏡)的輸入必須連到filters_descr中最後
    // 一個濾鏡的輸出;filters_descr中最後一個濾鏡的輸出標號未指定,故
    // 默認爲"out",此處將buffersink_ctx的輸出標號也設爲"out",就實現了
    // 同標號相連
    AVFilterInOut *inputs  = avfilter_inout_alloc();
    inputs->name       = av_strdup("out");
    inputs->filter_ctx = fctx->bufsink_ctx;
    inputs->pad_idx    = 0;
    inputs->next       = NULL;

    // 將filters_descr描述的濾鏡圖添加到filter_graph濾鏡圖中
    // 調用前:filter_graph包含兩個濾鏡buffersrc_ctx和buffersink_ctx
    // 調用後:filters_descr描述的濾鏡圖插入到filter_graph中,buffersrc_ctx鏈接到filters_descr
    //         的輸入,filters_descr的輸出鏈接到buffersink_ctx,filters_descr只進行了解析而不
    //         創建內部濾鏡間的鏈接。filters_desc與filter_graph間的鏈接是利用AVFilterInOut inputs
    //         和AVFilterInOut outputs鏈接起來的,AVFilterInOut是一個鏈表,最終可用的連在一塊兒的
    //         濾鏡鏈/濾鏡圖就是經過這個鏈表串在一塊兒的。
    ret = avfilter_graph_parse_ptr(fctx->filter_graph, filters_descr,
                                   &inputs, &outputs, NULL);

filters_descr描述的濾鏡以下圖所示:
pic1

調用avfilter_graph_parse_ptr()後,濾鏡圖以下所示:
pic2

3.1.3. 創建濾鏡鏈接

調用avfilter_graph_config()將上一步獲得的濾鏡圖進行配置,創建濾鏡間的鏈接,此步完成後即生了一個可用的濾鏡圖,以下圖所示:
pic3

3.2 使用濾鏡處理原始幀

配置好濾鏡後,可在音視頻處理過程當中使用濾鏡。使用濾鏡比配置濾鏡簡單不少,主要調用以下兩個API函數:

  1. 調用av_buffersrc_add_frame_flags()將音視頻幀發送給濾鏡
  2. 調用av_buffersink_get_frame()取得經濾鏡處理後的音視頻幀

4. 濾鏡API應用實例分析

濾鏡接收原始音視頻幀,通過各類效果的濾鏡處理後輸出的仍然是原始音視頻幀。在濾鏡API應用實例中,核心內容是「濾鏡配置」和「濾鏡使用」兩個部分,濾鏡接收什麼樣的輸入源不重要,對濾鏡的輸出作什麼處理也不重要。不一樣的輸入源,及不一樣的輸出處理方式僅僅是爲了加深對濾鏡API使用的理解,以及方便觀察濾鏡的處理效果。

濾鏡的輸入能夠是解碼器的輸出、原始YUV文件及測試圖。本文三個示例只針對視頻濾鏡:
示例1:編碼器的輸出做爲濾鏡的輸入,濾鏡的輸出簡單處理,沒法觀察濾鏡效果。
示例2:編碼器的輸出做爲濾鏡的輸入,濾鏡的輸出能夠播放,可直觀觀察濾鏡效果。
示例3:測試圖做爲濾鏡的輸入(而測試圖自己也是由特殊濾鏡生成),濾鏡的輸出能夠播放,可直接觀察濾鏡效果。

示例1源碼下載:https://github.com/FFmpeg/FFmpeg/blob/n4.1/doc/examples/filtering_video.c
示例2與示例3源碼下載(SHELL中運行以下命令):

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/ffmpeg_vfilter/

4.1 示例1:官方例程

官方例程實現的功能是:打開一個視頻文件,解碼後通過濾鏡處理,而後以簡單灰度模式在命令窗口中播放視頻幀。

例程中使用的濾鏡選項是scale=78:24,transpose=cclock,表示先用scale濾鏡將視頻幀縮放到78x24像素,再用transpose濾鏡將視頻幀逆時針旋轉90度。

簡述一下例程的步驟:

  1. 打開視頻文件,調用open_input_file()實現
  2. 初始化濾鏡,調用init_filters()實現
  3. 解碼獲得視頻幀,調用avcodec_send_packet()avcodec_receive_frame()得到解碼後的原始視頻幀
  4. 將視頻幀發給濾鏡,調用av_buffersrc_add_frame_flags()實現
  5. 從濾鏡輸出端取視頻幀,調用av_buffersink_get_frame()實現
  6. 播放視頻幀,調用display_frame()實現

例程核心是濾鏡相關的代碼,所以視頻幀播放部分作了簡化處理。

4.2 示例2:可播放版本

官方例程主要演示濾鏡API的使用方法,代碼量較少,簡化了視頻播放部分,這樣使得濾鏡的處理效果沒法直觀觀察。示例2針對此問題,在官方代碼基礎上增長了正常的視頻播放效果。

4.2.1 代碼

下載代碼後,源碼目錄下有以下幾個文件,說明以下:

vfilter_filesrc.c   用於示例2:輸入源爲視頻文件,經濾鏡處理後播放
vfilter_testsrc.c   用於示例3:輸入源爲測試圖,經濾鏡處理後播放
video_filter.c      濾鏡處理功能
video_play.c        視頻播放功能
Makefile

video_filter.c封裝了濾鏡處理相關代碼,詳參本文第3節。
video_play.c實現了視頻播放功能,本例無需過多關注,實現原理可參考以下兩篇文章:
FFmpeg簡易播放器的實現-視頻播放
ffplay源碼分析5-圖像格式轉換
vfilter_filesrc.c是示例2的主程序,實現了打開視頻文件,解碼,濾鏡處理,播放的主流程

4.2.2 編譯

進入代碼目錄,在命令行運行make vf_file命令,將生成vf_file可執行文件

4.2.3 測試

進入代碼目錄,在命令行運行./vf_file ./ring.flv -vf crop=iw/2:ih:0:0,pad=iw*2:ih
濾鏡選項-vf crop=iw/2:ih:0:0,pad=iw*2:ih表示先將視頻裁剪爲一半寬度,再填充爲二倍寬度,預期結果爲視頻的右半部分爲黑邊。
測試文件下載(右鍵另存爲):ring.flv
未經濾鏡處理和通過濾鏡處理的視頻效果對好比下兩圖所示:
ring
ring_vf

4.3 示例3:測試圖做輸入源

示例3使用測試圖(test pattern)做爲濾鏡的輸入,測試圖(test pattern)是由FFmpeg內部產生的測試圖案,用於測試很是方便。
因測試圖直接輸出原始視頻幀,不需解碼器,所以示例3中用到AVFilter庫,不須要用到AVFormat庫。

4.3.1 代碼

4.2節源碼目錄中vfilter_testsrc.c就是用於示例3的主程序,實現了構建測試源,濾鏡處理,播放的主流程。除濾鏡輸入源的獲取方式與示例2不一樣以外,其餘過程並沒有不一樣。

示例3增長的關鍵內容是構造測試源,參考vfilter_testsrc.c中以下函數:

// @filter [i]  產生測試圖案的filter
// @vfmt   [i]  @filter的參數
// @fctx   [o]  用戶定義的數據類型,輸出供調用者使用
static int open_testsrc(const char *filter, const input_vfmt_t *vfmt, filter_ctx_t *fctx)
{
    int ret = 0;

    // 分配一個濾鏡圖filter_graph
    fctx->filter_graph = avfilter_graph_alloc();
    if (!fctx->filter_graph)
    {
        return AVERROR(ENOMEM);
    }

    // source濾鏡:合法值有"testsrc"/"smptebars"/"color"/...
    const AVFilter *bufsrc  = avfilter_get_by_name(filter);
    // 爲buffersrc濾鏡建立濾鏡實例buffersrc_ctx,命名爲"in"
    // 將新建立的濾鏡實例buffersrc_ctx添加到濾鏡圖filter_graph中
    ret = avfilter_graph_create_filter(&fctx->bufsrc_ctx, bufsrc, "in",
                                       NULL, NULL, fctx->filter_graph);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot create filter testsrc\n");
        goto end;
    }

    // "buffersink"濾鏡:緩衝視頻幀,做爲濾鏡圖的輸出
    const AVFilter *bufsink = avfilter_get_by_name("buffersink");
    /* buffer video sink: to terminate the filter chain. */
    // 爲buffersink濾鏡建立濾鏡實例buffersink_ctx,命名爲"out"
    // 將新建立的濾鏡實例buffersink_ctx添加到濾鏡圖filter_graph中
    ret = avfilter_graph_create_filter(&fctx->bufsink_ctx, bufsink, "out",
                                       NULL, NULL, fctx->filter_graph);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot create filter buffersink\n");
        goto end;
    }

    if ((ret = avfilter_link(fctx->bufsrc_ctx, 0, fctx->bufsink_ctx, 0)) < 0)
    {
        goto end;
    }


    // 驗證有效性並配置filtergraph中全部鏈接和格式
    ret = avfilter_graph_config(fctx->filter_graph, NULL);
    if (ret < 0)
    {
        goto end;
    }

    vfmt->pix_fmt = av_buffersink_get_format(fctx->bufsink_ctx);
    vfmt->width = av_buffersink_get_w(fctx->bufsink_ctx);
    vfmt->height = av_buffersink_get_h(fctx->bufsink_ctx);
    vfmt->sar = av_buffersink_get_sample_aspect_ratio(fctx->bufsink_ctx);
    vfmt->time_base = av_buffersink_get_time_base(fctx->bufsink_ctx);
    vfmt->frame_rate = av_buffersink_get_frame_rate(fctx->bufsink_ctx);

    av_log(NULL, AV_LOG_INFO, "probe video format: "
           "%dx%d, pix_fmt %d, SAR %d/%d, tb %d/%d, rate %d/%d\n",
           vfmt->width, vfmt->height, vfmt->pix_fmt,
           vfmt->sar.num, vfmt->sar.den,
           vfmt->time_base.num, vfmt->time_base.den,
           vfmt->frame_rate.num, vfmt->frame_rate.den);

    return 0;

end:
    avfilter_graph_free(&fctx->filter_graph);
    return ret;
}

測試源的本質是使用FFmpeg提供的用於產生測試圖案的濾鏡來生成視頻數據。具體到代碼實現層面,將testsrc/smptebars等濾鏡代替經常使用的buffer濾鏡做爲源濾鏡,而後直接與buffersink濾鏡相連,以輸出測試圖案,以下圖:
pic4

4.3.2 編譯

進入源碼目錄,在命令行運行make vf_test,將生成vf_test可執行文件

4.3.3 測試

測試濾鏡選項-vf transpose=cclock,pad=iw+80:ih:40,此濾鏡選項表示先將視頻逆時針旋轉90度,而後將視頻左右兩邊各增長40像素寬度的黑邊

使用「testsrc」測試圖做輸入源
運行以下命令:

ffplay -f lavfi -i testsrc

無濾鏡處理的效果如圖所示:
testsrc

運行帶濾鏡選項的ffplay命令:

ffplay -f lavfi -i testsrc -vf transpose=cclock,pad=iw+80:ih:40

運行帶濾鏡選項的測試程序(效果等同於上述ffplay命令):

./vf_test testsrc -vf transpose=cclock,pad=iw+80:ih:40

經濾鏡處理的效果如圖所示:
testsrc_vf

使用「smptebars」測試圖做輸入源
運行以下命令:

ffplay -f lavfi -i smptebars

無濾鏡處理的效果如圖所示:
smptebars

運行帶濾鏡選項的ffplay命令:

ffplay -f lavfi -i smptebars -vf transpose=cclock,pad=iw+80:ih:40

運行帶濾鏡選項的測試程序(效果等同於上述ffplay命令):

./vf_test smptebars -vf transpose=cclock,pad=iw+80:ih:40

經濾鏡處理的效果如圖所示:
smptebars_vf

5. 遺留問題

[1] 不支持多輸入多輸出的複雜濾鏡圖,待改進驗證
[2] 如何使用API以相似打開普通輸入文件的方法來獲取測試圖的格式,即ffprobe -f lavfi -i testsrc的內部原理是什麼?

think@linux-1phi:~> ffprobe -f lavfi -i testsrc
ffprobe version 4.1 Copyright (c) 2007-2018 the FFmpeg developers
Input #0, lavfi, from 'testsrc':
  Duration: N/A, start: 0.000000, bitrate: N/A
    Stream #0:0: Video: rawvideo (RGB[24] / 0x18424752), rgb24, 320x240 [SAR 1:1 DAR 4:3], 25 tbr, 25 tbn, 25 tbc

6. 參考資料

[1] 劉歧,FFmpeg Filter深度應用https://yq.aliyun.com/articles/628153?utm_content=m_1000014065

7. 修改記錄

2019-02-24 V1.0 初稿

相關文章
相關標籤/搜索