Commit dcd3ff4b98 for ffmpeg
commit dcd3ff4b98d2980750df320d4fb1af354f1481b5
Author: Romain Beauxis <romain.beauxis@gmail.com>
Date: Mon Nov 9 19:36:13 2026 -0600
avformat/ogg/muxer: start a new Theora link on new extradata
A stream copied chained Theora file keeps the headers of each later
link, so the copy decodes as the original.
A later link changing the frame rate ends the output at the link before
it, as packets keep the stream time base.
diff --git a/doc/muxers.texi b/doc/muxers.texi
index 9343c21236..ebca0971be 100644
--- a/doc/muxers.texi
+++ b/doc/muxers.texi
@@ -3425,11 +3425,14 @@ ffmpeg -i INPUT -f_strict experimental -syncpoints none - | processor
Ogg container muxer.
-A new link is started on new metadata or extradata of a single Vorbis, FLAC or
-Opus stream.
+A new link is started on new metadata or extradata of a single Vorbis, FLAC,
+Opus or Theora stream.
Other outputs ignore new metadata.
-New extradata ends them at the current link, and the packets that follow are
-dropped.
+So does Theora on an inter frame.
+New extradata ends them at the current link.
+So does new Theora extradata that changes the frame rate.
+The packet carrying it and the ones that follow are dropped, and their writes
+still return success.
New extradata on the first packet is ignored.
@table @option
diff --git a/libavformat/ogg/muxer.c b/libavformat/ogg/muxer.c
index e21f91bb56..409478e5c5 100644
--- a/libavformat/ogg/muxer.c
+++ b/libavformat/ogg/muxer.c
@@ -698,6 +698,22 @@ static int ogg_write_header(AVFormatContext *s)
return 0;
}
+/**
+ * Whether Theora headers set a frame rate other than the stream time base,
+ * which packets keep across links.
+ */
+static int ogg_theora_rate_changed(const AVStream *st, const uint8_t *extradata, size_t size)
+{
+ const uint8_t *header[3];
+ int header_len[3];
+
+ if (size > INT_MAX ||
+ avpriv_split_xiph_headers(extradata, size, 42, header, header_len) < 0)
+ return 0;
+ return av_cmp_q(st->time_base,
+ (AVRational){ AV_RB32(header[0] + 26), AV_RB32(header[0] + 22) }) != 0;
+}
+
/**
* Keep the current link when a new one cannot be started. New metadata is
* then dropped, while new extradata ends the output, as the packets that
@@ -749,9 +765,18 @@ static int ogg_check_new_link(AVFormatContext *s, AVPacket *pkt)
if (par->codec_id != AV_CODEC_ID_VORBIS &&
par->codec_id != AV_CODEC_ID_FLAC &&
- par->codec_id != AV_CODEC_ID_OPUS)
+ par->codec_id != AV_CODEC_ID_OPUS &&
+ par->codec_id != AV_CODEC_ID_THEORA)
return ogg_skip_new_link(s, new_extradata,
- "New links are only supported for vorbis, flac and opus streams");
+ "New links are only supported for vorbis, flac, opus and theora streams");
+
+ if (par->codec_id == AV_CODEC_ID_THEORA && !new_extradata &&
+ !(pkt->flags & AV_PKT_FLAG_KEY))
+ return ogg_skip_new_link(s, NULL, "New Theora links must start on a keyframe");
+
+ if (par->codec_id == AV_CODEC_ID_THEORA && new_extradata &&
+ ogg_theora_rate_changed(st, new_extradata, extradata_size))
+ return ogg_skip_new_link(s, new_extradata, "New Theora links cannot change the frame rate");
ret = ogg_write_trailer(s);
if (ret < 0)
diff --git a/tests/fate/ogg-theora.mak b/tests/fate/ogg-theora.mak
index 44b0806a23..5117e51d80 100644
--- a/tests/fate/ogg-theora.mak
+++ b/tests/fate/ogg-theora.mak
@@ -26,11 +26,13 @@ OGG_THEORA_DECODE = ffmpeg -i $(1) -noautoscale -fps_mode passthrough -f rawvide
# $(1).1.ogg and $(1).2.ogg chained must give what decoding them one by one
# does, written to $(1).raw as the reference. $(2) holds further options for
# the second link.
-OGG_THEORA_CHAINED = \
+OGG_THEORA_CHAINED_LINKS = \
$(call OGG_THEORA_LINK,160x120,,$(1).1.ogg) && \
$(call OGG_THEORA_LINK,320x240,-serial_offset 1 $(2),$(1).2.ogg) && \
{ $(call OGG_THEORA_DECODE,$(1).1.ogg) && $(call OGG_THEORA_DECODE,$(1).2.ogg); } > $(1).raw && \
- test -s $(1).raw && \
+ test -s $(1).raw
+OGG_THEORA_CHAINED = \
+ $(call OGG_THEORA_CHAINED_LINKS,$(1),$(2)) && \
cat $(1).1.ogg $(1).2.ogg | $(call OGG_THEORA_DECODE,pipe:)
OGG_THEORA_CHAINED_CONTENT_DEPS = LAVFI_INDEV TESTSRC_FILTER SCALE_FILTER WRAPPED_AVFRAME_DECODER RAWVIDEO_MUXER
@@ -67,6 +69,20 @@ fate-ogg-theora-chained-large-comment: CMP = stddev
fate-ogg-theora-chained-large-comment: CMP_TARGET = 0
fate-ogg-theora-chained-large-comment: FUZZ = 0
+# The chain stream copied must decode as its links do.
+OGG_THEORA_CHAINED_COPY_LINKS = tests/data/fate/ogg-theora-chained-copy.links
+FATE_OGG_THEORA_CONTENT-$(call TRANSCODE, LIBTHEORA THEORA, OGG, $(OGG_THEORA_CHAINED_CONTENT_DEPS)) += fate-ogg-theora-chained-copy
+fate-ogg-theora-chained-copy: CMD = \
+ $(call OGG_THEORA_CHAINED_LINKS,$(OGG_THEORA_CHAINED_COPY_LINKS)) && \
+ cat $(OGG_THEORA_CHAINED_COPY_LINKS).1.ogg $(OGG_THEORA_CHAINED_COPY_LINKS).2.ogg | \
+ ffmpeg -i pipe: -c copy -fflags +bitexact -f ogg -y $(OGG_THEORA_CHAINED_COPY_LINKS).copy.ogg && \
+ $(call OGG_THEORA_DECODE,$(OGG_THEORA_CHAINED_COPY_LINKS).copy.ogg)
+fate-ogg-theora-chained-copy: REF = $(OGG_THEORA_CHAINED_COPY_LINKS).raw
+fate-ogg-theora-chained-copy: CLEANFILES = $(addprefix $(OGG_THEORA_CHAINED_COPY_LINKS),.1.ogg .2.ogg .copy.ogg .raw)
+fate-ogg-theora-chained-copy: CMP = stddev
+fate-ogg-theora-chained-copy: CMP_TARGET = 0
+fate-ogg-theora-chained-copy: FUZZ = 0
+
FATE_FFMPEG += $(FATE_OGG_THEORA_CONTENT-yes)
fate-ogg-theora: $(FATE_OGG_THEORA_CHAIN-yes) $(FATE_OGG_THEORA_CONTENT-yes)