PR #25820 opened by Romain Beauxis (toots)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25820
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25820.patch

This PR implements decoding of chained Ogg Vorbis streams whose links have 
their own setup header. The decoder currently keeps the first link's codebooks, 
so a later link encoded with different settings decodes to garbage.

The Ogg demuxer passes each new link's headers as AV_PKT_DATA_NEW_EXTRADATA, 
and the decoder sets itself up again from them. The packet that carries them 
then starts a fresh stream, whose first frame only primes the overlap and is 
dropped.

That dropped frame carries the new link's tags, which the demuxer attaches to 
the link's first packet. The decoder holds them and attaches them to the first 
frame with samples, so that a player shows a link's tags with the first sample 
it plays:

```c
vc->pending_metadata = frame->metadata;   /* priming frame, dropped whole */
frame->metadata      = NULL;
```

This also gives the first link of a file its tags on its first output frame, 
which they never reached before.

A new FATE test chains two links encoded at different qualities and checks the 
level of each half second of the decoded output, and the existing chained-meta 
reference shows the tags of each link moving to its first frame with samples.


>From 006346b1b213c47e427c5bdd5bb23c940a603b0b Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Sat, 31 Oct 2026 09:57:15 -0500
Subject: [PATCH 1/4] fate/ogg-vorbis: add a chained decoding test

Two generated links encoded at different qualities are piped into
one decoder, so that each link needs its own codebooks.

The reference records the level of each half second today, which
drops once the second link is decoded with the first link's codebooks.
---
 tests/fate/ogg-vorbis.mak               | 16 +++++++++++++++-
 tests/ref/fate/ogg-vorbis-chained-setup |  5 +++++
 2 files changed, 20 insertions(+), 1 deletion(-)
 create mode 100644 tests/ref/fate/ogg-vorbis-chained-setup

diff --git a/tests/fate/ogg-vorbis.mak b/tests/fate/ogg-vorbis.mak
index d009f4e8180..3abeb8e0c70 100644
--- a/tests/fate/ogg-vorbis.mak
+++ b/tests/fate/ogg-vorbis.mak
@@ -40,6 +40,18 @@ fate-ogg-vorbis-libvorbis-1s: CMD = run_with_temp "$(FFMPEG) 
-nostdin -hide_bann
 FATE_OGG_VORBIS_ENCODE_PROBE-$(call ENCDEC, LIBVORBIS VORBIS, OGG, LAVFI_INDEV 
SINE_FILTER) += fate-ogg-vorbis-libvorbis-30s
 fate-ogg-vorbis-libvorbis-30s: CMD = run_with_temp "$(FFMPEG) -nostdin 
-hide_banner -loglevel quiet -f lavfi -i 
sine=frequency=440:duration=30:sample_rate=44100 -c:a libvorbis -bitexact -f 
ogg -y" "ffprobe$(PROGSSUF)$(EXESUF) -print_filename $$test -of default 
-bitexact -threads 1 -read_intervals %+\#5 -show_entries 
stream=codec_name,initial_padding:packet=pts,pts_time,dts,dts_time,duration,duration_time,flags:packet_side_data_list:format=filename,nb_streams,nb_programs,nb_stream_groups,format_name,start_time,duration,probe_score"
 ogg
 
+# Two links encoded at different qualities, so each has its own codebooks.
+# Decoded floats differ across platforms, so the output is the level of each
+# half second, in whole decibels.
+FATE_OGG_VORBIS_CHAIN-$(call TRANSCODE, LIBVORBIS VORBIS, OGG, LAVFI_INDEV 
SINE_FILTER ARESAMPLE_FILTER ASETNSAMPLES_FILTER ASTATS_FILTER AMETADATA_FILTER 
NULL_MUXER) += fate-ogg-vorbis-chained-setup
+fate-ogg-vorbis-chained-setup: CMD = \
+    { ffmpeg -auto_conversion_filters -f lavfi -i 
sine=frequency=440:duration=1 -c:a libvorbis -q:a 1 \
+        -flags +bitexact -fflags +bitexact -f ogg - && \
+      ffmpeg -auto_conversion_filters -f lavfi -i 
sine=frequency=660:duration=1 -c:a libvorbis -q:a 9 \
+        -flags +bitexact -fflags +bitexact -serial_offset 1 -f ogg -; } | \
+    ffmpeg -auto_conversion_filters -i - -af 
asetnsamples=n=22050:p=0,astats=metadata=1:reset=1,ametadata=print:key=lavfi.astats.Overall.RMS_level:file=-
 \
