diff mbox series

[FFmpeg-devel,v5,2/2] libavformat/webp: add WebP demuxer

Message ID 20200911063613.4475-2-josef@pex.com
State New
Headers show
Series animated WebP support
Related show

Checks

Context Check Description
andriy/default pending
andriy/make success Make finished
andriy/make_fate success Make fate finished

Commit Message

Zlomek, Josef Sept. 11, 2020, 6:36 a.m. UTC
Adds the demuxer of animated WebP files.
It supports non-animated, animated, truncated, and concatenated files.
Reading from a pipe (and other non-seekable inputs) is also supported.

The WebP demuxer splits the input stream into packets containing one frame.
It also marks the key frames properly.
The loop count is ignored by default (same behaviour as animated PNG and GIF),
it may be enabled by the option '-ignore_loop 0'.

The frame rate is set according to the frame delay in the ANMF chunk.
If the delay is too low, or the image is not animated, the default frame rate
is set to 10 fps, similarly to other WebP libraries and browsers.
The fate suite was updated accordingly.

Signed-off-by: Josef Zlomek <josef@pex.com>
---
 Changelog                                   |   1 +
 doc/demuxers.texi                           |  28 +
 libavformat/Makefile                        |   1 +
 libavformat/allformats.c                    |   1 +
 libavformat/version.h                       |   2 +-
 libavformat/webpdec.c                       | 733 ++++++++++++++++++++
 tests/ref/fate/exif-image-webp              |   8 +-
 tests/ref/fate/webp-rgb-lena-lossless       |   2 +-
 tests/ref/fate/webp-rgb-lena-lossless-rgb24 |   2 +-
 tests/ref/fate/webp-rgb-lossless            |   2 +-
 tests/ref/fate/webp-rgb-lossy-q80           |   2 +-
 tests/ref/fate/webp-rgba-lossless           |   2 +-
 tests/ref/fate/webp-rgba-lossy-q80          |   2 +-
 13 files changed, 775 insertions(+), 11 deletions(-)
 create mode 100644 libavformat/webpdec.c

Comments

Lynne Sept. 12, 2020, 1 p.m. UTC | #1
On 11/09/2020 08:36, Josef Zlomek wrote:
> Adds the demuxer of animated WebP files.
> It supports non-animated, animated, truncated, and concatenated files.
> Reading from a pipe (and other non-seekable inputs) is also supported.
> 
> The WebP demuxer splits the input stream into packets containing one frame.
> It also marks the key frames properly.
> The loop count is ignored by default (same behaviour as animated PNG and GIF),
> it may be enabled by the option '-ignore_loop 0'.
> 
> The frame rate is set according to the frame delay in the ANMF chunk.
> If the delay is too low, or the image is not animated, the default frame rate
> is set to 10 fps, similarly to other WebP libraries and browsers.
> The fate suite was updated accordingly.
> 
> Signed-off-by: Josef Zlomek <josef@pex.com>
Can someone take a look at the demuxer? I'd rather not apply the decoder
without the demuxer, and lavf isn't my specialty.
Carl Eugen Hoyos Sept. 12, 2020, 1:41 p.m. UTC | #2
Am Fr., 11. Sept. 2020 um 08:36 Uhr schrieb Josef Zlomek <josef@pex.com>:

This is not the requested review, I am just curious about the
behaviour:

