FFmpeg原始帧处理-滤镜API用法详解

简介: 在 FFmpeg 中,滤镜(filter)处理的是未压缩的原始音视频数据(RGB/YUV视频帧,PCM音频帧等)。一个滤镜的输出可以连接到另一个滤镜的输入,多个滤镜可以连接起来,构成滤镜链/滤镜图,各种滤镜的组合为 FFmpeg 提供了丰富的音视频处理功能。

作者:叶余

来源: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;

2.4 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
    ......
}

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 滤镜添加进滤镜图中后,如下图所示:

image.png

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 描述的滤镜如下图所示:

image.png

调用 avfilter_graph_parse_ptr() 后,滤镜图如下所示:

image.png

3.1.3. 建立滤镜连接

调用 avfilter_graph_config() 将上一步得到的滤镜图进行配置,建立滤镜间的连接,此步完成后即生了一个可用的滤镜图,如下图所示:

image.png

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

未经滤镜处理和经过滤镜处理的视频效果对比如下两图所示:

image.png

image.png

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 滤镜相连,以输出测试图案,如下图:

image.png

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

无滤镜处理的效果如图所示:

image.png

运行带滤镜选项的 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

经滤镜处理的效果如图所示:

image.png

使用“smptebars”测试图作输入源

运行如下命令:

ffplay -f lavfi -i smptebars

无滤镜处理的效果如图所示:

image.png

运行带滤镜选项的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

经滤镜处理的效果如图所示:

image.png

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


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

阿里云视频云@凡科快图.png

相关文章
|
7月前
|
JavaScript API 开发工具
uni.app cell的用法以及相关api
uni.app cell的用法以及相关api
96 0
|
6月前
|
Java API
JavaSE——常用API进阶二(6/8)-ZoneId、ZoneDateTime、Instant(常见方法、用法示例)
JavaSE——常用API进阶二(6/8)-ZoneId、ZoneDateTime、Instant(常见方法、用法示例)
53 1
|
7月前
|
JSON API 数据格式
详细介绍下PYTHON API的用法
详细介绍下PYTHON API的用法
|
4月前
|
移动开发 数据挖掘 API
HTML5 中 Web Workers API 的用法
【8月更文挑战第24天】
53 0
|
5月前
|
人工智能 API
应用工程化架构问题之Agent如何掌握API的用法
应用工程化架构问题之Agent如何掌握API的用法
|
6月前
|
存储 Java API
JavaSE——常用API进阶二(8/8)-Arrays、Comparable、Comparator(Arrays类提供的的常见方法、用法示例)
JavaSE——常用API进阶二(8/8)-Arrays、Comparable、Comparator(Arrays类提供的的常见方法、用法示例)
39 2
|
6月前
|
安全 Java API
JavaSE——常用API进阶二(7/8)-DateTimeFormatter、Period、Duration(常见方法、用法示例)
JavaSE——常用API进阶二(7/8)-DateTimeFormatter、Period、Duration(常见方法、用法示例)
50 2
|
6月前
|
存储 Java API
JavaSE——常用API进阶二(4/8)-秒杀案例(需求与分析、代码与运行结果)、Calendar(Calendar日历类的常见方法、用法示例)
JavaSE——常用API进阶二(4/8)-秒杀案例(需求与分析、代码与运行结果)、Calendar(Calendar日历类的常见方法、用法示例)
44 1
|
6月前
|
Java API
JavaSE——常用API进阶二(3/8)-Date、SimpleDateFormat(构造器、常用的方法、用法示例、时间格式的常见符号)
JavaSE——常用API进阶二(3/8)-Date、SimpleDateFormat(构造器、常用的方法、用法示例、时间格式的常见符号)
28 1
|
6月前
|
存储 Java API
JavaSE——常用API进阶二(2/8)-BigDecimal(BigDecimal的常见构造器、常用方法,用法示例,使用规范)
JavaSE——常用API进阶二(2/8)-BigDecimal(BigDecimal的常见构造器、常用方法,用法示例,使用规范)
53 1