This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

commit 20b009e30136cb55022ee32cfb4b2dcae1630bb4
Author:     Ramiro Polla <[email protected]>
AuthorDate: Wed Apr 22 18:09:53 2026 +0200
Commit:     Ramiro Polla <[email protected]>
CommitDate: Tue May 19 11:36:10 2026 +0200

    avformat/webp: add Animated WebP demuxer
    
    Original work by Josef Zlomek <[email protected]>
    
    Signed-off-by: Ramiro Polla <[email protected]>
---
 Changelog                   |   1 +
 doc/demuxers.texi           |  35 +++++
 libavformat/Makefile        |   1 +
 libavformat/allformats.c    |   1 +
 libavformat/version.h       |   2 +-
 libavformat/webp_anim_dec.c | 371 ++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 410 insertions(+), 1 deletion(-)

diff --git a/Changelog b/Changelog
index 9834f78261..6df9bed4aa 100644
--- a/Changelog
+++ b/Changelog
@@ -13,6 +13,7 @@ version <next>:
 - ProRes RAW VideoToolbox hwaccel
 - APV Vulkan hwaccel
 - Animated WebP decoder
+- Animated WebP demuxer
 
 
 version 8.1:
diff --git a/doc/demuxers.texi b/doc/demuxers.texi
index a49d3406f6..170fafb4bb 100644
--- a/doc/demuxers.texi
+++ b/doc/demuxers.texi
@@ -1187,4 +1187,39 @@ this is set to 0, which means that a sensible value is 
chosen based on the
 input format.
 @end table
 
+@section webp
+
+Animated WebP demuxer.
+
+It accepts the following options:
+
+@table @option
+@item -min_delay @var{int}
+Set the minimum valid delay between frames in milliseconds.
+Range is 0 to 60000. Default value is 10.
+
+@item -max_webp_delay @var{int}
+Set the maximum valid delay between frames in milliseconds.
+Range is 0 to 16777215. Default value is 16777215 (over four hours),
+the maximum value allowed by the specification.
+
+@item -default_delay @var{int}
+Set the default delay between frames in milliseconds.
+Range is 0 to 60000. Default value is 100.
+
+@item -ignore_loop @var{bool}
+WebP files can contain information to loop a certain number of times
+(or infinitely). If @option{ignore_loop} is set to true, then the loop
+setting from the input will be ignored and looping will not occur.
+If set to false, then looping will occur and will cycle the number
+of times according to the WebP. Default value is true.
+
+@item usebgcolor @var{bool}
+WebP files contain a background color hint in the ANIM chunk, but the
+WebP specification says that viewer applications are not required to
+use it. If @option{usebgcolor} is set to true, then the background
+color hint will be used, otherwise transparent black will be used for
+the background. Default value is false.
+@end table
+
 @c man end DEMUXERS
diff --git a/libavformat/Makefile b/libavformat/Makefile
index ac56e54aa8..123de41184 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -646,6 +646,7 @@ OBJS-$(CONFIG_WEBM_MUXER)                += matroskaenc.o 
matroska.o \
                                             avlanguage.o
 OBJS-$(CONFIG_WEBM_DASH_MANIFEST_MUXER)  += webmdashenc.o
 OBJS-$(CONFIG_WEBM_CHUNK_MUXER)          += webm_chunk.o
+OBJS-$(CONFIG_WEBP_ANIM_DEMUXER)         += webp_anim_dec.o
 OBJS-$(CONFIG_WEBP_MUXER)                += webpenc.o
 OBJS-$(CONFIG_WEBVTT_DEMUXER)            += webvttdec.o subtitles.o
 OBJS-$(CONFIG_WEBVTT_MUXER)              += webvttenc.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index a491e3a7e8..af7eea5e5c 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -516,6 +516,7 @@ extern const FFInputFormat  ff_webm_dash_manifest_demuxer;
 extern const FFOutputFormat ff_webm_dash_manifest_muxer;
 extern const FFOutputFormat ff_webm_chunk_muxer;
 extern const FFOutputFormat ff_webp_muxer;
