PR #22975 opened by Ramiro Polla (ramiro)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22975
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22975.patch

This PR is based on the work by Josef Zlomek and Thilo Borgmann.

A different approach has been used from [the previous 
patchset](https://ffmpeg.org/pipermail/ffmpeg-devel/2024-June/thread.html#330023),
 having a separate simpler demuxer dedicated to Animated WebP (which does not 
support still WebP like in Josef's demuxer), and a separate decoder dedicated 
to Animated WebP (that does not modify the still WebP decoder). This way, it's 
still possible to run `ffmpeg -i frame%03d.webp` with frame-based parallelism.

Animated WebP files are based on a persistent canvas. Each frame will update 
(overwriting or blending) a region of the canvas using a VP8 (lossy or 
lossless) subframe, which may be disposed before the next frame. The pixel 
format may change between frames (`ARGB` for lossless or `YUV(A)` for lossy), 
but the canvas may still contain data from a different pixel format. To work 
around this discrepancy, when a new frame is lossless (`ARGB`) and the previous 
frame was lossy (`YUV(A)`), the canvas is upgraded to `ARGB`. But when a new 
frame is lossy over a lossless canvas, the subframe is converted to `ARGB` 
instead, to preserve the higher quality of the canvas. I don't know of any 
real-world files that would hit this condition, but since it is not forbidden 
by the spec, we support it.


>From 0ab5cf146f937c9f9f3aae9b2bd0348800a770ff Mon Sep 17 00:00:00 2001
From: Thilo Borgmann <[email protected]>
Date: Fri, 21 Jun 2024 12:43:23 +0200
Subject: [PATCH 1/5] avcodec/webp: export XMP metadata

Signed-off-by: Ramiro Polla <[email protected]>
---
 libavcodec/webp.c | 31 +++++++++++++++++++++++++++++--
 1 file changed, 29 insertions(+), 2 deletions(-)

diff --git a/libavcodec/webp.c b/libavcodec/webp.c
index 39fdd961a1..b34ed5e44d 100644
--- a/libavcodec/webp.c
+++ b/libavcodec/webp.c
@@ -35,9 +35,11 @@
  * Exif metadata
  * ICC profile
  *
+ * @author Thilo Borgmann <thilo.borgmann _at_ mail.de>
+ * XMP metadata
+ *
  * Unimplemented:
  *   - Animation
- *   - XMP metadata
  */
 
 #include "libavutil/imgutils.h"
@@ -205,6 +207,7 @@ typedef struct WebPContext {
     int alpha_data_size;                /* alpha chunk data size */
     int has_exif;                       /* set after an EXIF chunk has been 
processed */
     int has_iccp;                       /* set after an ICCP chunk has been 
processed */
+    int has_xmp;                        /* set after an XMP chunk has been 
processed */
     int width;                          /* image width */
     int height;                         /* image height */
 
@@ -1350,6 +1353,7 @@ static int webp_decode_frame(AVCodecContext *avctx, 
AVFrame *p,
     s->has_alpha = 0;
     s->has_exif  = 0;
     s->has_iccp  = 0;
+    s->has_xmp   = 0;
     bytestream2_init(&gb, avpkt->data, avpkt->size);
 
     if (bytestream2_get_bytes_left(&gb) < 12)
@@ -1509,11 +1513,34 @@ exif_end:
         }
         case MKTAG('A', 'N', 'I', 'M'):
         case MKTAG('A', 'N', 'M', 'F'):
-        case MKTAG('X', 'M', 'P', ' '):
             av_log(avctx, AV_LOG_WARNING, "skipping unsupported chunk: %s\n",
                    av_fourcc2str(chunk_type));
             bytestream2_skip(&gb, chunk_size);
             break;
+        case MKTAG('X', 'M', 'P', ' '): {
+            if (s->has_xmp) {
+                av_log(avctx, AV_LOG_VERBOSE, "Ignoring extra XMP chunk\n");
+                bytestream2_skip(&gb, chunk_size);
+                break;
+            }
+            if (!(vp8x_flags & VP8X_FLAG_XMP_METADATA))
+                av_log(avctx, AV_LOG_WARNING,
+                       "XMP chunk present, but XMP bit not set in the "
+                       "VP8X header\n");
+
+            s->has_xmp = 1;
+
+            // there are at least chunk_size bytes left to read
+            uint8_t *buffer = av_malloc(chunk_size + 1);
+            if (!buffer)
+                return AVERROR(ENOMEM);
+
+            bytestream2_get_buffer(&gb, buffer, chunk_size);
+            buffer[chunk_size] = '\0';
+
+            av_dict_set(&p->metadata, "xmp", buffer, AV_DICT_DONT_STRDUP_VAL);
+            break;
+        }
         default:
             av_log(avctx, AV_LOG_VERBOSE, "skipping unknown chunk: %s\n",
                    av_fourcc2str(chunk_type));
-- 
2.52.0


>From 8c0a6cd6fdc65b3f58c40e65313ed1e6882e2a79 Mon Sep 17 00:00:00 2001
From: Ramiro Polla <[email protected]>
Date: Wed, 29 Apr 2026 22:20:38 +0200
Subject: [PATCH 2/5] avcodec/codec_id: add Animated WebP codec ID and
 descriptor

Signed-off-by: Ramiro Polla <[email protected]>
---
 libavcodec/codec_desc.c | 8 ++++++++
 libavcodec/codec_id.h   | 1 +
 2 files changed, 9 insertions(+)

diff --git a/libavcodec/codec_desc.c b/libavcodec/codec_desc.c
index a9f21f8152..99e7c0de3d 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_WEBP_ANIM,
+        .type      = AVMEDIA_TYPE_VIDEO,
+        .name      = "webp_anim",
+        .long_name = NULL_IF_CONFIG_SMALL("Animated WebP"),
+        .props     = AV_CODEC_PROP_LOSSY | AV_CODEC_PROP_LOSSLESS,
+        .mime_types= MT("image/webp"),
+    },
 
     /* various PCM "codecs" */
     {
diff --git a/libavcodec/codec_id.h b/libavcodec/codec_id.h
index 6529f0a6bc..bb2c16a958 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_WEBP_ANIM,
 
     /* various PCM "codecs" */
     AV_CODEC_ID_FIRST_AUDIO = 0x10000,     ///< A dummy id pointing at the 
start of audio codecs
-- 
2.52.0


>From bfd8c958c48e503e3a4db4274cb40fa13a2d59de Mon Sep 17 00:00:00 2001
From: Ramiro Polla <[email protected]>
Date: Wed, 22 Apr 2026 18:09:26 +0200
Subject: [PATCH 3/5] avcodec/webp: add support for Animated WebP decoding

Fixes: 4907

Adds Animated WebP feature according to spec:
https://developers.google.com/speed/webp/docs/riff_container#animation

Original work by Josef Zlomek <[email protected]>
and Thilo Borgmann <[email protected]>

Signed-off-by: Ramiro Polla <[email protected]>
---
 Changelog              |   1 +
 configure              |   1 +
 libavcodec/Makefile    |   1 +
 libavcodec/allcodecs.c |   1 +
 libavcodec/version.h   |   2 +-
 libavcodec/webp.c      | 675 ++++++++++++++++++++++++++++++++++++++++-
 6 files changed, 678 insertions(+), 3 deletions(-)

diff --git a/Changelog b/Changelog
index 3f5e38e0c7..4af1667d54 100644
--- a/Changelog
+++ b/Changelog
@@ -9,6 +9,7 @@ version <next>:
 - HE-AAC 960 decoding (DAB+)
 - transpose_cuda filter
 - Add AMF Frame Rate Converter (vf_frc_amf) filter
+- Animated WebP decoder
 
 
 version 8.1:
diff --git a/configure b/configure
index 9bce658932..692bda984f 100755
--- a/configure
+++ b/configure
@@ -3364,6 +3364,7 @@ vp9_decoder_select="videodsp vp9_parser cbs_vp9 
vp9_superframe_split_bsf"
 vvc_decoder_select="cabac cbs_h266 golomb videodsp vvc_sei"
 wcmv_decoder_select="inflate_wrapper"
 webp_decoder_select="vp8_decoder"
+webp_anim_decoder_select="vp8_decoder"
 wmalossless_decoder_select="llauddsp"
 wmapro_decoder_select="sinewin wma_freqs"
 wmav1_decoder_select="sinewin wma_freqs"
diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 85d35913f3..56cf04d2c9 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -846,6 +846,7 @@ OBJS-$(CONFIG_WBMP_DECODER)            += wbmpdec.o
 OBJS-$(CONFIG_WBMP_ENCODER)            += wbmpenc.o
 OBJS-$(CONFIG_WCMV_DECODER)            += wcmv.o
 OBJS-$(CONFIG_WEBP_DECODER)            += webp.o
+OBJS-$(CONFIG_WEBP_ANIM_DECODER)       += webp.o
 OBJS-$(CONFIG_WEBVTT_DECODER)          += webvttdec.o ass.o
 OBJS-$(CONFIG_WEBVTT_ENCODER)          += webvttenc.o ass_split.o
 OBJS-$(CONFIG_WMALOSSLESS_DECODER)     += wmalosslessdec.o wma_common.o
diff --git a/libavcodec/allcodecs.c b/libavcodec/allcodecs.c
index 5e798c1430..e29a3a4020 100644
--- a/libavcodec/allcodecs.c
+++ b/libavcodec/allcodecs.c
@@ -396,6 +396,7 @@ extern const FFCodec ff_vqc_decoder;
 extern const FFCodec ff_vvc_decoder;
 extern const FFCodec ff_wbmp_decoder;
 extern const FFCodec ff_wbmp_encoder;
+extern const FFCodec ff_webp_anim_decoder;
 extern const FFCodec ff_webp_decoder;
 extern const FFCodec ff_wcmv_decoder;
 extern const FFCodec ff_wrapped_avframe_encoder;
diff --git a/libavcodec/version.h b/libavcodec/version.h
index aeb58e3fed..497389d3f3 100644
--- a/libavcodec/version.h
+++ b/libavcodec/version.h
@@ -29,7 +29,7 @@
 
 #include "version_major.h"
 
-#define LIBAVCODEC_VERSION_MINOR  30
+#define LIBAVCODEC_VERSION_MINOR  31
 #define LIBAVCODEC_VERSION_MICRO 100
 
 #define LIBAVCODEC_VERSION_INT  AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
diff --git a/libavcodec/webp.c b/libavcodec/webp.c
index b34ed5e44d..c38f443f04 100644
--- a/libavcodec/webp.c
+++ b/libavcodec/webp.c
@@ -2,6 +2,7 @@
  * WebP (.webp) image decoder
  * Copyright (c) 2013 Aneesh Dogra <[email protected]>
  * Copyright (c) 2013 Justin Ruggles <[email protected]>
+ * Copyright (c) 2020 Pexeso Inc.
  *
  * This file is part of FFmpeg.
  *
@@ -38,10 +39,13 @@
  * @author Thilo Borgmann <thilo.borgmann _at_ mail.de>
  * XMP metadata
  *
- * Unimplemented:
- *   - Animation
+ * @author Josef Zlomek, Pexeso Inc. <[email protected]>
+ * Animation
  */
 
+#include "config_components.h"
+
+#include "libavutil/colorspace.h"
 #include "libavutil/imgutils.h"
 #include "libavutil/mem.h"
 
@@ -1593,3 +1597,670 @@ const FFCodec ff_webp_decoder = {
     .caps_internal  = FF_CODEC_CAP_ICC_PROFILES |
                       FF_CODEC_CAP_USES_PROGRESSFRAMES,
 };
+
+#if CONFIG_WEBP_ANIM_DECODER
+
+#define ANMF_DISPOSAL_METHOD            0x01
+#define ANMF_DISPOSAL_METHOD_UNCHANGED  0x00
+#define ANMF_DISPOSAL_METHOD_BACKGROUND 0x01
+
+#define ANMF_BLENDING_METHOD            0x02
+#define ANMF_BLENDING_METHOD_ALPHA      0x00
+#define ANMF_BLENDING_METHOD_OVERWRITE  0x02
+
+typedef struct AnimatedWebPContext {
+    WebPContext w;
+
+    AVFrame *canvas;                    /* AVFrame for canvas */
+    AVFrame *subframe;                  /* AVFrame for subframe */
+    int canvas_width;                   /* canvas width */
+    int canvas_height;                  /* canvas height */
+    int anmf_flags;                     /* frame flags from ANMF chunk */
+    int pos_x;                          /* frame position X */
+    int pos_y;                          /* frame position Y */
+    int duration;                       /* frame duration */
+    int prev_anmf_flags;                /* previous frame flags from ANMF 
chunk */
+    int prev_width;                     /* previous frame width */
+    int prev_height;                    /* previous frame height */
+    int prev_pos_x;                     /* previous frame position X */
+    int prev_pos_y;                     /* previous frame position Y */
+    uint8_t background_argb[4];         /* background color in ARGB format */
+    uint8_t background_yuva[4];         /* background color in YUVA format */
+} AnimatedWebPContext;
+
+/*
+ * Blend src1 (foreground) and src2 (background) into dest, in ARGB format.
+ * width, height are the dimensions of src1.
+ * pos_x, pos_y is the position in src2 and in dest.
+ */
+static void blend_alpha_argb(uint8_t *dest_data[4], int dest_linesize[4],
+                             const uint8_t *src1_data[4], int src1_linesize[4],
+                             const uint8_t *src2_data[4], int src2_linesize[4],
+                             int width, int height, int pos_x, int pos_y)
+{
+    for (int y = 0; y < height; y++) {
+        const uint8_t *src1 = src1_data[0] + y * src1_linesize[0];
+        const uint8_t *src2 = src2_data[0] + (pos_y + y) * src2_linesize[0] + 
pos_x * sizeof(uint32_t);
+        uint8_t       *dest = dest_data[0] + (pos_y + y) * dest_linesize[0] + 
pos_x * sizeof(uint32_t);
+        for (int x = 0; x < width; x++) {
+            int src1_alpha = src1[0];
+            int src2_alpha = src2[0];
+
+            if (src1_alpha == 255) {
+                memcpy(dest, src1, sizeof(uint32_t));
+            } else if (src1_alpha == 0) {
+                memcpy(dest, src2, sizeof(uint32_t));
+            } else {
+                int tmp_alpha = (src2_alpha * (256 - src1_alpha)) >> 8;
+                int blend_alpha = src1_alpha + tmp_alpha;
+                int scale = (1UL << 24) / blend_alpha;
+
+                dest[0] = blend_alpha;
+                dest[1] = (((uint32_t) (src1[1] * src1_alpha + src2[1] * 
tmp_alpha)) * scale) >> 24;
+                dest[2] = (((uint32_t) (src1[2] * src1_alpha + src2[2] * 
tmp_alpha)) * scale) >> 24;
+                dest[3] = (((uint32_t) (src1[3] * src1_alpha + src2[3] * 
tmp_alpha)) * scale) >> 24;
+            }
+            src1 += sizeof(uint32_t);
+            src2 += sizeof(uint32_t);
+            dest += sizeof(uint32_t);
+        }
+    }
+}
+
+/*
+ * Blend src1 (foreground) and src2 (background) into dest, in YUVA format.
+ * width, height are the dimensions of src1.
+ * pos_x, pos_y is the position in src2 and in dest.
+ */
+static void blend_alpha_yuva(uint8_t *dest_data[4], int dest_linesize[4],
+                             const uint8_t *src1_data[4], int src1_linesize[4],
+                             int src1_format,
+                             const uint8_t *src2_data[4], int src2_linesize[4],
+                             int width, int height, int pos_x, int pos_y)
+{
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(src1_format);
+
+    int plane_y = desc->comp[0].plane;
+    int plane_u = desc->comp[1].plane;
+    int plane_v = desc->comp[2].plane;
+    int plane_a = desc->comp[3].plane;
+
+    // blend U & V planes first, because the later step may modify alpha plane
+    int w  = AV_CEIL_RSHIFT(width,  1);
+    int h  = AV_CEIL_RSHIFT(height, 1);
+    int px = AV_CEIL_RSHIFT(pos_x,  1);
+    int py = AV_CEIL_RSHIFT(pos_y,  1);
+    int tile_w = 2;
+    int tile_h = 2;
+
+    for (int y = 0; y < h; y++) {
+        const uint8_t *src1_u = src1_data[plane_u] + y * 
src1_linesize[plane_u];
+        const uint8_t *src1_v = src1_data[plane_v] + y * 
src1_linesize[plane_v];
+        const uint8_t *src2_u = src2_data[plane_u] + (py + y) * 
src2_linesize[plane_u] + px;
+        const uint8_t *src2_v = src2_data[plane_v] + (py + y) * 
src2_linesize[plane_v] + px;
+        uint8_t       *dest_u = dest_data[plane_u] + (py + y) * 
dest_linesize[plane_u] + px;
+        uint8_t       *dest_v = dest_data[plane_v] + (py + y) * 
dest_linesize[plane_v] + px;
+        for (int x = 0; x < w; x++) {
+            // calculate the average alpha of the tile
+            int src1_alpha = 0;
+            int src2_alpha = 0;
+            for (int yy = 0; yy < tile_h; yy++) {
+                for (int xx = 0; xx < tile_w; xx++) {
+                    src1_alpha += src1_data[plane_a][(y * tile_h + yy) * 
src1_linesize[plane_a] +
+                                                     (x * tile_w + xx)];
+                    src2_alpha += src2_data[plane_a][((py + y) * tile_h + yy) 
* src2_linesize[plane_a] +
+                                                     ((px + x) * tile_w + xx)];
+                }
+            }
+            src1_alpha = AV_CEIL_RSHIFT(src1_alpha, 2);
+            src2_alpha = AV_CEIL_RSHIFT(src2_alpha, 2);
+
+            if (src1_alpha == 255) {
+                *dest_u = *src1_u;
+                *dest_v = *src1_v;
+            } else if (src1_alpha == 0) {
+                *dest_u = *src2_u;
+                *dest_v = *src2_v;
+            } else {
+                int tmp_alpha = (src2_alpha * (256 - src1_alpha)) >> 8;
+                int blend_alpha = src1_alpha + tmp_alpha;
+                int scale = (1UL << 24) / blend_alpha;
+                *dest_u = (((uint32_t) (*src1_u * src1_alpha + *src2_u * 
tmp_alpha)) * scale) >> 24;
+                *dest_v = (((uint32_t) (*src1_v * src1_alpha + *src2_v * 
tmp_alpha)) * scale) >> 24;
+            }
+            src1_u++;
+            src1_v++;
+            src2_u++;
+            src2_v++;
+            dest_u++;
+            dest_v++;
+        }
+    }
+
+    // blend Y & A planes
+    for (int y = 0; y < height; y++) {
+        const uint8_t *src1_y = src1_data[plane_y] + y * 
src1_linesize[plane_y];
+        const uint8_t *src1_a = src1_data[plane_a] + y * 
src1_linesize[plane_a];
+        const uint8_t *src2_y = src2_data[plane_y] + (pos_y + y) * 
src2_linesize[plane_y] + pos_x;
+        const uint8_t *src2_a = src2_data[plane_a] + (pos_y + y) * 
src2_linesize[plane_a] + pos_x;
+        uint8_t       *dest_y = dest_data[plane_y] + (pos_y + y) * 
dest_linesize[plane_y] + pos_x;
+        uint8_t       *dest_a = dest_data[plane_a] + (pos_y + y) * 
dest_linesize[plane_a] + pos_x;
+        for (int x = 0; x < width; x++) {
+            int src1_alpha = *src1_a;
+            int src2_alpha = *src2_a;
+
+            if (src1_alpha == 255) {
+                *dest_y = *src1_y;
+                *dest_a = 255;
+            } else if (src1_alpha == 0) {
+                *dest_y = *src2_y;
+                *dest_a = src2_alpha;
+            } else {
+                int tmp_alpha = (src2_alpha * (256 - src1_alpha)) >> 8;
+                int blend_alpha = src1_alpha + tmp_alpha;
+                int scale = (1UL << 24) / blend_alpha;
+                *dest_y = (((uint32_t) (*src1_y * src1_alpha + *src2_y * 
tmp_alpha)) * scale) >> 24;
+                *dest_a = blend_alpha;
+            }
+            src1_y++;
+            src1_a++;
+            src2_y++;
+            src2_a++;
+            dest_y++;
+            dest_a++;
+        }
+    }
+}
+
+static av_always_inline void webp_yuva2argb(uint8_t *out, int Y, int U, int V, 
int A)
+{
+    // variables used in macros
+    const uint8_t *cm = ff_crop_tab + MAX_NEG_CROP;
+    uint8_t r, g, b;
+    int y, cb, cr;
+    int r_add, g_add, b_add;
+
+    YUV_TO_RGB1_CCIR(U, V);
+    YUV_TO_RGB2_CCIR(r, g, b, Y);
+
+    out[0] = av_clip_uint8(A);
+    out[1] = av_clip_uint8(r);
+    out[2] = av_clip_uint8(g);
+    out[3] = av_clip_uint8(b);
+}
+
+static void copy_yuva2argb(AVFrame *dst, AVFrame *src, int pos_x, int pos_y)
+{
+    const AVPixFmtDescriptor *src_desc = av_pix_fmt_desc_get(src->format);
+
+    int alpha   = src_desc->nb_components > 3;
+    int plane_y = src_desc->comp[0].plane;
+    int plane_u = src_desc->comp[1].plane;
+    int plane_v = src_desc->comp[2].plane;
+    int plane_a = src_desc->comp[3].plane;
+
+    for (int y = 0; y < src->height; y++) {
+        uint8_t *dest = dst->data[0] + (pos_y + y) * dst->linesize[0] + pos_x 
* 4;
+        uint8_t *src_y = src->data[plane_y] +  y       * 
src->linesize[plane_y];
+        uint8_t *src_u = src->data[plane_u] + (y >> 1) * 
src->linesize[plane_u];
+        uint8_t *src_v = src->data[plane_v] + (y >> 1) * 
src->linesize[plane_v];
+        uint8_t *src_a = NULL;
+        if (alpha)
+            src_a = src->data[plane_a] + y * src->linesize[plane_a];
+
+        for (int x = 0; x < src->width; x++) {
+            webp_yuva2argb(dest, *src_y, *src_u, *src_v, (alpha ? *src_a : 
255));
+            src_y += 1;
+            src_u += x & 1;
+            src_v += x & 1;
+            if (alpha)
+                src_a += 1;
+            dest  += 4;
+        }
+    }
+}
+
+static void blend_yuva2argb(AVFrame *dst, AVFrame *src, int pos_x, int pos_y)
+{
+    const AVPixFmtDescriptor *src_desc = av_pix_fmt_desc_get(src->format);
+
+    int plane_y = src_desc->comp[0].plane;
+    int plane_u = src_desc->comp[1].plane;
+    int plane_v = src_desc->comp[2].plane;
+    int plane_a = src_desc->comp[3].plane;
+
+    for (int y = 0; y < src->height; y++) {
+        uint8_t *dest = dst->data[0] + (pos_y + y) * dst->linesize[0] + pos_x 
* 4;
+        uint8_t *src_y = src->data[plane_y] +  y       * 
src->linesize[plane_y];
+        uint8_t *src_u = src->data[plane_u] + (y >> 1) * 
src->linesize[plane_u];
+        uint8_t *src_v = src->data[plane_v] + (y >> 1) * 
src->linesize[plane_v];
+        uint8_t *src_a = src->data[plane_a] +  y       * 
src->linesize[plane_a];
+
+        for (int x = 0; x < src->width; x++) {
+            int dst_alpha = dest[0];
+            int src_alpha = *src_a;
+
+            if (src_alpha == 255) {
+                webp_yuva2argb(dest, *src_y, *src_u, *src_v, src_alpha);
+            } else if (src_alpha == 0) {
+                // no-op
+            } else {
+                uint8_t tmp[4];
+                int tmp_alpha = (dst_alpha * (256 - src_alpha)) >> 8;
+                int blend_alpha = src_alpha + tmp_alpha;
+                int scale = (1UL << 24) / blend_alpha;
+
+                webp_yuva2argb(tmp, *src_y, *src_u, *src_v, src_alpha);
+
+                dest[0] = blend_alpha;
+                dest[1] = (((uint32_t) (tmp[1] * src_alpha + dest[1] * 
tmp_alpha)) * scale) >> 24;
+                dest[2] = (((uint32_t) (tmp[2] * src_alpha + dest[2] * 
tmp_alpha)) * scale) >> 24;
+                dest[3] = (((uint32_t) (tmp[3] * src_alpha + dest[3] * 
tmp_alpha)) * scale) >> 24;
+            }
+
+            src_y += 1;
+            src_u += x & 1;
+            src_v += x & 1;
+            src_a += 1;
+            dest  += 4;
+        }
+    }
+}
+
+static int blend_subframe_into_canvas(AnimatedWebPContext *s)
+{
+    AVFrame *canvas = s->canvas;
+    AVFrame *frame = s->subframe;
+
+    if ((s->anmf_flags & ANMF_BLENDING_METHOD) == 
ANMF_BLENDING_METHOD_OVERWRITE
+        || frame->format == AV_PIX_FMT_YUV420P) {
+        // do not blend, overwrite
+
+        if (canvas->format == AV_PIX_FMT_ARGB) {
+            if (canvas->format == frame->format) {
+                for (int y = 0; y < s->w.height; y++) {
+                    const uint32_t *src = (uint32_t *) (frame->data[0] + y * 
frame->linesize[0]);
+                    uint32_t *dst = (uint32_t *) (canvas->data[0] + (s->pos_y 
+ y) * canvas->linesize[0]) + s->pos_x;
+                    memcpy(dst, src, s->w.width * sizeof(uint32_t));
+                }
+            } else {
+                copy_yuva2argb(canvas, frame, s->pos_x, s->pos_y);
+            }
+        } else /* if (canvas->format == AV_PIX_FMT_YUVA420P) */ {
+            const AVPixFmtDescriptor *desc = 
av_pix_fmt_desc_get(frame->format);
+
+            for (int comp = 0; comp < desc->nb_components; comp++) {
+                int plane  = desc->comp[comp].plane;
+                int width  = s->w.width;
+                int height = s->w.height;
+                int pos_x  = s->pos_x;
+                int pos_y  = s->pos_y;
+                if (comp == 1 || comp == 2) {
+                    width  = AV_CEIL_RSHIFT(width,  1);
+                    height = AV_CEIL_RSHIFT(height, 1);
+                    pos_x  = AV_CEIL_RSHIFT(pos_x,  1);
+                    pos_y  = AV_CEIL_RSHIFT(pos_y,  1);
+                }
+
+                for (int y = 0; y < height; y++) {
+                    const uint8_t *src = frame->data[plane] + y * 
frame->linesize[plane];
+                    uint8_t *dst = canvas->data[plane] + (pos_y + y) * 
canvas->linesize[plane] + pos_x;
+                    memcpy(dst, src, width);
+                }
+            }
+
+            if (canvas->format == AV_PIX_FMT_YUVA420P && desc->nb_components < 
4) {
+                // frame does not have alpha, set alpha to 255
+                const AVPixFmtDescriptor *canvas_desc = 
av_pix_fmt_desc_get(canvas->format);
+                int plane  = canvas_desc->comp[3].plane;
+                int width  = s->w.width;
+                int height = s->w.height;
+                int pos_x  = s->pos_x;
+                int pos_y  = s->pos_y;
+
+                for (int y = 0; y < height; y++) {
+                    uint8_t *dst = canvas->data[plane] + (pos_y + y) * 
canvas->linesize[plane] + pos_x;
+                    memset(dst, 255, width);
+                }
+            }
+        }
+    } else {
+        // alpha blending
+
+        if (canvas->format == AV_PIX_FMT_ARGB) {
+            if (canvas->format == frame->format) {
+                blend_alpha_argb(canvas->data, canvas->linesize,
+                                 (const uint8_t **) frame->data, 
frame->linesize,
+                                 (const uint8_t **) canvas->data, 
canvas->linesize,
+                                 s->w.width, s->w.height, s->pos_x, s->pos_y);
+            } else {
+                blend_yuva2argb(canvas, frame, s->pos_x, s->pos_y);
+            }
+        } else /* if (canvas->format == AV_PIX_FMT_YUVA420P) */ {
+            blend_alpha_yuva(canvas->data, canvas->linesize,
+                             (const uint8_t **) frame->data, frame->linesize,
+                             frame->format,
+                             (const uint8_t **) canvas->data, canvas->linesize,
+                             s->w.width, s->w.height, s->pos_x, s->pos_y);
+        }
+    }
+
+    return 0;
+}
+
+/**
+ * Fill a rectangle on the canvas with the background color (transparent black
+ * by default, or the color from the ANIM chunk if provided by the demuxer).
+ */
+static void fill_canvas_rect(AnimatedWebPContext *s, int pos_x, int pos_y, int 
width, int height)
+{
+    AVFrame *canvas = s->canvas;
+
+    if (canvas->format == AV_PIX_FMT_ARGB) {
+        uint32_t bg_color = AV_RN32(s->background_argb);
+        int is_repeatable = (bg_color == ((bg_color & 0xff) * 0x01010101));
+        for (int y = 0; y < height; y++) {
+            uint32_t *dst = (uint32_t *) (canvas->data[0] + (pos_y + y) * 
canvas->linesize[0]) + pos_x;
+            if (is_repeatable) {
+                memset(dst, bg_color, width * sizeof(uint32_t));
+            } else {
+                for (int x = 0; x < width; x++)
+                    dst[x] = bg_color;
+            }
+        }
+    } else /* if (canvas->format == AV_PIX_FMT_YUVA420P) */ {
+        const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(canvas->format);
+        for (int comp = 0; comp < desc->nb_components; comp++) {
+            int shift = (comp == 1 || comp == 2) ? 1 : 0;
+            int plane = desc->comp[comp].plane;
+            uint8_t *dst = canvas->data[plane] +
+                           AV_CEIL_RSHIFT(pos_y, shift) * 
canvas->linesize[plane] +
+                           AV_CEIL_RSHIFT(pos_x, shift);
+            for (int y = 0; y < AV_CEIL_RSHIFT(height, shift); y++) {
+                memset(dst, s->background_yuva[plane], AV_CEIL_RSHIFT(width, 
shift));
+                dst += canvas->linesize[plane];
+            }
+        }
+    }
+}
+
+static int allocate_canvas(AnimatedWebPContext *s, int format)
+{
+    s->w.avctx->pix_fmt = format;
+    int ret = ff_set_dimensions(s->w.avctx, s->canvas_width, s->canvas_height);
+    if (ret < 0)
+        return ret;
+    return ff_reget_buffer(s->w.avctx, s->canvas, 0);
+}
+
+static int prepare_canvas(AnimatedWebPContext *s, int key_frame, int format)
+{
+    int ret;
+
+    /**
+     * Clear the canvas on keyframes and frames that overwrite the entire
+     * canvas.
+     */
+    if (key_frame ||
+        ((s->anmf_flags & ANMF_BLENDING_METHOD) == 
ANMF_BLENDING_METHOD_OVERWRITE &&
+         (s->pos_x == 0) && (s->pos_x + s->w.width == s->canvas_width) &&
+         (s->pos_y == 0) && (s->pos_y + s->w.height == s->canvas_height)))
+        av_frame_unref(s->canvas);
+
+    if (!s->canvas->buf[0]) {
+        /* Allocate new canvas frame */
+        ret = allocate_canvas(s, format);
+        if (ret < 0)
+            return ret;
+        /* ... and initialize it. */
+        fill_canvas_rect(s, 0, 0, s->canvas->width, s->canvas->height);
+    } else {
+        if (format == AV_PIX_FMT_ARGB && s->canvas->format == 
AV_PIX_FMT_YUVA420P) {
+            /**
+             * If we have a lossless frame following a lossy frame, we upgrade
+             * the canvas to ARGB, but we don't convert the canvas back to YUVA
+             * if there is a lossy frame following a lossless frame.
+             */
+            AVFrame *yuva_canvas = av_frame_clone(s->canvas);
+            if (!yuva_canvas)
+                return AVERROR(ENOMEM);
+            av_frame_unref(s->canvas);
+            ret = allocate_canvas(s, AV_PIX_FMT_ARGB);
+            if (ret < 0) {
+                av_frame_free(&yuva_canvas);
+                return ret;
+            }
+            copy_yuva2argb(s->canvas, yuva_canvas, 0, 0);
+            av_frame_free(&yuva_canvas);
+        } else {
+            /**
+             * The decode frame function returns a reference to the canvas,
+             * therefore we have to ensure it is writable before using it
+             * for a new frame.
+             */
+            ret = av_frame_make_writable(s->canvas);
+            if (ret < 0)
+                return ret;
+        }
+        /* Dispose of previous frame if needed. */
+        if ((s->prev_anmf_flags & ANMF_DISPOSAL_METHOD) == 
ANMF_DISPOSAL_METHOD_BACKGROUND)
+            fill_canvas_rect(s, s->prev_pos_x, s->prev_pos_y, s->prev_width, 
s->prev_height);
+    }
+
+    return 0;
+}
+
+static int webp_anim_decode_frame(AVCodecContext *avctx, AVFrame *p,
+                                  int *got_frame, AVPacket *avpkt)
+{
+    AnimatedWebPContext *s = avctx->priv_data;
+    int key_frame = (avpkt->flags & AV_PKT_FLAG_KEY);
+    int ret;
+
+    GetByteContext gb;
+    bytestream2_init(&gb, avpkt->data, avpkt->size);
+
+    /* Parse ANMF header. */
+    s->pos_x      = bytestream2_get_le24(&gb) * 2;
+    s->pos_y      = bytestream2_get_le24(&gb) * 2;
+    s->w.width    = bytestream2_get_le24(&gb) + 1;
+    s->w.height   = bytestream2_get_le24(&gb) + 1;
+    s->duration   = bytestream2_get_le24(&gb);
+    s->anmf_flags = bytestream2_get_byte(&gb);
+
+    av_log(avctx, AV_LOG_DEBUG,
+           "ANMF frame pos: %dx%d size: %dx%d duration: %d\n",
+           s->pos_x, s->pos_y, s->w.width, s->w.height, s->duration);
+
+    if (s->pos_x + s->w.width  > s->canvas_width ||
+        s->pos_y + s->w.height > s->canvas_height) {
+        av_log(avctx, AV_LOG_ERROR,
+               "Frame (%dx%d at pos %dx%d) does not fit into canvas (%dx%d)\n",
+               s->w.width, s->w.height, s->pos_x, s->pos_y,
+               s->canvas_width, s->canvas_height);
+        ret = AVERROR_INVALIDDATA;
+        goto end;
+    }
+
+    /* Reset alpha field from previous frame. */
+    s->w.has_alpha = 0;
+
+    /* Parse ANMF subchunks. */
+    while (bytestream2_get_bytes_left(&gb) > 8) {
+        uint32_t chunk_type = bytestream2_get_le32(&gb);
+        uint32_t chunk_size = bytestream2_get_le32(&gb);
+
+        if (chunk_size == UINT32_MAX) {
+            ret = AVERROR_INVALIDDATA;
+            goto end;
+        }
+        chunk_size += chunk_size & 1;
+
+        if (bytestream2_get_bytes_left(&gb) < chunk_size) {
+            /* we seem to be running out of data, but it could also be that the
+             * bitstream has trailing junk leading to bogus chunk_size. */
+            break;
+        }
+
+        switch (chunk_type) {
+        case MKTAG('A', 'L', 'P', 'H'): {
+            if (chunk_size == 0) {
+                av_log(avctx, AV_LOG_ERROR, "invalid ALPHA chunk size\n");
+                ret = AVERROR_INVALIDDATA;
+                goto end;
+            }
+            int alpha_header     = bytestream2_get_byte(&gb);
+            s->w.alpha_data      = avpkt->data + bytestream2_tell(&gb);
+            s->w.alpha_data_size = chunk_size - 1;
+            bytestream2_skip(&gb, s->w.alpha_data_size);
+
+            int filter_m    = (alpha_header >> 2) & 0x03;
+            int compression =  alpha_header       & 0x03;
+
+            if (compression > ALPHA_COMPRESSION_VP8L) {
+                av_log(avctx, AV_LOG_VERBOSE,
+                       "skipping unsupported ALPHA chunk\n");
+            } else {
+                s->w.has_alpha         = 1;
+                s->w.alpha_compression = compression;
+                s->w.alpha_filter      = filter_m;
+            }
+
+            break;
+        }
+        case MKTAG('V', 'P', '8', ' '):
+            if (*got_frame) {
+                av_log(avctx, AV_LOG_VERBOSE, "Ignoring extra VP8 chunk\n");
+                bytestream2_skip(&gb, chunk_size);
+                break;
+            }
+            ret = prepare_canvas(s, key_frame, AV_PIX_FMT_YUVA420P);
+            if (ret < 0)
+                goto end;
+            ret = vp8_lossy_decode_frame(avctx, s->subframe, got_frame,
+                                         avpkt->data + bytestream2_tell(&gb),
+                                         chunk_size);
+            if (ret < 0)
+                goto end;
+            bytestream2_skip(&gb, chunk_size);
+            break;
+        case MKTAG('V', 'P', '8', 'L'):
+            if (*got_frame) {
+                av_log(avctx, AV_LOG_VERBOSE, "Ignoring extra VP8L chunk\n");
+                bytestream2_skip(&gb, chunk_size);
+                break;
+            }
+            ret = prepare_canvas(s, key_frame, AV_PIX_FMT_ARGB);
+            if (ret < 0)
+                goto end;
+            ret = vp8_lossless_decode_frame(avctx, s->subframe, got_frame,
+                                            avpkt->data + 
bytestream2_tell(&gb),
+                                            chunk_size, 0);
+            if (ret < 0)
+                goto end;
+#if FF_API_CODEC_PROPS
+FF_DISABLE_DEPRECATION_WARNINGS
+            avctx->properties |= FF_CODEC_PROPERTY_LOSSLESS;
+FF_ENABLE_DEPRECATION_WARNINGS
+#endif
+            bytestream2_skip(&gb, chunk_size);
+            break;
+        default:
+            av_log(avctx, AV_LOG_VERBOSE, "skipping unknown chunk: %s\n",
+                   av_fourcc2str(chunk_type));
+            bytestream2_skip(&gb, chunk_size);
+            break;
+        }
+    }
+
+    if (!*got_frame) {
+        av_log(avctx, AV_LOG_ERROR, "image data not found\n");
+        ret = AVERROR_INVALIDDATA;
+        goto end;
+    }
+
+    ret = blend_subframe_into_canvas(s);
+    if (ret < 0)
+        goto end;
+
+    ret = av_frame_ref(p, s->canvas);
+    if (ret < 0)
+        goto end;
+
+    p->pict_type = key_frame ? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_P;
+    p->pts       = avpkt->pts;
+    p->duration  = s->duration;
+
+    s->prev_anmf_flags = s->anmf_flags;
+    s->prev_width      = s->w.width;
+    s->prev_height     = s->w.height;
+    s->prev_pos_x      = s->pos_x;
+    s->prev_pos_y      = s->pos_y;
+
+    ret = avpkt->size;
+
+end:
+    av_frame_unref(s->subframe);
+    return ret;
+}
+
+static av_cold int webp_anim_decode_init(AVCodecContext *avctx)
+{
+    AnimatedWebPContext *s = avctx->priv_data;
+
+    s->w.avctx = avctx;
+    s->canvas_width  = avctx->width;
+    s->canvas_height = avctx->height;
+
+    s->canvas = av_frame_alloc();
+    if (!s->canvas)
+        return AVERROR(ENOMEM);
+
+    s->subframe = av_frame_alloc();
+    if (!s->subframe)
+        return AVERROR(ENOMEM);
+
+    /**
+     * Use background color if it was provided by the demuxer. Otherwise, the
+     * background color will be 0x00000000 (transparent black).
+     */
+    if (avctx->extradata_size >= 4) {
+        s->background_argb[0] = avctx->extradata[3];
+        s->background_argb[1] = avctx->extradata[2];
+        s->background_argb[2] = avctx->extradata[1];
+        s->background_argb[3] = avctx->extradata[0];
+    }
+
+    /* Convert background color to YUVA. */
+    const uint8_t *argb = s->background_argb;
+    s->background_yuva[0] = RGB_TO_Y_CCIR(argb[1], argb[2], argb[3]);
+    s->background_yuva[1] = RGB_TO_U_CCIR(argb[1], argb[2], argb[3], 0);
+    s->background_yuva[2] = RGB_TO_V_CCIR(argb[1], argb[2], argb[3], 0);
+    s->background_yuva[3] = argb[0];
+
+    return webp_decode_init(avctx);
+}
+
+static av_cold int webp_anim_decode_close(AVCodecContext *avctx)
+{
+    AnimatedWebPContext *s = avctx->priv_data;
+
+    av_frame_free(&s->canvas);
+    av_frame_free(&s->subframe);
+
+    return webp_decode_close(avctx);
+}
+
+const FFCodec ff_webp_anim_decoder = {
+    .p.name         = "webp_anim",
+    CODEC_LONG_NAME("Animated WebP image"),
+    .p.type         = AVMEDIA_TYPE_VIDEO,
+    .p.id           = AV_CODEC_ID_WEBP_ANIM,
+    .priv_data_size = sizeof(AnimatedWebPContext),
+    .init           = webp_anim_decode_init,
+    FF_CODEC_DECODE_CB(webp_anim_decode_frame),
+    .close          = webp_anim_decode_close,
+    .p.capabilities = AV_CODEC_CAP_DR1 | AV_CODEC_CAP_SLICE_THREADS,
+    .caps_internal  = FF_CODEC_CAP_USES_PROGRESSFRAMES,
+};
+#endif /* CONFIG_WEBP_ANIM_DECODER */
-- 
2.52.0


>From 952cdc1ad46ffca81d4b514c3ee9497e8cae5a62 Mon Sep 17 00:00:00 2001
From: Ramiro Polla <[email protected]>
Date: Wed, 22 Apr 2026 18:09:53 +0200
Subject: [PATCH 4/5] 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       |   4 +-
 libavformat/webp_anim_dec.c | 371 ++++++++++++++++++++++++++++++++++++
 6 files changed, 411 insertions(+), 2 deletions(-)
 create mode 100644 libavformat/webp_anim_dec.c

diff --git a/Changelog b/Changelog
index 4af1667d54..bdbdcdc58b 100644
--- a/Changelog
+++ b/Changelog
@@ -10,6 +10,7 @@ version <next>:
 - transpose_cuda filter
 - Add AMF Frame Rate Converter (vf_frc_amf) filter
 - 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 7c2fcad93e..8f2ed1c749 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -644,6 +644,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 ff831498b3..14bf700274 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,8 +31,8 @@
 
 #include "version_major.h"
 
-#define LIBAVFORMAT_VERSION_MINOR  13
-#define LIBAVFORMAT_VERSION_MICRO 102
+#define LIBAVFORMAT_VERSION_MINOR  14
+#define LIBAVFORMAT_VERSION_MICRO 100
 
 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
                                                LIBAVFORMAT_VERSION_MINOR, \
diff --git a/libavformat/webp_anim_dec.c b/libavformat/webp_anim_dec.c
new file mode 100644
index 0000000000..f54544ba5a
--- /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,
+};
-- 
2.52.0


>From 69a0297df2127b5529f46c2e4b6fc1dc676dd4eb Mon Sep 17 00:00:00 2001
From: Thilo Borgmann via ffmpeg-devel <ffmpeg-devel at ffmpeg.org>
Date: Fri, 21 Jun 2024 12:43:22 +0200
Subject: [PATCH 5/5] fate: add test for animated WebP

Signed-off-by: Ramiro Polla <[email protected]>
---
 tests/fate/image.mak       |  13 +++++
 tests/ref/fate/webp-anim   |  22 ++++++++
 tests/ref/fate/webp-chfmt1 |  23 ++++++++
 tests/ref/fate/webp-chfmt2 | 106 +++++++++++++++++++++++++++++++++++++
 4 files changed, 164 insertions(+)
 create mode 100644 tests/ref/fate/webp-anim
 create mode 100644 tests/ref/fate/webp-chfmt1
 create mode 100644 tests/ref/fate/webp-chfmt2

diff --git a/tests/fate/image.mak b/tests/fate/image.mak
index 334c421140..d4b5ec5437 100644
--- a/tests/fate/image.mak
+++ b/tests/fate/image.mak
@@ -589,6 +589,19 @@ FATE_WEBP-$(call DEMDEC, IMAGE2, WEBP) += $(FATE_WEBP)
 FATE_IMAGE_FRAMECRC += $(FATE_WEBP-yes)
 fate-webp: $(FATE_WEBP-yes)
 
+FATE_WEBP_ANIM += fate-webp-anim
+fate-webp-anim: CMD = framecrc -i $(TARGET_SAMPLES)/webp/anim.webp
+
+FATE_WEBP_ANIM += fate-webp-chfmt1
+fate-webp-chfmt1: CMD = framecrc -i $(TARGET_SAMPLES)/webp/anim_rgb_yuv.webp
+
+FATE_WEBP_ANIM += fate-webp-chfmt2
+fate-webp-chfmt2: CMD = framecrc -i $(TARGET_SAMPLES)/webp/anim_yuv_rgb.webp
+
+FATE_WEBP_ANIM-$(call DEMDEC, WEBP_ANIM, WEBP_ANIM) += $(FATE_WEBP_ANIM)
+FATE_IMAGE_FRAMECRC += $(FATE_WEBP_ANIM-yes)
+fate-webp: $(FATE_WEBP_ANIM-yes)
+
 FATE_IMAGE_FRAMECRC-$(call DEMDEC, IMAGE2, XFACE) += fate-xface
 fate-xface: CMD = framecrc -i $(TARGET_SAMPLES)/xface/lena.xface
 
diff --git a/tests/ref/fate/webp-anim b/tests/ref/fate/webp-anim
new file mode 100644
index 0000000000..1c9ec18b18
--- /dev/null
+++ b/tests/ref/fate/webp-anim
@@ -0,0 +1,22 @@
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 100x70
+#sar 0: 0/1
+0,          0,          0,        2,    28000, 0x2023ba6e
+0,          2,          2,        2,    28000, 0x4292b778
+0,          4,          4,        2,    28000, 0x1c972ef1
+0,          6,          6,        2,    28000, 0xa98d8d04
+0,          8,          8,        2,    28000, 0xd323b6af
+0,         10,         10,        2,    28000, 0x508aba99
+0,         12,         12,        2,    28000, 0x5c672dda
+0,         14,         14,        2,    28000, 0xc8961ebb
+0,         16,         16,       25,    28000, 0x82460e1b
+0,         41,         41,        2,    28000, 0x3debbfc9
+0,         43,         43,        2,    28000, 0x427ab31f
+0,         45,         45,        2,    28000, 0x6bbdec2e
+0,         47,         47,        2,    28000, 0x5690b56b
+0,         49,         49,        2,    28000, 0xb62963f3
+0,         51,         51,        2,    28000, 0x68dd37b2
+0,         53,         53,        2,    28000, 0x465c47d2
+0,         55,         55,      250,    28000, 0xa92033df
diff --git a/tests/ref/fate/webp-chfmt1 b/tests/ref/fate/webp-chfmt1
new file mode 100644
index 0000000000..bd0de28718
--- /dev/null
+++ b/tests/ref/fate/webp-chfmt1
@@ -0,0 +1,23 @@
+#tb 0: 2/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 488x488
+#sar 0: 0/1
+0,          0,          0,        1,   952576, 0x5f74d3ae
+0,          1,          1,        1,   952576, 0x8b0f6cfe
+0,          2,          2,        1,   952576, 0x3e47771e
+0,          3,          3,        1,   952576, 0x63816e85
+0,          4,          4,        1,   952576, 0xf7a5d79a
+0,          5,          5,        6,   952576, 0x50c75e53
+0,         11,         11,        1,   952576, 0x54d4cdd3
+0,         12,         12,        1,   952576, 0x00648e70
+0,         13,         13,        2,   952576, 0xd9f7f652
+0,         15,         15,        2,   952576, 0x34c15d97
+0,         17,         17,        2,   952576, 0x94b9ab85
+0,         19,         19,        7,   952576, 0xa702612f
+0,         26,         26,        1,   952576, 0xd248d21e
+0,         27,         27,        1,   952576, 0xc0f7ffcf
+0,         28,         28,        9,   952576, 0x4e281a20
+0,         37,         37,        1,   952576, 0x114617ec
+0,         38,         38,        1,   952576, 0x79ba99a5
+0,         39,         39,        9,   952576, 0x79cb003d
diff --git a/tests/ref/fate/webp-chfmt2 b/tests/ref/fate/webp-chfmt2
new file mode 100644
index 0000000000..cb4d48cb5a
--- /dev/null
+++ b/tests/ref/fate/webp-chfmt2
@@ -0,0 +1,106 @@
+#tb 0: 3/100
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 320x240
+#sar 0: 0/1
+0,          0,          0,        1,   192000, 0x41a50269
+0,          1,          1,        1,   192000, 0x963823c8
+0,          2,          2,        1,   192000, 0x43bf206f
+0,          3,          3,        1,   192000, 0x4ce51d59
+0,          4,          4,        1,   192000, 0x528d1a81
+0,          5,          5,        1,   192000, 0x1e11fb1d
+0,          6,          6,        1,   192000, 0x37672368
+0,          7,          7,        1,   192000, 0x7a6f20f9
+0,          8,          8,        1,   192000, 0xe6aa1dfc
+0,          9,          9,        1,   192000, 0x295b10e6
+0,         10,         10,        1,   192000, 0xd263ff87
+0,         11,         11,        1,   192000, 0x3cf105ee
+0,         12,         12,        1,   192000, 0x0e26f9c3
+0,         13,         13,        1,   192000, 0x62fa2869
+0,         14,         14,        1,   192000, 0xed44fcca
+0,         15,         15,        1,   192000, 0x0874fd8d
+0,         16,         16,        1,   192000, 0x87412a3c
+0,         17,         17,        1,   192000, 0x3d522eb5
+0,         18,         18,        1,   192000, 0xda660879
+0,         19,         19,        1,   192000, 0x78211c16
+0,         20,         20,        1,   192000, 0xe19e0f5d
+0,         21,         21,        1,   192000, 0xbf95f295
+0,         22,         22,        1,   192000, 0x2986e59e
+0,         23,         23,        1,   192000, 0x7087e1f0
+0,         24,         24,        1,   192000, 0xee15e30e
+0,         25,         25,        1,   192000, 0xfd10df47
+0,         26,         26,        1,   192000, 0xa5c6d5df
+0,         27,         27,        1,   192000, 0x3ee9d40b
+0,         28,         28,        1,   192000, 0xd3bed274
+0,         29,         29,        1,   192000, 0x3374cfab
+0,         30,         30,        1,   192000, 0x993bdae7
+0,         31,         31,        1,   192000, 0xed14d83a
+0,         32,         32,        1,   192000, 0x0703dada
+0,         33,         33,        1,   192000, 0xbfd5df7c
+0,         34,         34,        1,   192000, 0x4796f2ee
+0,         35,         35,        1,   192000, 0xce73fa45
+0,         36,         36,        1,   192000, 0x5c8928fc
+0,         37,         37,        1,   192000, 0x5e84fc03
+0,         38,         38,        1,   192000, 0xe14c3368
+0,         39,         39,        1,   192000, 0x4436fc7a
+0,         40,         40,        1,   192000, 0x53510a70
+0,         41,         41,        1,   192000, 0xf228071c
+0,         42,         42,        1,   192000, 0xb9c00645
+0,         43,         43,        1,   192000, 0xe82606f9
+0,         44,         44,        1,   192000, 0x40a7f673
+0,         45,         45,        1,   192000, 0xb1c11d82
+0,         46,         46,        1,   192000, 0xc6991a22
+0,         47,         47,        1,   192000, 0x653a1837
+0,         48,         48,        1,   192000, 0x240516f4
+0,         49,         49,        1,   192000, 0x12a8f922
+0,         50,         50,        1,   192000, 0xe1921ebe
+0,         51,         51,        1,   192000, 0x80a01c6d
+0,         52,         52,        1,   192000, 0xbdc21afb
+0,         53,         53,        1,   192000, 0x25be1c54
+0,         54,         54,        1,   192000, 0x582bff4e
+0,         55,         55,        1,   192000, 0x54772168
+0,         56,         56,        1,   192000, 0x6ad31e78
+0,         57,         57,        1,   192000, 0xcf001d7e
+0,         58,         58,        1,   192000, 0xea6f1d81
+0,         59,         59,        1,   192000, 0xc769ff48
+0,         60,         60,        1,   192000, 0xea192087
+0,         61,         61,        1,   192000, 0x30dd1e4e
+0,         62,         62,        1,   192000, 0x941f1f9b
+0,         63,         63,        1,   192000, 0xfc0f1d1b
+0,         64,         64,        1,   192000, 0xbd961a3e
+0,         65,         65,        1,   192000, 0x4eb3fd83
+0,         66,         66,        1,   192000, 0xcdd11c59
+0,         67,         67,        1,   192000, 0xbfec1a23
+0,         68,         68,        1,   192000, 0x10cf19b0
+0,         69,         69,        1,   192000, 0xb7d61910
+0,         70,         70,        1,   192000, 0xc95d04f3
+0,         71,         71,        1,   192000, 0xdc6725a6
+0,         72,         72,        1,   192000, 0xb3c32418
+0,         73,         73,        1,   192000, 0x2c87240d
+0,         74,         74,        1,   192000, 0x502123b5
+0,         75,         75,        1,   192000, 0x2549049c
+0,         76,         76,        1,   192000, 0x900627cd
+0,         77,         77,        1,   192000, 0x16f6256f
+0,         78,         78,        1,   192000, 0x112c245c
+0,         79,         79,        1,   192000, 0xb9d308b0
+0,         80,         80,        1,   192000, 0x86c92bb8
+0,         81,         81,        1,   192000, 0x59832991
+0,         82,         82,        1,   192000, 0x224528e1
+0,         83,         83,        1,   192000, 0x23c9282c
+0,         84,         84,        1,   192000, 0x9c7a0a6a
+0,         85,         85,        1,   192000, 0x1cef27c4
+0,         86,         86,        1,   192000, 0x822124b4
+0,         87,         87,        1,   192000, 0xd1a22470
+0,         88,         88,        1,   192000, 0x695f22f6
+0,         89,         89,        1,   192000, 0x84191ff2
+0,         90,         90,        1,   192000, 0x022701d8
+0,         91,         91,        1,   192000, 0xa8622719
+0,         92,         92,        1,   192000, 0xdb412440
+0,         93,         93,        1,   192000, 0xb9a222ee
+0,         94,         94,        1,   192000, 0x904f0405
+0,         95,         95,        1,   192000, 0x8d142732
+0,         96,         96,        1,   192000, 0xb0f92390
+0,         97,         97,        1,   192000, 0x75ab21bd
+0,         98,         98,        1,   192000, 0x6e9d2047
+0,         99,         99,        1,   192000, 0x586a1e6b
+0,        100,        100,        1,   192000, 0x7c6bfe5e
-- 
2.52.0

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

Reply via email to