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.
Add to the Makefile:
Openlibavfilter/Makefileand add your filter object to the video filters section:OBJS-$(CONFIG_CUSTOM_FILTER) += vf_custom.oAdd to the Filters List:
Openlibavfilter/allfilters.cand 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 customYou can now test the filter on an input video:
./ffmpeg -i input.mp4 -vf "custom=shift=50" output.mp4