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


本文為作者原創,轉載請注明出處:https://www.cnblogs.com/leisure_chn/p/10429145.html

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

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

本文將通過實例詳細介紹濾鏡 API 的使用方法。

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

參考 “FFmpeg使用基礎” 第 4 節 “濾鏡”。

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

待補充

2.1 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;

2.2 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

    ......
};

2.3 struct AVFilterGraph

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

    ......
} AVFilterGraph;
/**
 * 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
    
    ......
}

2.5 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;

2.6 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);

2.7 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);

2.8 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);

2.9 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);

2.10 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函數  

3.1 濾鏡配置

在代碼中,濾鏡配置比濾鏡使用復雜,濾鏡配置代碼如下:

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

    // 1. 配置濾鏡圖輸入端和輸出端
    // 分配一個濾鏡圖filter_graph
    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;
    }

    // buffer濾鏡:緩沖視頻幀,作為濾鏡圖的輸入
    const AVFilter *bufsrc  = avfilter_get_by_name("buffer");
    // 創建濾鏡實例fctx->bufsrc_ctx,此濾鏡實例從bufsrc中創建,並使用參數p_args進行初始化
    // 新創建的濾鏡實例命名為"in",並被添加到濾鏡圖fctx->filter_graph中
    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;
    }

    // buffersink濾鏡:緩沖視頻幀,作為濾鏡圖的輸出
    const AVFilter *bufsink = avfilter_get_by_name("buffersink");
    /* buffer video sink: to terminate the filter chain. */
    // 創建濾鏡實例buffersink_ctx,此濾鏡實例從bufsink中創建
    // 新創建的濾鏡實例命名為"out",並被添加到濾鏡圖fctx->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 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描述的濾鏡圖添加到fctx->filter_graph濾鏡圖中
    /*
     * Set the endpoints for the filter graph. The filter_graph will
     * be linked to the graph described by filters_descr.
     */
    // 設置濾鏡圖的端點,將filters_descr描述的濾鏡圖連接到此濾鏡圖,
    // 兩個濾鏡圖的連接是通過端點連接(AVFilterInOut)完成的

    /*
     * The buffer source output must be connected to the input pad of
     * the first filter described by filters_descr; since the first
     * filter input label is not specified, it is set to "in" by
     * default.
     */
    // 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;

    /*
     * The buffer sink input must be connected to the output pad of
     * the last filter described by filters_descr; since the last
     * filter output label is not specified, it is set to "out" by
     * default.
     */
    // 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描述的濾鏡圖添加到fctx->filter_graph濾鏡圖中
    // 調用前:fctx->filter_graph包含兩個濾鏡fctx->bufsrc_ctx和fctx->bufsink_ctx
    // 調用后:filters_descr描述的濾鏡圖插入到fctx->filter_graph中,fctx->bufsrc_ctx連接到filters_descr
    //         的輸入,filters_descr的輸出連接到fctx->bufsink_ctx,filters_descr只進行了解析而不
    //         建立內部濾鏡間的連接。filters_desc與fctx->filter_graph間的連接是利用AVFilterInOut inputs
    //         和AVFilterInOut outputs連接起來的,AVFilterInOut是一個鏈表,最終可用的連在一起的
    //         濾鏡鏈/濾鏡圖就是通過這個鏈表串在一起的。
    ret = avfilter_graph_parse_ptr(fctx->filter_graph, filters_descr,
                                   &inputs, &outputs, NULL);
    if (ret < 0)
    {
        goto end;
    }
    // 2. end

    // 3. 配置filtergraph濾鏡圖,建立濾鏡間的連接
    // 驗證有效性並配置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_video_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)
    {
        /* 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;
    }

    // buffer濾鏡:緩沖視頻幀,作為濾鏡圖的輸入
    const AVFilter *bufsrc  = avfilter_get_by_name("buffer");
    // 創建濾鏡實例fctx->bufsrc_ctx,此濾鏡實例從bufsrc中創建,並使用參數p_args進行初始化
    // 新創建的濾鏡實例命名為"in",並被添加到濾鏡圖fctx->filter_graph中
    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;
    }

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 編譯

進入源碼目錄,編譯生成 vf_file 可執行文件:

make 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 編譯

進入源碼目錄,編譯生成 vf_test 可執行文件:

make 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 初稿


免責聲明!

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



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