> +static int webp_probe(const AVProbeData *p)
> +{
> +    const uint8_t *b = p->buf;
> +
> +    if (AV_RB32(b)     == MKBETAG('R', 'I', 'F', 'F') &&
> +        AV_RB32(b + 8) == MKBETAG('W', 'E', 'B', 'P'))
> +        return AVPROBE_SCORE_MAX;

What happens if you pipe several (not animated) webp images
through your new demuxer?
Does it behave like the existing pipe demuxer?

> +static int ensure_seekback(AVFormatContext *s, int64_t bytes)
> +{
> +    WebPDemuxContext *wdc = s->priv_data;
> +    AVIOContext      *pb  = s->pb;
> +    int ret;
> +
> +    int64_t pos = avio_tell(pb);
> +    if (pos < 0)
> +        return pos;
> +
> +    if (pos + bytes <= wdc->seekback_buffer_end)
> +        return 0;
> +
> +    if ((ret = ffio_ensure_seekback(pb, bytes)) < 0)
> +        return ret;
> +
> +    wdc->seekback_buffer_end = pos + bytes;
> +    return 0;
> +}
> +
> +static int resync(AVFormatContext *s, int seek_to_start)
> +{
> +    WebPDemuxContext *wdc = s->priv_data;
> +    AVIOContext      *pb  = s->pb;
> +    int ret;
> +    int i;
> +    uint64_t state = 0;
> +
> +    // ensure seek back for the file header and the first chunk header
> +    if ((ret = ensure_seekback(s, 12 + 8)) < 0)
> +        return ret;
> +
> +    for (i = 0; i < 12; i++) {
> +        state = (state << 8) | avio_r8(pb);

> +        if (i == 11) {
> +            if ((uint32_t) state == MKBETAG('W', 'E', 'B', 'P'))

The cast looks really ugly: Why is it necessary?

Carl Eugen
Zlomek, Josef Sept. 13, 2020, 3:59 a.m. UTC | #3
Dne so 12. 9. 2020 22:35 uživatel Carl Eugen Hoyos <ceffmpeg@gmail.com>
napsal:

> Am Fr., 11. Sept. 2020 um 08:36 Uhr schrieb Josef Zlomek <josef@pex.com>:
>
> This is not the requested review, I am just curious about the
> behaviour:
>
> > +static int webp_probe(const AVProbeData *p)
> > +{
> > +    const uint8_t *b = p->buf;
> > +
> > +    if (AV_RB32(b)     == MKBETAG('R', 'I', 'F', 'F') &&
> > +        AV_RB32(b + 8) == MKBETAG('W', 'E', 'B', 'P'))
> > +        return AVPROBE_SCORE_MAX;
>
> What happens if you pipe several (not animated) webp images
> through your new demuxer?
> Does it behave like the existing pipe demuxer?
>

Piping several WebP images (may be non-animated or animated) is supported.
webp_probe checks if the first one is WebP so the WebP demuxer should be
used.

> +static int resync(AVFormatContext *s, int seek_to_start)
> > +{
> > +    WebPDemuxContext *wdc = s->priv_data;
> > +    AVIOContext      *pb  = s->pb;
> > +    int ret;
> > +    int i;
> > +    uint64_t state = 0;
> > +
> > +    // ensure seek back for the file header and the first chunk header
> > +    if ((ret = ensure_seekback(s, 12 + 8)) < 0)
> > +        return ret;
> > +
> > +    for (i = 0; i < 12; i++) {
> > +        state = (state << 8) | avio_r8(pb);
>
> > +        if (i == 11) {
> > +            if ((uint32_t) state == MKBETAG('W', 'E', 'B', 'P'))
>
> The cast looks really ugly: Why is it necessary?
>

The signature of the WebP file is the following 12 bytes: "RIFF" <uint32_t
size> "WEBP".
The state is uint64_t. At this point, the state should contain bytes 4-11
of the signature (size and "WEBP") so the code checks that the lower 32
bits are "WEBP".
Alternatively there may be "state & 0xffffffff". I used the typecast as I
have seen it somewhere else (original WebP parser).

Josef
Zlomek, Josef Sept. 24, 2020, 4:51 a.m. UTC | #4
On Sat, Sep 12, 2020 at 3:01 PM Lynne <dev@lynne.ee> wrote:

> On 11/09/2020 08:36, Josef Zlomek wrote:
> > Adds the demuxer of animated WebP files.
> > It supports non-animated, animated, truncated, and concatenated files.
> > Reading from a pipe (and other non-seekable inputs) is also supported.
> >
> > The WebP demuxer splits the input stream into packets containing one
> frame.
> > It also marks the key frames properly.
> > The loop count is ignored by default (same behaviour as animated PNG and
> GIF),
> > it may be enabled by the option '-ignore_loop 0'.
> >
> > The frame rate is set according to the frame delay in the ANMF chunk.
> > If the delay is too low, or the image is not animated, the default frame
> rate
> > is set to 10 fps, similarly to other WebP libraries and browsers.
> > The fate suite was updated accordingly.
> >
> > Signed-off-by: Josef Zlomek <josef@pex.com>
> Can someone take a look at the demuxer? I'd rather not apply the decoder
> without the demuxer, and lavf isn't my specialty.
>

Does someone have time to look at the WebP demuxer?

Josef
diff mbox series

Patch

diff --git a/Changelog b/Changelog
index 886f91c5c8..9c7a26a80b 100644
--- a/Changelog
+++ b/Changelog
@@ -23,6 +23,7 @@  version <next>:
 - PhotoCD decoder
 - MCA demuxer
 - animated WebP parser/decoder
+- animated WebP demuxer
 
 
 version 4.3:
diff --git a/doc/demuxers.texi b/doc/demuxers.texi
index 3c15ab9eee..f935c49b48 100644
--- a/doc/demuxers.texi
+++ b/doc/demuxers.texi
@@ -832,4 +832,32 @@  which in turn, acts as a ceiling for the size of scripts that can be read.
 Default is 1 MiB.
 @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.
+@end table
+
 @c man end DEMUXERS
diff --git a/libavformat/Makefile b/libavformat/Makefile
index a1a2a7e0e1..3cd9d21171 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -564,6 +564,7 @@  OBJS-$(CONFIG_WEBM_MUXER)                += matroskaenc.o matroska.o \
                                             wv.o vorbiscomment.o
 OBJS-$(CONFIG_WEBM_DASH_MANIFEST_MUXER)  += webmdashenc.o
 OBJS-$(CONFIG_WEBM_CHUNK_MUXER)          += webm_chunk.o
+OBJS-$(CONFIG_WEBP_DEMUXER)              += webpdec.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 97e9be4269..bcb8d46ae6 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -461,6 +461,7 @@  extern AVOutputFormat ff_webm_muxer;
 extern AVInputFormat  ff_webm_dash_manifest_demuxer;
 extern AVOutputFormat ff_webm_dash_manifest_muxer;
 extern AVOutputFormat ff_webm_chunk_muxer;
+extern AVInputFormat  ff_webp_demuxer;
 extern AVOutputFormat ff_webp_muxer;
 extern AVInputFormat  ff_webvtt_demuxer;
 extern AVOutputFormat ff_webvtt_muxer;
diff --git a/libavformat/version.h b/libavformat/version.h
index 7771a6abf2..ec3286e73a 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -32,7 +32,7 @@ 
 // Major bumping may affect Ticket5467, 5421, 5451(compatibility with Chromium)
 // Also please add any ticket numbers that you believe might be affected here
 #define LIBAVFORMAT_VERSION_MAJOR  58
-#define LIBAVFORMAT_VERSION_MINOR  54
+#define LIBAVFORMAT_VERSION_MINOR  55
 #define LIBAVFORMAT_VERSION_MICRO 100
 
 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
diff --git a/libavformat/webpdec.c b/libavformat/webpdec.c
new file mode 100644
index 0000000000..244042f2db
--- /dev/null
+++ b/libavformat/webpdec.c
@@ -0,0 +1,733 @@ 
+/*
+ * WebP demuxer
+ * Copyright (c) 2020 Pexeso Inc.
+ *
+ * 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
+ * WebP demuxer.
+ */
+
+#include "avformat.h"
+#include "avio_internal.h"
+#include "internal.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/opt.h"
+#include "libavcodec/webp.h"
+
+/**
+ * WebP headers (chunks before the first frame) and important info from them.
+ */
+typedef struct WebPHeaders {
+    int64_t offset;                 ///< offset in the (concatenated) file
+    uint8_t *data;                  ///< raw data
+    uint32_t size;                  ///< size of data
+    uint32_t webp_size;             ///< size of the WebP file
+    int canvas_width;               ///< width of the canvas
+    int canvas_height;              ///< height of the canvas
+    int num_loop;                   ///< number of times to loop the animation
+} WebPHeaders;
+
+typedef struct WebPDemuxContext {
+    const AVClass *class;
+    /**
+     * Time span in milliseconds before the next frame
+     * should be drawn on screen.
+     */
+    int delay;
+    /**
+     * 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 num_loop;                   ///< number of times to loop the animation
+    int cur_loop;                   ///< current loop counter
+    int64_t file_start;             ///< start position of the current animation file
+    int64_t infinite_loop_start;    ///< start position of the infinite loop
+
+    uint32_t remaining_size;        ///< remaining size of the current animation file
+    int64_t seekback_buffer_end;    ///< position of the end of the seek back buffer
+    int64_t prev_end_position;      ///< position after the previous packet
+    size_t num_webp_headers;        ///< number of (concatenated) WebP files' headers
+    WebPHeaders *webp_headers;      ///< (concatenated) WebP files' headers
+
+    /*
+     * variables for the key frame detection
+     */
+    int nb_frames;                  ///< number of frames of the current animation file
+    int canvas_width;               ///< width of the canvas
+    int canvas_height;              ///< height of the canvas
+    int prev_width;                 ///< width of the previous frame
+    int prev_height;                ///< height of the previous frame
+    int prev_anmf_flags;            ///< flags of the previous frame
+    int prev_key_frame;             ///< flag if the previous frame was a key frame
+} WebPDemuxContext;
+
+/**
+ * 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_probe(const AVProbeData *p)
+{
+    const uint8_t *b = p->buf;
+
+    if (AV_RB32(b)     == MKBETAG('R', 'I', 'F', 'F') &&
+        AV_RB32(b + 8) == MKBETAG('W', 'E', 'B', 'P'))
+        return AVPROBE_SCORE_MAX;
+
+    return 0;
+}
+
+static int ensure_seekback(AVFormatContext *s, int64_t bytes)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+    AVIOContext      *pb  = s->pb;
+    int ret;
+
+    int64_t pos = avio_tell(pb);
+    if (pos < 0)
+        return pos;
+
+    if (pos + bytes <= wdc->seekback_buffer_end)
+        return 0;
+
+    if ((ret = ffio_ensure_seekback(pb, bytes)) < 0)
+        return ret;
+
+    wdc->seekback_buffer_end = pos + bytes;
+    return 0;
+}
+
+static int resync(AVFormatContext *s, int seek_to_start)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+    AVIOContext      *pb  = s->pb;
+    int ret;
+    int i;
+    uint64_t state = 0;
+
+    // ensure seek back for the file header and the first chunk header
+    if ((ret = ensure_seekback(s, 12 + 8)) < 0)
+        return ret;
+
+    for (i = 0; i < 12; i++) {
+        state = (state << 8) | avio_r8(pb);
+        if (i == 11) {
+            if ((uint32_t) state == MKBETAG('W', 'E', 'B', 'P'))
+                break;
+            i -= 4;
+        }
+        if (i == 7) {
+            // ensure seek back for the rest of file header and the chunk header
+            if ((ret = ensure_seekback(s, 4 + 8)) < 0)
+                return ret;
+
+            if ((state >> 32) != MKBETAG('R', 'I', 'F', 'F'))
+                i--;
+            else {
+                uint32_t fsize = av_bswap32(state);
+                if (!(fsize > 15 && fsize <= UINT32_MAX - 10))
+                    i -= 4;
+                else
+                    wdc->remaining_size = fsize - 4;
+            }
+        }
+        if (avio_feof(pb))
+            return AVERROR_EOF;
+    }
+
+    wdc->file_start = avio_tell(pb) - 12;
+
+    if (seek_to_start) {
+        if ((ret = avio_seek(pb, -12, SEEK_CUR)) < 0)
+            return ret;
+        wdc->remaining_size += 12;
+    }
+
+    return 0;
+}
+
+static int is_key_frame(AVFormatContext *s, int has_alpha, int anmf_flags,
+                        int width, int height)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+
+    if (wdc->nb_frames == 1)
+        return 1;
+
+    if (width  == wdc->canvas_width &&
+        height == wdc->canvas_height &&
+        (!has_alpha || (anmf_flags & ANMF_BLENDING_METHOD) == ANMF_BLENDING_METHOD_OVERWRITE))
+        return 1;
+
+    if ((wdc->prev_anmf_flags & ANMF_DISPOSAL_METHOD) == ANMF_DISPOSAL_METHOD_BACKGROUND &&
+        (wdc->prev_key_frame || (wdc->prev_width  == wdc->canvas_width &&
+                                 wdc->prev_height == wdc->canvas_height)))
+        return 1;
+
+    return 0;
+}
+
+static int webp_read_header(AVFormatContext *s)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+    AVIOContext      *pb  = s->pb;
+    AVStream         *st;
+    int ret, n;
+    uint32_t chunk_type, chunk_size;
+    int canvas_width  = 0;
+    int canvas_height = 0;
+    int width         = 0;
+    int height        = 0;
+    int is_frame      = 0;
+
+    wdc->delay = wdc->default_delay;
+    wdc->num_loop = 1;
+    wdc->infinite_loop_start = -1;
+
+    if ((ret = resync(s, 0)) < 0)
+        return ret;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st)
+        return AVERROR(ENOMEM);
+
+    while (!is_frame && wdc->remaining_size > 0 && !avio_feof(pb)) {
+        chunk_type = avio_rl32(pb);
+        chunk_size = avio_rl32(pb);
+        if (chunk_size == UINT32_MAX)
+            return AVERROR_INVALIDDATA;
+        chunk_size += chunk_size & 1;
+        if (avio_feof(pb))
+            break;
+
+        if (wdc->remaining_size < 8 + chunk_size)
+            return AVERROR_INVALIDDATA;
+        wdc->remaining_size -= 8 + chunk_size;
+
+        // ensure seek back for the chunk body and the next chunk header
+        if ((ret = ensure_seekback(s, chunk_size + 8)) < 0)
+            return ret;
+
+        switch (chunk_type) {
+        case MKTAG('V', 'P', '8', 'X'):
+            if (chunk_size >= 10) {
+                avio_skip(pb, 4);
+                canvas_width  = avio_rl24(pb) + 1;
+                canvas_height = avio_rl24(pb) + 1;
+                ret = avio_skip(pb, chunk_size - 10);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('V', 'P', '8', ' '):
+            if (chunk_size >= 10) {
+                avio_skip(pb, 6);
+                width  = avio_rl16(pb) & 0x3fff;
+                height = avio_rl16(pb) & 0x3fff;
+                is_frame = 1;
+                ret = avio_skip(pb, chunk_size - 10);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('V', 'P', '8', 'L'):
+            if (chunk_size >= 5) {
+                avio_skip(pb, 1);
+                n = avio_rl32(pb);
+                width  = (n & 0x3fff) + 1;          // first 14 bits
+                height = ((n >> 14) & 0x3fff) + 1;  // next 14 bits
+                is_frame = 1;
+                ret = avio_skip(pb, chunk_size - 5);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('A', 'N', 'M', 'F'):
+            if (chunk_size >= 12) {
+                avio_skip(pb, 6);
+                width  = avio_rl24(pb) + 1;
+                height = avio_rl24(pb) + 1;
+                is_frame = 1;
+                ret = avio_skip(pb, chunk_size - 12);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        default:
+            ret = avio_skip(pb, chunk_size);
+            break;
+        }
+
+        if (ret < 0)
+            return ret;
+
+        // fallback if VP8X chunk was not present
+        if (!canvas_width && width > 0)
+            canvas_width = width;
+        if (!canvas_height && height > 0)
+            canvas_height = height;
+    }
+
+    // WebP format operates with time in "milliseconds", therefore timebase is 1/100
+    avpriv_set_pts_info(st, 64, 1, 1000);
+    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    st->codecpar->codec_id   = AV_CODEC_ID_WEBP;
+    st->codecpar->codec_tag  = MKTAG('W', 'E', 'B', 'P');
+    st->codecpar->width      = canvas_width;
+    st->codecpar->height     = canvas_height;
+    st->start_time           = 0;
+
+    // jump to start because WebP decoder needs header data too
+    if ((ret = avio_seek(pb, wdc->file_start, SEEK_SET)) < 0)
+        return ret;
+    wdc->remaining_size = 0;
+
+    return 0;
+}
+
+static WebPHeaders *webp_headers_lower_or_equal(WebPHeaders *headers, size_t n,
+                                                int64_t offset)
+{
+    size_t s, e;
+
+    if (n == 0)
+        return NULL;
+    if (headers[0].offset > offset)
+        return NULL;
+
+    s = 0;
+    e = n - 1;
+    while (s < e) {
+        size_t mid = (s + e + 1) / 2;
+        if (headers[mid].offset == offset)
+            return &headers[mid];
+        else if (headers[mid].offset > offset)
+            e = mid - 1;
+        else
+            s = mid;
+    }
+
+    return &headers[s];
+}
+
+static int append_chunk(WebPHeaders *headers, AVIOContext *pb,
+                        uint32_t chunk_size)
+{
+    uint32_t previous_size = headers->size;
+    uint8_t *new_data;
+
+    if (headers->size > UINT32_MAX - chunk_size)
+        return AVERROR_INVALIDDATA;
+
+    new_data = av_realloc(headers->data, headers->size + chunk_size);
+    if (!new_data)
+        return AVERROR(ENOMEM);
+
+    headers->data = new_data;
+    headers->size += chunk_size;
+
+    return avio_read(pb, headers->data + previous_size, chunk_size);
+}
+
+static int webp_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+    AVIOContext      *pb  = s->pb;
+    int ret, n;
+    int64_t packet_start = avio_tell(pb), packet_end;
+    uint32_t chunk_type, chunk_size;
+    int width = 0, height = 0;
+    int is_frame = 0;
+    int key_frame = 0;
+    int anmf_flags = 0;
+    int has_alpha = 0;
+    int reading_headers = 0;
+    int reset_key_frame = 0;
+    WebPHeaders *headers = NULL;
+
+    if (packet_start != wdc->prev_end_position) {
+        // seek occurred, find the corresponding WebP headers
+        headers = webp_headers_lower_or_equal(wdc->webp_headers, wdc->num_webp_headers,
+                                              packet_start);
+        if (!headers)
+            return AVERROR_BUG;
+
+        wdc->file_start     = headers->offset;
+        wdc->remaining_size = headers->webp_size - (packet_start - headers->offset);
+        wdc->canvas_width   = headers->canvas_width;
+        wdc->canvas_height  = headers->canvas_height;
+        wdc->num_loop       = headers->num_loop;
+        wdc->cur_loop       = 0;
+        reset_key_frame     = 1;
+    }
+
+    if (wdc->remaining_size == 0) {
+        // if the loop count is finite, loop the current animation
+        if (avio_tell(pb) != wdc->file_start &&
+            !wdc->ignore_loop && wdc->num_loop > 1 && ++wdc->cur_loop < wdc->num_loop) {
+            if ((ret = avio_seek(pb, wdc->file_start, SEEK_SET)) < 0)
+                return ret;
+            packet_start = avio_tell(pb);
+        } else {
+            // start of a new animation file
+            wdc->delay = wdc->default_delay;
+            if (wdc->num_loop)
+                wdc->num_loop = 1;
+        }
+
+        // resync to the start of the next file
+        ret = resync(s, 1);
+        if (ret == AVERROR_EOF) {
+            // we reached EOF, if the loop count is infinite, loop the whole input
+            if (!wdc->ignore_loop && !wdc->num_loop) {
+                if ((ret = avio_seek(pb, wdc->infinite_loop_start, SEEK_SET)) < 0)
+                    return ret;
+                ret = resync(s, 1);
+            } else {
+                wdc->prev_end_position = avio_tell(pb);
+                return AVERROR_EOF;
+            }
+        }
+        if (ret < 0)
+            return ret;
+        packet_start = avio_tell(pb);
+
+        reset_key_frame = 1;
+    }
+
+    if (reset_key_frame) {
+        // reset variables used for key frame detection
+        wdc->nb_frames       = 0;
+        wdc->canvas_width    = 0;
+        wdc->canvas_height   = 0;
+        wdc->prev_width      = 0;
+        wdc->prev_height     = 0;
+        wdc->prev_anmf_flags = 0;
+        wdc->prev_key_frame  = 0;
+    }
+
+    if (packet_start == wdc->file_start) {
+        headers = webp_headers_lower_or_equal(wdc->webp_headers, wdc->num_webp_headers,
+                                              packet_start);
+        if (!headers || headers->offset != wdc->file_start) {
+            // grow the array of WebP files' headers
+            wdc->num_webp_headers++;
+            wdc->webp_headers = av_realloc_f(wdc->webp_headers,
+                                             wdc->num_webp_headers,
+                                             sizeof(WebPHeaders));
+            if (!wdc->webp_headers)
+                return AVERROR(ENOMEM);
+
+            headers = &wdc->webp_headers[wdc->num_webp_headers - 1];
+            memset(headers, 0, sizeof(*headers));
+            headers->offset = wdc->file_start;
+        } else {
+            // headers for this WebP file have been already read, skip them
+            if ((ret = avio_seek(pb, headers->size, SEEK_CUR)) < 0)
+                return ret;
+            packet_start = avio_tell(pb);
+
+            wdc->remaining_size = headers->webp_size - headers->size;
+            wdc->canvas_width   = headers->canvas_width;
+            wdc->canvas_height  = headers->canvas_height;
+
+            if (wdc->cur_loop >= wdc->num_loop)
+                wdc->cur_loop = 0;
+            wdc->num_loop = headers->num_loop;
+        }
+    }
+
+    while (wdc->remaining_size > 0 && !avio_feof(pb)) {
+        chunk_type = avio_rl32(pb);
+        chunk_size = avio_rl32(pb);
+        if (chunk_size == UINT32_MAX)
+            return AVERROR_INVALIDDATA;
+        chunk_size += chunk_size & 1;
+
+        if (avio_feof(pb))
+            break;
+
+        // dive into RIFF chunk and do not ensure seek back for the whole file
+        if (chunk_type == MKTAG('R', 'I', 'F', 'F') && chunk_size > 4)
+            chunk_size = 4;
+
+        // ensure seek back for the chunk body and the next chunk header
+        if ((ret = ensure_seekback(s, chunk_size + 8)) < 0)
+            return ret;
+
+        switch (chunk_type) {
+        case MKTAG('R', 'I', 'F', 'F'):
+            if (avio_tell(pb) != wdc->file_start + 8) {
+                // premature RIFF found, shorten the file size
+                WebPHeaders *tmp = webp_headers_lower_or_equal(wdc->webp_headers,
+                                                               wdc->num_webp_headers,
+                                                               avio_tell(pb));
+                tmp->webp_size -= wdc->remaining_size;
+                wdc->remaining_size = 0;
+                goto flush;
+            }
+
+            reading_headers = 1;
+            if ((ret = avio_seek(pb, -8, SEEK_CUR)) < 0 ||
+                (ret = append_chunk(headers, pb, 8 + chunk_size)) < 0)
+                return ret;
+            packet_start = avio_tell(pb);
+
+            headers->offset = wdc->file_start;
+            headers->webp_size = 8 + AV_RL32(headers->data + headers->size - chunk_size - 4);
+            break;
+        case MKTAG('V', 'P', '8', 'X'):
+            reading_headers = 1;
+            if ((ret = avio_seek(pb, -8, SEEK_CUR)) < 0 ||
+                (ret = append_chunk(headers, pb, 8 + chunk_size)) < 0)
+                return ret;
+            packet_start = avio_tell(pb);
+
+            if (chunk_size >= 10) {
+                headers->canvas_width  = AV_RL24(headers->data + headers->size - chunk_size + 4) + 1;
+                headers->canvas_height = AV_RL24(headers->data + headers->size - chunk_size + 7) + 1;
+            }
+            break;
+        case MKTAG('A', 'N', 'I', 'M'):
+            reading_headers = 1;
+            if ((ret = avio_seek(pb, -8, SEEK_CUR)) < 0 ||
+                (ret = append_chunk(headers, pb, 8 + chunk_size)) < 0)
+                return ret;
+            packet_start = avio_tell(pb);
+
+            if (chunk_size >= 6) {
+                headers->num_loop = AV_RL16(headers->data + headers->size - chunk_size + 4);
+                wdc->num_loop = headers->num_loop;
+                wdc->cur_loop = 0;
+                if (!wdc->ignore_loop && wdc->num_loop != 1) {
+                    // ensure seek back for the rest of the file
+                    // and for the header of the next concatenated file
+                    uint32_t loop_end = wdc->remaining_size - chunk_size + 12;
+                    if ((ret = ensure_seekback(s, loop_end)) < 0)
+                        return ret;
+
+                    if (!wdc->num_loop && wdc->infinite_loop_start < 0)
+                        wdc->infinite_loop_start = wdc->file_start;
+                }
+            }
+            break;
+        case MKTAG('V', 'P', '8', ' '):
+            if (is_frame)
+                // found a start of the next non-animated frame
+                goto flush;
+            is_frame = 1;
+
+            reading_headers = 0;
+            if (chunk_size >= 10) {
+                avio_skip(pb, 6);
+                width  = avio_rl16(pb) & 0x3fff;
+                height = avio_rl16(pb) & 0x3fff;
+                wdc->nb_frames++;
+                ret = avio_skip(pb, chunk_size - 10);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('V', 'P', '8', 'L'):
+            if (is_frame)
+                // found a start of the next non-animated frame
+                goto flush;
+            is_frame = 1;
+
+            reading_headers = 0;
+            if (chunk_size >= 5) {
+                avio_skip(pb, 1);
+                n = avio_rl32(pb);
+                width     = (n & 0x3fff) + 1;           // first 14 bits
+                height    = ((n >> 14) & 0x3fff) + 1;   ///next 14 bits
+                has_alpha = (n >> 28) & 1;              // next 1 bit
+                wdc->nb_frames++;
+                ret = avio_skip(pb, chunk_size - 5);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('A', 'N', 'M', 'F'):
+            if (is_frame)
+                // found a start of the next animated frame
+                goto flush;
+
+            reading_headers = 0;
+            if (chunk_size >= 16) {
+                avio_skip(pb, 6);
+                width      = avio_rl24(pb) + 1;
+                height     = avio_rl24(pb) + 1;
+                wdc->delay = avio_rl24(pb);
+                anmf_flags = avio_r8(pb);
+                if (wdc->delay < wdc->min_delay)
+                    wdc->delay = wdc->default_delay;
+                wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
+                // dive into the chunk to set the has_alpha flag
+                chunk_size = 16;
+                ret = 0;
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('A', 'L', 'P', 'H'):
+            reading_headers = 0;
+            has_alpha = 1;
+            ret = avio_skip(pb, chunk_size);
+            break;
+        default:
+            if (reading_headers) {
+                if ((ret = avio_seek(pb, -8, SEEK_CUR)) < 0 ||
+                    (ret = append_chunk(headers, pb, 8 + chunk_size)) < 0)
+                    return ret;
+                packet_start = avio_tell(pb);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        }
+        if (ret == AVERROR_EOF) {
+            // EOF was reached but the position may still be in the middle
+            // of the buffer. Seek to the end of the buffer so that EOF is
+            // handled properly in the next invocation of webp_read_packet.
+            if ((ret = avio_seek(pb, pb->buf_end - pb->buf_ptr, SEEK_CUR) < 0))
+                return ret;
+            wdc->prev_end_position = avio_tell(pb);
+            wdc->remaining_size = 0;
+            return AVERROR_EOF;
+        }
+        if (ret < 0)
+            return ret;
+
+        // fallback if VP8X chunk was not present
+        if (headers) {
+            if (!headers->canvas_width && width > 0)
+                headers->canvas_width = width;
+            if (!headers->canvas_height && height > 0)
+                headers->canvas_height = height;
+        }
+
+        if (wdc->remaining_size < 8 + chunk_size)
+            return AVERROR_INVALIDDATA;
+        wdc->remaining_size -= 8 + chunk_size;
+
+        packet_end = avio_tell(pb);
+    }
+
+    if (wdc->remaining_size > 0 && avio_feof(pb)) {
+        // premature EOF, shorten the file size
+        WebPHeaders *tmp = webp_headers_lower_or_equal(wdc->webp_headers,
+                                                       wdc->num_webp_headers,
+                                                       avio_tell(pb));
+        tmp->webp_size -= wdc->remaining_size;
+        wdc->remaining_size = 0;
+    }
+
+flush:
+    if ((ret = avio_seek(pb, packet_start, SEEK_SET)) < 0)
+        return ret;
+
+    if ((ret = av_get_packet(pb, pkt, packet_end - packet_start)) < 0)
+        return ret;
+
+    wdc->prev_end_position = packet_end;
+
+    if (headers && headers->data) {
+        uint8_t *data = av_packet_new_side_data(pkt, AV_PKT_DATA_NEW_EXTRADATA,
+                                                headers->size);
+        if (!data)
+            return AVERROR(ENOMEM);
+        memcpy(data, headers->data, headers->size);
+
+        s->streams[0]->internal->need_context_update = 1;
+        s->streams[0]->codecpar->width  = headers->canvas_width;
+        s->streams[0]->codecpar->height = headers->canvas_height;
+
+        // copy the fields needed for the key frame detection
+        wdc->canvas_width  = headers->canvas_width;
+        wdc->canvas_height = headers->canvas_height;
+    }
+
+    key_frame = is_frame && is_key_frame(s, has_alpha, anmf_flags, width, height);
+    if (key_frame)
+        pkt->flags |= AV_PKT_FLAG_KEY;
+    else
+        pkt->flags &= ~AV_PKT_FLAG_KEY;
+
+    wdc->prev_width      = width;
+    wdc->prev_height     = height;
+    wdc->prev_anmf_flags = anmf_flags;
+    wdc->prev_key_frame  = key_frame;
+
+    pkt->stream_index = 0;
+    pkt->duration = is_frame ? wdc->delay : 0;
+    pkt->pts = pkt->dts = AV_NOPTS_VALUE;
+
+    if (is_frame && wdc->nb_frames == 1)
+        s->streams[0]->r_frame_rate = (AVRational) {1000, pkt->duration};
+
+    return ret;
+}
+
+static int webp_read_close(AVFormatContext *s)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+
+    for (size_t i = 0; i < wdc->num_webp_headers; ++i)
+        av_freep(&wdc->webp_headers[i].data);
+    av_freep(&wdc->webp_headers);
+    wdc->num_webp_headers = 0;
+
+    return 0;
+}
+
+static const AVOption options[] = {
+    { "min_delay"     , "minimum valid delay between frames (in milliseconds)", offsetof(WebPDemuxContext, 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(WebPDemuxContext, max_delay)    , AV_OPT_TYPE_INT, {.i64 = 0xffffff}          , 0, 0xffffff , AV_OPT_FLAG_DECODING_PARAM },
+    { "default_delay" , "default delay between frames (in milliseconds)"      , offsetof(WebPDemuxContext, default_delay), AV_OPT_TYPE_INT, {.i64 = WEBP_DEFAULT_DELAY}, 0, 1000 * 60, AV_OPT_FLAG_DECODING_PARAM },
+    { "ignore_loop"   , "ignore loop setting"                                 , offsetof(WebPDemuxContext, ignore_loop)  , AV_OPT_TYPE_BOOL,{.i64 = 1}                 , 0, 1        , AV_OPT_FLAG_DECODING_PARAM },
+    { NULL },
+};
+
+static const AVClass demuxer_class = {
+    .class_name = "WebP demuxer",
+    .item_name  = av_default_item_name,
+    .option     = options,
+    .version    = LIBAVUTIL_VERSION_INT,
+    .category   = AV_CLASS_CATEGORY_DEMUXER,
+};
+
+AVInputFormat ff_webp_demuxer = {
+    .name           = "webp",
+    .long_name      = NULL_IF_CONFIG_SMALL("WebP image"),
+    .priv_data_size = sizeof(WebPDemuxContext),
+    .read_probe     = webp_probe,
+    .read_header    = webp_read_header,
+    .read_packet    = webp_read_packet,
+    .read_close     = webp_read_close,
+    .flags          = AVFMT_GENERIC_INDEX,
+    .priv_class     = &demuxer_class,
+};
diff --git a/tests/ref/fate/exif-image-webp b/tests/ref/fate/exif-image-webp
index d520ecd0db..4f97862ff4 100644
--- a/tests/ref/fate/exif-image-webp
+++ b/tests/ref/fate/exif-image-webp
@@ -8,10 +8,10 @@  pkt_dts=0
 pkt_dts_time=0.000000
 best_effort_timestamp=0
 best_effort_timestamp_time=0.000000
-pkt_duration=1
-pkt_duration_time=0.040000
-pkt_pos=0
-pkt_size=39276
+pkt_duration=100
+pkt_duration_time=0.100000
+pkt_pos=30
+pkt_size=39246
 width=400
 height=225
 pix_fmt=yuv420p
diff --git a/tests/ref/fate/webp-rgb-lena-lossless b/tests/ref/fate/webp-rgb-lena-lossless
index c00715a5e7..e784c501eb 100644
--- a/tests/ref/fate/webp-rgb-lena-lossless
+++ b/tests/ref/fate/webp-rgb-lena-lossless
@@ -1,4 +1,4 @@ 
-#tb 0: 1/25
+#tb 0: 1/10
 #media_type 0: video
 #codec_id 0: rawvideo
 #dimensions 0: 128x128
diff --git a/tests/ref/fate/webp-rgb-lena-lossless-rgb24 b/tests/ref/fate/webp-rgb-lena-lossless-rgb24
index 7f8f550afe..395a01fa1b 100644
--- a/tests/ref/fate/webp-rgb-lena-lossless-rgb24
+++ b/tests/ref/fate/webp-rgb-lena-lossless-rgb24
@@ -1,4 +1,4 @@ 
-#tb 0: 1/25
+#tb 0: 1/10
 #media_type 0: video
 #codec_id 0: rawvideo
 #dimensions 0: 128x128
diff --git a/tests/ref/fate/webp-rgb-lossless b/tests/ref/fate/webp-rgb-lossless
index 8dbdfd6887..1db3ce70f7 100644
--- a/tests/ref/fate/webp-rgb-lossless
+++ b/tests/ref/fate/webp-rgb-lossless
@@ -1,4 +1,4 @@ 
-#tb 0: 1/25
+#tb 0: 1/10
 #media_type 0: video
 #codec_id 0: rawvideo
 #dimensions 0: 12x8
diff --git a/tests/ref/fate/webp-rgb-lossy-q80 b/tests/ref/fate/webp-rgb-lossy-q80
index f61d75ac13..cd43415b95 100644
--- a/tests/ref/fate/webp-rgb-lossy-q80
+++ b/tests/ref/fate/webp-rgb-lossy-q80
@@ -1,4 +1,4 @@ 
-#tb 0: 1/25
+#tb 0: 1/10
 #media_type 0: video
 #codec_id 0: rawvideo
 #dimensions 0: 12x8
diff --git a/tests/ref/fate/webp-rgba-lossless b/tests/ref/fate/webp-rgba-lossless
index bb654ae442..2f763c6c46 100644
--- a/tests/ref/fate/webp-rgba-lossless
+++ b/tests/ref/fate/webp-rgba-lossless
@@ -1,4 +1,4 @@ 
-#tb 0: 1/25
+#tb 0: 1/10
 #media_type 0: video
 #codec_id 0: rawvideo
 #dimensions 0: 12x8
diff --git a/tests/ref/fate/webp-rgba-lossy-q80 b/tests/ref/fate/webp-rgba-lossy-q80
index d2c2aa3fce..6b114f772e 100644
--- a/tests/ref/fate/webp-rgba-lossy-q80
+++ b/tests/ref/fate/webp-rgba-lossy-q80
@@ -1,4 +1,4 @@ 
-#tb 0: 1/25
+#tb 0: 1/10
 #media_type 0: video
 #codec_id 0: rawvideo
 #dimensions 0: 12x8