+extern const FFInputFormat  ff_webp_anim_demuxer;
 extern const FFInputFormat  ff_webvtt_demuxer;
 extern const FFOutputFormat ff_webvtt_muxer;
 extern const FFInputFormat  ff_wsaud_demuxer;
diff --git a/libavformat/version.h b/libavformat/version.h
index 2a28a3bf40..6a80f3ac4e 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,7 +31,7 @@
 
 #include "version_major.h"
 
-#define LIBAVFORMAT_VERSION_MINOR  17
+#define LIBAVFORMAT_VERSION_MINOR  18
 #define LIBAVFORMAT_VERSION_MICRO 100
 
 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
diff --git a/libavformat/webp_anim_dec.c b/libavformat/webp_anim_dec.c
new file mode 100644
index 0000000000..badc36f936
--- /dev/null
+++ b/libavformat/webp_anim_dec.c
@@ -0,0 +1,371 @@
+/*
+ * Animated WebP demuxer
+ * Copyright (c) 2020 Pexeso Inc.
+ * Copyright (c) 2026 Ramiro Polla
+ *
+ * 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
+ * Animated WebP demuxer.
+ */
+
+#include "avio_internal.h"
+#include "demux.h"
+#include "avformat.h"
+#include "internal.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/mem.h"
+#include "libavutil/opt.h"
+
+#define VP8X_FLAG_ANIMATION             0x02
+#define VP8X_FLAG_XMP_METADATA          0x04
+#define VP8X_FLAG_EXIF_METADATA         0x08
+#define VP8X_FLAG_ALPHA                 0x10
+#define VP8X_FLAG_ICC                   0x20
+
+typedef struct WebPAnimDemuxContext {
+    const AVClass *class;
+
+    /**
+     * Minimum allowed delay between frames in milliseconds.
+     * Values below this threshold are considered to be invalid
+     * and set to value of default_delay.
+     */
+    int min_delay;
+    int max_delay;
+    int default_delay;
+
+    /*
+     * loop options
+     */
+    int ignore_loop;                ///< ignore loop setting
+    int loop_count;                 ///< number of times to loop the animation
+    int cur_loop;                   ///< current loop counter
+
+    /*
+     * variables for the key frame detection
+     */
+    int cur_frame;                  ///< number of frames of the current 
animation file
+    int vp8x_flags;
+
+    int has_iccp;
+    int has_exif;
+    int has_anim;
+    int has_xmp;
+
+    int usebgcolor;
+
+    int64_t first_anmf_offset;
+} WebPAnimDemuxContext;
+
+/**
+ * Major web browsers display WebPs at ~10-15fps when rate is not
+ * explicitly set or have too low values. We assume default rate to be 10.
+ * Default delay = 1000 microseconds / 10fps = 100 milliseconds per frame.
+ */
+#define WEBP_DEFAULT_DELAY   100
+/**
+ * By default delay values less than this threshold considered to be invalid.
+ */
+#define WEBP_MIN_DELAY       10
+
+static int webp_anim_probe(const AVProbeData *p)
+{
+    const uint8_t *b = p->buf;
+
+    if (AV_RL32(b)      == MKTAG('R', 'I', 'F', 'F') &&
+        AV_RL32(b +  8) == MKTAG('W', 'E', 'B', 'P') &&
+        AV_RL32(b + 12) == MKTAG('V', 'P', '8', 'X') &&
+        AV_RL32(b + 16) == 10 &&
+        (b[20] & VP8X_FLAG_ANIMATION))
+        return AVPROBE_SCORE_MAX;
+
+    return 0;
+}
+
+static int webp_anim_read_header(AVFormatContext *s)
+{
+    WebPAnimDemuxContext *ctx = s->priv_data;
+    AVIOContext *pb = s->pb;
+    int ret;
+
+    /* Check for signature. */
+    if (avio_rl32(pb) != MKTAG('R', 'I', 'F', 'F'))
+        return AVERROR_INVALIDDATA;
+    avio_skip(pb, 4); /* file size */
+    if (avio_rl32(pb) != MKTAG('W', 'E', 'B', 'P'))
+        return AVERROR_INVALIDDATA;
+
+    /* VP8X must be first chunk */
+    if (avio_rl32(pb) != MKTAG('V', 'P', '8', 'X') ||
+        avio_rl32(pb) != 10 /* chunk size */)
+        return AVERROR_INVALIDDATA;
+    ctx->vp8x_flags = avio_r8(pb);
+    if (!(ctx->vp8x_flags & VP8X_FLAG_ANIMATION))
+        return AVERROR_INVALIDDATA;
+    avio_skip(pb, 3);
+
+    AVStream *st = avformat_new_stream(s, NULL);
+    if (!st)
+        return AVERROR(ENOMEM);
+
+    avpriv_set_pts_info(st, 64, 1, 1000);
+    st->codecpar->format     = AV_PIX_FMT_ARGB;
+    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    st->codecpar->codec_id   = AV_CODEC_ID_WEBP_ANIM;
+    st->codecpar->width      = avio_rl24(pb) + 1;
+    st->codecpar->height     = avio_rl24(pb) + 1;
+    st->start_time           = 0;
+
+    int explode = (s->error_recognition & AV_EF_EXPLODE);
+    int loglevel = explode ? AV_LOG_ERROR : AV_LOG_WARNING;
+    while (1) {
+        int64_t offset = avio_tell(pb);
+        uint32_t fourcc = avio_rl32(pb);
+        uint32_t size = avio_rl32(pb);
+
+        av_log(s, AV_LOG_DEBUG, "Chunk %s of size %u at offset %" PRId64 "\n",
+               av_fourcc2str(fourcc), size, offset);
+
+        if (size == UINT32_MAX)
+            return AVERROR_INVALIDDATA;
+        size += size & 1;
+
+        if (avio_feof(pb))
+            break;
+
+        switch (fourcc) {
+        case MKTAG('I', 'C', 'C', 'P'):
+            if (ctx->has_iccp) {
+                av_log(s, loglevel, "Extra ICCP chunk found\n");
+                if (explode)
+                    return AVERROR_INVALIDDATA;
+                avio_skip(pb, size);
+            } else {
+                if (!(ctx->vp8x_flags & VP8X_FLAG_ICC)) {
+                    av_log(s, loglevel,
+                           "ICCP chunk present, but ICC Profile bit not set in 
the VP8X header\n");
+                    if (explode)
+                        return AVERROR_INVALIDDATA;
+                }
+
+                AVPacketSideData *sd = 
av_packet_side_data_new(&st->codecpar->coded_side_data,
+                                                               
&st->codecpar->nb_coded_side_data,
+                                                               
AV_PKT_DATA_ICC_PROFILE, size, 0);
+                if (!sd)
+                    return AVERROR(ENOMEM);
+                ret = avio_read(pb, sd->data, size);
+                if (ret < 0)
+                    return ret;
+                ctx->has_iccp = 1;
+            }
+            break;
+        case MKTAG('A', 'N', 'I', 'M'):
+            if (ctx->has_anim) {
+                av_log(s, loglevel, "Extra ANIM chunk found\n");
+                if (explode)
+                    return AVERROR_INVALIDDATA;
+                avio_skip(pb, size);
+            } else {
+                if (size != 6)
+                    return AVERROR_INVALIDDATA;
+                uint32_t bg_color = avio_rb32(pb);
+                ctx->loop_count   = avio_rl16(pb);
+                if (ctx->usebgcolor) {
+                    st->codecpar->extradata = av_mallocz(4 + 
AV_INPUT_BUFFER_PADDING_SIZE);
+                    if (!st->codecpar->extradata)
+                        return AVERROR(ENOMEM);
+                    AV_WB32(st->codecpar->extradata, bg_color);
+                    st->codecpar->extradata_size = 4;
+                }
+                av_log(s, AV_LOG_DEBUG,
+                       "ANIM: background BGRA 0x%08x loop count %d\n",
+                       bg_color, ctx->loop_count);
+                ctx->has_anim = 1;
+            }
+            break;
+        case MKTAG('A', 'N', 'M', 'F'):
+            if (!ctx->has_anim) {
+                av_log(s, loglevel,
+                       "ANMF chunk present, but no previous ANIM chunk 
found\n");
+                if (explode)
+                    return AVERROR_INVALIDDATA;
+                ctx->loop_count = 1;
+            } else if (!ctx->ignore_loop && ctx->loop_count != 1) {
+                int64_t file_size = avio_size(pb);
+                if (file_size < 0 || offset < 0 ||
+                    (ret = ffio_ensure_seekback(pb, file_size - offset)) < 0) {
+                    av_log(s, AV_LOG_WARNING,
+                           "Could not ensure seekback, will not loop\n");
+                    ctx->loop_count = 1;
+                }
+            }
+            ctx->first_anmf_offset = offset;
+            ret = avio_seek(pb, -8, SEEK_CUR);
+            if (ret < 0)
+                return ret;
+            return 0;
+        default:
+            av_log(s, AV_LOG_WARNING, "Skipping chunk: %s\n", 
av_fourcc2str(fourcc));
+            avio_skip(pb, size);
+            break;
+        }
+    }
+
+    return AVERROR_INVALIDDATA;
+}
+
+static int webp_anim_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    WebPAnimDemuxContext *ctx = s->priv_data;
+    AVIOContext *pb = s->pb;
+    int ret;
+
+    int explode = (s->error_recognition & AV_EF_EXPLODE);
+    int loglevel = explode ? AV_LOG_ERROR : AV_LOG_WARNING;
+    while (1) {
+        int64_t offset = avio_tell(pb);
+        uint32_t fourcc = avio_rl32(pb);
+        uint32_t size = avio_rl32(pb);
+
+        if (size == UINT32_MAX)
+            return AVERROR_INVALIDDATA;
+        size += size & 1;
+
+        if (avio_feof(pb)) {
+            if (!ctx->ignore_loop &&
+                (ctx->loop_count == 0 || ++ctx->cur_loop < ctx->loop_count)) {
+                ctx->cur_frame = 0;
+                ret = avio_seek(pb, ctx->first_anmf_offset, SEEK_SET);
+                if (ret < 0)
+                    return ret;
+                continue;
+            }
+            break;
+        }
+
+        av_log(s, AV_LOG_DEBUG, "Chunk %s of size %u at offset %" PRId64 "\n",
+               av_fourcc2str(fourcc), size, offset);
+
+        switch (fourcc) {
+        case MKTAG('A', 'N', 'M', 'F'):
+            if (size < 16)
+                return AVERROR_INVALIDDATA;
+            ret = av_get_packet(pb, pkt, size);
+            if (ret < 0)
+                return ret;
+            if (!ctx->cur_frame++)
+                pkt->flags |= AV_PKT_FLAG_KEY;
+            pkt->pts = AV_NOPTS_VALUE;
+            pkt->dts = AV_NOPTS_VALUE;
+            uint32_t duration = AV_RL24(pkt->data + 12);
+            if (duration <= ctx->min_delay)
+                duration = ctx->default_delay;
+            pkt->duration = FFMIN(duration, ctx->max_delay);
+            return ret;
+        case MKTAG('E', 'X', 'I', 'F'):
+            if (ctx->has_exif) {
+                av_log(s, loglevel, "Extra EXIF chunk found\n");
+                if (explode)
+                    return AVERROR_INVALIDDATA;
+                avio_skip(pb, size);
+            } else {
+                if (!(ctx->vp8x_flags & VP8X_FLAG_EXIF_METADATA)) {
+                    av_log(s, loglevel,
+                           "EXIF chunk present, but EXIF bit not set in the 
VP8X header\n");
+                    if (explode)
+                        return AVERROR_INVALIDDATA;
+                }
+
+                AVStream *st = s->streams[0];
+                AVPacketSideData *sd = 
av_packet_side_data_new(&st->codecpar->coded_side_data,
+                                                               
&st->codecpar->nb_coded_side_data,
+                                                               
AV_PKT_DATA_EXIF, size, 0);
+                if (!sd)
+                    return AVERROR(ENOMEM);
+                ret = avio_read(pb, sd->data, size);
+                if (ret < 0)
+                    return ret;
+                ctx->has_exif = 1;
+            }
+            break;
+        case MKTAG('X', 'M', 'P', ' '):
+            if (ctx->has_xmp) {
+                av_log(s, loglevel, "Extra XMP chunk found\n");
+                if (explode)
+                    return AVERROR_INVALIDDATA;
+                avio_skip(pb, size);
+            } else {
+                if (!(ctx->vp8x_flags & VP8X_FLAG_XMP_METADATA)) {
+                    av_log(s, loglevel,
+                           "XMP chunk present, but XMP bit not set in the VP8X 
header\n");
+                    if (explode)
+                        return AVERROR_INVALIDDATA;
+                }
+
+                uint8_t *xmp = av_malloc(size + 1);
+                if (!xmp)
+                    return AVERROR(ENOMEM);
+                ret = ffio_read_size(pb, xmp, size);
+                if (ret < 0) {
+                    av_free(xmp);
+                    return ret;
+                }
+                xmp[size] = '\0';
+                av_dict_set(&s->metadata, "xmp", xmp, AV_DICT_DONT_STRDUP_VAL);
+                ctx->has_xmp = 1;
+            }
+            break;
+        default:
+            av_log(s, AV_LOG_WARNING, "Skipping chunk: %s\n", 
av_fourcc2str(fourcc));
+            avio_skip(pb, size);
+            break;
+        }
+    }
+
+    return AVERROR_EOF;
+}
+
+static const AVOption options[] = {
+    { "min_delay",      "minimum valid delay between frames (in 
milliseconds)", offsetof(WebPAnimDemuxContext, min_delay),     AV_OPT_TYPE_INT, 
 {.i64 = WEBP_MIN_DELAY},     0, 1000 * 60, AV_OPT_FLAG_DECODING_PARAM },
+    { "max_webp_delay", "maximum valid delay between frames (in 
milliseconds)", offsetof(WebPAnimDemuxContext, max_delay),     AV_OPT_TYPE_INT, 
 {.i64 = 0xffffff},           0, 0xffffff,  AV_OPT_FLAG_DECODING_PARAM },
+    { "default_delay",  "default delay between frames (in milliseconds)",      
 offsetof(WebPAnimDemuxContext, default_delay), AV_OPT_TYPE_INT,  {.i64 = 
WEBP_DEFAULT_DELAY}, 0, 1000 * 60, AV_OPT_FLAG_DECODING_PARAM },
+    { "ignore_loop",    "ignore loop setting",                                 
 offsetof(WebPAnimDemuxContext, ignore_loop),   AV_OPT_TYPE_BOOL, {.i64 = 1},   
               0, 1,         AV_OPT_FLAG_DECODING_PARAM },
+    { "usebgcolor",     "use background color from ANIM chunk",                
 offsetof(WebPAnimDemuxContext, usebgcolor),    AV_OPT_TYPE_BOOL, {.i64 = 0},   
               0, 1,         AV_OPT_FLAG_DECODING_PARAM },
+    { NULL },
+};
+
+static const AVClass demuxer_class = {
+    .class_name = "Animated WebP demuxer",
+    .item_name  = av_default_item_name,
+    .option     = options,
+    .version    = LIBAVUTIL_VERSION_INT,
+    .category   = AV_CLASS_CATEGORY_DEMUXER,
+};
+
+const FFInputFormat ff_webp_anim_demuxer = {
+    .p.name         = "webp_anim",
+    .p.long_name    = NULL_IF_CONFIG_SMALL("Animated WebP"),
+    .p.flags        = AVFMT_GENERIC_INDEX,
+    .p.priv_class   = &demuxer_class,
+    .priv_data_size = sizeof(WebPAnimDemuxContext),
+    .read_probe     = webp_anim_probe,
+    .read_header    = webp_anim_read_header,
+    .read_packet    = webp_anim_read_packet,
+};

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

Reply via email to