PR #25657 opened by Romain Beauxis (toots) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25657 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25657.patch
This PR implements decoding of chained Ogg Opus streams whose links have their own header. The decoder currently keeps the first link's setup, so a later link's output gain and channel mapping are ignored and its decoder state carries over from the previous link. The Ogg demuxer passes each new link's OpusHead as AV_PKT_DATA_NEW_EXTRADATA, and the decoder sets itself up again from it. Setting the decoder up again would drop the samples its SILK resampler still holds back at the end of a link (24 samples at 16 kHz). This PR adds a public packet flag for the last packet of a sequence whose decoder state the next packet does not continue: ```c #define AV_PKT_FLAG_EOS 0x0020 ``` The Ogg Opus demuxer sets it on the last packet of each link, and the Opus decoder outputs the held-back samples with that packet's frame. They stay in the link they belong to, and that link's end trimming applies to them. Other decoders can use the flag for whatever ending a sequence requires, not only for flushing. New FATE tests chain two links, with and without an output gain on the second, and two SILK test vectors from the FATE suite, and check that each chain decodes exactly as its links do one by one. Seeking across links is left for a later PR. >From fa1abd3577f4ec85b01b4b7a312c11b750f38c41 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sun, 1 Nov 2026 09:01:33 -0600 Subject: [PATCH 1/9] avcodec/packet: add AV_PKT_FLAG_EOS A demuxer can know that a packet ends a sequence whose decoder state the next packet does not continue, such as the last packet of a chained Ogg link. This flag passes that on, so that a decoder can do whatever ending a sequence requires on that packet. --- doc/APIchanges | 3 +++ libavcodec/packet.h | 9 +++++++++ libavcodec/version.h | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/doc/APIchanges b/doc/APIchanges index a0373238a70..d8c608bcd4d 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-11-xx - xxxxxxxxxx - lavc 63.26.100 - packet.h + Add AV_PKT_FLAG_EOS. + 2026-10-xx - xxxxxxxxxx - lavfi 12.12.100 - avfilter.h Deprecate AVFilterGraph.max_buffered_frames in favor of avfilter_graph_set_max_buffered_frames(). diff --git a/libavcodec/packet.h b/libavcodec/packet.h index 64f687a729f..ce74113d0d5 100644 --- a/libavcodec/packet.h +++ b/libavcodec/packet.h @@ -674,6 +674,15 @@ typedef struct AVPacket { * be discarded by the decoder. I.e. Non-reference frames. */ #define AV_PKT_FLAG_DISPOSABLE 0x0010 +/** + * The packet ends a sequence: the next packet, if any, does not continue + * its decoder state. + * + * A decoder can do whatever ending a sequence requires when it decodes + * this packet. More packets may follow on the same stream, and a sequence + * may end without this flag when the input does not mark it. + */ +#define AV_PKT_FLAG_EOS 0x0020 enum AVSideDataParamChangeFlags { AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004, diff --git a/libavcodec/version.h b/libavcodec/version.h index 3ade8a83141..26ee41eb1fd 100644 --- a/libavcodec/version.h +++ b/libavcodec/version.h @@ -29,8 +29,8 @@ #include "version_major.h" -#define LIBAVCODEC_VERSION_MINOR 25 -#define LIBAVCODEC_VERSION_MICRO 102 +#define LIBAVCODEC_VERSION_MINOR 26 +#define LIBAVCODEC_VERSION_MICRO 100 #define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \ LIBAVCODEC_VERSION_MINOR, \ -- 2.54.0 >From 9a1839e495cdc3154eaf364354e011c59959074c Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sat, 31 Oct 2026 09:37:46 -0500 Subject: [PATCH 2/9] fate/ogg-opus: add chained decoding tests Each link of a chained stream is a stream of its own, so decoding two Opus links chained must give what decoding them one by one does. One test chains two plain links, one gives the second link an output gain of its own, and one chains two SILK test vectors. The references record today's differences, as the decoder carries the previous link's state across the boundary and never sees the second link's header. --- tests/fate/ogg-opus.mak | 61 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/tests/fate/ogg-opus.mak b/tests/fate/ogg-opus.mak index 71e84e32c26..ec415535a5f 100644 --- a/tests/fate/ogg-opus.mak +++ b/tests/fate/ogg-opus.mak @@ -7,12 +7,69 @@ fate-ogg-opus-copy-chained-meta: $(APITESTSDIR)/api-dump-stream-meta-test$(EXESU fate-ogg-opus-copy-chained-meta: REF = $(SRC_PATH)/tests/ref/fate/ogg-opus-chained-meta.txt fate-ogg-opus-copy-chained-meta: CMD = run_with_temp "$(FFMPEG) -nostdin -hide_banner -loglevel quiet -i $(TARGET_SAMPLES)/ogg-opus/chained-meta.ogg -c copy -f ogg -y" "$(APITESTSDIR)/api-dump-stream-meta-test$(EXESUF)" ogg +# One link of a tone, given its frequency $(1), further muxer options $(2) +# and output file $(3). +OGG_OPUS_LINK = ffmpeg -auto_conversion_filters -f lavfi -i sine=frequency=$(1):duration=1 \ + -c:a opus -strict experimental -flags +bitexact -fflags +bitexact $(2) -f ogg -y $(3) +OGG_OPUS_DECODE = ffmpeg -auto_conversion_filters -i $(1) -f s16le - + +# Each link of a chain is a stream of its own, so decoding the links +# $(1).1.ogg and $(1).2.ogg chained must give what decoding them one by one +# does, written to $(1).pcm as the reference. +OGG_OPUS_CHAIN_DECODE = \ + { $(call OGG_OPUS_DECODE,$(1).1.ogg) && $(call OGG_OPUS_DECODE,$(1).2.ogg); } > $(1).pcm && \ + test -s $(1).pcm && \ + cat $(1).1.ogg $(1).2.ogg | $(call OGG_OPUS_DECODE,-) + +# Two tones chained, $(2) holding further options for the second link. +# Links are written to files so that a failed encode fails the test. +OGG_OPUS_CHAINED = \ + $(call OGG_OPUS_LINK,440,,$(1).1.ogg) && \ + $(call OGG_OPUS_LINK,660,-serial_offset 1 $(2),$(1).2.ogg) && \ + $(call OGG_OPUS_CHAIN_DECODE,$(1)) + +OGG_OPUS_CHAINED_RESET_LINKS = tests/data/fate/ogg-opus-chained-reset.links +FATE_OGG_OPUS_CHAIN-$(call TRANSCODE, OPUS, OGG, LAVFI_INDEV SINE_FILTER ARESAMPLE_FILTER PCM_S16LE_DECODER PCM_S16LE_MUXER FD_PROTOCOL) += fate-ogg-opus-chained-reset +fate-ogg-opus-chained-reset: CMD = $(call OGG_OPUS_CHAINED,$(OGG_OPUS_CHAINED_RESET_LINKS),) +fate-ogg-opus-chained-reset: REF = $(OGG_OPUS_CHAINED_RESET_LINKS).pcm +fate-ogg-opus-chained-reset: CLEANFILES = $(addprefix $(OGG_OPUS_CHAINED_RESET_LINKS),.1.ogg .2.ogg .pcm) +fate-ogg-opus-chained-reset: CMP = stddev +fate-ogg-opus-chained-reset: CMP_TARGET = 95 +fate-ogg-opus-chained-reset: FUZZ = 5 + +# The second link sets an output gain of -6 dB, which the first one does not. +OGG_OPUS_CHAINED_GAIN_LINKS = tests/data/fate/ogg-opus-chained-gain.links +FATE_OGG_OPUS_CHAIN-$(call TRANSCODE, OPUS, OGG, LAVFI_INDEV SINE_FILTER ARESAMPLE_FILTER PCM_S16LE_DECODER PCM_S16LE_MUXER FD_PROTOCOL OPUS_METADATA_BSF) += fate-ogg-opus-chained-gain +fate-ogg-opus-chained-gain: CMD = $(call OGG_OPUS_CHAINED,$(OGG_OPUS_CHAINED_GAIN_LINKS),-bsf:a opus_metadata=gain=-1536) +fate-ogg-opus-chained-gain: REF = $(OGG_OPUS_CHAINED_GAIN_LINKS).pcm +fate-ogg-opus-chained-gain: CLEANFILES = $(addprefix $(OGG_OPUS_CHAINED_GAIN_LINKS),.1.ogg .2.ogg .pcm) +fate-ogg-opus-chained-gain: CMP = stddev +fate-ogg-opus-chained-gain: CMP_TARGET = 1040 +fate-ogg-opus-chained-gain: FUZZ = 10 + +# Two SILK test vectors chained: the first link ends with samples still +# buffered in the decoder. +OGG_OPUS_REMUX = ffmpeg -i $(TARGET_SAMPLES)/opus/$(1).mka -c copy $(2) -f ogg -y $(3) +OGG_OPUS_CHAINED_SILK_LINKS = tests/data/fate/ogg-opus-chained-silk.links +FATE_OGG_OPUS_CHAIN_SAMPLES-$(call DEMMUX, MATROSKA, OGG, OGG_DEMUXER OPUS_DECODER ARESAMPLE_FILTER PCM_S16LE_ENCODER PCM_S16LE_MUXER FD_PROTOCOL PIPE_PROTOCOL) += fate-ogg-opus-chained-silk +fate-ogg-opus-chained-silk: CMD = \ + $(call OGG_OPUS_REMUX,testvector02,,$(OGG_OPUS_CHAINED_SILK_LINKS).1.ogg) && \ + $(call OGG_OPUS_REMUX,testvector03,-serial_offset 1,$(OGG_OPUS_CHAINED_SILK_LINKS).2.ogg) && \ + $(call OGG_OPUS_CHAIN_DECODE,$(OGG_OPUS_CHAINED_SILK_LINKS)) +fate-ogg-opus-chained-silk: REF = $(OGG_OPUS_CHAINED_SILK_LINKS).pcm +fate-ogg-opus-chained-silk: CLEANFILES = $(addprefix $(OGG_OPUS_CHAINED_SILK_LINKS),.1.ogg .2.ogg .pcm) +fate-ogg-opus-chained-silk: CMP = stddev +fate-ogg-opus-chained-silk: CMP_TARGET = 0 +fate-ogg-opus-chained-silk: FUZZ = 1 + FATE_OGG_OPUS-$(call DEMDEC, OGG, OPUS) += $(FATE_OGG_OPUS) FATE_SAMPLES_DUMP_STREAM_META += $(FATE_OGG_OPUS-yes) FATE_EXTERN += $(FATE_OGG_OPUS-yes) -FATE_SAMPLES_FFMPEG += $(FATE_OGG_COPY_OPUS-yes) +FATE_SAMPLES_FFMPEG += $(FATE_OGG_COPY_OPUS-yes) $(FATE_OGG_OPUS_CHAIN_SAMPLES-yes) -fate-ogg-opus: $(FATE_OGG_OPUS-yes) $(FATE_OGG_COPY_OPUS-yes) +FATE_FFMPEG += $(FATE_OGG_OPUS_CHAIN-yes) + +fate-ogg-opus: $(FATE_OGG_OPUS-yes) $(FATE_OGG_COPY_OPUS-yes) $(FATE_OGG_OPUS_CHAIN-yes) $(FATE_OGG_OPUS_CHAIN_SAMPLES-yes) -- 2.54.0 >From 194f23001feeacca2739e4a475763b25d3447c1a Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sat, 31 Oct 2026 09:44:35 -0500 Subject: [PATCH 3/9] avcodec/opus/parse: take the header to parse as an argument Pass the header to ff_opus_parse_extradata() rather than reading it from the codec context, so that a header arriving later can be parsed the same way. --- libavcodec/opus/dec.c | 3 ++- libavcodec/opus/parse.c | 14 ++++++-------- libavcodec/opus/parse.h | 3 ++- libavcodec/opus/parser.c | 3 ++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/libavcodec/opus/dec.c b/libavcodec/opus/dec.c index 712a0351b68..4f296390d27 100644 --- a/libavcodec/opus/dec.c +++ b/libavcodec/opus/dec.c @@ -692,7 +692,8 @@ static av_cold int opus_decode_init(AVCodecContext *avctx) return AVERROR(ENOMEM); /* find out the channel configuration */ - ret = ff_opus_parse_extradata(avctx, &c->p); + ret = ff_opus_parse_extradata(avctx, &c->p, avctx->extradata, + avctx->extradata_size); if (ret < 0) return ret; if (c->p.gain_i) diff --git a/libavcodec/opus/parse.c b/libavcodec/opus/parse.c index 78442031ff0..5910ddf9e20 100644 --- a/libavcodec/opus/parse.c +++ b/libavcodec/opus/parse.c @@ -284,19 +284,20 @@ static int channel_reorder_unknown(int nb_channels, int channel_idx) } av_cold int ff_opus_parse_extradata(AVCodecContext *avctx, - OpusParseContext *s) + OpusParseContext *s, + const uint8_t *header, int header_size) { static const uint8_t default_channel_map[2] = { 0, 1 }; int (*channel_reorder)(int, int) = channel_reorder_unknown; int channels = avctx->ch_layout.nb_channels; - const uint8_t *extradata, *channel_map; - int extradata_size; + const uint8_t *extradata = header, *channel_map; + int extradata_size = header_size; int version, map_type, streams, stereo_streams, i, j, ret; AVChannelLayout layout = { 0 }; - if (!avctx->extradata) { + if (!header) { if (channels > 2) { av_log(avctx, AV_LOG_ERROR, "Multichannel configuration without extradata.\n"); @@ -304,9 +305,6 @@ av_cold int ff_opus_parse_extradata(AVCodecContext *avctx, } extradata = opus_default_extradata; extradata_size = sizeof(opus_default_extradata); - } else { - extradata = avctx->extradata; - extradata_size = avctx->extradata_size; } if (extradata_size < 19) { @@ -323,7 +321,7 @@ av_cold int ff_opus_parse_extradata(AVCodecContext *avctx, avctx->delay = AV_RL16(extradata + 10); - channels = avctx->extradata ? extradata[9] : (channels == 1) ? 1 : 2; + channels = header ? extradata[9] : (channels == 1) ? 1 : 2; if (!channels) { av_log(avctx, AV_LOG_ERROR, "Zero channel count specified in the extradata\n"); return AVERROR_INVALIDDATA; diff --git a/libavcodec/opus/parse.h b/libavcodec/opus/parse.h index 467957364f6..85cb7a782b6 100644 --- a/libavcodec/opus/parse.h +++ b/libavcodec/opus/parse.h @@ -72,6 +72,7 @@ typedef struct OpusParseContext { int ff_opus_parse_packet(OpusPacket *pkt, const uint8_t *buf, int buf_size, int self_delimited); -int ff_opus_parse_extradata(AVCodecContext *avctx, OpusParseContext *s); +int ff_opus_parse_extradata(AVCodecContext *avctx, OpusParseContext *s, + const uint8_t *header, int header_size); #endif /* AVCODEC_OPUS_PARSE_H */ diff --git a/libavcodec/opus/parser.c b/libavcodec/opus/parser.c index 7cecac88578..4332f1c09a0 100644 --- a/libavcodec/opus/parser.c +++ b/libavcodec/opus/parser.c @@ -180,7 +180,8 @@ static int opus_parse(AVCodecParserContext *ctx, AVCodecContext *avctx, avctx->sample_rate = 48000; if (avctx->extradata && !s->extradata_parsed) { - if (ff_opus_parse_extradata(avctx, &s->ctx) < 0) { + if (ff_opus_parse_extradata(avctx, &s->ctx, avctx->extradata, + avctx->extradata_size) < 0) { av_log(avctx, AV_LOG_ERROR, "Error parsing Ogg extradata.\n"); goto fail; } -- 2.54.0 >From 8921e1212a11bc307569c1470f0c0bfa46abd62d Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sun, 1 Nov 2026 08:37:16 -0600 Subject: [PATCH 4/9] avcodec/opus/dec: move init and close above decoding Move opus_decode_close() and opus_decode_init() above opus_decode_packet(), so that decoding can call into them without forward declarations. --- libavcodec/opus/dec.c | 212 +++++++++++++++++++++--------------------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/libavcodec/opus/dec.c b/libavcodec/opus/dec.c index 4f296390d27..3135eb30a91 100644 --- a/libavcodec/opus/dec.c +++ b/libavcodec/opus/dec.c @@ -474,6 +474,112 @@ finish: return output_samples; } +static av_cold int opus_decode_close(AVCodecContext *avctx) +{ + OpusContext *c = avctx->priv_data; + + for (int i = 0; i < c->p.nb_streams; i++) { + OpusStreamContext *s = &c->streams[i]; + + ff_silk_free(&s->silk); + ff_celt_free(&s->celt); + + av_freep(&s->out_dummy); + s->out_dummy_allocated_size = 0; + + av_audio_fifo_free(s->sync_buffer); + av_audio_fifo_free(s->celt_delay); + swr_free(&s->swr); + } + + av_freep(&c->streams); + + c->p.nb_streams = 0; + + av_freep(&c->p.channel_maps); + av_freep(&c->fdsp); + + return 0; +} + +static av_cold int opus_decode_init(AVCodecContext *avctx) +{ + OpusContext *c = avctx->priv_data; + int ret; + + avctx->sample_fmt = AV_SAMPLE_FMT_FLTP; + avctx->sample_rate = 48000; + + c->fdsp = avpriv_float_dsp_alloc(0); + if (!c->fdsp) + return AVERROR(ENOMEM); + + /* find out the channel configuration */ + ret = ff_opus_parse_extradata(avctx, &c->p, avctx->extradata, + avctx->extradata_size); + if (ret < 0) + return ret; + if (c->p.gain_i) + c->gain = ff_exp10(c->p.gain_i / (20.0 * 256)); + + /* allocate and init each independent decoder */ + c->streams = av_calloc(c->p.nb_streams, sizeof(*c->streams)); + if (!c->streams) { + c->p.nb_streams = 0; + return AVERROR(ENOMEM); + } + + for (int i = 0; i < c->p.nb_streams; i++) { + OpusStreamContext *s = &c->streams[i]; + AVChannelLayout layout; + + s->output_channels = (i < c->p.nb_stereo_streams) ? 2 : 1; + + s->avctx = avctx; + + for (int j = 0; j < s->output_channels; j++) { + s->silk_output[j] = s->silk_buf[j]; + s->celt_output[j] = s->celt_buf[j]; + s->redundancy_output[j] = s->redundancy_buf[j]; + } + + s->fdsp = c->fdsp; + + s->swr =swr_alloc(); + if (!s->swr) + return AVERROR(ENOMEM); + + layout = (s->output_channels == 1) ? (AVChannelLayout)AV_CHANNEL_LAYOUT_MONO : + (AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO; + av_opt_set_int(s->swr, "in_sample_fmt", avctx->sample_fmt, 0); + av_opt_set_int(s->swr, "out_sample_fmt", avctx->sample_fmt, 0); + av_opt_set_chlayout(s->swr, "in_chlayout", &layout, 0); + av_opt_set_chlayout(s->swr, "out_chlayout", &layout, 0); + av_opt_set_int(s->swr, "out_sample_rate", avctx->sample_rate, 0); + av_opt_set_int(s->swr, "filter_size", 16, 0); + + ret = ff_silk_init(avctx, &s->silk, s->output_channels); + if (ret < 0) + return ret; + + ret = ff_celt_init(avctx, &s->celt, s->output_channels, c->apply_phase_inv); + if (ret < 0) + return ret; + + s->celt_delay = av_audio_fifo_alloc(avctx->sample_fmt, + s->output_channels, 1024); + if (!s->celt_delay) + return AVERROR(ENOMEM); + + s->sync_buffer = av_audio_fifo_alloc(avctx->sample_fmt, + s->output_channels, 32); + if (!s->sync_buffer) + return AVERROR(ENOMEM); + } + + return 0; +} + static int opus_decode_packet(AVCodecContext *avctx, AVFrame *frame, int *got_frame_ptr, AVPacket *avpkt) { @@ -651,112 +757,6 @@ static av_cold void opus_decode_flush(AVCodecContext *ctx) } } -static av_cold int opus_decode_close(AVCodecContext *avctx) -{ - OpusContext *c = avctx->priv_data; - - for (int i = 0; i < c->p.nb_streams; i++) { - OpusStreamContext *s = &c->streams[i]; - - ff_silk_free(&s->silk); - ff_celt_free(&s->celt); - - av_freep(&s->out_dummy); - s->out_dummy_allocated_size = 0; - - av_audio_fifo_free(s->sync_buffer); - av_audio_fifo_free(s->celt_delay); - swr_free(&s->swr); - } - - av_freep(&c->streams); - - c->p.nb_streams = 0; - - av_freep(&c->p.channel_maps); - av_freep(&c->fdsp); - - return 0; -} - -static av_cold int opus_decode_init(AVCodecContext *avctx) -{ - OpusContext *c = avctx->priv_data; - int ret; - - avctx->sample_fmt = AV_SAMPLE_FMT_FLTP; - avctx->sample_rate = 48000; - - c->fdsp = avpriv_float_dsp_alloc(0); - if (!c->fdsp) - return AVERROR(ENOMEM); - - /* find out the channel configuration */ - ret = ff_opus_parse_extradata(avctx, &c->p, avctx->extradata, - avctx->extradata_size); - if (ret < 0) - return ret; - if (c->p.gain_i) - c->gain = ff_exp10(c->p.gain_i / (20.0 * 256)); - - /* allocate and init each independent decoder */ - c->streams = av_calloc(c->p.nb_streams, sizeof(*c->streams)); - if (!c->streams) { - c->p.nb_streams = 0; - return AVERROR(ENOMEM); - } - - for (int i = 0; i < c->p.nb_streams; i++) { - OpusStreamContext *s = &c->streams[i]; - AVChannelLayout layout; - - s->output_channels = (i < c->p.nb_stereo_streams) ? 2 : 1; - - s->avctx = avctx; - - for (int j = 0; j < s->output_channels; j++) { - s->silk_output[j] = s->silk_buf[j]; - s->celt_output[j] = s->celt_buf[j]; - s->redundancy_output[j] = s->redundancy_buf[j]; - } - - s->fdsp = c->fdsp; - - s->swr =swr_alloc(); - if (!s->swr) - return AVERROR(ENOMEM); - - layout = (s->output_channels == 1) ? (AVChannelLayout)AV_CHANNEL_LAYOUT_MONO : - (AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO; - av_opt_set_int(s->swr, "in_sample_fmt", avctx->sample_fmt, 0); - av_opt_set_int(s->swr, "out_sample_fmt", avctx->sample_fmt, 0); - av_opt_set_chlayout(s->swr, "in_chlayout", &layout, 0); - av_opt_set_chlayout(s->swr, "out_chlayout", &layout, 0); - av_opt_set_int(s->swr, "out_sample_rate", avctx->sample_rate, 0); - av_opt_set_int(s->swr, "filter_size", 16, 0); - - ret = ff_silk_init(avctx, &s->silk, s->output_channels); - if (ret < 0) - return ret; - - ret = ff_celt_init(avctx, &s->celt, s->output_channels, c->apply_phase_inv); - if (ret < 0) - return ret; - - s->celt_delay = av_audio_fifo_alloc(avctx->sample_fmt, - s->output_channels, 1024); - if (!s->celt_delay) - return AVERROR(ENOMEM); - - s->sync_buffer = av_audio_fifo_alloc(avctx->sample_fmt, - s->output_channels, 32); - if (!s->sync_buffer) - return AVERROR(ENOMEM); - } - - return 0; -} - #define OFFSET(x) offsetof(OpusContext, x) #define AD AV_OPT_FLAG_AUDIO_PARAM | AV_OPT_FLAG_DECODING_PARAM static const AVOption opus_options[] = { -- 2.54.0 >From 6b7aba64d2ed4c39921f26c62630786c5bd9c8ed Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sun, 1 Nov 2026 08:37:39 -0600 Subject: [PATCH 5/9] avcodec/opus/dec: split setting up from a header out of init and close Move setting the streams up from a header, and freeing them, out of initialization and closing, so that they can run again for a header arriving later. --- libavcodec/opus/dec.c | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/libavcodec/opus/dec.c b/libavcodec/opus/dec.c index 3135eb30a91..00e7866e718 100644 --- a/libavcodec/opus/dec.c +++ b/libavcodec/opus/dec.c @@ -474,10 +474,8 @@ finish: return output_samples; } -static av_cold int opus_decode_close(AVCodecContext *avctx) +static av_cold void opus_free_streams(OpusContext *c) { - OpusContext *c = avctx->priv_data; - for (int i = 0; i < c->p.nb_streams; i++) { OpusStreamContext *s = &c->streams[i]; @@ -497,26 +495,26 @@ static av_cold int opus_decode_close(AVCodecContext *avctx) c->p.nb_streams = 0; av_freep(&c->p.channel_maps); +} + +static av_cold int opus_decode_close(AVCodecContext *avctx) +{ + OpusContext *c = avctx->priv_data; + + opus_free_streams(c); av_freep(&c->fdsp); return 0; } -static av_cold int opus_decode_init(AVCodecContext *avctx) +static av_cold int opus_init_streams(AVCodecContext *avctx, + const uint8_t *header, int header_size) { OpusContext *c = avctx->priv_data; int ret; - avctx->sample_fmt = AV_SAMPLE_FMT_FLTP; - avctx->sample_rate = 48000; - - c->fdsp = avpriv_float_dsp_alloc(0); - if (!c->fdsp) - return AVERROR(ENOMEM); - /* find out the channel configuration */ - ret = ff_opus_parse_extradata(avctx, &c->p, avctx->extradata, - avctx->extradata_size); + ret = ff_opus_parse_extradata(avctx, &c->p, header, header_size); if (ret < 0) return ret; if (c->p.gain_i) @@ -580,6 +578,20 @@ static av_cold int opus_decode_init(AVCodecContext *avctx) return 0; } +static av_cold int opus_decode_init(AVCodecContext *avctx) +{ + OpusContext *c = avctx->priv_data; + + avctx->sample_fmt = AV_SAMPLE_FMT_FLTP; + avctx->sample_rate = 48000; + + c->fdsp = avpriv_float_dsp_alloc(0); + if (!c->fdsp) + return AVERROR(ENOMEM); + + return opus_init_streams(avctx, avctx->extradata, avctx->extradata_size); +} + static int opus_decode_packet(AVCodecContext *avctx, AVFrame *frame, int *got_frame_ptr, AVPacket *avpkt) { -- 2.54.0 >From 868a9006b5e490c83a68de078bfd85cfa612ed0b Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sun, 1 Nov 2026 09:01:33 -0600 Subject: [PATCH 6/9] avcodec/opus/dec: output held-back samples on AV_PKT_FLAG_EOS The resampler for SILK frames holds back some output samples, which are only output at the next mode or rate change, or when draining. Flush them into the frame of a packet flagged AV_PKT_FLAG_EOS, so that they end the sequence they belong to and its end trimming applies to them. --- libavcodec/opus/dec.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/libavcodec/opus/dec.c b/libavcodec/opus/dec.c index 00e7866e718..7f644d01612 100644 --- a/libavcodec/opus/dec.c +++ b/libavcodec/opus/dec.c @@ -474,6 +474,30 @@ finish: return output_samples; } +/** + * Append the samples the resampler holds back to the output of the + * sub-packet that just decoded, leaving the stream's output state as it was. + */ +static int opus_drain_subpacket(OpusStreamContext *s, int decoded_samples) +{ + float *out[2] = { s->out[0], s->out[1] }; + int out_size = s->out_size; + int ret; + + for (int i = 0; i < 2; i++) + if (s->out[i]) + s->out[i] += decoded_samples; + s->out_size -= decoded_samples * sizeof(float); + + ret = opus_decode_subpacket(s, NULL); + + s->out[0] = out[0]; + s->out[1] = out[1]; + s->out_size = out_size; + + return ret < 0 ? ret : decoded_samples + ret; +} + static av_cold void opus_free_streams(OpusContext *c) { for (int i = 0; i < c->p.nb_streams; i++) { @@ -697,6 +721,8 @@ static int opus_decode_packet(AVCodecContext *avctx, AVFrame *frame, } ret = opus_decode_subpacket(&c->streams[i], buf); + if (ret >= 0 && buf && (avpkt->flags & AV_PKT_FLAG_EOS)) + ret = opus_drain_subpacket(s, ret); if (ret < 0) return ret; s->decoded_samples = ret; -- 2.54.0 >From de1db76a893fe5216cefcb918ca002714a280b0f Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sat, 31 Oct 2026 09:44:35 -0500 Subject: [PATCH 7/9] avcodec/opus/dec: take a new header from packet side data A chained Ogg Opus link has its own header and starts a new logical stream, while the decoder keeps the first link's setup and state. Tear the decoder down and set it up again from a header passed as AV_PKT_DATA_NEW_EXTRADATA, before decoding the packet that carries it. --- libavcodec/opus/dec.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libavcodec/opus/dec.c b/libavcodec/opus/dec.c index 7f644d01612..f6a1c9c12c7 100644 --- a/libavcodec/opus/dec.c +++ b/libavcodec/opus/dec.c @@ -625,8 +625,26 @@ static int opus_decode_packet(AVCodecContext *avctx, AVFrame *frame, int coded_samples = 0; int decoded_samples = INT_MAX; int delayed_samples = 0; + const uint8_t *new_header; + size_t new_header_size; int i, ret; + /* A new header starts a new logical stream, decoded from a fresh state. */ + new_header = av_packet_get_side_data(avpkt, AV_PKT_DATA_NEW_EXTRADATA, + &new_header_size); + if (new_header) { + if (new_header_size > INT_MAX) + return AVERROR_INVALIDDATA; + opus_free_streams(c); + ret = opus_init_streams(avctx, new_header, new_header_size); + if (ret < 0) { + opus_free_streams(c); + return ret; + } + } + if (!c->streams) + return AVERROR_INVALIDDATA; + /* calculate the number of delayed samples */ for (int i = 0; i < c->p.nb_streams; i++) { OpusStreamContext *s = &c->streams[i]; -- 2.54.0 >From e482dfd7212d18f94e09d27b3b31eabf385bbff3 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sun, 1 Nov 2026 09:07:30 -0600 Subject: [PATCH 8/9] avformat/oggparseopus: flag the last packet of a link with AV_PKT_FLAG_EOS A link ends with samples the decoder may still hold back, and the next link does not continue its decoder state. Flag the last packet of an EOS page, so that the decoder can output them with the link they belong to. --- libavformat/oggparseopus.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libavformat/oggparseopus.c b/libavformat/oggparseopus.c index 43840c003ea..f01580878f3 100644 --- a/libavformat/oggparseopus.c +++ b/libavformat/oggparseopus.c @@ -212,6 +212,8 @@ static int opus_packet(AVFormatContext *avf, int idx) "Last packet was truncated to %d due to end trimming.\n", os->pduration); } + if (os->segp == os->nsegs) + os->pflags |= AV_PKT_FLAG_EOS; } return 0; -- 2.54.0 >From 6648841cec9f053bfa6f5e022616e8667ca54b7d Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Sun, 1 Nov 2026 08:44:45 -0600 Subject: [PATCH 9/9] avformat/oggparseopus: pass a later link's header to the decoder When a chained stream moves to a new link, the decoder is still set up from the first link's header. Attach the new link's header to its first packet as AV_PKT_DATA_NEW_EXTRADATA, as the vorbis and flac parsers do. All three chained tests now decode exactly as their links decoded one by one. --- libavformat/oggparseopus.c | 7 +++++++ tests/fate/ogg-opus.mak | 10 +++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/libavformat/oggparseopus.c b/libavformat/oggparseopus.c index f01580878f3..6dde828abfb 100644 --- a/libavformat/oggparseopus.c +++ b/libavformat/oggparseopus.c @@ -150,6 +150,13 @@ static int opus_packet(AVFormatContext *avf, int idx) if (ret < 0) return ret; + /* A later link's header: the decoder is still set up for the previous one. */ + av_freep(&os->new_extradata); + os->new_extradata = av_memdup(packet, os->psize); + if (!os->new_extradata) + return AVERROR(ENOMEM); + os->new_extradata_size = os->psize; + return 1; } diff --git a/tests/fate/ogg-opus.mak b/tests/fate/ogg-opus.mak index ec415535a5f..59e2794b292 100644 --- a/tests/fate/ogg-opus.mak +++ b/tests/fate/ogg-opus.mak @@ -34,8 +34,8 @@ fate-ogg-opus-chained-reset: CMD = $(call OGG_OPUS_CHAINED,$(OGG_OPUS_CHAINED_RE fate-ogg-opus-chained-reset: REF = $(OGG_OPUS_CHAINED_RESET_LINKS).pcm fate-ogg-opus-chained-reset: CLEANFILES = $(addprefix $(OGG_OPUS_CHAINED_RESET_LINKS),.1.ogg .2.ogg .pcm) fate-ogg-opus-chained-reset: CMP = stddev -fate-ogg-opus-chained-reset: CMP_TARGET = 95 -fate-ogg-opus-chained-reset: FUZZ = 5 +fate-ogg-opus-chained-reset: CMP_TARGET = 0 +fate-ogg-opus-chained-reset: FUZZ = 0 # The second link sets an output gain of -6 dB, which the first one does not. OGG_OPUS_CHAINED_GAIN_LINKS = tests/data/fate/ogg-opus-chained-gain.links @@ -44,8 +44,8 @@ fate-ogg-opus-chained-gain: CMD = $(call OGG_OPUS_CHAINED,$(OGG_OPUS_CHAINED_GAI fate-ogg-opus-chained-gain: REF = $(OGG_OPUS_CHAINED_GAIN_LINKS).pcm fate-ogg-opus-chained-gain: CLEANFILES = $(addprefix $(OGG_OPUS_CHAINED_GAIN_LINKS),.1.ogg .2.ogg .pcm) fate-ogg-opus-chained-gain: CMP = stddev -fate-ogg-opus-chained-gain: CMP_TARGET = 1040 -fate-ogg-opus-chained-gain: FUZZ = 10 +fate-ogg-opus-chained-gain: CMP_TARGET = 0 +fate-ogg-opus-chained-gain: FUZZ = 0 # Two SILK test vectors chained: the first link ends with samples still # buffered in the decoder. @@ -60,7 +60,7 @@ fate-ogg-opus-chained-silk: REF = $(OGG_OPUS_CHAINED_SILK_LINKS).pcm fate-ogg-opus-chained-silk: CLEANFILES = $(addprefix $(OGG_OPUS_CHAINED_SILK_LINKS),.1.ogg .2.ogg .pcm) fate-ogg-opus-chained-silk: CMP = stddev fate-ogg-opus-chained-silk: CMP_TARGET = 0 -fate-ogg-opus-chained-silk: FUZZ = 1 +fate-ogg-opus-chained-silk: FUZZ = 0 FATE_OGG_OPUS-$(call DEMDEC, OGG, OPUS) += $(FATE_OGG_OPUS) -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
