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 <jo...@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-requ...@ffmpeg.org with subject "unsubscribe".