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]
