Commit afe9d68be1 for ffmpeg
commit afe9d68be1cc64f7cd0664fa7f27549f1b39b09c
Author: Romain Beauxis <romain.beauxis@gmail.com>
Date: Sun Sep 27 12:07:26 2026 -0500
avformat/oggenc: derive bitexact serial numbers from the stream headers
Bitexact outputs all used serial_offset + index, with serial_offset
defaulting to 0, so chaining them broke the RFC 3533 requirement of a
unique serial number per chained stream.
Default serial_offset to -1, which derives the serial number from a CRC
of the stream's headers. Links with different headers or tags then rarely
share one. An explicit offset still allocates consecutive serial numbers.
diff --git a/doc/muxers.texi b/doc/muxers.texi
index 95f7ea33a7..af599893b2 100644
--- a/doc/muxers.texi
+++ b/doc/muxers.texi
@@ -3414,9 +3414,21 @@ possible. A value of 1 will effectively use 1 packet-per-page in most
situations, giving a small seek granularity at the cost of additional container
overhead.
@item -serial_offset @var{value}
-Serial value from which to set the streams serial number.
-Setting it to different and sufficiently large values ensures that the produced
-ogg files can be safely chained.
+Serial number of the first stream in bitexact mode, the others following
+consecutively.
+Chained links need distinct serial numbers, which disjoint ranges of values
+guarantee.
+The default of -1 derives each serial number from the stream's headers
+instead.
+This makes links with different headers or tags unlikely to share a serial
+number, without ruling it out.
+Links with identical headers and tags always share one, which separately muxed
+outputs avoid with explicit offsets.
+
+A link the muxer starts itself on new stream metadata keeps the offset of the
+link before it.
+With an explicit offset it then reuses its serial number.
+With the default it does so when its headers and tags are unchanged.
@end table
diff --git a/libavformat/oggenc.c b/libavformat/oggenc.c
index 5c7e3da0bc..f75923e4a7 100644
--- a/libavformat/oggenc.c
+++ b/libavformat/oggenc.c
@@ -88,8 +88,8 @@ typedef struct OGGContext {
static int ogg_write_trailer(AVFormatContext *s);
static const AVOption options[] = {
- { "serial_offset", "serial number offset",
- OFFSET(serial_offset), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, PARAM },
+ { "serial_offset", "serial number offset, -1 to derive it from the stream headers",
+ OFFSET(serial_offset), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, PARAM },
{ "page_duration", "preferred page duration, in microseconds",
OFFSET(pref_duration), AV_OPT_TYPE_INT64, { .i64 = 1000000 }, 0, INT64_MAX, PARAM },
{ NULL },
@@ -486,17 +486,43 @@ static void ogg_write_pages(AVFormatContext *s, int flush)
ogg->page_list = p;
}
+/**
+ * Derive a bitexact serial number from the stream's header packets, which
+ * makes links muxed apart unlikely to share one, though not certain not to.
+ */
+static unsigned ogg_bitexact_serial(const OGGStreamContext *oggstream, unsigned seed)
+{
+ const AVCRC *table = av_crc_get_table(AV_CRC_32_IEEE);
+ uint32_t crc = seed;
+
+ for (int i = 0; i < 3; i++)
+ if (oggstream->header_len[i])
+ crc = av_crc(table, crc, oggstream->header[i], oggstream->header_len[i]);
+
+ return crc;
+}
+
+static int ogg_serial_in_use(AVFormatContext *s, int nb_streams, unsigned serial_num)
+{
+ for (int i = 0; i < nb_streams; i++) {
+ const OGGStreamContext *oggstream = s->streams[i]->priv_data;
+ if (oggstream->serial_num == serial_num)
+ return 1;
+ }
+ return 0;
+}
+
// This function can be used on an initialized context to reinitialize the
// streams.
static int ogg_init(AVFormatContext *s)
{
OGGContext *ogg = s->priv_data;
OGGStreamContext *oggstream = NULL;
- int i, j;
+ int i;
for (i = 0; i < s->nb_streams; i++) {
AVStream *st = s->streams[i];
- unsigned serial_num = i + ogg->serial_offset;
+ unsigned serial_num;
if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
if (st->codecpar->codec_id == AV_CODEC_ID_OPUS)
@@ -535,17 +561,6 @@ static int ogg_init(AVFormatContext *s)
oggstream->page.stream_index = i;
- if (!(s->flags & AVFMT_FLAG_BITEXACT))
- do {
- serial_num = av_get_random_seed();
- for (j = 0; j < i; j++) {
- OGGStreamContext *sc = s->streams[j]->priv_data;
- if (serial_num == sc->serial_num)
- break;
- }
- } while (j < i);
- oggstream->serial_num = serial_num;
-
av_dict_copy(&st->metadata, s->metadata, AV_DICT_DONT_OVERWRITE);
if (st->codecpar->codec_id == AV_CODEC_ID_FLAC) {
@@ -620,6 +635,17 @@ static int ogg_init(AVFormatContext *s)
oggstream->kfgshift, oggstream->vrev);
}
}
+
+ if (s->flags & AVFMT_FLAG_BITEXACT && ogg->serial_offset >= 0)
+ serial_num = i + ogg->serial_offset;
+ else if (s->flags & AVFMT_FLAG_BITEXACT)
+ serial_num = ogg_bitexact_serial(oggstream, i);
+ else
+ serial_num = av_get_random_seed();
+ while (ogg_serial_in_use(s, i, serial_num))
+ serial_num = s->flags & AVFMT_FLAG_BITEXACT ? serial_num + 1
+ : av_get_random_seed();
+ oggstream->serial_num = serial_num;
}
return 0;
diff --git a/tests/ref/fate/limited_input_seek b/tests/ref/fate/limited_input_seek
index 7a5715cf17..95f50a107f 100644
--- a/tests/ref/fate/limited_input_seek
+++ b/tests/ref/fate/limited_input_seek
@@ -1 +1 @@
-4498ed5370e5a15904bad5d5a716d0b9
+e1068afb583b5f21a6faff94b458fd8f
diff --git a/tests/ref/fate/limited_input_seek-copyts b/tests/ref/fate/limited_input_seek-copyts
index 527a1fb0ad..f9b47b070e 100644
--- a/tests/ref/fate/limited_input_seek-copyts
+++ b/tests/ref/fate/limited_input_seek-copyts
@@ -1 +1 @@
-7a0febe8623a2838ac5878f58159c74f
+63e0dd3f351d2fb43f0cdfd12e517496
diff --git a/tests/ref/fate/ogg-flac-serial-default b/tests/ref/fate/ogg-flac-serial-default
index a578948769..d2bfab60a2 100644
--- a/tests/ref/fate/ogg-flac-serial-default
+++ b/tests/ref/fate/ogg-flac-serial-default
@@ -1,6 +1,6 @@
-pos=0 serial=0 sequence=0 granule=0 flags=-b- segments=1 size=51
-pos=79 serial=0 sequence=1 granule=0 flags=--- segments=1 size=70
-pos=177 serial=0 sequence=2 granule=8820 flags=--e segments=10 size=2418
-pos=2632 serial=0 sequence=0 granule=0 flags=-b- segments=1 size=51
-pos=2711 serial=0 sequence=1 granule=0 flags=--- segments=1 size=71
-pos=2810 serial=0 sequence=2 granule=17640 flags=--e segments=10 size=2418
+pos=0 serial=1798652137 sequence=0 granule=0 flags=-b- segments=1 size=51
+pos=79 serial=1798652137 sequence=1 granule=0 flags=--- segments=1 size=70
+pos=177 serial=1798652137 sequence=2 granule=8820 flags=--e segments=10 size=2418
+pos=2632 serial=670444992 sequence=0 granule=0 flags=-b- segments=1 size=51
+pos=2711 serial=670444992 sequence=1 granule=0 flags=--- segments=1 size=71
+pos=2810 serial=670444992 sequence=2 granule=17640 flags=--e segments=10 size=2418
diff --git a/tests/ref/lavf-fate/vp3.ogg b/tests/ref/lavf-fate/vp3.ogg
index f4e22dc16b..28491cec4e 100644
--- a/tests/ref/lavf-fate/vp3.ogg
+++ b/tests/ref/lavf-fate/vp3.ogg
@@ -1,3 +1,3 @@
-4bd51dac3194fa88ae33767c25b4b1e6 *tests/data/lavf-fate/lavf.vp3.ogg
+f89ded5881b4b002905594c3ff90f8ec *tests/data/lavf-fate/lavf.vp3.ogg
417621 tests/data/lavf-fate/lavf.vp3.ogg
tests/data/lavf-fate/lavf.vp3.ogg CRC=0x037e3e79
diff --git a/tests/ref/lavf-fate/vp8.ogg b/tests/ref/lavf-fate/vp8.ogg
index dd4ed162f9..8a5ef0909a 100644
--- a/tests/ref/lavf-fate/vp8.ogg
+++ b/tests/ref/lavf-fate/vp8.ogg
@@ -1,3 +1,3 @@
-20f1e9b1714513a0ba85ca636e818784 *tests/data/lavf-fate/lavf.vp8.ogg
+c396feea7c83ccc361b94af00967da2a *tests/data/lavf-fate/lavf.vp8.ogg
95009 tests/data/lavf-fate/lavf.vp8.ogg
tests/data/lavf-fate/lavf.vp8.ogg CRC=0xfb8d7a66
diff --git a/tests/ref/lavf/ogg b/tests/ref/lavf/ogg
index 3ac10e6f7c..0e7fa29933 100644
--- a/tests/ref/lavf/ogg
+++ b/tests/ref/lavf/ogg
@@ -1,3 +1,3 @@
-81b9366cacb23644c2803585dced9996 *tests/data/lavf/lavf.ogg
+f3e4bad31e324f91ffe525106dd53148 *tests/data/lavf/lavf.ogg
13516 tests/data/lavf/lavf.ogg
tests/data/lavf/lavf.ogg CRC=0x3a1da17e