[FFmpeg-devel] [PATCH 2/2] libavformat: add WebP demuxer
Pascal Massimino
pascal.massimino at gmail.com
Thu Jul 9 13:28:00 EEST 2020
Hi,
On Thu, Jul 9, 2020 at 11:44 AM Nicolas George <george at 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 at 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
> >
> > + at section webp
> > +
> > +Animated WebP demuxer.
> > +
> > +It accepts the following options:
> > +
> > + at table @option
>
> > + at item min_delay
> > +Set the minimum valid delay between frames in milliseconds.
> > +Range is 0 to 60000. Default value is 10.
> > +
> > + at 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.
> > +
> > + at 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.
>
> > +
> > + at 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?
>
> > + at 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 at ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request at ffmpeg.org with subject "unsubscribe".
More information about the ffmpeg-devel
mailing list