diff mbox series

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

Message ID 20200708052824.18582-2-josef@pex.com
State Superseded
Headers show
Series [FFmpeg-devel,1/2] libavcodec: add support for animated WebP decoding | expand

Checks

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

Commit Message

Zlomek, Josef July 8, 2020, 5:28 a.m. UTC
Fixes: 4907

Adds support for demuxing of animated WebP.

The WebP demuxer splits the input stream into packets containing one frame.
It also sets the timing information properly.

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    | 322 +++++++++++++++++++++++++++++++++++++++
 6 files changed, 354 insertions(+), 1 deletion(-)
 create mode 100644 libavformat/webpdec.c

Comments

Steven Liu July 8, 2020, 8:04 a.m. UTC | #1
Josef Zlomek <josef@pex.com> 于2020年7月8日周三 下午1:28写道:
>
> Fixes: 4907
>
> Adds support for demuxing of animated WebP.
>
> The WebP demuxer splits the input stream into packets containing one frame.
> It also sets the timing information properly.
>
> 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    | 322 +++++++++++++++++++++++++++++++++++++++
>  6 files changed, 354 insertions(+), 1 deletion(-)
>  create mode 100644 libavformat/webpdec.c
>
> diff --git a/Changelog b/Changelog
> index 1e41040a8e..fc0bbdca45 100644
> --- a/Changelog
> +++ b/Changelog
> @@ -6,6 +6,7 @@ version <next>:
>  - MacCaption demuxer
>  - PGX decoder
>  - animated WebP parser/decoder
> +- animated WebP demuxer
>
>
>  version 4.3:
> diff --git a/doc/demuxers.texi b/doc/demuxers.texi
> index 3c15ab9eee..9b5932308b 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
> +Set the minimum valid delay between frames in milliseconds.
> +Range is 0 to 60000. Default value is 10.
> +
> +@item max_webp_delay
> +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
> +Set the default delay between frames in milliseconds.
> +Range is 0 to 60000. Default value is 100.
> +
> +@item ignore_loop
> +WebP files can contain information to loop a certain number of times (or
> +infinitely). If @option{ignore_loop} is set to 1, then the loop setting
> +from the input will be ignored and looping will not occur. If set to 0,
> +then looping will occur and will cycle the number of times according to
> +the WebP. Default value is 1.
> +@end table
> +
>  @c man end DEMUXERS
> diff --git a/libavformat/Makefile b/libavformat/Makefile
> index 26af859a28..93793de45d 100644
> --- a/libavformat/Makefile
> +++ b/libavformat/Makefile
> @@ -557,6 +557,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 f8527b1fd4..389273ea52 100644
> --- a/libavformat/allformats.c
> +++ b/libavformat/allformats.c
> @@ -455,6 +455,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 75c03fde0a..33cebed85e 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  48
> +#define LIBAVFORMAT_VERSION_MINOR  49
>  #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..8d6e6df9c0
> --- /dev/null
> +++ b/libavformat/webpdec.c
> @@ -0,0 +1,322 @@
> +/*
> + * 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 "libavutil/intreadwrite.h"
> +#include "libavutil/opt.h"
> +#include "avformat.h"
> +#include "internal.h"
> +
> +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 total_iter;
> +    int iter_count;
> +    int ignore_loop;
> +
> +    int nb_frames;
> +    int remaining_size;
> +} 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 (p->filename && ff_guess_image2_codec(p->filename)) {
> +        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 resync(AVFormatContext *s)
> +{
> +    WebPDemuxContext *wdc = s->priv_data;
> +    AVIOContext      *pb  = s->pb;
> +    int i;
> +    uint64_t state = 0;
> +
> +    for (i = 0; i < 12; i++) {
> +        state = (state << 8) | avio_r8(pb);
> +        if (i == 11) {
> +            if ((uint32_t) state == MKBETAG('W', 'E', 'B', 'P'))
> +                return 0;
> +            i -= 4;
> +        }
> +        if (i == 7) {
> +            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;
> +    }
> +    return 0;
> +}
> +
> +static int webp_read_header(AVFormatContext *s)
> +{
> +    WebPDemuxContext *wdc = s->priv_data;
> +    AVIOContext      *pb  = s->pb;
> +    AVStream         *st;
> +    int ret, n;
> +    int64_t nb_frames = 0, duration = 0;
> +    int width = 0, height = 0;
> +    uint32_t chunk_type, chunk_size;
> +
> +    ret = resync(s);
> +    if (ret < 0)
> +        return ret;
> +
> +    st = avformat_new_stream(s, NULL);
> +    if (!st)
> +        return AVERROR(ENOMEM);
> +
> +    st->codecpar->width  = 0;
> +    st->codecpar->height = 0;
> +    wdc->delay  = wdc->default_delay;
> +
> +    while (1) {
> +        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;
> +
> +        switch (chunk_type) {
> +        case MKTAG('V', 'P', '8', 'X'):
> +            avio_skip(pb, 4);
> +            width  = avio_rl24(pb) + 1;
> +            height = avio_rl24(pb) + 1;
> +            break;
> +        case MKTAG('V', 'P', '8', ' '):
> +            avio_skip(pb, 6);
> +            width  = avio_rl16(pb) & 0x3fff;
> +            height = avio_rl16(pb) & 0x3fff;
> +            duration += wdc->delay;
> +            nb_frames++;
> +            avio_skip(pb, chunk_size - 10);
> +            break;
> +        case MKTAG('V', 'P', '8', 'L'):
> +            avio_skip(pb, 1);
> +            n = avio_rl32(pb);
> +            width  = (n & 0x3fff) + 1;          /* first 14 bits */
> +            height = ((n >> 14) & 0x3fff) + 1;  /* next 14 bits */
> +            duration += wdc->delay;
> +            nb_frames++;
> +            avio_skip(pb, chunk_size - 5);
> +            break;
> +        case MKTAG('A', 'N', 'M', 'F'):
> +            avio_skip(pb, 6);
> +            width  = avio_rl24(pb) + 1;
> +            height = avio_rl24(pb) + 1;
> +            wdc->delay = avio_rl24(pb);
> +            if (wdc->delay < wdc->min_delay)
> +                wdc->delay = wdc->default_delay;
> +            wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
> +            duration += wdc->delay;
> +            nb_frames++;
> +            avio_skip(pb, chunk_size - 15);
> +            break;
> +        default:
> +            avio_skip(pb, chunk_size);
> +        }
> +
> +        if (avio_feof(pb))
> +            break;
> +
> +        if (st->codecpar->width == 0 && width > 0)
> +            st->codecpar->width = width;
> +        if (st->codecpar->height == 0 && height > 0)
> +            st->codecpar->height = height;
> +    }
> +
> +    /* WebP format operates with time in "milliseconds",
> +     * therefore timebase is 1/1000 */
> +    avpriv_set_pts_info(st, 64, 1, 1000);
> +    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
> +    st->codecpar->codec_id   = AV_CODEC_ID_WEBP;
> +    st->start_time           = 0;
> +    st->duration             = duration;
> +    st->nb_frames            = nb_frames;
> +
> +    /* jump to start because WebP decoder needs header data too */
> +    if (avio_seek(pb, 0, SEEK_SET) != 0)
> +        return AVERROR(EIO);
> +    wdc->remaining_size = 0;
> +
> +    return 0;
> +}
> +
> +static int webp_read_packet(AVFormatContext *s, AVPacket *pkt)
> +{
> +    WebPDemuxContext *wdc = s->priv_data;
> +    AVIOContext *pb = s->pb;
> +    int ret;
> +    int64_t frame_start = avio_tell(pb), frame_end;
> +    uint32_t chunk_type, chunk_size;
> +    int is_frame = 0;
> +
> +    if (wdc->remaining_size == 0) {
> +        ret = resync(s);
> +        if (ret == AVERROR_EOF) {
> +            if (!wdc->ignore_loop && avio_feof(pb)
> +                && (wdc->total_iter < 0 || ++wdc->iter_count < wdc->total_iter))
> +                return avio_seek(pb, 0, SEEK_SET);
> +            return AVERROR_EOF;
> +        }
> +        if (ret < 0)
> +            return ret;
> +
> +        wdc->delay  = wdc->default_delay;
> +    }
> +
> +    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 (wdc->remaining_size < 8 + chunk_size)
> +            return AVERROR_INVALIDDATA;
> +        wdc->remaining_size -= 8 + chunk_size;
> +
> +        switch (chunk_type) {
> +            case MKTAG('A', 'N', 'I', 'M'):
> +                avio_skip(pb, 4);
> +                wdc->total_iter = avio_rl16(pb);
> +                if (wdc->total_iter == 0)
> +                    wdc->total_iter = -1;
> +                ret = avio_skip(pb, chunk_size - 6);
> +                break;
> +            case MKTAG('A', 'N', 'M', 'F'):
> +                avio_skip(pb, 12);
> +                wdc->delay = avio_rl24(pb);
> +                if (wdc->delay < wdc->min_delay)
> +                    wdc->delay = wdc->default_delay;
> +                wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
> +                wdc->nb_frames++;
> +                is_frame = 1;
> +                ret = avio_skip(pb, chunk_size - 15);
> +                break;
> +            case MKTAG('V', 'P', '8', ' '):
> +            case MKTAG('V', 'P', '8', 'L'):
> +                wdc->nb_frames++;
> +                is_frame = 1;
> +                /* fallthrough */
> +            default:
> +                ret = avio_skip(pb, chunk_size);
> +                break;
> +        }
> +
> +        if (ret < 0)
> +            return ret;
> +    }
> +
> +    frame_end = avio_tell(pb);
> +
> +    if (avio_seek(pb, frame_start, SEEK_SET) != frame_start)
> +        return AVERROR(EIO);
> +
> +    ret = av_get_packet(pb, pkt, frame_end - frame_start);
> +    if (ret < 0)
> +        return ret;
> +
> +    pkt->flags |= AV_PKT_FLAG_KEY;
> +    pkt->stream_index = 0;
> +    pkt->duration = is_frame ? wdc->delay : 0;
> +
> +    if (is_frame && wdc->nb_frames == 1) {
> +        s->streams[0]->r_frame_rate = (AVRational) {1000, pkt->duration};
> +    }
> +
> +    return ret;
> +}
> +
> +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,
> +    .flags          = AVFMT_GENERIC_INDEX,
> +    .priv_class     = &demuxer_class,
> +};
> --
> 2.17.1
>
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".