+        -f null - | awk -F= "/RMS_level/ { printf \"%.0f\\n\", \$$2 }"
+
 FATE_OGG_VORBIS-$(call DEMDEC, OGG, VORBIS) += $(FATE_OGG_VORBIS)
 
 FATE_SAMPLES_DUMP_STREAM_META += $(FATE_OGG_VORBIS-yes)
@@ -52,4 +64,6 @@ FATE_SAMPLES_FFPROBE += $(FATE_OGG_VORBIS_PROBE-yes)
 
 FATE_FFMPEG_FFPROBE += $(FATE_OGG_VORBIS_ENCODE_PROBE-yes)
 
-fate-ogg-vorbis: $(FATE_OGG_VORBIS-yes) $(FATE_OGG_COPY_VORBIS-yes) 
$(FATE_OGG_VORBIS_PROBE-yes) $(FATE_OGG_VORBIS_ENCODE_PROBE-yes)
+FATE_FFMPEG += $(FATE_OGG_VORBIS_CHAIN-yes)
+
+fate-ogg-vorbis: $(FATE_OGG_VORBIS-yes) $(FATE_OGG_COPY_VORBIS-yes) 
$(FATE_OGG_VORBIS_PROBE-yes) $(FATE_OGG_VORBIS_ENCODE_PROBE-yes) 
$(FATE_OGG_VORBIS_CHAIN-yes)
diff --git a/tests/ref/fate/ogg-vorbis-chained-setup 
b/tests/ref/fate/ogg-vorbis-chained-setup
new file mode 100644
index 00000000000..b36c826bd76
--- /dev/null
+++ b/tests/ref/fate/ogg-vorbis-chained-setup
@@ -0,0 +1,5 @@
+-21
+-21
+-51
+-49
+-45
-- 
2.54.0


>From b53744574987d0cc896bb93d3ffdb2c8c9974a42 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Fri, 6 Nov 2026 16:25:03 -0600
Subject: [PATCH 2/4] avcodec/vorbisdec: attach the tags of a dropped frame to
 the first frame with samples

The priming frame of a Vorbis stream is dropped whole, along with the tags it 
took from its packet, such as a chained Ogg link's title.

Hand those tags on to the first frame with samples, so that they line up with 
the first sample played.

The first link of the vorbis chained-meta test now reports its tags on its 
first frame.
---
 libavcodec/vorbisdec.c                     | 23 ++++++++++++++++++++++
 tests/ref/fate/ogg-vorbis-chained-meta.txt |  2 +-
 2 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/libavcodec/vorbisdec.c b/libavcodec/vorbisdec.c
index aeea1b49087..111fe44318e 100644
--- a/libavcodec/vorbisdec.c
+++ b/libavcodec/vorbisdec.c
@@ -30,6 +30,7 @@
 #include <math.h>
 
 #include "libavutil/avassert.h"
+#include "libavutil/dict.h"
 #include "libavutil/float_dsp.h"
 #include "libavutil/mem.h"
 #include "libavutil/tx.h"
@@ -158,6 +159,10 @@ typedef struct vorbis_context_s {
     int8_t       previous_window;
     float        *channel_residues;
     float        *saved;
+    /**
+     * Tags of a dropped priming frame, added to the first frame with samples.
+     */
+    AVDictionary *pending_metadata;
 } vorbis_context;
 
 /* Helper functions */
