diff --git a/Changelog b/Changelog index 518a469de1..055b0cfa79 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,7 @@ Entries are sorted chronologically from oldest to youngest within each release, releases are sorted from youngest to oldest. version : +- JPEG Multi-Picture Format demuxer version 9.1: - ASTC image encoding and decoding via libastcenc, with .astc and KTX 1.0 muxers and demuxers diff --git a/configure b/configure index 3bb4d9ef80..083bd652c4 100755 --- a/configure +++ b/configure @@ -3928,6 +3928,7 @@ imf_demuxer_select="mxf_demuxer" ipod_muxer_select="mov_muxer" ismv_muxer_select="mov_muxer" ivf_muxer_select="av1_metadata_bsf vp9_superframe_bsf" +jpeg_mpf_demuxer_select="mjpeg_parser" latm_muxer_select="aac_adtstoasc_bsf mpeg4audio" matroska_audio_muxer_select="matroska_muxer" matroska_demuxer_select="riffdec" diff --git a/doc/demuxers.texi b/doc/demuxers.texi index 7610055193..5b56e8b476 100644 --- a/doc/demuxers.texi +++ b/doc/demuxers.texi @@ -746,6 +746,20 @@ ffmpeg -framerate 10 -pattern_type glob -i "*.png" out.mkv @end example @end itemize +@section jpeg_mpf + +JPEG Multi-Picture Format demuxer. + +Exposes each JPEG image stored in a CIPA DC-007 MP file as a separate stream, +in the order of the MP Entries, with the First Individual Image flagged as the +default stream. If the file contains an ISO 21496-1 gain map, as is the case +for JPEG Ultra HDR, the primary image and the gain map are linked by a +@samp{gain_map} stream group, and the gain map parameters are exported as +frame side data by the decoder. + +Images of any other type (e.g. large thumbnails or multi-view images) are +exposed as additional streams. + @section ktx Khronos Texture 1.0 demuxer for ASTC textures. diff --git a/doc/general_contents.texi b/doc/general_contents.texi index 3e0c24e4d1..cfaf4252c6 100644 --- a/doc/general_contents.texi +++ b/doc/general_contents.texi @@ -650,6 +650,8 @@ library: @tab A format used by libvpx @item Internet Video Recording @tab @tab X @item IRCAM @tab X @tab X +@item JPEG Multi-Picture Format @tab X @tab X + @tab Includes JPEG Ultra HDR (ISO 21496-1 gain map) images. @item LAF @tab @tab X @tab Limitless Audio Format @item LATM @tab X @tab X diff --git a/libavformat/Makefile b/libavformat/Makefile index 3c875d2da6..dbf78aa4b9 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -346,6 +346,7 @@ OBJS-$(CONFIG_IVF_MUXER) += ivfenc.o OBJS-$(CONFIG_IVR_DEMUXER) += rmdec.o rm.o rmsipr.o OBJS-$(CONFIG_JACOSUB_DEMUXER) += jacosubdec.o subtitles.o OBJS-$(CONFIG_JACOSUB_MUXER) += jacosubenc.o rawenc.o +OBJS-$(CONFIG_JPEG_MPF_DEMUXER) += jpegmpfdec.o OBJS-$(CONFIG_JPEG_MPF_MUXER) += jpegmpfenc.o OBJS-$(CONFIG_JPEGXL_ANIM_DEMUXER) += jpegxl_anim_dec.o OBJS-$(CONFIG_JV_DEMUXER) += jvdec.o diff --git a/libavformat/allformats.c b/libavformat/allformats.c index c0da275626..a4e859da33 100644 --- a/libavformat/allformats.c +++ b/libavformat/allformats.c @@ -251,6 +251,7 @@ extern const FFInputFormat ff_ivr_demuxer; extern const FFInputFormat ff_jacosub_demuxer; extern const FFOutputFormat ff_jacosub_muxer; extern const FFInputFormat ff_jv_demuxer; +extern const FFInputFormat ff_jpeg_mpf_demuxer; extern const FFOutputFormat ff_jpeg_mpf_muxer; extern const FFInputFormat ff_jpegxl_anim_demuxer; extern const FFInputFormat ff_ktx_demuxer; diff --git a/libavformat/jpegmpf.h b/libavformat/jpegmpf.h index 69a6ab5a18..0cf1a6522e 100644 --- a/libavformat/jpegmpf.h +++ b/libavformat/jpegmpf.h @@ -30,16 +30,37 @@ enum MPFAttribute { MPF_DEPENDENT_PARENT = 0x80000000, MPF_DEPENDENT_CHILD = 0x40000000, + MPF_REPRESENTATIVE = 0x20000000, + MPF_ATTR_FORMAT_MASK = 0x07000000, + MPF_ATTR_TYPE_MASK = 0x00FFFFFF, MPF_FORMAT_JPEG = 0x00000000, }; /* MP Type Codes, from CIPA DC-007 Table 4 */ enum MPFType { - MPF_TYPE_UNDEFINED = 0x000000, - MPF_TYPE_PRIMARY = 0x030000, /* Baseline MP Primary Image */ - MPF_TYPE_GAIN_MAP = 0x050000, + MPF_TYPE_UNDEFINED = 0x000000, + MPF_TYPE_LARGE_THUMBNAIL = 0x010000, /* + class (1-5) */ + MPF_TYPE_MULTI_FRAME = 0x020000, /* + panorama / disparity / multi-angle */ + MPF_TYPE_PRIMARY = 0x030000, /* Baseline MP Primary Image */ + MPF_TYPE_ORIGINAL = 0x040000, /* Original preservation image */ + MPF_TYPE_GAIN_MAP = 0x050000, + + MPF_TYPE_INFO_MASK = 0xFF0000, /* see CIPA DC-007 Figure 9 */ }; +/* Returns whether images of this type only exist as dependent images */ +static inline int mpf_type_is_child(enum MPFType type) +{ + switch (type & MPF_TYPE_INFO_MASK) { + case MPF_TYPE_LARGE_THUMBNAIL: + case MPF_TYPE_ORIGINAL: + case MPF_TYPE_GAIN_MAP: + return 1; + default: + return 0; + } +} + /* MP Index IFD tags, from CIPA DC-007 Table 3 */ enum MPFTag { MPF_TAG_MPFVersion = 0xB000, diff --git a/libavformat/jpegmpfdec.c b/libavformat/jpegmpfdec.c new file mode 100644 index 0000000000..8dad0de780 --- /dev/null +++ b/libavformat/jpegmpfdec.c @@ -0,0 +1,507 @@ +/* + * JPEG Multi-Picture Format demuxer + * + * 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 + * Demuxer for the JPEG Multi-Picture Format file format. + */ + +#include +#include + +#include "libavutil/intreadwrite.h" +#include "libavutil/gain_map.h" +#include "libavutil/mem.h" + +#include "libavcodec/bytestream.h" +#include "libavcodec/exif.h" +#include "libavcodec/mjpeg.h" + +#include "avformat.h" +#include "demux.h" +#include "jpegmpf.h" +#include "internal.h" + +typedef struct MPFDemuxImage { + AVStream *st; + int64_t start; + int64_t size; + + uint32_t type; + int index; /* of the corresponding MP Entry */ + int child; /* is a dependent image */ +} MPFDemuxImage; + +typedef struct MPFDemuxContext { + MPFDemuxImage *images; /* sorted by position in the file */ + int nb_images; + + int cur_image; /* index of next image to return */ + int has_gain_map; /* the primary image carries ISO 21496-1 metadata */ + int passthrough; /* not an MPF file */ + int first_img; /* index of the first individual image */ + + /* image data parsed during mpf_read_header() */ + AVPacket *initial; + int64_t file_start; +} MPFDemuxContext; + +#define CHECK_IDENT(strc) (len >= sizeof(strc) && !memcmp(gb->buffer, strc, sizeof(strc))) + +static int mpf_probe(const AVProbeData *p) +{ + GetByteContext gbc, *gb = &gbc; + bytestream2_init(gb, p->buf, p->buf_size); + if (bytestream2_get_be16(gb) != (0xFF00 | SOI)) + return 0; + + while (bytestream2_get_bytes_left(gb) >= 4) { + if (bytestream2_get_byte(gb) != 0xFF) + return 0; /* not a JPEG marker */ + + int marker = bytestream2_get_byte(gb); + while (marker == 0xFF && bytestream2_get_bytes_left(gb) > 0) /* fill bytes */ + marker = bytestream2_get_byte(gb); + + if (marker == SOS || marker == TEM || (marker >= RST0 && marker <= EOI)) + return 0; /* start of entropy coded data */ + else if (marker == 0xFF || bytestream2_get_bytes_left(gb) < 2) + break; /* need more data */ + + const int len = bytestream2_get_be16(gb) - 2; + if (len < 0) + return 0; + if (len > bytestream2_get_bytes_left(gb)) + break; + + switch (marker) { + case APP2: + /* Marker segments unique to MPF files; the base image of a gain + * map file only carries the version fields of the ISO 21496-1 + * metadata (two 16-bit integers); we don't want to false positive + * on standalone JPEGs that happen to contain gain map metadata */ + if (CHECK_IDENT("MPF") || + (CHECK_IDENT(AV_ISO21496_IDENTIFIER) && + len == sizeof(AV_ISO21496_IDENTIFIER) + sizeof(uint16_t[2]))) + return AVPROBE_SCORE_EXTENSION + 10; + break; + } + + bytestream2_skip(gb, len); + } + + /* If we ran out of probe bytes, we don't know yet whether this is an + * MPF file or not, unless the extension is unique to MPF files */ + if (av_match_ext(p->filename, "mpo")) + return AVPROBE_SCORE_EXTENSION + 5; + + return AVERROR(EAGAIN); +} + +/* returns the offset of the first byte read */ +static int grow_header(AVFormatContext *ctx, int size) +{ + MPFDemuxContext *s = ctx->priv_data; + const int pos = s->initial->size; + int ret = av_append_packet(ctx->pb, s->initial, size); + if (ret < 0) + return ret; + if (ret < size) + return AVERROR_INVALIDDATA; + return pos; +} + +static int parse_mp_index(AVFormatContext *ctx, const uint8_t *data, int len, + int64_t tiff_base) +{ + MPFDemuxContext *s = ctx->priv_data; + AVExifMetadata ifd = { 0 }; + AVExifEntry *num = NULL, *entries = NULL; + int ret; + + if (len < 2) + return AVERROR_INVALIDDATA; + const int le = AV_RB16(data) == AV_RB16("II"); + + ret = av_exif_parse_buffer(ctx, data, len, &ifd, AV_EXIF_TIFF_HEADER); + if (ret < 0) + return ret; + + if ((ret = av_exif_get_entry(ctx, &ifd, MPF_TAG_NumberOfImages, 0, &num)) < 0 || + (ret = av_exif_get_entry(ctx, &ifd, MPF_TAG_MPEntry, 0, &entries)) < 0) + goto fail; + + if (!num || num->type != AV_TIFF_LONG || num->count != 1 || + !entries || entries->type != AV_TIFF_UNDEFINED) { + av_log(ctx, AV_LOG_ERROR, "Missing or malformed MP Index IFD\n"); + ret = AVERROR_INVALIDDATA; + goto fail; + } + + /* The spec asserts MPEntry count = 16 * NumberOfImages, but tolerate + * excess entries, e.g. left behind after deleting an image */ + const int entry_size = sizeof(uint32_t[3]) + sizeof(uint16_t[2]); + const uint64_t nb_images = num->value.uint[0]; + if (!nb_images || nb_images * entry_size > entries->count) { + av_log(ctx, AV_LOG_ERROR, "Invalid number of images: %"PRIu64"\n", + nb_images); + ret = AVERROR_INVALIDDATA; + goto fail; + } + + s->images = av_calloc(nb_images, sizeof(*s->images)); + if (!s->images) { + ret = AVERROR(ENOMEM); + goto fail; + } + + GetByteContext gb; + bytestream2_init(&gb, entries->value.ubytes, entries->count); + + const int explode = ctx->error_recognition & AV_EF_EXPLODE; + for (int i = 0; i < nb_images; i++) { + const uint32_t attr = le ? bytestream2_get_le32u(&gb) : bytestream2_get_be32u(&gb); + const uint32_t size = le ? bytestream2_get_le32u(&gb) : bytestream2_get_be32u(&gb); + const uint32_t offset = le ? bytestream2_get_le32u(&gb) : bytestream2_get_be32u(&gb); + bytestream2_skipu(&gb, sizeof(uint16_t[2])); /* dependent images */ + + if ((attr & MPF_ATTR_FORMAT_MASK) != MPF_FORMAT_JPEG) { + const int fatal = !offset || explode; + av_log(ctx, fatal ? AV_LOG_ERROR : AV_LOG_WARNING, "Image %d: " + "unsupported format 0x%"PRIx32"\n", i, + attr & MPF_ATTR_FORMAT_MASK); + if (fatal) { + ret = AVERROR_INVALIDDATA; + goto fail; + } + continue; + } + + /* the First Individual Image is identified by an offset of 0 */ + if (!offset) { + if (s->first_img >= 0) { + av_log(ctx, AV_LOG_ERROR, "Duplicate First Individual Image\n"); + ret = AVERROR_INVALIDDATA; + goto fail; + } + s->first_img = s->nb_images; + } + + MPFDemuxImage *img = &s->images[s->nb_images++]; + img->index = i; + img->start = offset ? tiff_base + offset : s->file_start; + img->size = size; + img->type = attr & MPF_ATTR_TYPE_MASK; + img->child = !!(attr & MPF_DEPENDENT_CHILD); + } + + ret = 0; +fail: + av_exif_free(&ifd); + return ret; +} + +static int parse_app2(AVFormatContext *ctx, int offset, int len) +{ + MPFDemuxContext *s = ctx->priv_data; + GetByteContext gbc, *gb = &gbc; + bytestream2_init(gb, s->initial->data + offset, len); + + if (CHECK_IDENT(AV_ISO21496_IDENTIFIER)) { + s->has_gain_map = 1; + } else if (CHECK_IDENT("MPF")) { + if (s->images) { + const int explode = ctx->error_recognition & AV_EF_EXPLODE; + av_log(ctx, explode ? AV_LOG_ERROR : AV_LOG_WARNING, + "Duplicate MP Index IFD\n"); + return explode ? AVERROR_INVALIDDATA : 0; + } + + /* MP offsets are relative to the start of the TIFF header */ + const int skip = sizeof("MPF"); + return parse_mp_index(ctx, gb->buffer + skip, len - skip, + s->file_start + offset + skip); + } + + return 0; +} + +static int cmp_image_start(const void *a, const void *b) +{ + const MPFDemuxImage *ia = a, *ib = b; + if (ia->start != ib->start) + return FFDIFFSIGN(ia->start, ib->start); + return FFDIFFSIGN(ia->index, ib->index); +} + +static int add_gain_map_group(AVFormatContext *ctx, const MPFDemuxImage *base, + const MPFDemuxImage *gain) +{ + if (gain == base) { + av_log(ctx, AV_LOG_ERROR, "The first image cannot be a gain map\n"); + return AVERROR_INVALIDDATA; + } + + AVStreamGroup *stg = avformat_stream_group_create(ctx, AV_STREAM_GROUP_PARAMS_GAIN_MAP, NULL); + if (!stg) + return AVERROR(ENOMEM); + + int ret; + if ((ret = avformat_stream_group_add_stream(stg, base->st)) < 0 || + (ret = avformat_stream_group_add_stream(stg, gain->st)) < 0) + return ret; + + stg->params.layered_video->el_index = stg->nb_entries - 1; + return 0; +} + +static int mpf_read_header(AVFormatContext *ctx) +{ + MPFDemuxContext *s = ctx->priv_data; + int ret; + + s->first_img = -1; + s->initial = av_packet_alloc(); + if (!s->initial) + return AVERROR(ENOMEM); + + s->file_start = avio_tell(ctx->pb); + if ((ret = grow_header(ctx, 2)) < 0) + return ret; + if (AV_RB16(s->initial->data) != (0xFF00 | SOI)) + return AVERROR_INVALIDDATA; + + /* Walk the JPEG marker sections up until the start of entropy coded data */ + for (;;) { + const int pos = s->initial->size; + if ((ret = grow_header(ctx, 1)) < 0) + return ret; + uint8_t marker = s->initial->data[ret]; + if (marker != 0xFF) { + av_log(ctx, AV_LOG_ERROR, "Malformed JPEG marker at 0x%x\n", pos); + return AVERROR_INVALIDDATA; + } + + while (marker == 0xFF) { + if ((ret = grow_header(ctx, 1)) < 0) + return ret; + marker = s->initial->data[ret]; + } + + if (marker == SOS || marker == TEM || (marker >= RST0 && marker <= EOI)) + break; /* start of entropy coded data */ + + if ((ret = grow_header(ctx, 2)) < 0) + return ret; + const int len = AV_RB16(s->initial->data + ret) - 2; + if (len < 0) { + av_log(ctx, AV_LOG_ERROR, "Invalid marker segment at 0x%x\n", pos); + return AVERROR_INVALIDDATA; + } + + if ((ret = grow_header(ctx, len)) < 0) + return ret; + if (marker == APP2 && (ret = parse_app2(ctx, ret, len)) < 0) + return ret; + } + + if (!s->images) { + /* Not an MPF file, just return as-is to the underlying decoder */ + const int64_t file_size = avio_size(ctx->pb); + s->passthrough = file_size <= s->file_start; + s->first_img = 0; + s->nb_images = 1; + s->images = av_malloc(sizeof(*s->images)); + if (!s->images) + return AVERROR(ENOMEM); + s->images[0] = (MPFDemuxImage){ + .type = MPF_TYPE_PRIMARY, + .start = s->file_start, + .size = s->passthrough ? s->initial->size + : file_size - s->file_start, + }; + } + + if (s->first_img < 0) { + av_log(ctx, AV_LOG_ERROR, "No MP Entry for the first image\n"); + return AVERROR_INVALIDDATA; + } + + /* Compatibility hack for libultrahdr, which produces invalid MPF files + * whose gain maps have no MP type. Needed to support a large number + * of JPEG Ultra HDR images in the wild */ + const MPFDemuxImage *first = &s->images[s->first_img]; + if (ctx->strict_std_compliance < FF_COMPLIANCE_STRICT && + s->has_gain_map && s->nb_images == 2) + { + MPFDemuxImage *other = &s->images[1 - s->first_img]; + if (other->type == MPF_TYPE_UNDEFINED) + other->type = MPF_TYPE_GAIN_MAP; + } + + for (int i = 0; i < s->nb_images; i++) { + s->images[i].st = avformat_new_stream(ctx, NULL); + if (!s->images[i].st) + return AVERROR(ENOMEM); + } + + for (int i = 0; i < s->nb_images; i++) { + MPFDemuxImage *img = &s->images[i]; + AVStream *st = img->st; + + st->id = img->index; + st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + st->codecpar->codec_id = AV_CODEC_ID_MJPEG; + if (img == first) + st->disposition |= AV_DISPOSITION_DEFAULT; + if (img->child || mpf_type_is_child(img->type)) + st->disposition |= AV_DISPOSITION_DEPENDENT; + avpriv_set_pts_info(st, 64, 1, 25); + if (s->passthrough) /* we don't know the image size */ + ffstream(st)->need_parsing = AVSTREAM_PARSE_FULL; + + switch (img->type & MPF_TYPE_INFO_MASK) { + case MPF_TYPE_GAIN_MAP: + ret = add_gain_map_group(ctx, first, img); + if (ret < 0) + return ret; + break; + case MPF_TYPE_LARGE_THUMBNAIL: + case MPF_TYPE_MULTI_FRAME: + case MPF_TYPE_PRIMARY: + case MPF_TYPE_ORIGINAL: + /* no special handling */ + break; + case MPF_TYPE_UNDEFINED: + default: + av_log(ctx, img->child ? AV_LOG_VERBOSE : AV_LOG_WARNING, + "Image %d: unhandled MP type 0x%06"PRIx32", treating as an " + "%s image\n", img->index, img->type, + img->child ? "unspecified dependent" : "unrelated"); + break; + } + } + + /* Sort the images array by file offset for mpf_read_packet() */ + qsort(s->images, s->nb_images, sizeof(*s->images), cmp_image_start); + s->first_img = 0; /* by construction */ + + for (int i = 0; i < s->nb_images - 1; i++) { + MPFDemuxImage *img = &s->images[i], *next = &s->images[i + 1]; + if (next->start < img->start + img->size) { + const int explode = ctx->error_recognition & AV_EF_EXPLODE; + av_log(ctx, explode ? AV_LOG_ERROR : AV_LOG_WARNING, + "Image %d overlaps image %d%s\n", img->index, next->index, + explode ? "" : ", truncating"); + if (explode) + return AVERROR_INVALIDDATA; + img->size = next->start - img->start; + } + } + + return 0; +} + +static int mpf_read_packet(AVFormatContext *ctx, AVPacket *pkt) +{ + MPFDemuxContext *s = ctx->priv_data; + AVIOContext *pb = ctx->pb; + int ret; + + if (s->cur_image >= s->nb_images) { + if (!s->passthrough) + return AVERROR_EOF; + + if ((ret = av_get_packet(pb, pkt, 4096)) < 0) + return ret; + pkt->flags &= ~AV_PKT_FLAG_CORRUPT; /* short read at EOF */ + pkt->stream_index = 0; + return 0; + } + + const MPFDemuxImage *img = &s->images[s->cur_image++]; + if (img->size > INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE) + return AVERROR(ERANGE); + + if (s->initial) { + av_packet_move_ref(pkt, s->initial); + av_packet_free(&s->initial); + av_shrink_packet(pkt, FFMIN(pkt->size, img->size)); + } else { + int64_t pos = avio_seek(pb, img->start, SEEK_SET); + if (pos < 0) { + av_log(ctx, AV_LOG_ERROR, "Failed seeking to image %d at offset " + "%"PRId64"\n", img->st->id, img->start); + return pos; + } + } + + if (pkt->size < img->size) { + ret = av_append_packet(pb, pkt, img->size - pkt->size); + if (ret < 0 && !pkt->size) + return ret; + } + + pkt->stream_index = img->st->index; + pkt->pts = pkt->dts = 0; + pkt->duration = 1; + pkt->flags |= AV_PKT_FLAG_KEY; + return 0; +} + +static int mpf_read_seek(AVFormatContext *ctx, int stream_index, + int64_t timestamp, int flags) +{ + MPFDemuxContext *s = ctx->priv_data; + if (s->passthrough || timestamp != 0) + return -1; + + /* Don't seek if the initial packet is still pending; since this would + * desynchronize the input stream otherwise */ + if (!s->initial) { + int64_t pos = avio_seek(ctx->pb, s->images[0].start, SEEK_SET); + if (pos < 0) + return pos; + } + + s->cur_image = 0; + return 0; +} + +static int mpf_read_close(AVFormatContext *ctx) +{ + MPFDemuxContext *s = ctx->priv_data; + av_freep(&s->images); + av_packet_free(&s->initial); + return 0; +} + +const FFInputFormat ff_jpeg_mpf_demuxer = { + .p.name = "jpeg_mpf", + .p.long_name = NULL_IF_CONFIG_SMALL("JPEG Multi-Picture Format"), + .p.extensions = "mpo", + .priv_data_size = sizeof(MPFDemuxContext), + .flags_internal = FF_INFMT_FLAG_INIT_CLEANUP | FF_INFMT_FLAG_ID3V2_AUTO, + .read_probe = mpf_probe, + .read_header = mpf_read_header, + .read_packet = mpf_read_packet, + .read_close = mpf_read_close, + .read_seek = mpf_read_seek, +}; diff --git a/libavformat/version.h b/libavformat/version.h index f45b935203..eaf74946de 100644 --- a/libavformat/version.h +++ b/libavformat/version.h @@ -31,8 +31,8 @@ #include "version_major.h" -#define LIBAVFORMAT_VERSION_MINOR 14 -#define LIBAVFORMAT_VERSION_MICRO 102 +#define LIBAVFORMAT_VERSION_MINOR 15 +#define LIBAVFORMAT_VERSION_MICRO 100 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \ LIBAVFORMAT_VERSION_MINOR, \