Great job Josef,

Test file attached.

(base) liuqi05:dash liuqi$ ./ffplay_g test.webp
ffplay version N-98438-g002779180d Copyright (c) 2003-2020 the FFmpeg developers
  built with Apple clang version 11.0.3 (clang-1103.0.32.59)
  configuration: --quiet --enable-htmlpages --enable-libx264
--enable-libxml2 --enable-gpl --extra-ldflags=-I/usr/local/include
--extra-ldflags=-L/usr/local/lib --enable-libfreetype
--enable-fontconfig --enable-libspeex --enable-libopus --enable-libzmq
--enable-libx265 --enable-libass --enable-videotoolbox
--disable-optimizations --disable-stripping --enable-libmp3lame
--samples=fate-suite/ --cc=clang
  libavutil      56. 55.100 / 56. 55.100
  libavcodec     58. 95.100 / 58. 95.100
  libavformat    58. 49.100 / 58. 49.100
  libavdevice    58. 11.101 / 58. 11.101
  libavfilter     7. 86.100 /  7. 86.100
  libswscale      5.  8.100 /  5.  8.100
  libswresample   3.  8.100 /  3.  8.100
  libpostproc    55.  8.100 / 55.  8.100
[swscaler @ 0x110be6000] Warning: data is not aligned! This can lead
to a speed loss
Input #0, webp, from 'test.webp':
  Duration: 00:00:00.93, start: 0.000000, bitrate: 1315 kb/s
    Stream #0:0: Video: webp, yuva420p(tv, bt470bg/unknown/unknown),
422x480, 15 fps, 15.15 tbr, 1k tbn, 1k tbc
Segmentation fault: 11
(base) liuqi05:dash liuqi$
Carl Eugen Hoyos July 8, 2020, 8:33 p.m. UTC | #2
Am Mi., 8. Juli 2020 um 07:28 Uhr schrieb Josef Zlomek <josef@pex.com>:
>
> Fixes: 4907

It seems surprising that two commits should fix a ticket.

> Adds support for demuxing of animated WebP.

Does this demuxer also support single frame files?
What about concatenated webps?

[...]

