[PR] avformat: add AVFMT_FLAG_SKIP_ATTACHED_PICS (PR #24086)

OursCodeur via ffmpeg-devel <[email protected]>
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <178647243404.59.14609802070753836513@29965ddac10e>
PR #24086 opened by OursCodeur
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24086
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24086.patch

libavformat reads embedded cover art unconditionally: id3v2 APIC frames are parsed with the tag, FLAC PICTURE blocks with the header chain, whether the caller wants them or not.
On slow storage that gets expensive for audio-only callers: a Morrowind soundtrack mod ships 21 FLAC tracks with ~4.3MB of art each, and through emulated slow storage (open 30ms, read 8ms) a single avformat_open_input takes 4365ms against 25ms with the art hidden from the demuxer.

This adds an `AVFMT_FLAG_SKIP_ATTACHED_PICS` fflag: the id3v2 parser skips APIC/PIC payloads unread, the flac demuxer seeks past PICTURE blocks, and no attached_pic stream is created.
ff_id3v2_read_dict gains the AVFormatContext parameter so the generic open path can honor the flag; the hls and mpegts timed ID3 paths pass NULL and keep their behavior. Default unchanged.

Validated on mp3 and FLAC with embedded art: the flag drops the attached_pic stream, text metadata still reads (title checked on a real 4.3MB-art FLAC), files without art unaffected.
On non-seekable input the FLAC path degrades to read-and-discard, so the saving applies to seekable input. hls/mpegts ignore the flag on purpose, their APIC feeds consumers that may rely on it; extendable later if wanted.

Same caller class as #24084, a game engine opening audio at interactive latency (the OpenMW MR at gitlab.com/OpenMW/openmw/-/merge_requests/5492 carries the workaround this flag would retire).


>From 5926af2267a6d457a4a1d78b28d99f04c59f29b0 Mon Sep 17 00:00:00 2001
From: Thomas Portal <[email protected]>
Date: Tue, 11 Aug 2026 20:13:48 +0200
Subject: [PATCH] avformat: add AVFMT_FLAG_SKIP_ATTACHED_PICS

libavformat reads embedded cover art unconditionally: id3v2 APIC
frames are parsed with the tag and FLAC PICTURE blocks are read with
the header chain, whether or not the caller wants an attached_pic
stream. For audio-only callers on slow storage that is a large cost:
a soundtrack mod for OpenMW ships 21 FLAC tracks with ~4.3MB of art
each, and through emulated slow storage (open 30ms, read 8ms) a
single avformat_open_input takes 4365ms against 25ms with the art
bytes hidden from the demuxer.

Add an AVFMT_FLAG_SKIP_ATTACHED_PICS fflag. When set, the id3v2
parser skips APIC/PIC frame payloads unread and the flac demuxer
seeks past PICTURE metadata blocks; no attached_pic stream is
created. ff_id3v2_read_dict gains the AVFormatContext parameter so
the generic open path can honor the flag; the hls and mpegts timed
ID3 paths pass NULL and keep their current behavior. Default
behavior is unchanged.

Signed-off-by: Thomas Portal <[email protected]>
---
 doc/APIchanges              | 3 +++
 doc/formats.texi            | 5 +++++
 libavformat/aacdec.c        | 2 +-
 libavformat/avformat.h      | 1 +
 libavformat/demux.c         | 2 +-
 libavformat/flacdec.c       | 9 +++++++--
 libavformat/hls.c           | 2 +-
 libavformat/id3v2.c         | 9 +++++++--
 libavformat/id3v2.h         | 2 +-
 libavformat/mpegts.c        | 2 +-
 libavformat/options_table.h | 1 +
 libavformat/version.h       | 4 ++--
 12 files changed, 31 insertions(+), 11 deletions(-)

diff --git a/doc/APIchanges b/doc/APIchanges
index 7907af9290..51bc3cba49 100644
--- a/doc/APIchanges
+++ b/doc/APIchanges
@@ -2,6 +2,9 @@ The last version increases of all libraries were on 2026-06-23.
 
 API changes, most recent first:
 
+2026-08-11 - xxxxxxxxxx - lavf 63.6.100 - avformat.h
+  Add AVFMT_FLAG_SKIP_ATTACHED_PICS.
+
 2026-07-04 - xxxxxxxxxx - lavc 63.7.100 - codec_id.h
   Add AV_CODEC_ID_PCM_DVDA.
 
diff --git a/doc/formats.texi b/doc/formats.texi
index 0722e3f512..c7b0f7de13 100644
--- a/doc/formats.texi
+++ b/doc/formats.texi
@@ -56,6 +56,11 @@ Reduce the latency introduced by buffering during initial input streams analysis
 Do not fill in missing values in packet fields that can be exactly calculated.
 @item noparse
 Disable AVParsers, this needs @code{+nofillin} too.
+@item skip_attached_pics
+Do not read attached pictures (album art): id3v2 APIC frames and FLAC
+PICTURE blocks are skipped unread and no attached_pic streams are created.
+Saves opening cost on files with large embedded art when the pictures are
+not wanted.
 @item sortdts
 Try to interleave output packets by DTS. At present, available only for AVIs with an index.
 @end table
diff --git a/libavformat/aacdec.c b/libavformat/aacdec.c
index fef3c69f0b..a54574f46c 100644
--- a/libavformat/aacdec.c
+++ b/libavformat/aacdec.c
@@ -147,7 +147,7 @@ static int handle_id3(AVFormatContext *s, AVPacket *pkt)
         return ret;
 
     ffio_init_read_context(&pb, pkt->data, pkt->size);
-    ff_id3v2_read_dict(&pb.pub, &metadata, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);
+    ff_id3v2_read_dict(&pb.pub, &metadata, s, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);
     if ((ret = ff_id3v2_parse_priv_dict(&metadata, id3v2_extra_meta)) < 0)
         goto error;
 
diff --git a/libavformat/avformat.h b/libavformat/avformat.h
index d4f10122e6..4453a5bae4 100644
--- a/libavformat/avformat.h
+++ b/libavformat/avformat.h
@@ -1504,6 +1504,7 @@ typedef struct AVFormatContext {
 #define AVFMT_FLAG_SORT_DTS    0x10000 ///< try to interleave outputted packets by dts (using this flag can slow demuxing down)
 #define AVFMT_FLAG_FAST_SEEK   0x80000 ///< Enable fast, but inaccurate seeks for some formats
 #define AVFMT_FLAG_AUTO_BSF   0x200000 ///< Add bitstream filters as requested by the muxer
+#define AVFMT_FLAG_SKIP_ATTACHED_PICS 0x400000 ///< Do not read attached pictures (album art); no attached_pic streams are created.
 
     /**
      * Maximum number of bytes read from input in order to determine stream
diff --git a/libavformat/demux.c b/libavformat/demux.c
index 2375b277e4..0c4a031395 100644
--- a/libavformat/demux.c
+++ b/libavformat/demux.c
@@ -317,7 +317,7 @@ int avformat_open_input(AVFormatContext **ps, const char *filename,
 
     /* e.g. AVFMT_NOFILE formats will not have an AVIOContext */
     if (s->pb && is_id3v2_format(s->iformat))
-        ff_id3v2_read_dict(s->pb, &si->id3v2_meta, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);
+        ff_id3v2_read_dict(s->pb, &si->id3v2_meta, s, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);
 
     if (ffifmt(s->iformat)->read_header)
         if ((ret = ffifmt(s->iformat)->read_header(s)) < 0) {
diff --git a/libavformat/flacdec.c b/libavformat/flacdec.c
index e80b49307d..239d9548d0 100644
--- a/libavformat/flacdec.c
+++ b/libavformat/flacdec.c
@@ -91,10 +91,13 @@ static int flac_read_header(AVFormatContext *s)
         flac_parse_block_header(header, &metadata_last, &metadata_type,
                                    &metadata_size);
         switch (metadata_type) {
+        case FLAC_METADATA_TYPE_PICTURE:
+            if (s->flags & AVFMT_FLAG_SKIP_ATTACHED_PICS)
+                goto skip_block;
+            /* fall through */
         /* allocate and read metadata block for supported types */
         case FLAC_METADATA_TYPE_STREAMINFO:
         case FLAC_METADATA_TYPE_CUESHEET:
-        case FLAC_METADATA_TYPE_PICTURE:
         case FLAC_METADATA_TYPE_VORBIS_COMMENT:
         case FLAC_METADATA_TYPE_SEEKTABLE:
             buffer = av_mallocz(metadata_size + AV_INPUT_BUFFER_PADDING_SIZE);
@@ -108,6 +111,7 @@ static int flac_read_header(AVFormatContext *s)
             break;
         /* skip metadata block for unsupported types */
         default:
+        skip_block:
             ret = avio_skip(s->pb, metadata_size);
             if (ret < 0)
                 return ret;
@@ -165,7 +169,8 @@ static int flac_read_header(AVFormatContext *s)
                 avpriv_new_chapter(s, track, st->time_base, start, AV_NOPTS_VALUE, isrc);
             }
             av_freep(&buffer);
-        } else if (metadata_type == FLAC_METADATA_TYPE_PICTURE) {
+        } else if (metadata_type == FLAC_METADATA_TYPE_PICTURE
+                   && !(s->flags & AVFMT_FLAG_SKIP_ATTACHED_PICS)) {
             ret = ff_flac_parse_picture(s, &buffer, metadata_size, 1);
             av_freep(&buffer);
             if (ret < 0) {
diff --git a/libavformat/hls.c b/libavformat/hls.c
index eca5788279..8f853e69f0 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -1241,7 +1241,7 @@ static void parse_id3(AVFormatContext *s, AVIOContext *pb,
     static const char id3_priv_owner_audio_setup[] = "com.apple.streaming.audioDescription";
     ID3v2ExtraMeta *meta;
 
-    ff_id3v2_read_dict(pb, metadata, ID3v2_DEFAULT_MAGIC, extra_meta);
+    ff_id3v2_read_dict(pb, metadata, NULL, ID3v2_DEFAULT_MAGIC, extra_meta);
     for (meta = *extra_meta; meta; meta = meta->next) {
         if (!strcmp(meta->tag, "PRIV")) {
             ID3v2ExtraMetaPRIV *priv = &meta->data.priv;
diff --git a/libavformat/id3v2.c b/libavformat/id3v2.c
index ad670052ff..4202db5a7f 100644
--- a/libavformat/id3v2.c
+++ b/libavformat/id3v2.c
@@ -991,6 +991,10 @@ static void id3v2_parse(AVIOContext *pb, AVDictionary **metadata,
 
             av_log(s, AV_LOG_WARNING, "Skipping %s ID3v2 frame %s.\n", type, tag);
             avio_skip(pb, tlen);
+        /* attached pictures can be megabytes; drop them unread when asked */
+        } else if (s && (s->flags & AVFMT_FLAG_SKIP_ATTACHED_PICS) &&
+                   !strcmp(tag, isv34 ? "APIC" : "PIC")) {
+            avio_skip(pb, tlen);
         /* check for text tag or supported special meta tag */
         } else if (tag[0] == 'T' ||
                    !memcmp(tag, "USLT", 4) ||
@@ -1171,9 +1175,10 @@ static void id3v2_read_internal(AVIOContext *pb, AVDictionary **metadata,
 }
 
 void ff_id3v2_read_dict(AVIOContext *pb, AVDictionary **metadata,
-                        const char *magic, ID3v2ExtraMeta **extra_meta)
+                        AVFormatContext *s, const char *magic,
+                        ID3v2ExtraMeta **extra_meta)
 {
-    id3v2_read_internal(pb, metadata, NULL, magic, extra_meta, 0);
+    id3v2_read_internal(pb, metadata, s, magic, extra_meta, 0);
 }
 
 void ff_id3v2_read(AVFormatContext *s, const char *magic,
diff --git a/libavformat/id3v2.h b/libavformat/id3v2.h
index 9afa5a2ddc..fe5b772af4 100644
--- a/libavformat/id3v2.h
+++ b/libavformat/id3v2.h
@@ -114,7 +114,7 @@ int ff_id3v2_tag_len(const uint8_t *buf);
  * @param[out] extra_meta If not NULL, extra metadata is parsed into a list of
  * ID3v2ExtraMeta structs and *extra_meta points to the head of the list
  */
-void ff_id3v2_read_dict(AVIOContext *pb, AVDictionary **metadata, const char *magic, ID3v2ExtraMeta **extra_meta);
+void ff_id3v2_read_dict(AVIOContext *pb, AVDictionary **metadata, AVFormatContext *s, const char *magic, ID3v2ExtraMeta **extra_meta);
 
 /**
  * Read an ID3v2 tag, including supported extra metadata.
diff --git a/libavformat/mpegts.c b/libavformat/mpegts.c
index 076020509d..0fa69c1b01 100644
--- a/libavformat/mpegts.c
+++ b/libavformat/mpegts.c
@@ -1048,7 +1048,7 @@ static int timed_id3_update_metadata(AVStream *s, AVPacket *pkt)
     int ret = 0;
 
     ffio_init_read_context(&id3_buf, pkt->data, pkt->size);
-    ff_id3v2_read_dict(&id3_buf.pub, &metadata, ID3v2_DEFAULT_MAGIC, &extra_meta);
+    ff_id3v2_read_dict(&id3_buf.pub, &metadata, NULL, ID3v2_DEFAULT_MAGIC, &extra_meta);
     ret = ff_id3v2_parse_priv_dict(&metadata, extra_meta);
     ff_id3v2_free_extra_meta(&extra_meta);
 
diff --git a/libavformat/options_table.h b/libavformat/options_table.h
index 915c553f54..6db99555ed 100644
--- a/libavformat/options_table.h
+++ b/libavformat/options_table.h
@@ -50,6 +50,7 @@ static const AVOption avformat_options[] = {
 {"sortdts", "try to interleave outputted packets by dts", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_SORT_DTS }, INT_MIN, INT_MAX, D, .unit = "fflags"},
 {"fastseek", "fast but inaccurate seeks", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_FAST_SEEK }, INT_MIN, INT_MAX, D, .unit = "fflags"},
 {"nobuffer", "reduce the latency introduced by optional buffering", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_NOBUFFER }, 0, INT_MAX, D, .unit = "fflags"},
+{"skip_attached_pics", "do not read attached pictures (album art)", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_SKIP_ATTACHED_PICS }, INT_MIN, INT_MAX, D, .unit = "fflags"},
 {"bitexact", "do not write random/volatile data", 0, AV_OPT_TYPE_CONST, { .i64 = AVFMT_FLAG_BITEXACT }, 0, 0, E, .unit = "fflags" },
 {"autobsf", "add needed bsfs automatically", 0, AV_OPT_TYPE_CONST, { .i64 = AVFMT_FLAG_AUTO_BSF }, 0, 0, E, .unit = "fflags" },
 {"seek2any", "allow seeking to non-keyframes on demuxer level when supported", OFFSET(seek2any), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, D},
diff --git a/libavformat/version.h b/libavformat/version.h
index 384cbd49cc..4bde82abb4 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,8 +31,8 @@
 
 #include "version_major.h"
 
-#define LIBAVFORMAT_VERSION_MINOR   5
-#define LIBAVFORMAT_VERSION_MICRO 101
+#define LIBAVFORMAT_VERSION_MINOR   6
+#define LIBAVFORMAT_VERSION_MICRO 100
 
 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
                                                LIBAVFORMAT_VERSION_MINOR, \
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.