@@ -1859,6 +1864,22 @@ static int vorbis_decode_frame(AVCodecContext *avctx, 
AVFrame *frame,
     if (!vc->first_frame) {
         vc->first_frame = 1;
         avctx->internal->skip_samples = len;
+        if (!(avctx->flags2 & AV_CODEC_FLAG2_SKIP_MANUAL)) {
+            av_dict_free(&vc->pending_metadata);
+            vc->pending_metadata = frame->metadata;
+            frame->metadata      = NULL;
+        }
+    } else if (vc->pending_metadata) {
+        if (!frame->metadata) {
+            frame->metadata      = vc->pending_metadata;
+            vc->pending_metadata = NULL;
+        } else {
+            ret = av_dict_copy(&frame->metadata, vc->pending_metadata,
+                               AV_DICT_DONT_OVERWRITE);
+            av_dict_free(&vc->pending_metadata);
+            if (ret < 0)
+                return ret;
+        }
     }
 
     ff_dlog(NULL, "parsed %d bytes %d bits, returned %d samples (*ch*bits) \n",
@@ -1877,6 +1898,7 @@ static av_cold int vorbis_decode_close(AVCodecContext 
*avctx)
     vorbis_context *vc = avctx->priv_data;
 
     vorbis_free(vc);
+    av_dict_free(&vc->pending_metadata);
 
     return 0;
 }
@@ -1885,6 +1907,7 @@ static av_cold void vorbis_decode_flush(AVCodecContext 
*avctx)
 {
     vorbis_context *vc = avctx->priv_data;
 
+    av_dict_free(&vc->pending_metadata);
     if (vc->saved) {
         memset(vc->saved, 0, (vc->blocksize[1] / 4) * vc->audio_channels *
                              sizeof(*vc->saved));
diff --git a/tests/ref/fate/ogg-vorbis-chained-meta.txt 
b/tests/ref/fate/ogg-vorbis-chained-meta.txt
index 5a102fdd7e9..84feb88ddd6 100644
--- a/tests/ref/fate/ogg-vorbis-chained-meta.txt
+++ b/tests/ref/fate/ogg-vorbis-chained-meta.txt
@@ -2,7 +2,7 @@ Stream ID: 0, codec name: vorbis, metadata: 
encoder=Lavc61.19.100 libvorbis:titl
 Stream ID: 0, packet PTS: -128, packet DTS: -128
 Stream ID: 0, new metadata: encoder=Lavc61.19.100 libvorbis:title=First Stream
 Stream ID: 0, packet PTS: 0, packet DTS: 0
-Stream ID: 0, frame PTS: 0, metadata: N/A
+Stream ID: 0, frame PTS: 0, metadata: encoder=Lavc61.19.100 
libvorbis:title=First Stream
 Stream ID: 0, packet PTS: 576, packet DTS: 576
 Stream ID: 0, frame PTS: 576, metadata: N/A
 Stream ID: 0, packet PTS: 1600, packet DTS: 1600
-- 
2.54.0


>From 182c546208a0a92a749e14c5be25117266cea484 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Sat, 31 Oct 2026 09:27:28 -0500
Subject: [PATCH 3/4] avcodec/vorbisdec: factor out parsing the headers

Move parsing the identification and setup headers out of initialization,
so that headers arriving later can be parsed the same way.
---
 libavcodec/vorbisdec.c | 32 +++++++++++++++++++-------------
 1 file changed, 19 insertions(+), 13 deletions(-)

diff --git a/libavcodec/vorbisdec.c b/libavcodec/vorbisdec.c
index 111fe44318e..e952af825cc 100644
--- a/libavcodec/vorbisdec.c
+++ b/libavcodec/vorbisdec.c
@@ -1057,26 +1057,15 @@ static int vorbis_parse_id_hdr(vorbis_context *vc)
 
 // Process the extradata using the functions above (identification header, 
setup header)
 
-static av_cold int vorbis_decode_init(AVCodecContext *avctx)
+static int vorbis_parse_headers(AVCodecContext *avctx,
+                                const uint8_t *headers, int headers_len)
 {
     vorbis_context *vc = avctx->priv_data;
-    uint8_t *headers   = avctx->extradata;
-    int headers_len    = avctx->extradata_size;
     const uint8_t *header_start[3];
     int header_len[3];
     GetBitContext *gb = &vc->gb;
     int hdr_type, ret;
 
-    vc->avctx = avctx;
-    ff_vorbisdsp_init(&vc->dsp);
-
-    avctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
-
-    if (!headers_len) {
-        av_log(avctx, AV_LOG_ERROR, "Extradata missing.\n");
-        return AVERROR_INVALIDDATA;
-    }
-
     if ((ret = avpriv_split_xiph_headers(headers, headers_len, 30, 
header_start, header_len)) < 0) {
         av_log(avctx, AV_LOG_ERROR, "Extradata corrupt.\n");
         return ret;
@@ -1120,6 +1109,23 @@ static av_cold int vorbis_decode_init(AVCodecContext 
*avctx)
     return 0;
 }
 
+static av_cold int vorbis_decode_init(AVCodecContext *avctx)
+{
+    vorbis_context *vc = avctx->priv_data;
+
+    vc->avctx = avctx;
+    ff_vorbisdsp_init(&vc->dsp);
+
+    avctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
+
+    if (!avctx->extradata_size) {
+        av_log(avctx, AV_LOG_ERROR, "Extradata missing.\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    return vorbis_parse_headers(avctx, avctx->extradata, 
avctx->extradata_size);
+}
+
 // Decode audiopackets -------------------------------------------------
 
 // Read and decode floor
-- 
2.54.0


>From fa485ca654f03f81c93e52383e468d621bd6ff7a Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Sat, 31 Oct 2026 09:27:28 -0500
Subject: [PATCH 4/4] avcodec/vorbisdec: take new headers from packet side data

A chained Ogg Vorbis link has its own setup header, which the demuxer
passes as AV_PKT_DATA_NEW_EXTRADATA while the decoder keeps the first
link's codebooks.

Parse the new headers before the packet that carries them, and treat
that packet as the first of a fresh stream, whose samples only prime
the overlap.

The second link of chained-setup now matches that link decoded on its
own, and the chained-meta tags of the second link move to its first
frame actually output.
---
 libavcodec/vorbisdec.c                     | 13 +++++++++++++
 tests/ref/fate/ogg-vorbis-chained-meta.txt |  3 +--
 tests/ref/fate/ogg-vorbis-chained-setup    |  5 ++---
 3 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/libavcodec/vorbisdec.c b/libavcodec/vorbisdec.c
index e952af825cc..6c5b9c52283 100644
--- a/libavcodec/vorbisdec.c
+++ b/libavcodec/vorbisdec.c
@@ -1797,10 +1797,23 @@ static int vorbis_decode_frame(AVCodecContext *avctx, 
AVFrame *frame,
     vorbis_context *vc = avctx->priv_data;
     GetBitContext *gb = &vc->gb;
     float *channel_ptrs[255];
+    const uint8_t *new_headers;
+    size_t new_headers_size;
     int i, len, ret;
 
     ff_dlog(NULL, "packet length %d \n", buf_size);
 
+    new_headers = av_packet_get_side_data(avpkt, AV_PKT_DATA_NEW_EXTRADATA,
+                                          &new_headers_size);
+    if (new_headers) {
+        if (new_headers_size > INT_MAX)
+            return AVERROR_INVALIDDATA;
+        vorbis_free(vc);
+        vc->first_frame = 0;
+        if ((ret = vorbis_parse_headers(avctx, new_headers, new_headers_size)) 
< 0)
+            return ret;
+    }
+
     if (*buf == 1 && buf_size > 7) {
         if ((ret = init_get_bits8(gb, buf + 1, buf_size - 1)) < 0)
             return ret;
diff --git a/tests/ref/fate/ogg-vorbis-chained-meta.txt 
b/tests/ref/fate/ogg-vorbis-chained-meta.txt
index 84feb88ddd6..f46532b6f85 100644
--- a/tests/ref/fate/ogg-vorbis-chained-meta.txt
+++ b/tests/ref/fate/ogg-vorbis-chained-meta.txt
@@ -7,8 +7,7 @@ Stream ID: 0, packet PTS: 576, packet DTS: 576
 Stream ID: 0, frame PTS: 576, metadata: N/A
 Stream ID: 0, packet PTS: 1600, packet DTS: 1600
 Stream ID: 0, new metadata: encoder=Lavc61.19.100 libvorbis:title=Second Stream
-Stream ID: 0, frame PTS: 1600, metadata: encoder=Lavc61.19.100 
libvorbis:title=Second Stream
 Stream ID: 0, packet PTS: 1728, packet DTS: 1728
-Stream ID: 0, frame PTS: 1728, metadata: N/A
+Stream ID: 0, frame PTS: 1728, metadata: encoder=Lavc61.19.100 
libvorbis:title=Second Stream
 Stream ID: 0, packet PTS: 2304, packet DTS: 2304
 Stream ID: 0, frame PTS: 2304, metadata: N/A
diff --git a/tests/ref/fate/ogg-vorbis-chained-setup 
b/tests/ref/fate/ogg-vorbis-chained-setup
index b36c826bd76..4be3e37008e 100644
--- a/tests/ref/fate/ogg-vorbis-chained-setup
+++ b/tests/ref/fate/ogg-vorbis-chained-setup
@@ -1,5 +1,4 @@
 -21
 -21
--51
--49
--45
+-21
+-21
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to