> +static int webp_probe(const AVProbeData *p)
> +{
> +    const uint8_t *b = p->buf;
> +
> +    if (p->filename && ff_guess_image2_codec(p->filename)) {

Why is this useful?

> +        if (AV_RB32(b)     == MKBETAG('R', 'I', 'F', 'F') &&
> +            AV_RB32(b + 8) == MKBETAG('W', 'E', 'B', 'P'))
> +            return AVPROBE_SCORE_MAX;
> +    }

[...]


> +    frame_end = avio_tell(pb);
> +
> +    if (avio_seek(pb, frame_start, SEEK_SET) != frame_start)
> +        return AVERROR(EIO);

Instead I believe you should use ffio_ensure_seekback() or do
I miss something?
Same above.

Carl Eugen
Zlomek, Josef July 9, 2020, 9:24 a.m. UTC | #3
On Wed, Jul 8, 2020 at 10:34 PM Carl Eugen Hoyos <ceffmpeg@gmail.com> wrote:

> Am Mi., 8. Juli 2020 um 07:28 Uhr schrieb Josef Zlomek <josef@pex.com>:
> >
> > Fixes: 4907
>
> It seems surprising that two commits should fix a ticket.
>

My understanding of ffmpeg internals is limited:
The parser is used for input from a pipe, the demuxer is used for input
from a file or other seekable streams.
I did not find out how parsers can set frame rate and time base of the
input stream so the playback speed is not correct.
The demuxer sets the frame rate and time base based on the information in
the input file. It also supports looping the input file.

> Adds support for demuxing of animated WebP.

>
> Does this demuxer also support single frame files?
> What about concatenated webps?
>

Non-animated/single frame files are still supported.
Concatenated files are also supported.

> +static int webp_probe(const AVProbeData *p)
> > +{
> > +    const uint8_t *b = p->buf;
> > +
> > +    if (p->filename && ff_guess_image2_codec(p->filename)) {
>
> Why is this useful?
>

This is a not very nice way to check that the input is from a file, and not
from a pipe.
Probably it will not be needed if demuxer uses ffio_ensure_seekback() as
you suggested below.

> +    frame_end = avio_tell(pb);
> > +
> > +    if (avio_seek(pb, frame_start, SEEK_SET) != frame_start)
> > +        return AVERROR(EIO);
>
> Instead I believe you should use ffio_ensure_seekback() or do
>
I miss something?
>

I guess this will help to use the demuxer also for input from a pipe, right?
Then probably we could get rid of the parser and keep only the demuxer.

Josef
Zlomek, Josef July 9, 2020, 9:26 a.m. UTC | #4
On Wed, Jul 8, 2020 at 10:04 AM Steven Liu <lingjiujianke@gmail.com> wrote:

>
> (base) liuqi05:dash liuqi$ ./ffplay_g test.webp
> [...]
> Segmentation fault: 11
> (base) liuqi05:dash liuqi$
>

The segfault was caused by the usage of unaligned buffers for frames.
When using aligned buffers, it does not crash anymore.
Thank you for finding this bug.
Nicolas George July 9, 2020, 9:44 a.m. UTC | #5
Josef Zlomek (12020-07-08):
> Fixes: 4907
> 
> Adds support for demuxing of animated WebP.
> 
> The WebP demuxer splits the input stream into packets containing one frame.
> It also sets the timing information properly.

Thanks for the patch. A few comments.

> 
> 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    | 322 +++++++++++++++++++++++++++++++++++++++
>  6 files changed, 354 insertions(+), 1 deletion(-)
>  create mode 100644 libavformat/webpdec.c
> 
> diff --git a/Changelog b/Changelog
> index 1e41040a8e..fc0bbdca45 100644
> --- a/Changelog
> +++ b/Changelog
> @@ -6,6 +6,7 @@ version <next>:
>  - MacCaption demuxer
>  - PGX decoder
>  - animated WebP parser/decoder
> +- animated WebP demuxer
>  
>  
>  version 4.3:
> diff --git a/doc/demuxers.texi b/doc/demuxers.texi
> index 3c15ab9eee..9b5932308b 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
> +Set the minimum valid delay between frames in milliseconds.
> +Range is 0 to 60000. Default value is 10.
> +
> +@item max_webp_delay
> +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
> +Set the default delay between frames in milliseconds.
> +Range is 0 to 60000. Default value is 100.

Make these durations, with option type AV_OPT_TYPE_DURATION and internal
semantic in microseconds.

> +
> +@item ignore_loop
> +WebP files can contain information to loop a certain number of times (or
> +infinitely). If @option{ignore_loop} is set to 1, then the loop setting
> +from the input will be ignored and looping will not occur. If set to 0,
> +then looping will occur and will cycle the number of times according to
> +the WebP. Default value is 1.

Make it boolean.

Why default to true?

> +@end table
> +
>  @c man end DEMUXERS
> diff --git a/libavformat/Makefile b/libavformat/Makefile
> index 26af859a28..93793de45d 100644
> --- a/libavformat/Makefile
> +++ b/libavformat/Makefile
> @@ -557,6 +557,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 f8527b1fd4..389273ea52 100644
> --- a/libavformat/allformats.c
> +++ b/libavformat/allformats.c
> @@ -455,6 +455,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 75c03fde0a..33cebed85e 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  48
> +#define LIBAVFORMAT_VERSION_MINOR  49
>  #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..8d6e6df9c0
> --- /dev/null
> +++ b/libavformat/webpdec.c
> @@ -0,0 +1,322 @@
> +/*
> + * 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 "libavutil/intreadwrite.h"
> +#include "libavutil/opt.h"
> +#include "avformat.h"
> +#include "internal.h"
> +
> +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 total_iter;
> +    int iter_count;
> +    int ignore_loop;
> +
> +    int nb_frames;
> +    int remaining_size;
> +} 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 (p->filename && ff_guess_image2_codec(p->filename)) {
> +        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 resync(AVFormatContext *s)
> +{
> +    WebPDemuxContext *wdc = s->priv_data;
> +    AVIOContext      *pb  = s->pb;
> +    int i;
> +    uint64_t state = 0;
> +
> +    for (i = 0; i < 12; i++) {
> +        state = (state << 8) | avio_r8(pb);
> +        if (i == 11) {
> +            if ((uint32_t) state == MKBETAG('W', 'E', 'B', 'P'))
> +                return 0;
> +            i -= 4;
> +        }
> +        if (i == 7) {
> +            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;
> +    }
> +    return 0;
> +}
> +
> +static int webp_read_header(AVFormatContext *s)
> +{
> +    WebPDemuxContext *wdc = s->priv_data;
> +    AVIOContext      *pb  = s->pb;
> +    AVStream         *st;
> +    int ret, n;
> +    int64_t nb_frames = 0, duration = 0;
> +    int width = 0, height = 0;
> +    uint32_t chunk_type, chunk_size;
> +
> +    ret = resync(s);
> +    if (ret < 0)
> +        return ret;
> +
> +    st = avformat_new_stream(s, NULL);
> +    if (!st)
> +        return AVERROR(ENOMEM);
> +
> +    st->codecpar->width  = 0;
> +    st->codecpar->height = 0;
> +    wdc->delay  = wdc->default_delay;
> +
> +    while (1) {
> +        chunk_type = avio_rl32(pb);
> +        chunk_size = avio_rl32(pb);
> +        if (chunk_size == UINT32_MAX)
> +            return AVERROR_INVALIDDATA;
> +        chunk_size += chunk_size & 1;

chunk_size needs to be validated better than that. Otherwise,
chunk_size-10 or such can underflow and that could be bad.

> +        if (avio_feof(pb))
> +            break;
> +
> +        switch (chunk_type) {
> +        case MKTAG('V', 'P', '8', 'X'):
> +            avio_skip(pb, 4);
> +            width  = avio_rl24(pb) + 1;
> +            height = avio_rl24(pb) + 1;
> +            break;
> +        case MKTAG('V', 'P', '8', ' '):
> +            avio_skip(pb, 6);
> +            width  = avio_rl16(pb) & 0x3fff;
> +            height = avio_rl16(pb) & 0x3fff;
> +            duration += wdc->delay;
> +            nb_frames++;
> +            avio_skip(pb, chunk_size - 10);
> +            break;
> +        case MKTAG('V', 'P', '8', 'L'):
> +            avio_skip(pb, 1);
> +            n = avio_rl32(pb);
> +            width  = (n & 0x3fff) + 1;          /* first 14 bits */
> +            height = ((n >> 14) & 0x3fff) + 1;  /* next 14 bits */
> +            duration += wdc->delay;
> +            nb_frames++;
> +            avio_skip(pb, chunk_size - 5);
> +            break;
> +        case MKTAG('A', 'N', 'M', 'F'):
> +            avio_skip(pb, 6);
> +            width  = avio_rl24(pb) + 1;
> +            height = avio_rl24(pb) + 1;
> +            wdc->delay = avio_rl24(pb);
> +            if (wdc->delay < wdc->min_delay)
> +                wdc->delay = wdc->default_delay;
> +            wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
> +            duration += wdc->delay;
> +            nb_frames++;
> +            avio_skip(pb, chunk_size - 15);
> +            break;
> +        default:
> +            avio_skip(pb, chunk_size);
> +        }
> +
> +        if (avio_feof(pb))
> +            break;
> +
> +        if (st->codecpar->width == 0 && width > 0)
> +            st->codecpar->width = width;
> +        if (st->codecpar->height == 0 && height > 0)
> +            st->codecpar->height = height;
> +    }
> +
> +    /* WebP format operates with time in "milliseconds",
> +     * therefore timebase is 1/1000 */
> +    avpriv_set_pts_info(st, 64, 1, 1000);
> +    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
> +    st->codecpar->codec_id   = AV_CODEC_ID_WEBP;
> +    st->start_time           = 0;
> +    st->duration             = duration;
> +    st->nb_frames            = nb_frames;
> +
> +    /* jump to start because WebP decoder needs header data too */
> +    if (avio_seek(pb, 0, SEEK_SET) != 0)
> +        return AVERROR(EIO);
> +    wdc->remaining_size = 0;
> +
> +    return 0;
> +}
> +
> +static int webp_read_packet(AVFormatContext *s, AVPacket *pkt)
> +{
> +    WebPDemuxContext *wdc = s->priv_data;
> +    AVIOContext *pb = s->pb;
> +    int ret;
> +    int64_t frame_start = avio_tell(pb), frame_end;
> +    uint32_t chunk_type, chunk_size;
> +    int is_frame = 0;
> +
> +    if (wdc->remaining_size == 0) {
> +        ret = resync(s);
> +        if (ret == AVERROR_EOF) {
> +            if (!wdc->ignore_loop && avio_feof(pb)
> +                && (wdc->total_iter < 0 || ++wdc->iter_count < wdc->total_iter))
> +                return avio_seek(pb, 0, SEEK_SET);
> +            return AVERROR_EOF;
> +        }
> +        if (ret < 0)
> +            return ret;
> +
> +        wdc->delay  = wdc->default_delay;
> +    }
> +
> +    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 (wdc->remaining_size < 8 + chunk_size)
> +            return AVERROR_INVALIDDATA;
> +        wdc->remaining_size -= 8 + chunk_size;
> +
> +        switch (chunk_type) {
> +            case MKTAG('A', 'N', 'I', 'M'):
> +                avio_skip(pb, 4);
> +                wdc->total_iter = avio_rl16(pb);
> +                if (wdc->total_iter == 0)
> +                    wdc->total_iter = -1;
> +                ret = avio_skip(pb, chunk_size - 6);
> +                break;
> +            case MKTAG('A', 'N', 'M', 'F'):
> +                avio_skip(pb, 12);
> +                wdc->delay = avio_rl24(pb);
> +                if (wdc->delay < wdc->min_delay)
> +                    wdc->delay = wdc->default_delay;
> +                wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
> +                wdc->nb_frames++;
> +                is_frame = 1;
> +                ret = avio_skip(pb, chunk_size - 15);
> +                break;
> +            case MKTAG('V', 'P', '8', ' '):
> +            case MKTAG('V', 'P', '8', 'L'):
> +                wdc->nb_frames++;
> +                is_frame = 1;
> +                /* fallthrough */
> +            default:
> +                ret = avio_skip(pb, chunk_size);
> +                break;
> +        }
> +
> +        if (ret < 0)
> +            return ret;
> +    }
> +
> +    frame_end = avio_tell(pb);
> +

