PR #22782 opened by damitha
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22782
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22782.patch

Add a video filter that renders Lottie JSON animations using the ThorVG
vector graphics engine.

[Lottie](https://lottie.airbnb.tech/#/) is a JSON-based format for describing 
scalable vector animations,
commonly used for UI and motion graphics. 
[ThorVG](https://github.com/thorvg/thorvg) provides a lightweight
renderer for vector graphics, enabling rasterization of Lottie content
into video frames.



>From fb8fd5079f2db1763e81b16570ae7efbf6202ae4 Mon Sep 17 00:00:00 2001
From: Damitha Gunawardena <[email protected]>
Date: Sat, 11 Apr 2026 05:40:25 +1000
Subject: [PATCH] avfilter/vf_lottie: add Lottie rendering filter using ThorVG

Render Lottie JSON animations using the ThorVG vector graphics engine.

Signed-off-by: Damitha Gunawardena <[email protected]>
---
 configure                 |   4 +
 doc/filters.texi          |  47 ++++++
 libavfilter/Makefile      |   1 +
 libavfilter/allfilters.c  |   1 +
 libavfilter/vsrc_lottie.c | 312 ++++++++++++++++++++++++++++++++++++++
 5 files changed, 365 insertions(+)
 create mode 100644 libavfilter/vsrc_lottie.c

diff --git a/configure b/configure
index 719642d96b..cc7456c5ed 100755
--- a/configure
+++ b/configure
@@ -271,6 +271,7 @@ External library support:
   --enable-librav1e        enable AV1 encoding via rav1e [no]
   --enable-librist         enable RIST via librist [no]
   --enable-librsvg         enable SVG rasterization via librsvg [no]
+  --enable-libthorvg       enable Lottie animation via ThorVG (lottie filter) 
[no]
   --enable-librubberband   enable rubberband needed for rubberband filter [no]
   --enable-librtmp         enable RTMP[E] support via librtmp [no]
   --enable-libshaderc      enable runtime GLSL->SPIRV compilation via 
libshaderc [no]
@@ -2125,6 +2126,7 @@ EXTERNAL_LIBRARY_LIST="
     librist
     librsvg
     librtmp
+    libthorvg
     libshaderc
     libshine
     libsmbclient
@@ -4177,6 +4179,7 @@ dnn_classify_filter_select="dnn"
 dnn_detect_filter_select="dnn"
 dnn_processing_filter_select="dnn"
 drawtext_filter_deps="libfreetype libharfbuzz"
+lottie_filter_deps="libthorvg"
 drawtext_filter_suggest="libfontconfig libfribidi"
 drawvg_filter_deps="cairo"
 elbg_filter_deps="avcodec"
@@ -7430,6 +7433,7 @@ enabled librabbitmq       && require_pkg_config 
librabbitmq "librabbitmq >= 0.7.
 enabled librav1e          && require_pkg_config librav1e "rav1e >= 0.5.0" 
rav1e.h rav1e_context_new
 enabled librist           && require_pkg_config librist "librist >= 0.2.7" 
librist/librist.h rist_receiver_create
 enabled librsvg           && require_pkg_config librsvg librsvg-2.0 
librsvg-2.0/librsvg/rsvg.h rsvg_handle_new_from_data
+enabled libthorvg         && require_pkg_config libthorvg thorvg-1 
thorvg_capi.h tvg_engine_init
 enabled librtmp           && require_pkg_config librtmp librtmp librtmp/rtmp.h 
RTMP_Socket
 enabled librubberband     && require_pkg_config librubberband "rubberband >= 
1.8.1" rubberband/rubberband-c.h rubberband_new -lstdc++ && append 
librubberband_extralibs "-lstdc++"
 enabled libshaderc        && require_pkg_config spirv_library "shaderc >= 
2019.1" shaderc/shaderc.h shaderc_compiler_initialize
diff --git a/doc/filters.texi b/doc/filters.texi
index 88d67cc70f..f7a1b38d73 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -30557,6 +30557,53 @@ ffplay -f lavfi 
life=s=300x200:mold=10:r=60:ratio=0.1:death_color=#C83232:life_c
 @end example
 @end itemize
 
+@section lottie
+
+Render a Lottie JSON animation using the ThorVG library.
+
+The filter loads a Lottie @file{.json} animation file and outputs RGBA video
+frames.  By default the animation loops indefinitely; set @option{loop=0} to
+play it once and signal EOF when the last frame has been emitted.
+
+It requires FFmpeg to be built with @code{--enable-libthorvg}.
+
+@subsection Options
+
+@table @option
+@item filename, f
+Path to the Lottie JSON animation file.  This option is mandatory.
+
+@item size, s
+Set the output frame size.  If not specified, the size is taken from the
+animation's own viewport.  For the syntax of this option, check the
+@ref{video size syntax,,"Video size" section in the ffmpeg-utils 
manual,ffmpeg-utils}.
+
+@item rate, r
+Set the output frame rate.  Default is @code{25}.
+
+@item loop
+If set to @code{1} (the default), the animation loops forever.  If set to
+@code{0}, the filter plays the animation once and then signals end-of-stream.
+@end table
+
+@subsection Examples
+
+@itemize
+@item
+Play a Lottie animation once at its native size:
+@example
+ffplay -f lavfi lottie=f=animation.json:loop=0
+@end example
+
+@item
+Overlay a looping Lottie animation in the bottom-right corner of a video:
+@example
+ffmpeg -i input.mp4 -filter_complex \
+  "lottie=f=animation.json:s=370x400[fg]; [0:v][fg]overlay=W-w-20:H-h-20" \
+  output.mp4
+@end example
+@end itemize
+
 @section perlin
 Generate Perlin noise.
 
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 6a44504ab3..85fdd816cb 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -619,6 +619,7 @@ OBJS-$(CONFIG_GFXCAPTURE_FILTER)             += 
vsrc_gfxcapture.o vsrc_gfxcaptur
 OBJS-$(CONFIG_GRADIENTS_FILTER)              += vsrc_gradients.o
 OBJS-$(CONFIG_HALDCLUTSRC_FILTER)            += vsrc_testsrc.o
 OBJS-$(CONFIG_LIFE_FILTER)                   += vsrc_life.o
+OBJS-$(CONFIG_LOTTIE_FILTER)                 += vsrc_lottie.o
 OBJS-$(CONFIG_MANDELBROT_FILTER)             += vsrc_mandelbrot.o
 OBJS-$(CONFIG_MPTESTSRC_FILTER)              += vsrc_mptestsrc.o
 OBJS-$(CONFIG_NULLSRC_FILTER)                += vsrc_testsrc.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index bbd52bbdc7..0b82c654d1 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -582,6 +582,7 @@ extern const FFFilter ff_vsrc_gfxcapture;
 extern const FFFilter ff_vsrc_gradients;
 extern const FFFilter ff_vsrc_haldclutsrc;
 extern const FFFilter ff_vsrc_life;
+extern const FFFilter ff_vsrc_lottie;
 extern const FFFilter ff_vsrc_mandelbrot;
 extern const FFFilter ff_vsrc_mptestsrc;
 extern const FFFilter ff_vsrc_nullsrc;
diff --git a/libavfilter/vsrc_lottie.c b/libavfilter/vsrc_lottie.c
new file mode 100644
index 0000000000..84616508a9
--- /dev/null
+++ b/libavfilter/vsrc_lottie.c
@@ -0,0 +1,312 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Lottie animation video source using the ThorVG rendering library.
+ *
+ * The filter loads a Lottie JSON animation file and outputs RGBA video
+ * frames rendered by ThorVG.  By default the animation loops forever;
+ * set loop=0 to play it once.
+ *
+ * Example overlay a looping Lottie animation over a video:
+ * @code
+ * ffmpeg -i input.mp4 \
+ *   -filter_complex \
+ *     "[0:v]scale=1280:720[bg]; \
+ *      lottie=f=animation.json:s=370x400[fg]; \
+ *      [bg][fg]overlay=W-w-10:H-h-10" \
+ *   output.mp4
+ * @endcode
+ */
+
+#include <math.h>
+
+#include "libavutil/mem.h"
+#include "libavutil/opt.h"
+#include "avfilter.h"
+#include "filters.h"
+#include "formats.h"
+#include "video.h"
+
+#include <thorvg_capi.h>
+
+typedef struct LottieContext {
+    const AVClass *class;
+
+    /* options */
+    char      *filename;
+    int        w, h;
+    AVRational frame_rate;
+    int        loop;
+
+    /* derived timing */
+    AVRational time_base;
+    int64_t    pts;
+
+    /* ThorVG objects */
+    Tvg_Canvas    canvas;
+    Tvg_Animation animation;
+
+    /* animation properties */
+    float      total_frames; /* from ThorVG */
+    float      duration;     /* animation duration in seconds */
+} LottieContext;
+
+#define OFFSET(x) offsetof(LottieContext, x)
+#define FLAGS (AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_FILTERING_PARAM)
+
+static const AVOption lottie_options[] = {
+    { "filename", "set Lottie JSON filename", OFFSET(filename),   
AV_OPT_TYPE_STRING,     { .str = NULL }, 0, 0, FLAGS },
+    { "f",        "set Lottie JSON filename", OFFSET(filename),   
AV_OPT_TYPE_STRING,     { .str = NULL }, 0, 0, FLAGS },
+    { "size",     "set output video size",    OFFSET(w),          
AV_OPT_TYPE_IMAGE_SIZE, { .str = NULL }, 0, 0, FLAGS },
+    { "s",        "set output video size",    OFFSET(w),          
AV_OPT_TYPE_IMAGE_SIZE, { .str = NULL }, 0, 0, FLAGS },
+    { "rate",     "set output frame rate",    OFFSET(frame_rate), 
AV_OPT_TYPE_VIDEO_RATE, { .str = "25" }, 0, INT_MAX, FLAGS },
+    { "r",        "set output frame rate",    OFFSET(frame_rate), 
AV_OPT_TYPE_VIDEO_RATE, { .str = "25" }, 0, INT_MAX, FLAGS },
+    { "loop",     "loop the animation",       OFFSET(loop),       
AV_OPT_TYPE_INT,        { .i64 = 1    }, 0, 1, FLAGS },
+    { NULL }
+};
+
+AVFILTER_DEFINE_CLASS(lottie);
+
+static av_cold int lottie_init(AVFilterContext *ctx)
+{
+    LottieContext *s = ctx->priv;
+    Tvg_Paint picture;
+    float pic_w = 0.0f, pic_h = 0.0f;
+
+    if (!s->filename) {
+        av_log(ctx, AV_LOG_ERROR, "The filename option is required.\n");
+        return AVERROR(EINVAL);
+    }
+
+    if (tvg_engine_init(0) != TVG_RESULT_SUCCESS) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to initialise ThorVG engine.\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    s->animation = tvg_animation_new();
+    if (!s->animation) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to create ThorVG animation.\n");
+        tvg_engine_term();
+        return AVERROR(ENOMEM);
+    }
+
+    picture = tvg_animation_get_picture(s->animation);
+    if (!picture) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to obtain ThorVG picture from 
animation.\n");
+        tvg_animation_del(s->animation);
+        tvg_engine_term();
+        return AVERROR_EXTERNAL;
+    }
+
+    if (tvg_picture_load(picture, s->filename) != TVG_RESULT_SUCCESS) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to load Lottie file '%s'.\n", 
s->filename);
+        tvg_animation_del(s->animation);
+        tvg_engine_term();
+        return AVERROR_EXTERNAL;
+    }
+
+    /* Retrieve animation timing. */
+    tvg_animation_get_total_frame(s->animation, &s->total_frames);
+    tvg_animation_get_duration(s->animation, &s->duration);
+
+    if (s->total_frames <= 0.0f || s->duration <= 0.0f) {
+        av_log(ctx, AV_LOG_ERROR,
+               "Invalid animation: total_frames=%.1f duration=%.3fs\n",
+               s->total_frames, s->duration);
+        tvg_animation_del(s->animation);
+        tvg_engine_term();
+        return AVERROR_EXTERNAL;
+    }
+
+    av_log(ctx, AV_LOG_VERBOSE,
+           "Lottie: total_frames=%.1f duration=%.3fs native_fps=%.3f\n",
+           s->total_frames, s->duration, s->total_frames / s->duration);
+
+    tvg_picture_get_size(picture, &pic_w, &pic_h);
+    if (s->w <= 0 || s->h <= 0) {
+        s->w = (pic_w > 0.0f) ? (int)pic_w : 512;
+        s->h = (pic_h > 0.0f) ? (int)pic_h : 512;
+    }
+
+    if (tvg_picture_set_size(picture, (float)s->w, (float)s->h) != 
TVG_RESULT_SUCCESS)
+        av_log(ctx, AV_LOG_WARNING, "Failed to set picture size.\n");
+
+    s->canvas = tvg_swcanvas_create(TVG_ENGINE_OPTION_DEFAULT);
+    if (!s->canvas) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to create ThorVG SW canvas.\n");
+        tvg_animation_del(s->animation);
+        tvg_engine_term();
+        return AVERROR(ENOMEM);
+    }
+
+    if (tvg_canvas_add(s->canvas, picture) != TVG_RESULT_SUCCESS) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to add picture to canvas.\n");
+        tvg_canvas_destroy(s->canvas);
+        s->canvas = NULL;
+        tvg_animation_del(s->animation);
+        tvg_engine_term();
+        return AVERROR_EXTERNAL;
+    }
+
+    s->time_base = av_inv_q(s->frame_rate);
+    s->pts       = 0;
+
+    return 0;
+}
+
+static av_cold void lottie_uninit(AVFilterContext *ctx)
+{
+    LottieContext *s = ctx->priv;
+
+    if (s->canvas) {
+        tvg_canvas_destroy(s->canvas);
+        s->canvas = NULL;
+    }
+    if (s->animation) {
+        tvg_animation_del(s->animation);
+        s->animation = NULL;
+    }
+    tvg_engine_term();
+}
+
+static int lottie_config_props(AVFilterLink *outlink)
+{
+    AVFilterContext *ctx = outlink->src;
+    LottieContext   *s   = ctx->priv;
+    FilterLink      *l   = ff_filter_link(outlink);
+
+    outlink->w                  = s->w;
+    outlink->h                  = s->h;
+    outlink->sample_aspect_ratio = (AVRational){ 1, 1 };
+    l->frame_rate               = s->frame_rate;
+    outlink->time_base          = s->time_base;
+
+    return 0;
+}
+
+static int lottie_activate(AVFilterContext *ctx)
+{
+    AVFilterLink *outlink = ctx->outputs[0];
+    LottieContext *s      = ctx->priv;
+    AVFrame       *frame;
+    double anim_time;
+    float  anim_frame;
+    int ret;
+
+    if (!ff_outlink_frame_wanted(outlink))
+        return FFERROR_NOT_READY;
+
+    anim_time = s->pts * av_q2d(s->time_base);
+
+    if (!s->loop && anim_time >= s->duration) {
+        ff_outlink_set_status(outlink, AVERROR_EOF, s->pts);
+        return 0;
+    }
+
+    if (s->duration > 0.0)
+        anim_time = fmod(anim_time, s->duration);
+
+    anim_frame = (float)(anim_time * s->total_frames / s->duration);
+    if (anim_frame >= s->total_frames)
+        anim_frame = s->total_frames - 1.0f;
+
+    {
+        Tvg_Result r = tvg_animation_set_frame(s->animation, anim_frame);
+        if (r != TVG_RESULT_SUCCESS && r != TVG_RESULT_INSUFFICIENT_CONDITION) 
{
+            av_log(ctx, AV_LOG_ERROR,
+                   "Failed to set animation frame %.2f (tvg error %d).\n",
+                   anim_frame, (int)r);
+            return AVERROR_EXTERNAL;
+        }
+    }
+
+    frame = ff_get_video_buffer(outlink, s->w, s->h);
+    if (!frame)
+        return AVERROR(ENOMEM);
+
+    if (tvg_swcanvas_set_target(s->canvas,
+                                (uint32_t *)frame->data[0],
+                                frame->linesize[0] / sizeof(uint32_t),
+                                s->w, s->h,
+                                TVG_COLORSPACE_ABGR8888S) != 
TVG_RESULT_SUCCESS) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to set canvas render target.\n");
+        av_frame_free(&frame);
+        return AVERROR_EXTERNAL;
+    }
+
+    if (tvg_canvas_update(s->canvas) != TVG_RESULT_SUCCESS) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to update canvas.\n");
+        av_frame_free(&frame);
+        return AVERROR_EXTERNAL;
+    }
+
+    if (tvg_canvas_draw(s->canvas, true) != TVG_RESULT_SUCCESS) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to draw canvas.\n");
+        av_frame_free(&frame);
+        return AVERROR_EXTERNAL;
+    }
+
+    if (tvg_canvas_sync(s->canvas) != TVG_RESULT_SUCCESS) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to sync canvas.\n");
+        av_frame_free(&frame);
+        return AVERROR_EXTERNAL;
+    }
+
+    frame->pts                 = s->pts++;
+    frame->duration            = 1;
+    frame->flags              |= AV_FRAME_FLAG_KEY;
+    frame->pict_type           = AV_PICTURE_TYPE_I;
+    frame->sample_aspect_ratio = (AVRational){ 1, 1 };
+
+    ret = ff_filter_frame(outlink, frame);
+    return ret;
+}
+
+static int lottie_query_formats(const AVFilterContext *ctx,
+                                AVFilterFormatsConfig **cfg_in,
+                                AVFilterFormatsConfig **cfg_out)
+{
+    static const enum AVPixelFormat pix_fmts[] = {
+        AV_PIX_FMT_RGBA,
+        AV_PIX_FMT_NONE
+    };
+    return ff_set_pixel_formats_from_list2(ctx, cfg_in, cfg_out, pix_fmts);
+}
+
+static const AVFilterPad lottie_outputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = lottie_config_props,
+    },
+};
+
+const FFFilter ff_vsrc_lottie = {
+    .p.name        = "lottie",
+    .p.description = NULL_IF_CONFIG_SMALL("Render a Lottie JSON animation 
using ThorVG."),
+    .p.priv_class  = &lottie_class,
+    .p.inputs      = NULL,
+    .priv_size     = sizeof(LottieContext),
+    .init          = lottie_init,
+    .uninit        = lottie_uninit,
+    .activate      = lottie_activate,
+    FILTER_OUTPUTS(lottie_outputs),
+    FILTER_QUERY_FUNC2(lottie_query_formats),
+};
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to