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]