> +    if (avio_seek(pb, frame_start, SEEK_SET) != frame_start)
> +        return AVERROR(EIO);

If avio_seek() returns an error, please forward it instead of inventing
EIO.

> +
> +    ret = av_get_packet(pb, pkt, frame_end - frame_start);
> +    if (ret < 0)
> +        return ret;
> +

> +    pkt->flags |= AV_PKT_FLAG_KEY;

You, yesterday:
> The memcpy is needed. The frames of the WebP animation do not cover the
> whole canvas of the picture, i.e. the decoded VP8 frame is just a
> sub-rectangle of the canvas. The frame changes just a part of the
> canvas.

That means the packet is not a key frame.


> +    pkt->stream_index = 0;
> +    pkt->duration = is_frame ? wdc->delay : 0;

What is the packet, if it is not a frame?

Where is the PTS?

> +
> +    if (is_frame && wdc->nb_frames == 1) {
> +        s->streams[0]->r_frame_rate = (AVRational) {1000, pkt->duration};
> +    }
> +
> +    return ret;
> +}
> +
> +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,
> +    .flags          = AVFMT_GENERIC_INDEX,
> +    .priv_class     = &demuxer_class,
> +};

Regards,
Pascal Massimino July 9, 2020, 10:28 a.m. UTC | #6
Hi,

