Writing a Custom Video Filter for FFmpeg libavfilter

This article provides a step-by-step guide on how to develop a custom video filter in C and integrate it into FFmpeg’s libavfilter library. You will learn how to set up the filter context, process video frames, define configuration parameters, and modify the build system to register and compile your new filter.

Step 1: Create the Filter Source File

FFmpeg video filters reside in the libavfilter/ directory. To start, create a new file named vf_custom.c in libavfilter/. This file will contain the filter’s structure, callbacks, and processing logic.

Header Includes and Boilerplate

Every filter requires access to the internal headers of libavfilter and libavutil.

#include "libavutil/opt.h"
#include "libavutil/imgutils.h"
#include "avfilter.h"
#include "internal.h"
#include "video.h"

Step 2: Define the Filter Context and Options

The filter context stores state variables, user options, and configuration data. We define a custom struct and use FFmpeg’s AVOption system to expose parameters to the command line.

typedef struct CustomContext {
    const AVClass *class;
    int shift_value; // Example user option
} CustomContext;

#define OFFSET(x) offsetof(CustomContext, x)
#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM

static const AVOption custom_options[] = {
    { "shift", "Value to add to the luma channel", OFFSET(shift_value), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 255, FLAGS },
    { NULL }
};

AVFILTER_DEFINE_CLASS(custom);

Step 3: Implement Frame Processing Logic

The core processing happens inside the filter_frame function. This callback receives an incoming video frame, processes the pixels, and passes the output frame to the next filter in the pipeline.

static int filter_frame(AVFilterLink *inlink, AVFrame *in)
{
    AVFilterContext *ctx = inlink->dst;
    CustomContext *s = ctx->priv;
    AVFilterLink *outlink = ctx->outputs[0];
    AVFrame *out;

    // Check if the input frame is writable; if not, allocate a new frame
    if (av_frame_is_writable(in)) {
        out = in;
    } else {
        out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
        if (!out) {
            av_frame_free(&in);
            return AVERROR(ENOMEM);
        }
        av_frame_copy_props(out, in);
    }

    // Example process: Modify the Y (Luma) channel of an 8-bit YUV format
    for (int y = 0; y < inlink->h; y++) {
        uint8_t *dst = out->data[0] + y * out->linesize[0];
        const uint8_t *src = in->data[0] + y * in->linesize[0];
        for (int x = 0; x < inlink->w; x++) {
            dst[x] = av_clip_uint8(src[x] + s->shift_value);
        }
    }

    // Copy chroma planes unmodified if using a new output frame
    if (in != out) {
        av_image_copy_plane(out->data[1], out->linesize[1], in->data[1], in->linesize[1], inlink->w / 2, inlink->h / 2);
        av_image_copy_plane(out->data[2], out->linesize[2], in->data[2], in->linesize[2], inlink->w / 2, inlink->h / 2);
        av_frame_free(&in);
    }

    return ff_filter_frame(outlink, out);
}

Step 4: Define Pads and the Filter Struct

Define the input and output ports (pads) for the filter, and assemble everything into the main AVFilter structure.

static const AVFilterPad custom_inputs[] = {
    {
        .name         = "default",
        .type         = AVMEDIA_TYPE_VIDEO,
        .filter_frame = filter_frame,
    },
};

static const AVFilterPad custom_outputs[] = {
    {
        .name = "default",
        .type = AVMEDIA_TYPE_VIDEO,
    },
};

const AVFilter ff_vf_custom = {
    .name          = "custom",
    .description   = NULL_IF_CONFIG_SMALL("A template custom video filter."),
    .priv_size     = sizeof(CustomContext),
    .priv_class    = &custom_class,
    FILTER_INPUTS(custom_inputs),
    FILTER_OUTPUTS(custom_outputs),
};

Step 5: Register the Filter with libavfilter

To make FFmpeg aware of your new filter, you must register it in the build system.

  1. Add to the Makefile:
    Open libavfilter/Makefile and add your filter object to the video filters section:

    OBJS-$(CONFIG_CUSTOM_FILTER)                 += vf_custom.o
  2. Add to the Filters List:
    Open libavfilter/allfilters.c and ensure your filter is declared. FFmpeg automatically generates declarations based on the Makefile config, but manual registration verification ensures compilation flags align correctly.

Step 6: Compile and Test

Reconfigure and compile FFmpeg from the root directory of the source tree:

./configure --enable-filter=custom
make -j$(nproc)

Verify that your custom filter is registered and available by running:

./ffmpeg -filters | grep custom

You can now test the filter on an input video:

./ffmpeg -i input.mp4 -vf "custom=shift=50" output.mp4