diff mbox series

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

Message ID 20200729120550.16828-2-josef@pex.com
State New
Headers show
Series [FFmpeg-devel,v2,1/2] libavcodec: add support for animated WebP decoding
Related show

Checks

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

Commit Message

Zlomek, Josef July 29, 2020, 12:05 p.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 and marks the key frames properly.

Signed-off-by: Josef Zlomek <josef@pex.com>
---
 Changelog                |   1 +
 doc/demuxers.texi        |  28 +++
 libavformat/Makefile     |   1 +
 libavformat/allformats.c |   1 +
 libavformat/webpdec.c    | 475 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 506 insertions(+)
 create mode 100644 libavformat/webpdec.c
diff mbox series

Patch

diff --git a/Changelog b/Changelog
index 51268221a9..f42d5f3ea6 100644
--- a/Changelog
+++ b/Changelog
@@ -10,6 +10,7 @@  version <next>:
 - ADPCM IMA Ubisoft APM encoder
 - Rayman 2 APM muxer
 - 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 62d8cbb54e..cf0c19edbc 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -558,6 +558,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 fd9e46e233..7883cb5499 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -456,6 +456,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/webpdec.c b/libavformat/webpdec.c
new file mode 100644
index 0000000000..070aafdd19
--- /dev/null
+++ b/libavformat/webpdec.c
@@ -0,0 +1,475 @@ 
+/*
+ * 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"
+
+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_position;    ///< start position of the current animation file
+    int64_t infinite_loop_start;    ///< start position of the infinite loop
+    int64_t loop_end_position;      ///< end position of the last buffered loop
+
+    int nb_frames;                  ///< number of frames of the current animation file
+    int remaining_size;             ///< remaining size of the current animation file
+
+    int canvas_width;
+    int canvas_height;
+    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 resync(AVFormatContext *s)
+{
+    WebPDemuxContext *wdc = s->priv_data;
+    AVIOContext      *pb  = s->pb;
+    int ret;
+    int i;
+    uint64_t state = 0;
+
+    // the seekback is already guaranteed until loop_end_position, which is the end of the last looped image
+    int ensure_seekback = avio_tell(pb) >= wdc->loop_end_position;
+
+    if (ensure_seekback && (ret = ffio_ensure_seekback(pb, 12)) < 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) {
+            if (ensure_seekback && (ret = ffio_ensure_seekback(pb, 4)) < 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_position = avio_tell(pb) - 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 width = 0, 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)
+        return ret;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st)
+        return AVERROR(ENOMEM);
+
+    while (!is_frame && wdc->remaining_size > 0 && !avio_feof(pb)) {
+        if ((ret = ffio_ensure_seekback(pb, 8)) < 0)
+            return ret;
+
+        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;
+
+        if ((ret = ffio_ensure_seekback(pb, chunk_size)) < 0)
+            return ret;
+
+        switch (chunk_type) {
+        case MKTAG('V', 'P', '8', 'X'):
+            if (chunk_size >= 10) {
+                avio_skip(pb, 4);
+                wdc->canvas_width  = avio_rl24(pb) + 1;
+                wdc->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);
+        }
+
+        if (ret < 0)
+            return ret;
+
+        if (!wdc->canvas_width && width > 0)
+            wdc->canvas_width = width;
+        if (!wdc->canvas_height && height > 0)
+            wdc->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      = wdc->canvas_width;
+    st->codecpar->height     = wdc->canvas_height;
+    st->start_time           = 0;
+
+    // jump to start because WebP decoder needs header data too
+    if ((ret = avio_seek(pb, wdc->file_start_position, SEEK_SET)) < 0)
+        return ret;
+    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, 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 ensure_seekback;
+
+    if (wdc->remaining_size == 0) {
+        // if the loop count is finite, loop the current animation
+        if (avio_tell(pb) != wdc->file_start_position &&
+            !wdc->ignore_loop && wdc->num_loop > 1 && ++wdc->cur_loop < wdc->num_loop) {
+            if ((ret = avio_seek(pb, wdc->file_start_position, 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);
+        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;
+                packet_start = avio_tell(pb);
+                ret = resync(s);
+            } else {
+                return AVERROR_EOF;
+            }
+        }
+        if (ret < 0)
+            return ret;
+
+        // reset variables used for key frame detection
+        wdc->nb_frames       = 0;
+        wdc->prev_width      = width;
+        wdc->prev_height     = height;
+        wdc->prev_anmf_flags = anmf_flags;
+        wdc->prev_key_frame  = key_frame;
+    }
+
+    // the seekback is already guaranteed until loop_end_position, which is the end of the last looped image
+    ensure_seekback = packet_start >= wdc->loop_end_position;
+
+    while (!is_frame && wdc->remaining_size > 0 && !avio_feof(pb)) {
+        if (ensure_seekback && (ret = ffio_ensure_seekback(pb, 8)) < 0)
+            return ret;
+
+        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;
+
+        if (ensure_seekback && (ret = ffio_ensure_seekback(pb, chunk_size)) < 0)
+            return ret;
+
+        switch (chunk_type) {
+        case MKTAG('V', 'P', '8', ' '):
+            if (chunk_size >= 10) {
+                avio_skip(pb, 6);
+                width  = avio_rl16(pb) & 0x3fff;
+                height = avio_rl16(pb) & 0x3fff;
+                wdc->nb_frames++;
+                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
+                has_alpha = (n >> 28) & 1;              // next 1 bit
+                av_log(s, AV_LOG_WARNING, "VP8L %dx%d alpha %d\n",
+                       width, height, has_alpha);
+                wdc->nb_frames++;
+                is_frame = 1;
+                ret = avio_skip(pb, chunk_size - 5);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('A', 'L', 'P', 'H'):
+            has_alpha = 1;
+            ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('A', 'N', 'I', 'M'):
+            if (chunk_size >= 6) {
+                if (wdc->cur_loop >= wdc->num_loop)
+                    wdc->cur_loop = 0;
+
+                avio_skip(pb, 4);
+                wdc->num_loop = avio_rl16(pb);
+                if (!wdc->ignore_loop && wdc->num_loop != 1) {
+                    int64_t end = avio_tell(pb) + chunk_size - 6 + wdc->remaining_size;
+                    if (wdc->loop_end_position < end) {
+                        ret = ffio_ensure_seekback(pb, chunk_size - 6 + wdc->remaining_size);
+                        if (ret < 0)
+                            return ret;
+                        wdc->loop_end_position = end;
+                        ensure_seekback = 0;
+                    }
+                    if (!wdc->num_loop && wdc->infinite_loop_start < 0)
+                        wdc->infinite_loop_start = wdc->file_start_position;
+                }
+                ret = avio_skip(pb, chunk_size - 6);
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        case MKTAG('A', 'N', 'M', 'F'):
+            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
+                wdc->remaining_size += chunk_size - 16;
+                ret = 0;
+            } else
+                ret = avio_skip(pb, chunk_size);
+            break;
+        default:
+            ret = avio_skip(pb, chunk_size);
+            break;
+        }
+
+        if (ret < 0)
+            return ret;
+    }
+
+    packet_end = avio_tell(pb);
+
+    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;
+
+    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 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,
+};