On Thu, Jul 9, 2020 at 11:44 AM Nicolas George <george@nsup.org> wrote:

> Josef Zlomek (12020-07-08):
> > Fixes: 4907
> >
> > Adds support for demuxing of animated WebP.
> >
> > The WebP demuxer splits the input stream into packets containing one
> frame.
> > It also sets the timing information properly.
>
> Thanks for the patch. A few comments.
>
> >
> > 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    | 322 +++++++++++++++++++++++++++++++++++++++
> >  6 files changed, 354 insertions(+), 1 deletion(-)
> >  create mode 100644 libavformat/webpdec.c
> >
> > diff --git a/Changelog b/Changelog
> > index 1e41040a8e..fc0bbdca45 100644
> > --- a/Changelog
> > +++ b/Changelog
> > @@ -6,6 +6,7 @@ version <next>:
> >  - MacCaption demuxer
> >  - PGX decoder
> >  - animated WebP parser/decoder
> > +- animated WebP demuxer
> >
> >
> >  version 4.3:
> > diff --git a/doc/demuxers.texi b/doc/demuxers.texi
> > index 3c15ab9eee..9b5932308b 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
> > +Set the minimum valid delay between frames in milliseconds.
> > +Range is 0 to 60000. Default value is 10.
> > +
> > +@item max_webp_delay
> > +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
> > +Set the default delay between frames in milliseconds.
> > +Range is 0 to 60000. Default value is 100.
>
> Make these durations, with option type AV_OPT_TYPE_DURATION and internal
> semantic in microseconds.
>
> > +
> > +@item ignore_loop
> > +WebP files can contain information to loop a certain number of times (or
> > +infinitely). If @option{ignore_loop} is set to 1, then the loop setting
> > +from the input will be ignored and looping will not occur. If set to 0,
> > +then looping will occur and will cycle the number of times according to
> > +the WebP. Default value is 1.
>
> Make it boolean.
>
> Why default to true?
>
> > +@end table
> > +
> >  @c man end DEMUXERS
> > diff --git a/libavformat/Makefile b/libavformat/Makefile
> > index 26af859a28..93793de45d 100644
> > --- a/libavformat/Makefile
> > +++ b/libavformat/Makefile
> > @@ -557,6 +557,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 f8527b1fd4..389273ea52 100644
> > --- a/libavformat/allformats.c
> > +++ b/libavformat/allformats.c
> > @@ -455,6 +455,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 75c03fde0a..33cebed85e 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  48
> > +#define LIBAVFORMAT_VERSION_MINOR  49
> >  #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..8d6e6df9c0
> > --- /dev/null
> > +++ b/libavformat/webpdec.c
> > @@ -0,0 +1,322 @@
> > +/*
> > + * 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 "libavutil/intreadwrite.h"
> > +#include "libavutil/opt.h"
> > +#include "avformat.h"
> > +#include "internal.h"
> > +
> > +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 total_iter;
> > +    int iter_count;
> > +    int ignore_loop;
> > +
> > +    int nb_frames;
> > +    int remaining_size;
> > +} 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 (p->filename && ff_guess_image2_codec(p->filename)) {
> > +        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 resync(AVFormatContext *s)
> > +{
> > +    WebPDemuxContext *wdc = s->priv_data;
> > +    AVIOContext      *pb  = s->pb;
> > +    int i;
> > +    uint64_t state = 0;
> > +
> > +    for (i = 0; i < 12; i++) {
> > +        state = (state << 8) | avio_r8(pb);
> > +        if (i == 11) {
> > +            if ((uint32_t) state == MKBETAG('W', 'E', 'B', 'P'))
> > +                return 0;
> > +            i -= 4;
> > +        }
> > +        if (i == 7) {
> > +            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;
> > +    }
> > +    return 0;
> > +}
> > +
> > +static int webp_read_header(AVFormatContext *s)
> > +{
> > +    WebPDemuxContext *wdc = s->priv_data;
> > +    AVIOContext      *pb  = s->pb;
> > +    AVStream         *st;
> > +    int ret, n;
> > +    int64_t nb_frames = 0, duration = 0;
> > +    int width = 0, height = 0;
> > +    uint32_t chunk_type, chunk_size;
> > +
> > +    ret = resync(s);
> > +    if (ret < 0)
> > +        return ret;
> > +
> > +    st = avformat_new_stream(s, NULL);
> > +    if (!st)
> > +        return AVERROR(ENOMEM);
> > +
> > +    st->codecpar->width  = 0;
> > +    st->codecpar->height = 0;
> > +    wdc->delay  = wdc->default_delay;
> > +
> > +    while (1) {
> > +        chunk_type = avio_rl32(pb);
> > +        chunk_size = avio_rl32(pb);
> > +        if (chunk_size == UINT32_MAX)
> > +            return AVERROR_INVALIDDATA;
> > +        chunk_size += chunk_size & 1;
>
> chunk_size needs to be validated better than that. Otherwise,
> chunk_size-10 or such can underflow and that could be bad.
>
> > +        if (avio_feof(pb))
> > +            break;
> > +
> > +        switch (chunk_type) {
> > +        case MKTAG('V', 'P', '8', 'X'):
> > +            avio_skip(pb, 4);
> > +            width  = avio_rl24(pb) + 1;
> > +            height = avio_rl24(pb) + 1;
> > +            break;
> > +        case MKTAG('V', 'P', '8', ' '):
> > +            avio_skip(pb, 6);
> > +            width  = avio_rl16(pb) & 0x3fff;
> > +            height = avio_rl16(pb) & 0x3fff;
> > +            duration += wdc->delay;
> > +            nb_frames++;
> > +            avio_skip(pb, chunk_size - 10);
> > +            break;
> > +        case MKTAG('V', 'P', '8', 'L'):
> > +            avio_skip(pb, 1);
> > +            n = avio_rl32(pb);
> > +            width  = (n & 0x3fff) + 1;          /* first 14 bits */
> > +            height = ((n >> 14) & 0x3fff) + 1;  /* next 14 bits */
> > +            duration += wdc->delay;
> > +            nb_frames++;
> > +            avio_skip(pb, chunk_size - 5);
> > +            break;
> > +        case MKTAG('A', 'N', 'M', 'F'):
> > +            avio_skip(pb, 6);
> > +            width  = avio_rl24(pb) + 1;
> > +            height = avio_rl24(pb) + 1;
> > +            wdc->delay = avio_rl24(pb);
> > +            if (wdc->delay < wdc->min_delay)
> > +                wdc->delay = wdc->default_delay;
> > +            wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
> > +            duration += wdc->delay;
> > +            nb_frames++;
> > +            avio_skip(pb, chunk_size - 15);
> > +            break;
> > +        default:
> > +            avio_skip(pb, chunk_size);
> > +        }
> > +
> > +        if (avio_feof(pb))
> > +            break;
> > +
> > +        if (st->codecpar->width == 0 && width > 0)
> > +            st->codecpar->width = width;
> > +        if (st->codecpar->height == 0 && height > 0)
> > +            st->codecpar->height = height;
> > +    }
> > +
> > +    /* WebP format operates with time in "milliseconds",
> > +     * therefore timebase is 1/1000 */
> > +    avpriv_set_pts_info(st, 64, 1, 1000);
> > +    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
> > +    st->codecpar->codec_id   = AV_CODEC_ID_WEBP;
> > +    st->start_time           = 0;
> > +    st->duration             = duration;
> > +    st->nb_frames            = nb_frames;
> > +
> > +    /* jump to start because WebP decoder needs header data too */
> > +    if (avio_seek(pb, 0, SEEK_SET) != 0)
> > +        return AVERROR(EIO);
> > +    wdc->remaining_size = 0;
> > +
> > +    return 0;
> > +}
> > +
> > +static int webp_read_packet(AVFormatContext *s, AVPacket *pkt)
> > +{
> > +    WebPDemuxContext *wdc = s->priv_data;
> > +    AVIOContext *pb = s->pb;
> > +    int ret;
> > +    int64_t frame_start = avio_tell(pb), frame_end;
> > +    uint32_t chunk_type, chunk_size;
> > +    int is_frame = 0;
> > +
> > +    if (wdc->remaining_size == 0) {
> > +        ret = resync(s);
> > +        if (ret == AVERROR_EOF) {
> > +            if (!wdc->ignore_loop && avio_feof(pb)
> > +                && (wdc->total_iter < 0 || ++wdc->iter_count <
> wdc->total_iter))
> > +                return avio_seek(pb, 0, SEEK_SET);
> > +            return AVERROR_EOF;
> > +        }
> > +        if (ret < 0)
> > +            return ret;
> > +
> > +        wdc->delay  = wdc->default_delay;
> > +    }
> > +
> > +    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 (wdc->remaining_size < 8 + chunk_size)
> > +            return AVERROR_INVALIDDATA;
> > +        wdc->remaining_size -= 8 + chunk_size;
> > +
> > +        switch (chunk_type) {
> > +            case MKTAG('A', 'N', 'I', 'M'):
> > +                avio_skip(pb, 4);
> > +                wdc->total_iter = avio_rl16(pb);
> > +                if (wdc->total_iter == 0)
> > +                    wdc->total_iter = -1;
> > +                ret = avio_skip(pb, chunk_size - 6);
> > +                break;
> > +            case MKTAG('A', 'N', 'M', 'F'):
> > +                avio_skip(pb, 12);
> > +                wdc->delay = avio_rl24(pb);
> > +                if (wdc->delay < wdc->min_delay)
> > +                    wdc->delay = wdc->default_delay;
> > +                wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
> > +                wdc->nb_frames++;
> > +                is_frame = 1;
> > +                ret = avio_skip(pb, chunk_size - 15);
> > +                break;
> > +            case MKTAG('V', 'P', '8', ' '):
> > +            case MKTAG('V', 'P', '8', 'L'):
> > +                wdc->nb_frames++;
> > +                is_frame = 1;
> > +                /* fallthrough */
> > +            default:
> > +                ret = avio_skip(pb, chunk_size);
> > +                break;
> > +        }
> > +
> > +        if (ret < 0)
> > +            return ret;
> > +    }
> > +
> > +    frame_end = avio_tell(pb);
> > +
>
> > +    if (avio_seek(pb, frame_start, SEEK_SET) != frame_start)
> > +        return AVERROR(EIO);
>
> If avio_seek() returns an error, please forward it instead of inventing
> EIO.
>
> > +
> > +    ret = av_get_packet(pb, pkt, frame_end - frame_start);
> > +    if (ret < 0)
> > +        return ret;
> > +
>
> > +    pkt->flags |= AV_PKT_FLAG_KEY;
>
> You, yesterday:
> > The memcpy is needed. The frames of the WebP animation do not cover the
> > whole canvas of the picture, i.e. the decoded VP8 frame is just a
> > sub-rectangle of the canvas. The frame changes just a part of the
> > canvas.
>
> That means the packet is not a key frame.
>

