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