From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from ffbox0-bg.ffmpeg.org (ffbox0-bg.ffmpeg.org [79.124.17.100]) by master.gitmailbox.com (Postfix) with ESMTPS id 00C4E4BA87 for ; Sat, 23 Aug 2025 19:24:59 +0000 (UTC) Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.ffmpeg.org (Postfix) with ESMTP id 456ED68D20D; Sat, 23 Aug 2025 22:24:55 +0300 (EEST) Received: from 0f4167fb2350 (code.ffmpeg.org [188.245.149.3]) by ffbox0-bg.ffmpeg.org (Postfix) with ESMTPS id 2B441680158 for ; Sat, 23 Aug 2025 22:24:53 +0300 (EEST) MIME-Version: 1.0 To: ffmpeg-devel@ffmpeg.org Subject: [FFmpeg-devel] =?utf-8?q?=5BPATCH=5D_avformat=3A_add_container_l?= =?utf-8?q?evel_Exif_metadata_support_=28PR_=2320321=29?= X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: James Almer via ffmpeg-devel Reply-To: FFmpeg development discussions and patches Cc: James Almer Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" Message-Id: <20250823192455.456ED68D20D@ffbox0-bg.ffmpeg.org> Date: Sat, 23 Aug 2025 22:24:55 +0300 (EEST) Archived-At: List-Archive: List-Post: PR #20321 opened by James Almer (jamrial) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/20321 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/20321.patch Includes Exif packet side data, and starting with support in Heif. >From 962fea2d2cb56450d5325969cb655543895d4885 Mon Sep 17 00:00:00 2001 From: James Almer Date: Wed, 20 Aug 2025 13:23:05 -0300 Subject: [PATCH 01/11] avcodec/packet: add an Exif side data type Signed-off-by: James Almer --- libavcodec/avcodec.c | 1 + libavcodec/options_table.h | 1 + libavcodec/packet.c | 1 + libavcodec/packet.h | 6 ++++++ 4 files changed, 9 insertions(+) diff --git a/libavcodec/avcodec.c b/libavcodec/avcodec.c index 834b7ad242..2181439113 100644 --- a/libavcodec/avcodec.c +++ b/libavcodec/avcodec.c @@ -66,6 +66,7 @@ const SideDataMap ff_sd_global_map[] = { { AV_PKT_DATA_ICC_PROFILE, AV_FRAME_DATA_ICC_PROFILE }, { AV_PKT_DATA_AMBIENT_VIEWING_ENVIRONMENT,AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT }, { AV_PKT_DATA_3D_REFERENCE_DISPLAYS, AV_FRAME_DATA_3D_REFERENCE_DISPLAYS }, + { AV_PKT_DATA_EXIF, AV_FRAME_DATA_EXIF }, { AV_PKT_DATA_NB }, }; diff --git a/libavcodec/options_table.h b/libavcodec/options_table.h index 25da169343..43aec402ba 100644 --- a/libavcodec/options_table.h +++ b/libavcodec/options_table.h @@ -406,6 +406,7 @@ static const AVOption avcodec_options[] = { {"mastering_display_metadata", .default_val.i64 = AV_PKT_DATA_MASTERING_DISPLAY_METADATA, .type = AV_OPT_TYPE_CONST, .flags = A|D, .unit = "side_data_pkt" }, {"content_light_level", .default_val.i64 = AV_PKT_DATA_CONTENT_LIGHT_LEVEL, .type = AV_OPT_TYPE_CONST, .flags = A|D, .unit = "side_data_pkt" }, {"icc_profile", .default_val.i64 = AV_PKT_DATA_ICC_PROFILE, .type = AV_OPT_TYPE_CONST, .flags = A|D, .unit = "side_data_pkt" }, + {"exif", .default_val.i64 = AV_PKT_DATA_EXIF, .type = AV_OPT_TYPE_CONST, .flags = A|D, .unit = "side_data_pkt" }, {NULL}, }; diff --git a/libavcodec/packet.c b/libavcodec/packet.c index 2d1f282927..9420c10be0 100644 --- a/libavcodec/packet.c +++ b/libavcodec/packet.c @@ -310,6 +310,7 @@ const char *av_packet_side_data_name(enum AVPacketSideDataType type) case AV_PKT_DATA_LCEVC: return "LCEVC NAL data"; case AV_PKT_DATA_3D_REFERENCE_DISPLAYS: return "3D Reference Displays Info"; case AV_PKT_DATA_RTCP_SR: return "RTCP Sender Report"; + case AV_PKT_DATA_EXIF: return "EXIF metadata"; } return NULL; } diff --git a/libavcodec/packet.h b/libavcodec/packet.h index 55389a957d..5e27f9ceb5 100644 --- a/libavcodec/packet.h +++ b/libavcodec/packet.h @@ -362,6 +362,12 @@ enum AVPacketSideDataType { */ AV_PKT_DATA_RTCP_SR, + /** + * Extensible image file format metadata. The payload is a buffer containing + * EXIF metadata, starting with either 49 49 2a 00, or 4d 4d 00 2a. + */ + AV_PKT_DATA_EXIF, + /** * The number of side data types. * This is not part of the public API/ABI in the sense that it may -- 2.49.1 >From 01896153bb13272cb6e1f1fb3c95b47a6c79771c Mon Sep 17 00:00:00 2001 From: James Almer Date: Wed, 20 Aug 2025 22:00:33 -0300 Subject: [PATCH 02/11] ffprobe: print EXIF packet side data size Signed-off-by: James Almer --- fftools/ffprobe.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fftools/ffprobe.c b/fftools/ffprobe.c index 018111318e..f4eb87aefa 100644 --- a/fftools/ffprobe.c +++ b/fftools/ffprobe.c @@ -1063,6 +1063,8 @@ static void print_pkt_side_data(AVTextFormatContext *tfc, print_int("crop_right", AV_RL32(sd->data + 12)); } else if (sd->type == AV_PKT_DATA_AFD && sd->size > 0) { print_int("active_format", *sd->data); + } else if (sd->type == AV_PKT_DATA_EXIF) { + print_int("size", sd->size); } } -- 2.49.1 >From 3e4e14d3e80ae61cb7d7c9c8f5e60e786eefd87c Mon Sep 17 00:00:00 2001 From: James Almer Date: Wed, 20 Aug 2025 13:23:49 -0300 Subject: [PATCH 03/11] avformat/dump: print side data type names generically Based on vf_showinfo behavior. Signed-off-by: James Almer --- libavformat/dump.c | 45 +++++++++++++++------------------------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/libavformat/dump.c b/libavformat/dump.c index 02f69f9a3a..dfa33fd3c9 100644 --- a/libavformat/dump.c +++ b/libavformat/dump.c @@ -342,7 +342,7 @@ static void dump_mastering_display_metadata(void *ctx, const AVPacketSideData *s { const AVMasteringDisplayMetadata *metadata = (const AVMasteringDisplayMetadata *)sd->data; - av_log(ctx, log_level, "Mastering Display Metadata, " + av_log(ctx, log_level, "has_primaries:%d has_luminance:%d " "r(%5.4f,%5.4f) g(%5.4f,%5.4f) b(%5.4f %5.4f) wp(%5.4f, %5.4f) " "min_luminance=%f, max_luminance=%f", @@ -362,7 +362,7 @@ static void dump_content_light_metadata(void *ctx, const AVPacketSideData *sd, { const AVContentLightMetadata *metadata = (const AVContentLightMetadata *)sd->data; - av_log(ctx, log_level, "Content Light Level Metadata, " + av_log(ctx, log_level, "MaxCLL=%d, MaxFALL=%d", metadata->MaxCLL, metadata->MaxFALL); } @@ -371,7 +371,7 @@ static void dump_ambient_viewing_environment_metadata(void *ctx, const AVPacketS { const AVAmbientViewingEnvironment *ambient = (const AVAmbientViewingEnvironment *)sd->data; - av_log(ctx, AV_LOG_INFO, "Ambient Viewing Environment, " + av_log(ctx, AV_LOG_INFO, "ambient_illuminance=%f, ambient_light_x=%f, ambient_light_y=%f", av_q2d(ambient->ambient_illuminance), av_q2d(ambient->ambient_light_x), @@ -481,81 +481,66 @@ static void dump_sidedata(void *ctx, const AVPacketSideData *side_data, int nb_s for (i = 0; i < nb_side_data; i++) { const AVPacketSideData *sd = &side_data[i]; - av_log(ctx, log_level, "%s ", indent); + const char *name = av_packet_side_data_name(sd->type); + av_log(ctx, log_level, "%s ", indent); + if (name) + av_log(ctx, AV_LOG_INFO, "%s: ", name); switch (sd->type) { - case AV_PKT_DATA_PALETTE: - av_log(ctx, log_level, "palette"); - break; - case AV_PKT_DATA_NEW_EXTRADATA: - av_log(ctx, log_level, "new extradata"); - break; case AV_PKT_DATA_PARAM_CHANGE: - av_log(ctx, log_level, "paramchange: "); dump_paramchange(ctx, sd, log_level); break; - case AV_PKT_DATA_H263_MB_INFO: - av_log(ctx, log_level, "H.263 macroblock info"); - break; case AV_PKT_DATA_REPLAYGAIN: - av_log(ctx, log_level, "replaygain: "); dump_replaygain(ctx, sd, log_level); break; case AV_PKT_DATA_DISPLAYMATRIX: - av_log(ctx, log_level, "displaymatrix: rotation of %.2f degrees", + av_log(ctx, log_level, "rotation of %.2f degrees", av_display_rotation_get((const int32_t *)sd->data)); break; case AV_PKT_DATA_STEREO3D: - av_log(ctx, log_level, "stereo3d: "); dump_stereo3d(ctx, sd, log_level); break; case AV_PKT_DATA_AUDIO_SERVICE_TYPE: - av_log(ctx, log_level, "audio service type: "); dump_audioservicetype(ctx, sd, log_level); break; case AV_PKT_DATA_QUALITY_STATS: - av_log(ctx, log_level, "quality factor: %"PRId32", pict_type: %c", + av_log(ctx, log_level, "%"PRId32", pict_type: %c", AV_RL32(sd->data), av_get_picture_type_char(sd->data[4])); break; case AV_PKT_DATA_CPB_PROPERTIES: - av_log(ctx, log_level, "cpb: "); dump_cpb(ctx, sd, log_level); break; case AV_PKT_DATA_MASTERING_DISPLAY_METADATA: dump_mastering_display_metadata(ctx, sd, log_level); break; case AV_PKT_DATA_SPHERICAL: - av_log(ctx, log_level, "spherical: "); dump_spherical(ctx, w, h, sd, log_level); break; case AV_PKT_DATA_CONTENT_LIGHT_LEVEL: dump_content_light_metadata(ctx, sd, log_level); break; - case AV_PKT_DATA_ICC_PROFILE: - av_log(ctx, log_level, "ICC Profile"); - break; case AV_PKT_DATA_DOVI_CONF: - av_log(ctx, log_level, "DOVI configuration record: "); dump_dovi_conf(ctx, sd, log_level); break; case AV_PKT_DATA_S12M_TIMECODE: - av_log(ctx, log_level, "SMPTE ST 12-1:2014: "); dump_s12m_timecode(ctx, avg_frame_rate, sd, log_level); break; case AV_PKT_DATA_AMBIENT_VIEWING_ENVIRONMENT: dump_ambient_viewing_environment_metadata(ctx, sd); break; case AV_PKT_DATA_FRAME_CROPPING: - av_log(ctx, AV_LOG_INFO, "Frame cropping: "); dump_cropping(ctx, sd); break; case AV_PKT_DATA_3D_REFERENCE_DISPLAYS: - av_log(ctx, log_level, "3D Reference Displays Information: "); dump_tdrdi(ctx, sd); break; default: - av_log(ctx, log_level, "unknown side data type %d " - "(%"SIZE_SPECIFIER" bytes)", sd->type, sd->size); + if (name) + av_log(ctx, log_level, + "(%"SIZE_SPECIFIER" bytes)", sd->size); + else + av_log(ctx, log_level, "unknown side data type %d " + "(%"SIZE_SPECIFIER" bytes)", sd->type, sd->size); break; } -- 2.49.1 >From 7b6874c4395f69b1b4bd130b4b188a6a52a1fae2 Mon Sep 17 00:00:00 2001 From: James Almer Date: Wed, 20 Aug 2025 21:59:25 -0300 Subject: [PATCH 04/11] tests/mov: also print stream side data in fate-mov-heic-demux-still-image-multiple-thumb This is in preparation for a following commit. Signed-off-by: James Almer --- tests/fate/mov.mak | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fate/mov.mak b/tests/fate/mov.mak index 421845969b..21c155ff2a 100644 --- a/tests/fate/mov.mak +++ b/tests/fate/mov.mak @@ -188,7 +188,7 @@ fate-mov-heic-demux-clap-irot-imir: CMD = stream_demux mov $(TARGET_SAMPLES)/hei FATE_MOV_FFMPEG_FFPROBE_SAMPLES-$(call FRAMECRC, MOV, HEVC MJPEG, HEVC_PARSER) \ += fate-mov-heic-demux-still-image-multiple-thumb fate-mov-heic-demux-still-image-multiple-thumb: CMD = stream_demux mov $(TARGET_SAMPLES)/heif/P1001091.HIF "" "-c:v copy -map 0" \ - "-show_entries stream=index,id:stream_disposition" + "-show_entries stream=index,id:stream_disposition:stream_side_data_list" # heic demuxing - still image with multiple items in a grid. FATE_MOV_FFMPEG_FFPROBE_SAMPLES-$(call FRAMECRC, MOV, HEVC, HEVC_PARSER) \ -- 2.49.1 >From db113a531f1a16906888285edec82e8cbe912d89 Mon Sep 17 00:00:00 2001 From: James Almer Date: Sat, 23 Aug 2025 11:04:48 -0300 Subject: [PATCH 05/11] avformat/mov: reduce code duplication when setting tile group properties Signed-off-by: James Almer --- libavformat/mov.c | 46 +++++++++++++++------------------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/libavformat/mov.c b/libavformat/mov.c index e9a582e5aa..c846e0d4e8 100644 --- a/libavformat/mov.c +++ b/libavformat/mov.c @@ -10188,21 +10188,6 @@ static int read_image_grid(AVFormatContext *s, const HEIFGrid *grid, tile_grid->width = (flags & 1) ? avio_rb32(s->pb) : avio_rb16(s->pb); tile_grid->height = (flags & 1) ? avio_rb32(s->pb) : avio_rb16(s->pb); - /* ICC profile */ - if (item->icc_profile_size) { - int ret = set_icc_profile_from_item(&tile_grid->coded_side_data, - &tile_grid->nb_coded_side_data, item); - if (ret < 0) - return ret; - } - /* rotation */ - if (item->rotation || item->hflip || item->vflip) { - int ret = set_display_matrix_from_item(&tile_grid->coded_side_data, - &tile_grid->nb_coded_side_data, item); - if (ret < 0) - return ret; - } - av_log(c->fc, AV_LOG_TRACE, "grid: grid_rows %d grid_cols %d output_width %d output_height %d\n", tile_rows, tile_cols, tile_grid->width, tile_grid->height); @@ -10294,22 +10279,6 @@ static int read_image_iovl(AVFormatContext *s, const HEIFGrid *grid, tile_grid->height = tile_grid->coded_height = (flags & 1) ? avio_rb32(s->pb) : avio_rb16(s->pb); - /* rotation */ - if (item->rotation || item->hflip || item->vflip) { - int ret = set_display_matrix_from_item(&tile_grid->coded_side_data, - &tile_grid->nb_coded_side_data, item); - if (ret < 0) - return ret; - } - - /* ICC profile */ - if (item->icc_profile_size) { - int ret = set_icc_profile_from_item(&tile_grid->coded_side_data, - &tile_grid->nb_coded_side_data, item); - if (ret < 0) - return ret; - } - av_log(c->fc, AV_LOG_TRACE, "iovl: output_width %d, output_height %d\n", tile_grid->width, tile_grid->height); @@ -10421,6 +10390,21 @@ static int mov_parse_tiles(AVFormatContext *s) if (err < 0) return err; + /* rotation */ + if (grid->item->rotation || grid->item->hflip || grid->item->vflip) { + err = set_display_matrix_from_item(&tile_grid->coded_side_data, + &tile_grid->nb_coded_side_data, grid->item); + if (err < 0) + return err; + } + + /* ICC profile */ + if (grid->item->icc_profile_size) { + err = set_icc_profile_from_item(&tile_grid->coded_side_data, + &tile_grid->nb_coded_side_data, grid->item); + if (err < 0) + return err; + } if (grid->item->name) av_dict_set(&stg->metadata, "title", grid->item->name, 0); -- 2.49.1 >From a62d48c823cc7e81ff5b94caaa07db36cbf01d5f Mon Sep 17 00:00:00 2001 From: James Almer Date: Wed, 20 Aug 2025 17:07:50 -0300 Subject: [PATCH 06/11] avformat/mov: make items referencing items generic Signed-off-by: James Almer --- libavformat/isom.h | 9 +++++-- libavformat/mov.c | 67 +++++++++++++++++++++------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/libavformat/isom.h b/libavformat/isom.h index 94c9c65989..1329cb9188 100644 --- a/libavformat/isom.h +++ b/libavformat/isom.h @@ -286,8 +286,15 @@ typedef struct MOVStreamContext { int iamf_stream_offset; } MOVStreamContext; +typedef struct HEIFItemRef { + unsigned type; + int item_id; +} HEIFItemRef; + typedef struct HEIFItem { AVStream *st; + HEIFItemRef *iref_list; + int nb_iref_list; char *name; int item_id; int64_t extent_length; @@ -376,8 +383,6 @@ typedef struct MOVContext { int nb_heif_item; HEIFGrid *heif_grid; int nb_heif_grid; - int* thmb_item_id; - int nb_thmb_item; int64_t idat_offset; int interleaved_read; } MOVContext; diff --git a/libavformat/mov.c b/libavformat/mov.c index c846e0d4e8..2944daf97c 100644 --- a/libavformat/mov.c +++ b/libavformat/mov.c @@ -188,14 +188,14 @@ static int mov_read_mac_string(MOVContext *c, AVIOContext *pb, int len, } /** - * Get the current item in the parsing process. + * Get the requested item. */ -static HEIFItem *heif_cur_item(MOVContext *c) +static HEIFItem *get_heif_item(MOVContext *c, unsigned id) { HEIFItem *item = NULL; for (int i = 0; i < c->nb_heif_item; i++) { - if (!c->heif_item[i] || c->heif_item[i]->item_id != c->cur_item_id) + if (!c->heif_item[i] || c->heif_item[i]->item_id != id) continue; item = c->heif_item[i]; @@ -220,7 +220,7 @@ static AVStream *get_curr_st(MOVContext *c) if (c->cur_item_id == -1) return c->fc->streams[c->fc->nb_streams-1]; - item = heif_cur_item(c); + item = get_heif_item(c, c->cur_item_id); if (item) st = item->st; @@ -1245,7 +1245,7 @@ static int mov_read_clap(MOVContext *c, AVIOContext *pb, MOVAtom atom) AVRational pc_x, pc_y; uint64_t top, bottom, left, right; - item = heif_cur_item(c); + item = get_heif_item(c, c->cur_item_id); st = get_curr_st(c); if (!st) return 0; @@ -2078,7 +2078,7 @@ static int mov_read_colr(MOVContext *c, AVIOContext *pb, MOVAtom atom) st = get_curr_st(c); if (!st) { - item = heif_cur_item(c); + item = get_heif_item(c, c->cur_item_id); if (!item) return 0; } @@ -9087,30 +9087,33 @@ static int mov_read_iref_dimg(MOVContext *c, AVIOContext *pb, int version) static int mov_read_iref_thmb(MOVContext *c, AVIOContext *pb, int version) { - int *thmb_item_id; + HEIFItem *from_item = NULL; int entries; - int to_item_id, from_item_id = version ? avio_rb32(pb) : avio_rb16(pb); + int from_item_id = version ? avio_rb32(pb) : avio_rb16(pb); + const HEIFItemRef ref = { MKTAG('t','h','m','b'), from_item_id }; + + from_item = get_heif_item(c, from_item_id); + if (!from_item) { + av_log(c->fc, AV_LOG_ERROR, "Missing stream referenced by thmb item\n"); + return AVERROR_INVALIDDATA; + } entries = avio_rb16(pb); - if (entries > 1) { - avpriv_request_sample(c->fc, "thmb in iref referencing several items"); - return AVERROR_PATCHWELCOME; - } /* 'to' item ids */ - to_item_id = version ? avio_rb32(pb) : avio_rb16(pb); + for (int i = 0; i < entries; i++) { + HEIFItem *item = get_heif_item(c, version ? avio_rb32(pb) : avio_rb16(pb)); + if (!item) { + av_log(c->fc, AV_LOG_WARNING, "Missing stream referenced by thmb item\n"); + continue; + } - if (to_item_id != c->primary_item_id) - return 0; + if (!av_dynarray2_add((void **)&item->iref_list, &item->nb_iref_list, + sizeof(*item->iref_list), (const uint8_t *)&ref)) + return AVERROR(ENOMEM); + } - /* Put thumnbail IDs into an array */ - thmb_item_id = av_dynarray2_add((void **)&c->thmb_item_id, &c->nb_thmb_item, - sizeof(*c->thmb_item_id), - (const void *)&from_item_id); - if (!thmb_item_id) - return AVERROR(ENOMEM); - - av_log(c->fc, AV_LOG_TRACE, "thmb: from_item_id %d, entries %d, nb_thmb: %d\n", - from_item_id, entries, c->nb_thmb_item); + av_log(c->fc, AV_LOG_TRACE, "thmb: from_item_id %d, entries %d\n", + from_item_id, entries); return 0; } @@ -9166,7 +9169,7 @@ static int mov_read_ispe(MOVContext *c, AVIOContext *pb, MOVAtom atom) av_log(c->fc, AV_LOG_TRACE, "ispe: item_id %d, width %u, height %u\n", c->cur_item_id, width, height); - item = heif_cur_item(c); + item = get_heif_item(c, c->cur_item_id); if (item) { item->width = width; item->height = height; @@ -9185,7 +9188,7 @@ static int mov_read_irot(MOVContext *c, AVIOContext *pb, MOVAtom atom) av_log(c->fc, AV_LOG_TRACE, "irot: item_id %d, angle %u\n", c->cur_item_id, angle); - item = heif_cur_item(c); + item = get_heif_item(c, c->cur_item_id); if (item) { // angle * 90 specifies the angle (in anti-clockwise direction) // in units of degrees. @@ -9205,7 +9208,7 @@ static int mov_read_imir(MOVContext *c, AVIOContext *pb, MOVAtom atom) av_log(c->fc, AV_LOG_TRACE, "imir: item_id %d, axis %u\n", c->cur_item_id, axis); - item = heif_cur_item(c); + item = get_heif_item(c, c->cur_item_id); if (item) { item->hflip = axis; item->vflip = !axis; @@ -9970,6 +9973,7 @@ static int mov_read_close(AVFormatContext *s) if (!mov->heif_item[i]) continue; av_freep(&mov->heif_item[i]->name); + av_freep(&mov->heif_item[i]->iref_list); av_freep(&mov->heif_item[i]->icc_profile); av_freep(&mov->heif_item[i]); } @@ -9980,7 +9984,6 @@ static int mov_read_close(AVFormatContext *s) av_freep(&mov->heif_grid[i].tile_item_list); } av_freep(&mov->heif_grid); - av_freep(&mov->thmb_item_id); return 0; } @@ -10429,13 +10432,6 @@ static int mov_parse_heif_items(AVFormatContext *s) if (!item) continue; if (!item->st) { - for (int j = 0; j < mov->nb_thmb_item; j++) { - if (item->item_id == mov->thmb_item_id[j]) { - av_log(s, AV_LOG_ERROR, "HEIF thumbnail ID %d doesn't reference a stream\n", - item->item_id); - return AVERROR_INVALIDDATA; - } - } continue; } if (item->is_idat_relative) { @@ -10587,7 +10583,6 @@ static int mov_read_header(AVFormatContext *s) mov->fc = s; mov->trak_index = -1; - mov->thmb_item_id = NULL; mov->primary_item_id = -1; mov->cur_item_id = -1; /* .mov and .mp4 aren't streamable anyway (only progressive download if moov is before mdat) */ -- 2.49.1 >From c5d13a638f77131174233348e0fd96079be95e42 Mon Sep 17 00:00:00 2001 From: James Almer Date: Wed, 20 Aug 2025 19:53:53 -0300 Subject: [PATCH 07/11] avformat/mov: export Exif metadata from HEIF streams Signed-off-by: James Almer --- libavformat/mov.c | 133 +++++++++++++++++- .../mov-heic-demux-still-image-multiple-thumb | 4 + 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/libavformat/mov.c b/libavformat/mov.c index 2944daf97c..4abd7e9397 100644 --- a/libavformat/mov.c +++ b/libavformat/mov.c @@ -51,6 +51,7 @@ #include "libavutil/timecode.h" #include "libavutil/uuid.h" #include "libavcodec/ac3tab.h" +#include "libavcodec/exif.h" #include "libavcodec/flac.h" #include "libavcodec/hevc/hevc.h" #include "libavcodec/mpegaudiodecheader.h" @@ -9085,12 +9086,12 @@ static int mov_read_iref_dimg(MOVContext *c, AVIOContext *pb, int version) return 0; } -static int mov_read_iref_thmb(MOVContext *c, AVIOContext *pb, int version) +static int mov_read_iref_cdsc(MOVContext *c, AVIOContext *pb, unsigned type, int version) { HEIFItem *from_item = NULL; int entries; int from_item_id = version ? avio_rb32(pb) : avio_rb16(pb); - const HEIFItemRef ref = { MKTAG('t','h','m','b'), from_item_id }; + const HEIFItemRef ref = { type, from_item_id }; from_item = get_heif_item(c, from_item_id); if (!from_item) { @@ -9103,7 +9104,8 @@ static int mov_read_iref_thmb(MOVContext *c, AVIOContext *pb, int version) for (int i = 0; i < entries; i++) { HEIFItem *item = get_heif_item(c, version ? avio_rb32(pb) : avio_rb16(pb)); if (!item) { - av_log(c->fc, AV_LOG_WARNING, "Missing stream referenced by thmb item\n"); + av_log(c->fc, AV_LOG_WARNING, "Missing stream referenced by %s item\n", + av_fourcc2str(type)); continue; } @@ -9112,8 +9114,8 @@ static int mov_read_iref_thmb(MOVContext *c, AVIOContext *pb, int version) return AVERROR(ENOMEM); } - av_log(c->fc, AV_LOG_TRACE, "thmb: from_item_id %d, entries %d\n", - from_item_id, entries); + av_log(c->fc, AV_LOG_TRACE, "%s: from_item_id %d, entries %d\n", + av_fourcc2str(type), from_item_id, entries); return 0; } @@ -9139,11 +9141,14 @@ static int mov_read_iref(MOVContext *c, AVIOContext *pb, MOVAtom atom) next += size - 4; type = avio_rl32(pb); switch (type) { + case MKTAG('c','d','s','c'): + mov_read_iref_cdsc(c, pb, type, version); + break; case MKTAG('d','i','m','g'): mov_read_iref_dimg(c, pb, version); break; case MKTAG('t','h','m','b'): - mov_read_iref_thmb(c, pb, version); + mov_read_iref_cdsc(c, pb, type, version); break; default: av_log(c->fc, AV_LOG_DEBUG, "Unknown iref type %s size %"PRIu32"\n", @@ -10308,6 +10313,90 @@ fail: return ret; } +static int mov_parse_exif_item(AVFormatContext *s, + AVPacketSideData **coded_side_data, int *nb_coded_side_data, + const HEIFItem *ref) +{ + MOVContext *c = s->priv_data; + AVPacketSideData *sd; + AVExifMetadata ifd = { 0 }; + AVExifEntry *entry = NULL; + AVBufferRef *buf; + int64_t offset = 0, pos = avio_tell(s->pb); + unsigned orientation_id = av_exif_get_tag_id("Orientation"); + int err; + + if (!(s->pb->seekable & AVIO_SEEKABLE_NORMAL)) { + av_log(c->fc, AV_LOG_WARNING, "Exif metadata with non seekable input\n"); + return AVERROR_PATCHWELCOME; + } + if (ref->is_idat_relative) { + if (!c->idat_offset) { + av_log(c->fc, AV_LOG_ERROR, "missing idat box required by the Exif metadata\n"); + return AVERROR_INVALIDDATA; + } + offset = c->idat_offset; + } + + buf = av_buffer_alloc(ref->extent_length); + if (!buf) + return AVERROR(ENOMEM); + + avio_seek(s->pb, ref->extent_offset + offset, SEEK_SET); + err = avio_read(s->pb, buf->data, ref->extent_length); + if (err != ref->extent_length) { + if (err > 0) + err = AVERROR_INVALIDDATA; + goto fail; + } + + // HEIF spec states that Exif metadata is informative. The irot item property is + // the normative source of rotation information. So we remove any Orientation tag + // present in the Exif buffer. + err = av_exif_parse_buffer(s, buf->data, ref->extent_length, &ifd, AV_EXIF_T_OFF); + if (err < 0) { + av_log(s, AV_LOG_ERROR, "Unable to parse Exif metadata\n"); + goto fail; + } + + err = av_exif_get_entry(s, &ifd, orientation_id, 0, &entry); + if (err < 0) + goto fail; + else if (!err) + goto finish; + + err = av_exif_remove_entry(s, &ifd, orientation_id, 0); + if (err < 0) + goto fail; + + av_buffer_unref(&buf); + err = av_exif_write(s, &ifd, &buf, AV_EXIF_T_OFF); + if (err < 0) + goto fail; + +finish: + offset = AV_RB32(buf->data) + 4; + if (offset >= buf->size) { + err = AVERROR_INVALIDDATA; + goto fail; + } + sd = av_packet_side_data_new(coded_side_data, nb_coded_side_data, + AV_PKT_DATA_EXIF, buf->size - offset, 0); + if (!sd) { + err = AVERROR(ENOMEM); + goto fail; + } + memcpy(sd->data, buf->data + offset, buf->size - offset); + + err = 0; +fail: + av_buffer_unref(&buf); + av_exif_free(&ifd); + avio_seek(s->pb, pos, SEEK_SET); + + return err; +} + static int mov_parse_tiles(AVFormatContext *s) { MOVContext *mov = s->priv_data; @@ -10393,6 +10482,22 @@ static int mov_parse_tiles(AVFormatContext *s) if (err < 0) return err; + for (int j = 0; j < grid->item->nb_iref_list; j++) { + HEIFItem *ref = get_heif_item(mov, grid->item->iref_list[j].item_id); + + av_assert0(ref); + switch(ref->type) { + case MKTAG('E','x','i','f'): + err = mov_parse_exif_item(s, &tile_grid->coded_side_data, + &tile_grid->nb_coded_side_data, ref); + if (err < 0 && (s->error_recognition & AV_EF_EXPLODE)) + return err; + break; + default: + break; + } + } + /* rotation */ if (grid->item->rotation || grid->item->hflip || grid->item->vflip) { err = set_display_matrix_from_item(&tile_grid->coded_side_data, @@ -10459,6 +10564,22 @@ static int mov_parse_heif_items(AVFormatContext *s) if (item->item_id == mov->primary_item_id) st->disposition |= AV_DISPOSITION_DEFAULT; + for (int j = 0; j < item->nb_iref_list; j++) { + HEIFItem *ref = get_heif_item(mov, item->iref_list[j].item_id); + + av_assert0(ref); + switch(ref->type) { + case MKTAG('E','x','i','f'): + err = mov_parse_exif_item(s, &st->codecpar->coded_side_data, + &st->codecpar->nb_coded_side_data, ref); + if (err < 0 && (s->error_recognition & AV_EF_EXPLODE)) + return err; + break; + default: + break; + } + } + if (item->rotation || item->hflip || item->vflip) { err = set_display_matrix_from_item(&st->codecpar->coded_side_data, &st->codecpar->nb_coded_side_data, item); diff --git a/tests/ref/fate/mov-heic-demux-still-image-multiple-thumb b/tests/ref/fate/mov-heic-demux-still-image-multiple-thumb index 88263f0d0b..e5764f6de8 100644 --- a/tests/ref/fate/mov-heic-demux-still-image-multiple-thumb +++ b/tests/ref/fate/mov-heic-demux-still-image-multiple-thumb @@ -39,6 +39,10 @@ DISPOSITION:metadata=0 DISPOSITION:dependent=0 DISPOSITION:still_image=0 DISPOSITION:multilayer=0 +[SIDE_DATA] +side_data_type=EXIF metadata +size=30308 +[/SIDE_DATA] [/STREAM] [STREAM] index=1 -- 2.49.1 >From 6e776057f868d4ceaff41719d74c657b3c58f9a1 Mon Sep 17 00:00:00 2001 From: James Almer Date: Fri, 22 Aug 2025 20:30:40 -0300 Subject: [PATCH 08/11] avcodec/decode: use av_exif_get_tag_id() where useful Removes dependency on exif_internal.h Signed-off-by: James Almer --- libavcodec/decode.c | 5 +++-- libavcodec/exif.c | 7 +++++++ libavcodec/exif_internal.h | 8 -------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/libavcodec/decode.c b/libavcodec/decode.c index b0ce94cadd..2877010682 100644 --- a/libavcodec/decode.c +++ b/libavcodec/decode.c @@ -47,7 +47,7 @@ #include "codec_desc.h" #include "codec_internal.h" #include "decode.h" -#include "exif_internal.h" +#include "exif.h" #include "hwaccel_internal.h" #include "hwconfig.h" #include "internal.h" @@ -2278,7 +2278,8 @@ static int exif_attach_ifd(AVCodecContext *avctx, AVFrame *frame, const AVExifMe for (size_t i = 0; i < ifd->count; i++) { const AVExifEntry *entry = &ifd->entries[i]; - if (entry->id == ORIENTATION_TAG && entry->count > 0 && entry->type == AV_TIFF_SHORT) { + if (entry->id == av_exif_get_tag_id("Orientation") && + entry->count > 0 && entry->type == AV_TIFF_SHORT) { orient = entry; break; } diff --git a/libavcodec/exif.c b/libavcodec/exif.c index 1332fa68bb..2359566a47 100644 --- a/libavcodec/exif.c +++ b/libavcodec/exif.c @@ -46,6 +46,13 @@ #define IFD_EXTRA_SIZE 6 #define EXIF_TAG_NAME_LENGTH 32 +#define MAKERNOTE_TAG 0x927c +#define ORIENTATION_TAG 0x112 +#define EXIFIFD_TAG 0x8769 +#define IMAGE_WIDTH_TAG 0x100 +#define IMAGE_LENGTH_TAG 0x101 +#define PIXEL_X_TAG 0xa002 +#define PIXEL_Y_TAG 0xa003 struct exif_tag { const char name[EXIF_TAG_NAME_LENGTH]; diff --git a/libavcodec/exif_internal.h b/libavcodec/exif_internal.h index 565e747353..0169fb3e00 100644 --- a/libavcodec/exif_internal.h +++ b/libavcodec/exif_internal.h @@ -42,14 +42,6 @@ int avpriv_exif_decode_ifd(void *logctx, const uint8_t *buf, int size, int le, int depth, AVDictionary **metadata); #endif /* FF_API_OLD_EXIF */ -#define MAKERNOTE_TAG 0x927c -#define ORIENTATION_TAG 0x112 -#define EXIFIFD_TAG 0x8769 -#define IMAGE_WIDTH_TAG 0x100 -#define IMAGE_LENGTH_TAG 0x101 -#define PIXEL_X_TAG 0xa002 -#define PIXEL_Y_TAG 0xa003 - /** * Compares values in the IFD with data in the provided AVFrame and sets the values * in that IFD to match the ones in that AVFrame. This is mostly useful for an -- 2.49.1 >From a0c32a9d6db522c46f9fe055669532576d7cb4a9 Mon Sep 17 00:00:00 2001 From: James Almer Date: Thu, 21 Aug 2025 20:03:03 -0300 Subject: [PATCH 09/11] avcodec/decode: use ff_frame_new_side_data() to export Exif side data Otherwise, the user requested priority of packet side data will be ignored. Signed-off-by: James Almer --- libavcodec/decode.c | 29 +++++++++++++---------------- libavcodec/decode.h | 7 +++---- libavcodec/pngdec.c | 5 +---- libavcodec/webp.c | 4 +--- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/libavcodec/decode.c b/libavcodec/decode.c index 2877010682..fa76dba51c 100644 --- a/libavcodec/decode.c +++ b/libavcodec/decode.c @@ -2268,12 +2268,10 @@ static int attach_displaymatrix(AVCodecContext *avctx, AVFrame *frame, int orien return ret; } -static int exif_attach_ifd(AVCodecContext *avctx, AVFrame *frame, const AVExifMetadata *ifd, AVBufferRef *og) +static int exif_attach_ifd(AVCodecContext *avctx, AVFrame *frame, const AVExifMetadata *ifd, AVBufferRef **pbuf) { const AVExifEntry *orient = NULL; - AVFrameSideData *sd; AVExifMetadata *cloned = NULL; - AVBufferRef *written = NULL; int ret; for (size_t i = 0; i < ifd->count; i++) { @@ -2305,25 +2303,21 @@ static int exif_attach_ifd(AVCodecContext *avctx, AVFrame *frame, const AVExifMe if (ret < 0) goto end; - if (cloned || !og) { - ret = av_exif_write(avctx, ifd, &written, AV_EXIF_TIFF_HEADER); + if (cloned || !*pbuf) { + av_buffer_unref(pbuf); + ret = av_exif_write(avctx, ifd, pbuf, AV_EXIF_TIFF_HEADER); if (ret < 0) goto end; } - sd = av_frame_new_side_data_from_buf(frame, AV_FRAME_DATA_EXIF, written ? written : og); - if (!sd) { - if (written) - av_buffer_unref(&written); - ret = AVERROR(ENOMEM); + ret = ff_frame_new_side_data_from_buf(avctx, frame, AV_FRAME_DATA_EXIF, pbuf); + if (ret < 0) goto end; - } ret = 0; end: - if (og && written && ret >= 0) - av_buffer_unref(&og); // as though we called new_side_data on og; + av_buffer_unref(pbuf); av_exif_free(cloned); av_free(cloned); return ret; @@ -2331,22 +2325,25 @@ end: int ff_decode_exif_attach_ifd(AVCodecContext *avctx, AVFrame *frame, const AVExifMetadata *ifd) { - return exif_attach_ifd(avctx, frame, ifd, NULL); + AVBufferRef *dummy = NULL; + return exif_attach_ifd(avctx, frame, ifd, &dummy); } -int ff_decode_exif_attach_buffer(AVCodecContext *avctx, AVFrame *frame, AVBufferRef *data, +int ff_decode_exif_attach_buffer(AVCodecContext *avctx, AVFrame *frame, AVBufferRef **pbuf, enum AVExifHeaderMode header_mode) { int ret; + AVBufferRef *data = *pbuf; AVExifMetadata ifd = { 0 }; ret = av_exif_parse_buffer(avctx, data->data, data->size, &ifd, header_mode); if (ret < 0) goto end; - ret = exif_attach_ifd(avctx, frame, &ifd, data); + ret = exif_attach_ifd(avctx, frame, &ifd, pbuf); end: + av_buffer_unref(pbuf); av_exif_free(&ifd); return ret; } diff --git a/libavcodec/decode.h b/libavcodec/decode.h index f285da924d..561dc6a69c 100644 --- a/libavcodec/decode.h +++ b/libavcodec/decode.h @@ -230,11 +230,10 @@ enum AVExifHeaderMode; * attaches that information as an AV_FRAME_DATA_DISPLAYMATRIX instead * of including it in the AV_FRAME_DATA_EXIF side data buffer. * - * On a success, the caller loses ownership of the data buffer. Either it is - * unrefed, or its ownership is transferred to the frame directly. On failure, - * the data buffer is left owned by the caller. + * *buf is ALWAYS consumed by this function and NULL written in its place, even + * on failure. */ -int ff_decode_exif_attach_buffer(AVCodecContext *avctx, AVFrame *frame, AVBufferRef *data, +int ff_decode_exif_attach_buffer(AVCodecContext *avctx, AVFrame *frame, AVBufferRef **buf, enum AVExifHeaderMode header_mode); struct AVExifMetadata; diff --git a/libavcodec/pngdec.c b/libavcodec/pngdec.c index 8a63f0ad90..ce817d44fa 100644 --- a/libavcodec/pngdec.c +++ b/libavcodec/pngdec.c @@ -1753,15 +1753,12 @@ exit_loop: if (s->exif_data) { // we swap because ff_decode_exif_attach_buffer adds to p->metadata FFSWAP(AVDictionary *, p->metadata, s->frame_metadata); - ret = ff_decode_exif_attach_buffer(avctx, p, s->exif_data, AV_EXIF_TIFF_HEADER); + ret = ff_decode_exif_attach_buffer(avctx, p, &s->exif_data, AV_EXIF_TIFF_HEADER); FFSWAP(AVDictionary *, p->metadata, s->frame_metadata); if (ret < 0) { av_log(avctx, AV_LOG_WARNING, "unable to attach EXIF buffer\n"); return ret; } - // ff_decode_exif_attach_buffer takes ownership so - // we do not want to call av_buffer_unref here - s->exif_data = NULL; } if (s->color_type == PNG_COLOR_TYPE_PALETTE && avctx->codec_id == AV_CODEC_ID_APNG) { diff --git a/libavcodec/webp.c b/libavcodec/webp.c index 0dca130ff3..796f089437 100644 --- a/libavcodec/webp.c +++ b/libavcodec/webp.c @@ -1476,11 +1476,9 @@ FF_ENABLE_DEPRECATION_WARNINGS s->has_exif = 1; memcpy(exif_buf->data, gb.buffer, chunk_size); - /* if this succeeds then exif_buf is either freed or transferred to the AVFrame */ - ret = ff_decode_exif_attach_buffer(avctx, p, exif_buf, AV_EXIF_TIFF_HEADER); + ret = ff_decode_exif_attach_buffer(avctx, p, &exif_buf, AV_EXIF_TIFF_HEADER); if (ret < 0) { av_log(avctx, AV_LOG_WARNING, "unable to attach EXIF buffer\n"); - av_buffer_unref(&exif_buf); } exif_end: -- 2.49.1 >From 91b006364075467817a176c6280a528f1df6bd62 Mon Sep 17 00:00:00 2001 From: James Almer Date: Thu, 21 Aug 2025 21:44:27 -0300 Subject: [PATCH 10/11] avcodec/decode: parse Exif packet side data before passing it to frames Extract Orientation and export it as a display matrix if present, and set the frame's metadata with the remaining Exif entries. Signed-off-by: James Almer --- libavcodec/decode.c | 87 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/libavcodec/decode.c b/libavcodec/decode.c index fa76dba51c..995d92c630 100644 --- a/libavcodec/decode.c +++ b/libavcodec/decode.c @@ -1389,6 +1389,75 @@ static int side_data_stereo3d_merge(AVFrameSideData *sd_frame, return 0; } +static int side_data_exif_parse(AVFrame *dst, const AVPacketSideData *sd_pkt) +{ + AVExifMetadata ifd = { 0 }; + AVExifEntry *entry = NULL; + AVBufferRef *buf = NULL; + AVFrameSideData *sd_frame; + int ret; + + ret = av_exif_parse_buffer(NULL, sd_pkt->data, sd_pkt->size, &ifd, + AV_EXIF_TIFF_HEADER); + if (ret < 0) + return ret; + + ret = av_exif_get_entry(NULL, &ifd, av_exif_get_tag_id("Orientation"), 0, &entry); + if (ret < 0) + goto end; + + if (!entry) { + ret = av_exif_ifd_to_dict(NULL, &ifd, &dst->metadata); + if (ret < 0) + goto end; + + sd_frame = av_frame_side_data_new(&dst->side_data, &dst->nb_side_data, AV_FRAME_DATA_EXIF, + sd_pkt->size, 0); + if (sd_frame) + memcpy(sd_frame->data, sd_pkt->data, sd_pkt->size); + ret = sd_frame ? 0 : AVERROR(ENOMEM); + + goto end; + } + + // If a display matrix already exists in the frame, give it priority + if (av_frame_side_data_get(dst->side_data, dst->nb_side_data, AV_FRAME_DATA_DISPLAYMATRIX)) + goto finish; + + sd_frame = av_frame_side_data_new(&dst->side_data, &dst->nb_side_data, AV_FRAME_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9, 0); + if (!sd_frame) { + ret = AVERROR(ENOMEM); + goto end; + } + + ret = av_exif_orientation_to_matrix((int32_t *)sd_frame->data, entry->value.uint[0]); + if (ret < 0) + goto end; + +finish: + av_exif_remove_entry(NULL, &ifd, entry->id, 0); + + ret = av_exif_ifd_to_dict(NULL, &ifd, &dst->metadata); + if (ret < 0) + goto end; + + ret = av_exif_write(NULL, &ifd, &buf, AV_EXIF_TIFF_HEADER); + if (ret < 0) + goto end; + + if (!av_frame_side_data_add(&dst->side_data, &dst->nb_side_data, AV_FRAME_DATA_EXIF, &buf, 0)) { + ret = AVERROR(ENOMEM); + goto end; + } + + ret = 0; +end: + av_buffer_unref(&buf); + av_exif_free(&ifd); + return ret; +} + static int side_data_map(AVFrame *dst, const AVPacketSideData *sd_src, int nb_sd_src, const SideDataMap *map) @@ -1415,11 +1484,21 @@ static int side_data_map(AVFrame *dst, continue; } - sd_frame = av_frame_new_side_data(dst, type_frame, sd_pkt->size); - if (!sd_frame) - return AVERROR(ENOMEM); + switch (type_pkt) { + case AV_PKT_DATA_EXIF: { + int ret = side_data_exif_parse(dst, sd_pkt); + if (ret < 0) + return ret; + break; + } + default: + sd_frame = av_frame_new_side_data(dst, type_frame, sd_pkt->size); + if (!sd_frame) + return AVERROR(ENOMEM); - memcpy(sd_frame->data, sd_pkt->data, sd_pkt->size); + memcpy(sd_frame->data, sd_pkt->data, sd_pkt->size); + break; + } } return 0; -- 2.49.1 >From 533845d0ccd8368e09bacc2d48288efaa29171ed Mon Sep 17 00:00:00 2001 From: James Almer Date: Thu, 21 Aug 2025 23:39:58 -0300 Subject: [PATCH 11/11] avcodec/decode: always extract display matrix from Exif in frames And ensure decoders don't export both Exif metadata containing an Orientation tag as well as a display matrix from some other source. Signed-off-by: James Almer --- libavcodec/decode.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libavcodec/decode.c b/libavcodec/decode.c index 995d92c630..b013f99ddb 100644 --- a/libavcodec/decode.c +++ b/libavcodec/decode.c @@ -2362,8 +2362,9 @@ static int exif_attach_ifd(AVCodecContext *avctx, AVFrame *frame, const AVExifMe } } - if (orient && orient->value.uint[0] > 1) { - av_log(avctx, AV_LOG_DEBUG, "found nontrivial EXIF orientation: %" PRIu64 "\n", orient->value.uint[0]); + if (orient) { + av_assert0(!av_frame_get_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX)); + av_log(avctx, AV_LOG_DEBUG, "found EXIF orientation: %" PRIu64 "\n", orient->value.uint[0]); ret = attach_displaymatrix(avctx, frame, orient->value.uint[0]); if (ret < 0) { av_log(avctx, AV_LOG_WARNING, "unable to attach displaymatrix from EXIF\n"); -- 2.49.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".