FYI, the conditions under which a packet can be considered a 'key-frame'
(that is: seekable) are
listed here:
https://github.com/webmproject/libwebp/blob/master/src/demux/anim_decode.c#L176
in libwebp.

skal/


>
>
> > +    pkt->stream_index = 0;
> > +    pkt->duration = is_frame ? wdc->delay : 0;
>
> What is the packet, if it is not a frame?
>
> Where is the PTS?
>
> > +
> > +    if (is_frame && wdc->nb_frames == 1) {
> > +        s->streams[0]->r_frame_rate = (AVRational) {1000,
> pkt->duration};
> > +    }
> > +
> > +    return ret;
> > +}
> > +
> > +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,
> > +    .flags          = AVFMT_GENERIC_INDEX,
> > +    .priv_class     = &demuxer_class,
> > +};
>
> Regards,
>
> --
>   Nicolas George
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
Zlomek, Josef July 9, 2020, 11:03 a.m. UTC | #7
On Thu, Jul 9, 2020 at 11:44 AM Nicolas George <george@nsup.org> wrote:

> Josef Zlomek (12020-07-08):
>
> > +@item min_delay
> > +Set the minimum valid delay between frames in milliseconds.
> > +Range is 0 to 60000. Default value is 10.
> > +
> > +@item max_webp_delay
> > +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
> > +Set the default delay between frames in milliseconds.
> > +Range is 0 to 60000. Default value is 100.
>
> Make these durations, with option type AV_OPT_TYPE_DURATION and internal
> semantic in microseconds.
>

