PR #22860 opened by damitha URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22860 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22860.patch
Lottie is an open-source, JSON-based animation file format that enables designers and developers to add lightweight, high-quality, and interactive animations to websites and apps. There are few renders such as skottie, rlottie and thorvg. However there is no ability to directly integrate with ffmpeg. This gap causes many developers to rasterize the animation frame to intermediate formats then use ffmpeg to convert to different format. I have previously [introduced](https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22782) the support as source filter, but suggested to support this as a codec. This is my first attempt to write a demuxer and decoder so this is my attempt with few assumptions I have noted in other demuxer/decoder implementations. >From be372816510a61ae1e2fd1014062af2740c9a42e Mon Sep 17 00:00:00 2001 From: Damitha Gunawardena <[email protected]> Date: Thu, 16 Apr 2026 15:41:36 +1000 Subject: [PATCH 1/4] lottiedec: add lottie demuxer and decoder support using thorvg Add a lottie demuxer and decoder support Signed-off-by: Damitha Gunawardena <[email protected]> --- configure | 4 + doc/decoders.texi | 27 +++++ doc/demuxers.texi | 12 ++ libavcodec/Makefile | 1 + libavcodec/allcodecs.c | 1 + libavcodec/codec_desc.c | 8 ++ libavcodec/codec_id.h | 1 + libavcodec/libthorvgdec.c | 248 ++++++++++++++++++++++++++++++++++++++ libavformat/Makefile | 1 + libavformat/allformats.c | 1 + libavformat/lottiedec.c | 220 +++++++++++++++++++++++++++++++++ tests/Makefile | 1 + 12 files changed, 525 insertions(+) create mode 100644 libavcodec/libthorvgdec.c create mode 100644 libavformat/lottiedec.c diff --git a/configure b/configure index 719642d96b..20188eabd4 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 [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] @@ -2124,6 +2125,7 @@ EXTERNAL_LIBRARY_LIST=" librav1e librist librsvg + libthorvg librtmp libshaderc libshine @@ -3860,6 +3862,7 @@ libopus_encoder_deps="libopus" libopus_encoder_select="audio_frame_queue" librav1e_encoder_deps="librav1e" librsvg_decoder_deps="librsvg" +libthorvg_decoder_deps="libthorvg" libshine_encoder_deps="libshine" libshine_encoder_select="audio_frame_queue mpegaudioheader" libspeex_decoder_deps="libspeex" @@ -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/decoders.texi b/doc/decoders.texi index 1db297f42f..4f3fda6407 100644 --- a/doc/decoders.texi +++ b/doc/decoders.texi @@ -146,6 +146,33 @@ This decoder allows libavcodec to decode AVS2 streams with davs2 library. @c man end VIDEO DECODERS +@anchor{libthorvg} +@section libthorvg + +Lottie animation decoder using the ThorVG rendering library. + +libthorvg allows libavcodec to decode Lottie JSON animations by +rasterising each frame with ThorVG's software renderer. Output is +always @code{rgba} pixel format. +Requires the presence of the ThorVG headers and library during configuration. +You need to explicitly configure the build with @code{--enable-libthorvg}. + +@subsection Options + +The following options are supported by the libthorvg wrapper. + +@table @option + +@item width +Override the output width in pixels. The default value is 0 (use the +width declared in the animation). + +@item height +Override the output height in pixels. The default value is 0 (use the +height declared in the animation). + +@end table + @section libuavs3d AVS3-P2/IEEE1857.10 video decoder. diff --git a/doc/demuxers.texi b/doc/demuxers.texi index a49d3406f6..607162f7e3 100644 --- a/doc/demuxers.texi +++ b/doc/demuxers.texi @@ -838,6 +838,18 @@ Set the sample rate for libopenmpt to output. Range is from 1000 to INT_MAX. The value default is 48000. @end table +@section lottie + +Demuxer for Lottie JSON animation files. + +Lottie is a JSON-based animation format widely used for vector animations. +This demuxer parses the top-level animation properties (width, height, frame +rate, in-point and out-point) and exposes the animation as a video stream. +The full JSON is passed as extradata to the decoder. + +No external library is required for demuxing; decoding requires +@ref{libthorvg}. + @anchor{mccdec} @section mcc diff --git a/libavcodec/Makefile b/libavcodec/Makefile index 85d35913f3..a397cf4a4a 100644 --- a/libavcodec/Makefile +++ b/libavcodec/Makefile @@ -1205,6 +1205,7 @@ OBJS-$(CONFIG_LIBOPUS_ENCODER) += libopusenc.o libopus.o \ vorbis_data.o OBJS-$(CONFIG_LIBRAV1E_ENCODER) += librav1e.o OBJS-$(CONFIG_LIBRSVG_DECODER) += librsvgdec.o +OBJS-$(CONFIG_LIBTHORVG_DECODER) += libthorvgdec.o OBJS-$(CONFIG_LIBSHINE_ENCODER) += libshine.o OBJS-$(CONFIG_LIBSPEEX_DECODER) += libspeexdec.o OBJS-$(CONFIG_LIBSPEEX_ENCODER) += libspeexenc.o diff --git a/libavcodec/allcodecs.c b/libavcodec/allcodecs.c index 5e798c1430..211ebaa515 100644 --- a/libavcodec/allcodecs.c +++ b/libavcodec/allcodecs.c @@ -815,6 +815,7 @@ extern const FFCodec ff_libopus_encoder; extern const FFCodec ff_libopus_decoder; extern const FFCodec ff_librav1e_encoder; extern const FFCodec ff_librsvg_decoder; +extern const FFCodec ff_libthorvg_decoder; extern const FFCodec ff_libshine_encoder; extern const FFCodec ff_libspeex_encoder; extern const FFCodec ff_libspeex_decoder; diff --git a/libavcodec/codec_desc.c b/libavcodec/codec_desc.c index a9f21f8152..86a1dc11cb 100644 --- a/libavcodec/codec_desc.c +++ b/libavcodec/codec_desc.c @@ -2009,6 +2009,14 @@ static const AVCodecDescriptor codec_descriptors[] = { AV_CODEC_PROP_LOSSLESS, .mime_types= MT("image/jxs"), }, + { + .id = AV_CODEC_ID_LOTTIE, + .type = AVMEDIA_TYPE_VIDEO, + .name = "lottie", + .long_name = NULL_IF_CONFIG_SMALL("Lottie JSON animation"), + .props = AV_CODEC_PROP_LOSSLESS, + .mime_types= MT("application/json"), + }, /* various PCM "codecs" */ { diff --git a/libavcodec/codec_id.h b/libavcodec/codec_id.h index 6529f0a6bc..b8f54406e0 100644 --- a/libavcodec/codec_id.h +++ b/libavcodec/codec_id.h @@ -332,6 +332,7 @@ enum AVCodecID { AV_CODEC_ID_APV, AV_CODEC_ID_PRORES_RAW, AV_CODEC_ID_JPEGXS, + AV_CODEC_ID_LOTTIE, /* various PCM "codecs" */ AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs diff --git a/libavcodec/libthorvgdec.c b/libavcodec/libthorvgdec.c new file mode 100644 index 0000000000..fafd56a8d6 --- /dev/null +++ b/libavcodec/libthorvgdec.c @@ -0,0 +1,248 @@ +/* + * ThorVG Lottie animation decoder + * Copyright (c) 2026 Damitha Gunawardena + * + * 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 decoder using ThorVG. + */ + +#include "avcodec.h" +#include "codec_internal.h" +#include "decode.h" +#include "libavutil/opt.h" +#include "libavutil/thread.h" + +#include <thorvg_capi.h> + +static AVMutex tvg_mutex = AV_MUTEX_INITIALIZER; +static int tvg_refcount = 0; + +static int tvg_global_init(void *logctx) +{ + int ret = 0; + ff_mutex_lock(&tvg_mutex); + if (tvg_refcount++ == 0) { + if (tvg_engine_init(0) != TVG_RESULT_SUCCESS) { + av_log(logctx, AV_LOG_ERROR, "Failed to initialise ThorVG engine.\n"); + ret = AVERROR_EXTERNAL; + tvg_refcount--; + } + } + ff_mutex_unlock(&tvg_mutex); + return ret; +} + +static void tvg_global_uninit(void) +{ + ff_mutex_lock(&tvg_mutex); + if (--tvg_refcount == 0) + tvg_engine_term(); + ff_mutex_unlock(&tvg_mutex); +} + +typedef struct LibThorVGContext { + const AVClass *class; + int width, height; + + Tvg_Canvas canvas; + Tvg_Animation animation; + float total_frames; + int engine_initialized; +} LibThorVGContext; + +static av_cold int libthorvg_init(AVCodecContext *avctx) +{ + LibThorVGContext *s = avctx->priv_data; + int ret; + + if (!avctx->extradata || avctx->extradata_size <= 0) { + av_log(avctx, AV_LOG_ERROR, "Missing Lottie JSON extradata.\n"); + return AVERROR_INVALIDDATA; + } + + if ((ret = tvg_global_init(avctx)) < 0) + return ret; + s->engine_initialized = 1; + + s->animation = tvg_lottie_animation_new(); + if (!s->animation) { + av_log(avctx, AV_LOG_ERROR, "Failed to create ThorVG Lottie animation.\n"); + return AVERROR(ENOMEM); + } + + if (s->width <= 0) s->width = avctx->width; + if (s->height <= 0) s->height = avctx->height; + + avctx->pix_fmt = AV_PIX_FMT_RGBA; + return 0; +} + +static int libthorvg_canvas_init(AVCodecContext *avctx) +{ + LibThorVGContext *s = avctx->priv_data; + Tvg_Paint picture; + float pic_w = 0.f, pic_h = 0.f; + + picture = tvg_animation_get_picture(s->animation); + if (!picture) { + av_log(avctx, AV_LOG_ERROR, "Failed to get ThorVG picture.\n"); + return AVERROR_EXTERNAL; + } + + if (tvg_picture_load_data(picture, + (const char *)avctx->extradata, + (uint32_t)avctx->extradata_size, + "lottie+json", NULL, true) + != TVG_RESULT_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to load Lottie JSON.\n"); + return AVERROR_INVALIDDATA; + } + + tvg_animation_get_total_frame(s->animation, &s->total_frames); + if (s->total_frames <= 0.f) { + av_log(avctx, AV_LOG_ERROR, "Animation has no frames.\n"); + return AVERROR_INVALIDDATA; + } + + tvg_picture_get_size(picture, &pic_w, &pic_h); + if (s->width <= 0) s->width = pic_w > 0.f ? (int)pic_w : avctx->width; + if (s->height <= 0) s->height = pic_h > 0.f ? (int)pic_h : avctx->height; + if (s->width <= 0 || s->height <= 0) { + av_log(avctx, AV_LOG_ERROR, "Cannot determine output dimensions.\n"); + return AVERROR_INVALIDDATA; + } + + if (tvg_picture_set_size(picture, (float)s->width, (float)s->height) + != TVG_RESULT_SUCCESS) + av_log(avctx, AV_LOG_WARNING, "Failed to set picture size.\n"); + + s->canvas = tvg_swcanvas_create(TVG_ENGINE_OPTION_DEFAULT); + if (!s->canvas) { + av_log(avctx, AV_LOG_ERROR, "Failed to create ThorVG SW canvas.\n"); + return AVERROR(ENOMEM); + } + + if (tvg_canvas_add(s->canvas, picture) != TVG_RESULT_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to add picture to canvas.\n"); + tvg_canvas_destroy(s->canvas); + s->canvas = NULL; + return AVERROR_EXTERNAL; + } + + avctx->width = s->width; + avctx->height = s->height; + return 0; +} + +static int libthorvg_decode(AVCodecContext *avctx, AVFrame *frame, + int *got_frame, AVPacket *pkt) +{ + LibThorVGContext *s = avctx->priv_data; + float anim_frame = (float)FFMAX(pkt->pts, 0); + Tvg_Result r; + int ret; + + if (!s->canvas && (ret = libthorvg_canvas_init(avctx)) < 0) + return ret; + + if (anim_frame >= s->total_frames) + anim_frame = s->total_frames - 1.f; + + r = tvg_animation_set_frame(s->animation, anim_frame); + if (r != TVG_RESULT_SUCCESS && r != TVG_RESULT_INSUFFICIENT_CONDITION) { + av_log(avctx, AV_LOG_ERROR, "Failed to set animation frame %.2f.\n", + anim_frame); + return AVERROR_EXTERNAL; + } + + if ((ret = ff_get_buffer(avctx, frame, 0)) < 0) + return ret; + + if (tvg_swcanvas_set_target(s->canvas, + (uint32_t *)frame->data[0], + frame->linesize[0] / 4, + avctx->width, avctx->height, + TVG_COLORSPACE_ABGR8888S) != TVG_RESULT_SUCCESS || + tvg_canvas_update(s->canvas) != TVG_RESULT_SUCCESS || + tvg_canvas_draw(s->canvas, true) != TVG_RESULT_SUCCESS || + tvg_canvas_sync(s->canvas) != TVG_RESULT_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "ThorVG rendering failed.\n"); + av_frame_unref(frame); + return AVERROR_EXTERNAL; + } + + frame->pts = pkt->pts; + frame->duration = pkt->duration; + frame->pict_type = AV_PICTURE_TYPE_I; + frame->flags |= AV_FRAME_FLAG_KEY; + *got_frame = 1; + return pkt->size; +} + +static av_cold int libthorvg_close(AVCodecContext *avctx) +{ + LibThorVGContext *s = avctx->priv_data; + + if (s->canvas) { + tvg_canvas_destroy(s->canvas); /* also frees the picture */ + s->canvas = NULL; + } + if (s->animation) { + tvg_animation_del(s->animation); + s->animation = NULL; + } + if (s->engine_initialized) { + tvg_global_uninit(); + s->engine_initialized = 0; + } + return 0; +} + +#define OFFSET(x) offsetof(LibThorVGContext, x) +#define DEC (AV_OPT_FLAG_DECODING_PARAM | AV_OPT_FLAG_VIDEO_PARAM) +static const AVOption libthorvg_options[] = { + { "width", "output width override (0 = animation native)", + OFFSET(width), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, DEC }, + { "height", "output height override (0 = animation native)", + OFFSET(height), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, DEC }, + { NULL } +}; + +static const AVClass libthorvg_class = { + .class_name = "libthorvg", + .item_name = av_default_item_name, + .option = libthorvg_options, + .version = LIBAVUTIL_VERSION_INT, +}; + +const FFCodec ff_libthorvg_decoder = { + .p.name = "libthorvg", + CODEC_LONG_NAME("Lottie animation (ThorVG)"), + .p.priv_class = &libthorvg_class, + .p.type = AVMEDIA_TYPE_VIDEO, + .p.id = AV_CODEC_ID_LOTTIE, + .p.capabilities = AV_CODEC_CAP_DR1, + .p.wrapper_name = "libthorvg", + .priv_data_size = sizeof(LibThorVGContext), + .init = libthorvg_init, + .close = libthorvg_close, + FF_CODEC_DECODE_CB(libthorvg_decode), +}; diff --git a/libavformat/Makefile b/libavformat/Makefile index 7c2fcad93e..176d44d94d 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -352,6 +352,7 @@ OBJS-$(CONFIG_LC3_DEMUXER) += lc3.o OBJS-$(CONFIG_LC3_MUXER) += lc3.o OBJS-$(CONFIG_LMLM4_DEMUXER) += lmlm4.o OBJS-$(CONFIG_LOAS_DEMUXER) += loasdec.o rawdec.o +OBJS-$(CONFIG_LOTTIE_DEMUXER) += lottiedec.o OBJS-$(CONFIG_LUODAT_DEMUXER) += luodatdec.o OBJS-$(CONFIG_LRC_DEMUXER) += lrcdec.o lrc.o subtitles.o OBJS-$(CONFIG_LRC_MUXER) += lrcenc.o lrc.o diff --git a/libavformat/allformats.c b/libavformat/allformats.c index a491e3a7e8..ab5a2678bd 100644 --- a/libavformat/allformats.c +++ b/libavformat/allformats.c @@ -259,6 +259,7 @@ extern const FFInputFormat ff_lc3_demuxer; extern const FFOutputFormat ff_lc3_muxer; extern const FFInputFormat ff_lmlm4_demuxer; extern const FFInputFormat ff_loas_demuxer; +extern const FFInputFormat ff_lottie_demuxer; extern const FFInputFormat ff_luodat_demuxer; extern const FFInputFormat ff_lrc_demuxer; extern const FFOutputFormat ff_lrc_muxer; diff --git a/libavformat/lottiedec.c b/libavformat/lottiedec.c new file mode 100644 index 0000000000..87d013571e --- /dev/null +++ b/libavformat/lottiedec.c @@ -0,0 +1,220 @@ +/* + * Lottie JSON animation demuxer + * Copyright (c) 2026 Damitha Gunawardena + * + * 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 JSON animation demuxer. + */ + +#include <math.h> +#include <stdlib.h> +#include <string.h> + +#include "avformat.h" +#include "demux.h" +#include "internal.h" +#include "libavcodec/codec_id.h" +#include "libavutil/bprint.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/mem.h" + +typedef struct LottieDemuxContext { + int64_t cur_frame; + int64_t n_frames; +} LottieDemuxContext; + +static double lottie_parse_key(const char *json, const char *key) +{ + char needle[32]; + const char *p = json; + const char *layers = strstr(json, "\"layers\""); + size_t limit = layers ? (size_t)(layers - json) : strlen(json); + + snprintf(needle, sizeof(needle), "\"%s\"", key); + while ((p = strstr(p, needle))) { + const char *q; + char *endp; + double val; + + if ((size_t)(p - json) >= limit) + break; + q = p + strlen(needle); + while (*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n') q++; + if (*q != ':') { p++; continue; } + q++; + while (*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n') q++; + val = strtod(q, &endp); + if (endp != q) + return val; + p++; + } + return NAN; +} + +static int lottie_probe(const AVProbeData *p) +{ + const char *buf = p->buf; + int i = 0; + + while (buf[i] == ' ' || buf[i] == '\t' || buf[i] == '\r' || buf[i] == '\n') + i++; + if (buf[i] != '{') + return 0; + if (!strstr(buf, "\"layers\"")) + return 0; + if (isnan(lottie_parse_key(buf, "w"))) + return 0; + if (isnan(lottie_parse_key(buf, "h"))) + return 0; + if (isnan(lottie_parse_key(buf, "fr"))) + return 0; + if (isnan(lottie_parse_key(buf, "op"))) + return 0; + if (isnan(lottie_parse_key(buf, "ip"))) + return 0; + + return AVPROBE_SCORE_MAX - 1; +} + +static int lottie_read_header(AVFormatContext *s) +{ + LottieDemuxContext *ctx = s->priv_data; + AVStream *st; + AVCodecParameters *par; + AVBPrint bp; + double w, h, fr, ip, op; + AVRational frame_rate; + int ret; + + av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED); + ret = avio_read_to_bprint(s->pb, &bp, INT_MAX); + if (ret < 0 && ret != AVERROR_EOF) + goto fail; + if (!av_bprint_is_complete(&bp)) { + ret = AVERROR(ENOMEM); + goto fail; + } + ret = 0; + + w = lottie_parse_key(bp.str, "w"); + h = lottie_parse_key(bp.str, "h"); + fr = lottie_parse_key(bp.str, "fr"); + ip = lottie_parse_key(bp.str, "ip"); + op = lottie_parse_key(bp.str, "op"); + + if (isnan(w) || isnan(h) || w <= 0 || h <= 0) { + av_log(s, AV_LOG_ERROR, "Invalid or missing width/height.\n"); + ret = AVERROR_INVALIDDATA; + goto fail; + } + if (isnan(fr) || fr <= 0) { + av_log(s, AV_LOG_ERROR, "Invalid or missing frame rate.\n"); + ret = AVERROR_INVALIDDATA; + goto fail; + } + if (isnan(op) || op <= 0) { + av_log(s, AV_LOG_ERROR, "Invalid or missing out-point.\n"); + ret = AVERROR_INVALIDDATA; + goto fail; + } + if (isnan(ip)) + ip = 0; + + ctx->n_frames = (int64_t)(op - ip); + if (ctx->n_frames <= 0) { + av_log(s, AV_LOG_ERROR, "Animation has no frames.\n"); + ret = AVERROR_INVALIDDATA; + goto fail; + } + + st = avformat_new_stream(s, NULL); + if (!st) { + ret = AVERROR(ENOMEM); + goto fail; + } + + par = st->codecpar; + par->codec_type = AVMEDIA_TYPE_VIDEO; + par->codec_id = AV_CODEC_ID_LOTTIE; + par->width = (int)w; + par->height = (int)h; + + av_reduce(&frame_rate.num, &frame_rate.den, + (int64_t)(fr * 1000 + 0.5), 1000, INT_MAX); + st->avg_frame_rate = frame_rate; + st->nb_frames = ctx->n_frames; + st->duration = ctx->n_frames; + avpriv_set_pts_info(st, 64, frame_rate.den, frame_rate.num); + + par->extradata = av_mallocz(bp.len + AV_INPUT_BUFFER_PADDING_SIZE); + if (!par->extradata) { + ret = AVERROR(ENOMEM); + goto fail; + } + memcpy(par->extradata, bp.str, bp.len); + par->extradata_size = bp.len; + +fail: + av_bprint_finalize(&bp, NULL); + return ret; +} + +static int lottie_read_packet(AVFormatContext *s, AVPacket *pkt) +{ + LottieDemuxContext *ctx = s->priv_data; + int ret; + + if (ctx->cur_frame >= ctx->n_frames) + return AVERROR_EOF; + + if ((ret = av_new_packet(pkt, 4)) < 0) + return ret; + + AV_WL32(pkt->data, (uint32_t)ctx->cur_frame); + pkt->stream_index = 0; + pkt->pts = ctx->cur_frame; + pkt->dts = ctx->cur_frame; + pkt->duration = 1; + pkt->flags |= AV_PKT_FLAG_KEY; + ctx->cur_frame++; + return 0; +} + +static int lottie_read_seek(AVFormatContext *s, int stream_index, + int64_t timestamp, int flags) +{ + LottieDemuxContext *ctx = s->priv_data; + + if (timestamp < 0) + timestamp = 0; + ctx->cur_frame = FFMIN(timestamp, ctx->n_frames); + return 0; +} + +const FFInputFormat ff_lottie_demuxer = { + .p.name = "lottie", + .p.long_name = NULL_IF_CONFIG_SMALL("Lottie JSON animation"), + .priv_data_size = sizeof(LottieDemuxContext), + .read_probe = lottie_probe, + .read_header = lottie_read_header, + .read_packet = lottie_read_packet, + .read_seek = lottie_read_seek, +}; diff --git a/tests/Makefile b/tests/Makefile index 3e119979d1..1b2c5fe63d 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -214,6 +214,7 @@ include $(SRC_PATH)/tests/fate/libavformat.mak include $(SRC_PATH)/tests/fate/libavutil.mak include $(SRC_PATH)/tests/fate/libswresample.mak include $(SRC_PATH)/tests/fate/libswscale.mak +include $(SRC_PATH)/tests/fate/lottie.mak include $(SRC_PATH)/tests/fate/lossless-audio.mak include $(SRC_PATH)/tests/fate/lossless-video.mak include $(SRC_PATH)/tests/fate/matroska.mak -- 2.52.0 >From b0093f962f0a9b056796bb481565d7063be24caf Mon Sep 17 00:00:00 2001 From: Damitha Gunawardena <[email protected]> Date: Fri, 17 Apr 2026 11:02:59 +1000 Subject: [PATCH 2/4] review libthorvgdec --- libavcodec/libthorvgdec.c | 78 ++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/libavcodec/libthorvgdec.c b/libavcodec/libthorvgdec.c index fafd56a8d6..f63723e44c 100644 --- a/libavcodec/libthorvgdec.c +++ b/libavcodec/libthorvgdec.c @@ -32,32 +32,6 @@ #include <thorvg_capi.h> -static AVMutex tvg_mutex = AV_MUTEX_INITIALIZER; -static int tvg_refcount = 0; - -static int tvg_global_init(void *logctx) -{ - int ret = 0; - ff_mutex_lock(&tvg_mutex); - if (tvg_refcount++ == 0) { - if (tvg_engine_init(0) != TVG_RESULT_SUCCESS) { - av_log(logctx, AV_LOG_ERROR, "Failed to initialise ThorVG engine.\n"); - ret = AVERROR_EXTERNAL; - tvg_refcount--; - } - } - ff_mutex_unlock(&tvg_mutex); - return ret; -} - -static void tvg_global_uninit(void) -{ - ff_mutex_lock(&tvg_mutex); - if (--tvg_refcount == 0) - tvg_engine_term(); - ff_mutex_unlock(&tvg_mutex); -} - typedef struct LibThorVGContext { const AVClass *class; int width, height; @@ -71,15 +45,17 @@ typedef struct LibThorVGContext { static av_cold int libthorvg_init(AVCodecContext *avctx) { LibThorVGContext *s = avctx->priv_data; - int ret; if (!avctx->extradata || avctx->extradata_size <= 0) { av_log(avctx, AV_LOG_ERROR, "Missing Lottie JSON extradata.\n"); return AVERROR_INVALIDDATA; } - if ((ret = tvg_global_init(avctx)) < 0) - return ret; + if (tvg_engine_init(0) != TVG_RESULT_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to initialise ThorVG engine.\n"); + return AVERROR_EXTERNAL; + } + s->engine_initialized = 1; s->animation = tvg_lottie_animation_new(); @@ -100,6 +76,7 @@ static int libthorvg_canvas_init(AVCodecContext *avctx) LibThorVGContext *s = avctx->priv_data; Tvg_Paint picture; float pic_w = 0.f, pic_h = 0.f; + int ret; picture = tvg_animation_get_picture(s->animation); if (!picture) { @@ -147,8 +124,14 @@ static int libthorvg_canvas_init(AVCodecContext *avctx) return AVERROR_EXTERNAL; } - avctx->width = s->width; - avctx->height = s->height; + + ret = ff_set_dimensions(avctx, s->width, s->height); + if (ret < 0) { + av_log(avctx, AV_LOG_ERROR, + "Invalid output dimensions %dx%d.\n",s->width, s->height); + return ret; + } + return 0; } @@ -176,15 +159,34 @@ static int libthorvg_decode(AVCodecContext *avctx, AVFrame *frame, if ((ret = ff_get_buffer(avctx, frame, 0)) < 0) return ret; - if (tvg_swcanvas_set_target(s->canvas, + r = tvg_swcanvas_set_target(s->canvas, (uint32_t *)frame->data[0], frame->linesize[0] / 4, avctx->width, avctx->height, - TVG_COLORSPACE_ABGR8888S) != TVG_RESULT_SUCCESS || - tvg_canvas_update(s->canvas) != TVG_RESULT_SUCCESS || - tvg_canvas_draw(s->canvas, true) != TVG_RESULT_SUCCESS || - tvg_canvas_sync(s->canvas) != TVG_RESULT_SUCCESS) { - av_log(avctx, AV_LOG_ERROR, "ThorVG rendering failed.\n"); + TVG_COLORSPACE_ABGR8888S); + if (r != TVG_RESULT_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to set ThorVG canvas target.\n"); + av_frame_unref(frame); + return AVERROR_EXTERNAL; + } + + r = tvg_canvas_update(s->canvas); + if (r != TVG_RESULT_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to update ThorVG canvas.\n"); + av_frame_unref(frame); + return AVERROR_EXTERNAL; + } + + r = tvg_canvas_draw(s->canvas, true); + if (r != TVG_RESULT_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to draw ThorVG canvas.\n"); + av_frame_unref(frame); + return AVERROR_EXTERNAL; + } + + r = tvg_canvas_sync(s->canvas); + if (r != TVG_RESULT_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to sync ThorVG canvas.\n"); av_frame_unref(frame); return AVERROR_EXTERNAL; } @@ -210,7 +212,7 @@ static av_cold int libthorvg_close(AVCodecContext *avctx) s->animation = NULL; } if (s->engine_initialized) { - tvg_global_uninit(); + tvg_engine_term(); s->engine_initialized = 0; } return 0; -- 2.52.0 >From 19a6b4d40e081209adae4cc8d1b175c4b5fa0c0f Mon Sep 17 00:00:00 2001 From: Damitha Gunawardena <[email protected]> Date: Mon, 20 Apr 2026 07:42:24 +1000 Subject: [PATCH 3/4] better handling of looping --- libavcodec/libthorvgdec.c | 29 ++++++++++++++++------------- libavformat/lottiedec.c | 8 +++----- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/libavcodec/libthorvgdec.c b/libavcodec/libthorvgdec.c index f63723e44c..3ffd1ee3c1 100644 --- a/libavcodec/libthorvgdec.c +++ b/libavcodec/libthorvgdec.c @@ -27,8 +27,8 @@ #include "avcodec.h" #include "codec_internal.h" #include "decode.h" +#include "libavutil/intreadwrite.h" #include "libavutil/opt.h" -#include "libavutil/thread.h" #include <thorvg_capi.h> @@ -107,6 +107,13 @@ static int libthorvg_canvas_init(AVCodecContext *avctx) return AVERROR_INVALIDDATA; } + ret = ff_set_dimensions(avctx, s->width, s->height); + if (ret < 0) { + av_log(avctx, AV_LOG_ERROR, + "Invalid output dimensions %dx%d.\n",s->width, s->height); + return ret; + } + if (tvg_picture_set_size(picture, (float)s->width, (float)s->height) != TVG_RESULT_SUCCESS) av_log(avctx, AV_LOG_WARNING, "Failed to set picture size.\n"); @@ -124,14 +131,6 @@ static int libthorvg_canvas_init(AVCodecContext *avctx) return AVERROR_EXTERNAL; } - - ret = ff_set_dimensions(avctx, s->width, s->height); - if (ret < 0) { - av_log(avctx, AV_LOG_ERROR, - "Invalid output dimensions %dx%d.\n",s->width, s->height); - return ret; - } - return 0; } @@ -139,15 +138,19 @@ static int libthorvg_decode(AVCodecContext *avctx, AVFrame *frame, int *got_frame, AVPacket *pkt) { LibThorVGContext *s = avctx->priv_data; - float anim_frame = (float)FFMAX(pkt->pts, 0); + int frame_index; + int total_frames; + float anim_frame; Tvg_Result r; int ret; + frame_index = AV_RL32(pkt->data); + if (!s->canvas && (ret = libthorvg_canvas_init(avctx)) < 0) return ret; - if (anim_frame >= s->total_frames) - anim_frame = s->total_frames - 1.f; + total_frames = FFMAX((int)s->total_frames, 1); + anim_frame = (float)(frame_index % total_frames); r = tvg_animation_set_frame(s->animation, anim_frame); if (r != TVG_RESULT_SUCCESS && r != TVG_RESULT_INSUFFICIENT_CONDITION) { @@ -204,7 +207,7 @@ static av_cold int libthorvg_close(AVCodecContext *avctx) LibThorVGContext *s = avctx->priv_data; if (s->canvas) { - tvg_canvas_destroy(s->canvas); /* also frees the picture */ + tvg_canvas_destroy(s->canvas); s->canvas = NULL; } if (s->animation) { diff --git a/libavformat/lottiedec.c b/libavformat/lottiedec.c index 87d013571e..358bd02c1a 100644 --- a/libavformat/lottiedec.c +++ b/libavformat/lottiedec.c @@ -101,7 +101,6 @@ static int lottie_read_header(AVFormatContext *s) AVCodecParameters *par; AVBPrint bp; double w, h, fr, ip, op; - AVRational frame_rate; int ret; av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED); @@ -157,12 +156,11 @@ static int lottie_read_header(AVFormatContext *s) par->width = (int)w; par->height = (int)h; - av_reduce(&frame_rate.num, &frame_rate.den, - (int64_t)(fr * 1000 + 0.5), 1000, INT_MAX); - st->avg_frame_rate = frame_rate; + st->avg_frame_rate = av_d2q(fr,INT_MAX); + st->nb_frames = ctx->n_frames; st->duration = ctx->n_frames; - avpriv_set_pts_info(st, 64, frame_rate.den, frame_rate.num); + avpriv_set_pts_info(st, 64, st->avg_frame_rate.den, st->avg_frame_rate.num); par->extradata = av_mallocz(bp.len + AV_INPUT_BUFFER_PADDING_SIZE); if (!par->extradata) { -- 2.52.0 >From 437a0aecdc0e7013b63da3c8185f6fb5d9f4c32e Mon Sep 17 00:00:00 2001 From: Damitha Gunawardena <[email protected]> Date: Mon, 20 Apr 2026 07:44:44 +1000 Subject: [PATCH 4/4] removing lottie fate tests --- tests/Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Makefile b/tests/Makefile index 1b2c5fe63d..3e119979d1 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -214,7 +214,6 @@ include $(SRC_PATH)/tests/fate/libavformat.mak include $(SRC_PATH)/tests/fate/libavutil.mak include $(SRC_PATH)/tests/fate/libswresample.mak include $(SRC_PATH)/tests/fate/libswscale.mak -include $(SRC_PATH)/tests/fate/lottie.mak include $(SRC_PATH)/tests/fate/lossless-audio.mak include $(SRC_PATH)/tests/fate/lossless-video.mak include $(SRC_PATH)/tests/fate/matroska.mak -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