OK.

> +@item ignore_loop
> > +WebP files can contain information to loop a certain number of times (or
> > +infinitely). If @option{ignore_loop} is set to 1, then the loop setting
> > +from the input will be ignored and looping will not occur. If set to 0,
> > +then looping will occur and will cycle the number of times according to
> > +the WebP. Default value is 1.
>
> Make it boolean.
>

OK. I have just copied the options from gif demuxer.

Why default to true?
>

The loop count in the WebP files is usually 0 = forever.
If decoding/transcoding the file, people probably do not want to loop the
file forever, exhausting the disk capacity.
Also when reading from pipe, the whole file would have to be buffered in
memory to be able to loop.
So in my opinion, it is better to ignore loop count by default. Also gif
demuxer ignores loop by default..

> +    while (1) {
> > +        chunk_type = avio_rl32(pb);
> > +        chunk_size = avio_rl32(pb);
> > +        if (chunk_size == UINT32_MAX)
> > +            return AVERROR_INVALIDDATA;
> > +        chunk_size += chunk_size & 1;
>
> chunk_size needs to be validated better than that. Otherwise,
> chunk_size-10 or such can underflow and that could be bad.
>

Here it is just a validation that chunk_size will not overflow when making
it even.
I will add validations to the cases of the switch statement below.

> +        if (avio_feof(pb))
> > +            break;
> > +
> > +        switch (chunk_type) {
> > +        case MKTAG('V', 'P', '8', 'X'):
> > +            avio_skip(pb, 4);
> > +            width  = avio_rl24(pb) + 1;
> > +            height = avio_rl24(pb) + 1;
> > +            break;
> > +        case MKTAG('V', 'P', '8', ' '):
> > +            avio_skip(pb, 6);
> > +            width  = avio_rl16(pb) & 0x3fff;
> > +            height = avio_rl16(pb) & 0x3fff;
> > +            duration += wdc->delay;
> > +            nb_frames++;
> > +            avio_skip(pb, chunk_size - 10);
> > +            break;
> > +        case MKTAG('V', 'P', '8', 'L'):
> > +            avio_skip(pb, 1);
> > +            n = avio_rl32(pb);
> > +            width  = (n & 0x3fff) + 1;          /* first 14 bits */
> > +            height = ((n >> 14) & 0x3fff) + 1;  /* next 14 bits */
> > +            duration += wdc->delay;
> > +            nb_frames++;
> > +            avio_skip(pb, chunk_size - 5);
> > +            break;
> > +        case MKTAG('A', 'N', 'M', 'F'):
> > +            avio_skip(pb, 6);
> > +            width  = avio_rl24(pb) + 1;
> > +            height = avio_rl24(pb) + 1;
> > +            wdc->delay = avio_rl24(pb);
> > +            if (wdc->delay < wdc->min_delay)
> > +                wdc->delay = wdc->default_delay;
> > +            wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
> > +            duration += wdc->delay;
> > +            nb_frames++;
> > +            avio_skip(pb, chunk_size - 15);
> > +            break;
> > +        default:
> > +            avio_skip(pb, chunk_size);
> > +        }
>
[...]

> +    pkt->stream_index = 0;
> > +    pkt->duration = is_frame ? wdc->delay : 0;
>
> What is the packet, if it is not a frame?
>

It may be metadata (chunk "XMP " or "EXIF") or garbage at the end of the
file.
diff mbox series

Patch

diff --git a/Changelog b/Changelog
index 1e41040a8e..fc0bbdca45 100644
--- a/Changelog
+++ b/Changelog
@@ -6,6 +6,7 @@  version <next>:
 - MacCaption demuxer
 - PGX decoder
 - animated WebP parser/decoder
+- animated WebP demuxer
 
 
 version 4.3:
diff --git a/doc/demuxers.texi b/doc/demuxers.texi
index 3c15ab9eee..9b5932308b 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
+Set the minimum valid delay between frames in milliseconds.
+Range is 0 to 60000. Default value is 10.
+
+@item max_webp_delay
+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
+Set the default delay between frames in milliseconds.
+Range is 0 to 60000. Default value is 100.
+
+@item ignore_loop
+WebP files can contain information to loop a certain number of times (or
+infinitely). If @option{ignore_loop} is set to 1, then the loop setting
+from the input will be ignored and looping will not occur. If set to 0,
+then looping will occur and will cycle the number of times according to
+the WebP. Default value is 1.
+@end table
+
 @c man end DEMUXERS
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 26af859a28..93793de45d 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -557,6 +557,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 f8527b1fd4..389273ea52 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -455,6 +455,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 75c03fde0a..33cebed85e 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  48
+#define LIBAVFORMAT_VERSION_MINOR  49
 #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..8d6e6df9c0
--- /dev/null
+++ b/libavformat/webpdec.c
@@ -0,0 +1,322 @@ 
+/*
+ * 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 "libavutil/intreadwrite.h"
+#include "libavutil/opt.h"
+#include "avformat.h"
+#include "internal.h"
+
+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 total_iter;
+    int iter_count;
+    int ignore_loop;
+
+    int nb_frames;
+    int remaining_size;
+} 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 (p->filename && ff_guess_image2_codec(p->filename)) {
+        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 resync(AVFormatContext *s)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+    AVIOContext      *pb  = s->pb;
+    int i;
+    uint64_t state = 0;
+
+    for (i = 0; i < 12; i++) {
+        state = (state << 8) | avio_r8(pb);
+        if (i == 11) {
+            if ((uint32_t) state == MKBETAG('W', 'E', 'B', 'P'))
+                return 0;
+            i -= 4;
+        }
+        if (i == 7) {
+            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;
+    }
+    return 0;
+}
+
+static int webp_read_header(AVFormatContext *s)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+    AVIOContext      *pb  = s->pb;
+    AVStream         *st;
+    int ret, n;
+    int64_t nb_frames = 0, duration = 0;
+    int width = 0, height = 0;
+    uint32_t chunk_type, chunk_size;
+
+    ret = resync(s);
+    if (ret < 0)
+        return ret;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st)
+        return AVERROR(ENOMEM);
+
+    st->codecpar->width  = 0;
+    st->codecpar->height = 0;
+    wdc->delay  = wdc->default_delay;
+
+    while (1) {
+        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;
+
+        switch (chunk_type) {
+        case MKTAG('V', 'P', '8', 'X'):
+            avio_skip(pb, 4);
+            width  = avio_rl24(pb) + 1;
+            height = avio_rl24(pb) + 1;
+            break;
+        case MKTAG('V', 'P', '8', ' '):
+            avio_skip(pb, 6);
+            width  = avio_rl16(pb) & 0x3fff;
+            height = avio_rl16(pb) & 0x3fff;
+            duration += wdc->delay;
+            nb_frames++;
+            avio_skip(pb, chunk_size - 10);
+            break;
+        case MKTAG('V', 'P', '8', 'L'):
+            avio_skip(pb, 1);
+            n = avio_rl32(pb);
+            width  = (n & 0x3fff) + 1;          /* first 14 bits */
+            height = ((n >> 14) & 0x3fff) + 1;  /* next 14 bits */
+            duration += wdc->delay;
+            nb_frames++;
+            avio_skip(pb, chunk_size - 5);
+            break;
+        case MKTAG('A', 'N', 'M', 'F'):
+            avio_skip(pb, 6);
+            width  = avio_rl24(pb) + 1;
+            height = avio_rl24(pb) + 1;
+            wdc->delay = avio_rl24(pb);
+            if (wdc->delay < wdc->min_delay)
+                wdc->delay = wdc->default_delay;
+            wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
+            duration += wdc->delay;
+            nb_frames++;
+            avio_skip(pb, chunk_size - 15);
+            break;
+        default:
+            avio_skip(pb, chunk_size);
+        }
+
+        if (avio_feof(pb))
+            break;
+
+        if (st->codecpar->width == 0 && width > 0)
+            st->codecpar->width = width;
+        if (st->codecpar->height == 0 && height > 0)
+            st->codecpar->height = height;
+    }
+
+    /* WebP format operates with time in "milliseconds",
+     * therefore timebase is 1/1000 */
+    avpriv_set_pts_info(st, 64, 1, 1000);
+    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    st->codecpar->codec_id   = AV_CODEC_ID_WEBP;
+    st->start_time           = 0;
+    st->duration             = duration;
+    st->nb_frames            = nb_frames;
+
+    /* jump to start because WebP decoder needs header data too */
+    if (avio_seek(pb, 0, SEEK_SET) != 0)
+        return AVERROR(EIO);
+    wdc->remaining_size = 0;
+
+    return 0;
+}
+
+static int webp_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+    AVIOContext *pb = s->pb;
+    int ret;
+    int64_t frame_start = avio_tell(pb), frame_end;
+    uint32_t chunk_type, chunk_size;
+    int is_frame = 0;
+
+    if (wdc->remaining_size == 0) {
+        ret = resync(s);
+        if (ret == AVERROR_EOF) {
+            if (!wdc->ignore_loop && avio_feof(pb)
+                && (wdc->total_iter < 0 || ++wdc->iter_count < wdc->total_iter))
+                return avio_seek(pb, 0, SEEK_SET);
+            return AVERROR_EOF;
+        }
+        if (ret < 0)
+            return ret;
+
+        wdc->delay  = wdc->default_delay;
+    }
+
+    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 (wdc->remaining_size < 8 + chunk_size)
+            return AVERROR_INVALIDDATA;
+        wdc->remaining_size -= 8 + chunk_size;
+
+        switch (chunk_type) {
+            case MKTAG('A', 'N', 'I', 'M'):
+                avio_skip(pb, 4);
+                wdc->total_iter = avio_rl16(pb);
+                if (wdc->total_iter == 0)
+                    wdc->total_iter = -1;
+                ret = avio_skip(pb, chunk_size - 6);
+                break;
+            case MKTAG('A', 'N', 'M', 'F'):
+                avio_skip(pb, 12);
+                wdc->delay = avio_rl24(pb);
+                if (wdc->delay < wdc->min_delay)
+                    wdc->delay = wdc->default_delay;
+                wdc->delay = FFMIN(wdc->delay, wdc->max_delay);
+                wdc->nb_frames++;
+                is_frame = 1;
+                ret = avio_skip(pb, chunk_size - 15);
+                break;
+            case MKTAG('V', 'P', '8', ' '):
+            case MKTAG('V', 'P', '8', 'L'):
+                wdc->nb_frames++;
+                is_frame = 1;
+                /* fallthrough */
+            default:
+                ret = avio_skip(pb, chunk_size);
+                break;
+        }
+
+        if (ret < 0)
+            return ret;
+    }
+
+    frame_end = avio_tell(pb);
+
+    if (avio_seek(pb, frame_start, SEEK_SET) != frame_start)
+        return AVERROR(EIO);
+
+    ret = av_get_packet(pb, pkt, frame_end - frame_start);
+    if (ret < 0)
+        return ret;
+
+    pkt->flags |= AV_PKT_FLAG_KEY;
+    pkt->stream_index = 0;
+    pkt->duration = is_frame ? wdc->delay : 0;
+
+    if (is_frame && wdc->nb_frames == 1) {
+        s->streams[0]->r_frame_rate = (AVRational) {1000, pkt->duration};
+    }
+
+    return ret;
+}
+
+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,
+    .flags          = AVFMT_GENERIC_INDEX,
+    .priv_class     = &demuxer_class,
+};