Commit efae536451 for ffmpeg

commit efae53645158208f65f4d9a5dce95712819639c0
Author: Romain Beauxis <romain.beauxis@gmail.com>
Date:   Sat Oct 31 09:55:33 2026 -0500

    avformat/ogg/mapping/theora: pass a later link's headers to the decoder

    When a chained stream moves to a new link, its headers are passed on
    as packets, which the decoder refuses with frame threading.

    Gather the new link's headers as the Vorbis mapping does, checking their
    order and leaving reserved packets to the decoder, and attach them
    packed like the extradata as AV_PKT_DATA_NEW_EXTRADATA.

    The frame-threaded chained Theora tests now decode the second link,
    one of them checking its pictures against the link decoded alone.

diff --git a/libavformat/ogg/mapping/theora.c b/libavformat/ogg/mapping/theora.c
index dae53be2ee..b720de45fb 100644
--- a/libavformat/ogg/mapping/theora.c
+++ b/libavformat/ogg/mapping/theora.c
@@ -31,12 +31,21 @@
 #include "libavformat/ogg/mapping.h"
 #include "libavformat/vorbis/comment.h"

+enum TheoraPacketType {
+    THEORA_PACKET_DATA,
+    THEORA_PACKET_ID,
+    THEORA_PACKET_COMMENT,
+    THEORA_PACKET_SETUP,
+    THEORA_PACKET_RESERVED,
+};
+
 typedef struct TheoraParams {
     int gpshift;
     int gpmask;
     unsigned version;
     uint8_t *packet[3];                 ///< headers of the link, until all three are in
     int len[3];
+    enum TheoraPacketType last_packet;  ///< type of the last packet seen by theora_packet()
 } TheoraParams;

 /**
@@ -209,11 +218,101 @@ static uint64_t theora_gptopts(AVFormatContext *ctx, int idx, uint64_t gp,
     return iframe + pframe;
 }

+static enum TheoraPacketType theora_packet_type(const struct ogg_stream *os)
+{
+    uint8_t type = os->psize > 0 ? os->buf[os->pstart] : 0;
+
+    if (!(type & 0x80))
+        return THEORA_PACKET_DATA;
+    return type <= 0x82 ? THEORA_PACKET_ID + (type & 0x7f) : THEORA_PACKET_RESERVED;
+}
+
+static int theora_check_packet_order(AVFormatContext *s, TheoraParams *thp,
+                                     enum TheoraPacketType type)
+{
+    enum TheoraPacketType last = thp->last_packet;
+    const char *error = NULL;
+
+    switch (type) {
+    case THEORA_PACKET_RESERVED:
+        return 0;
+    case THEORA_PACKET_DATA:
+        if (last == THEORA_PACKET_ID || last == THEORA_PACKET_COMMENT)
+            error = "data packet before the setup header";
+        break;
+    case THEORA_PACKET_ID:
+        if (last == THEORA_PACKET_ID || last == THEORA_PACKET_COMMENT)
+            error = "identification header before the setup header";
+        break;
+    case THEORA_PACKET_COMMENT:
+        if (last != THEORA_PACKET_ID)
+            error = "comment header without identification header";
+        break;
+    case THEORA_PACKET_SETUP:
+        if (last != THEORA_PACKET_COMMENT)
+            error = "setup header without comment header";
+        break;
+    }
+
+    if (error)
+        av_log(s, AV_LOG_WARNING,
+               "Invalid chained stream headers: %s, discarding them\n", error);
+
+    if (error || type == THEORA_PACKET_ID)
+        for (int i = 0; i < 3; i++)
+            av_freep(&thp->packet[i]);
+
+    thp->last_packet = error && type != THEORA_PACKET_ID ? THEORA_PACKET_DATA : type;
+    return error ? AVERROR_INVALIDDATA : 0;
+}
+
 static int theora_packet(AVFormatContext *s, int idx)
 {
     struct ogg *ogg = s->priv_data;
     struct ogg_stream *os = ogg->streams + idx;
-    int duration;
+    TheoraParams *thp = os->private;
+    enum TheoraPacketType type = theora_packet_type(os);
+    int duration, ret;
+
+    /* A later link's headers are skipped and attached as extradata to the
+     * first data packet that follows. That packet keeps its timestamp, which
+     * ogg_read_packet() would clear after a stream replacement. */
+    if (type != THEORA_PACKET_DATA && type != THEORA_PACKET_RESERVED) {
+        int i = type - THEORA_PACKET_ID;
+
+        os->replace = 0;
+
+        if (theora_check_packet_order(s, thp, type) < 0 && type != THEORA_PACKET_ID)
+            return 1;
+
+        ret = av_reallocp(&thp->packet[i], os->psize);
+        if (ret < 0)
+            return ret;
+        memcpy(thp->packet[i], os->buf + os->pstart, os->psize);
+        thp->len[i] = os->psize;
+
+        if (type == THEORA_PACKET_COMMENT && os->psize > 7) {
+            ret = ff_vorbis_update_metadata(s, s->streams[idx],
+                                            os->buf + os->pstart + 7, os->psize - 7);
+            if (ret == AVERROR_INVALIDDATA)
+                av_log(s, AV_LOG_WARNING, "Invalid Theora comment header, metadata ignored\n");
+            else if (ret < 0)
+                return ret;
+        }
+
+        if (thp->packet[0] && thp->packet[1] && thp->packet[2]) {
+            av_freep(&os->new_extradata);
+            ret = theora_pack_headers(thp, &os->new_extradata);
+            if (ret < 0) {
+                os->new_extradata_size = 0;
+                return ret;
+            }
+            os->new_extradata_size = ret;
+        }
+
+        return 1;
+    }
+    theora_check_packet_order(s, thp, type);

     /* first packet handling
        here we parse the duration of each packet in the first page and compare
diff --git a/tests/fate/ogg-theora.mak b/tests/fate/ogg-theora.mak
index 63f35c2f5c..44b0806a23 100644
--- a/tests/fate/ogg-theora.mak
+++ b/tests/fate/ogg-theora.mak
@@ -44,6 +44,17 @@ fate-ogg-theora-chained-content: CMP = stddev
 fate-ogg-theora-chained-content: CMP_TARGET = 0
 fate-ogg-theora-chained-content: FUZZ = 0

+OGG_THEORA_CHAINED_CONTENT_FT_LINKS = tests/data/fate/ogg-theora-chained-content-frame-threads.links
+FATE_OGG_THEORA_CONTENT-$(call TRANSCODE, LIBTHEORA THEORA, OGG, $(OGG_THEORA_CHAINED_CONTENT_DEPS)) += fate-ogg-theora-chained-content-frame-threads
+fate-ogg-theora-chained-content-frame-threads: CMD = $(call OGG_THEORA_CHAINED,$(OGG_THEORA_CHAINED_CONTENT_FT_LINKS))
+fate-ogg-theora-chained-content-frame-threads: REF = $(OGG_THEORA_CHAINED_CONTENT_FT_LINKS).raw
+fate-ogg-theora-chained-content-frame-threads: CLEANFILES = $(addprefix $(OGG_THEORA_CHAINED_CONTENT_FT_LINKS),.1.ogg .2.ogg .raw)
+fate-ogg-theora-chained-content-frame-threads: CMP = stddev
+fate-ogg-theora-chained-content-frame-threads: CMP_TARGET = 0
+fate-ogg-theora-chained-content-frame-threads: FUZZ = 0
+fate-ogg-theora-chained-content-frame-threads: THREADS = 2
+fate-ogg-theora-chained-content-frame-threads: THREAD_TYPE = frame
+
 # The second link's comment header is larger than 64 KiB.
 OGG_THEORA_CHAINED_LARGE_COMMENT_LINKS = tests/data/fate/ogg-theora-chained-large-comment.links
 FATE_OGG_THEORA_CONTENT-$(call TRANSCODE, LIBTHEORA THEORA, OGG, $(OGG_THEORA_CHAINED_CONTENT_DEPS)) += fate-ogg-theora-chained-large-comment
diff --git a/tests/ref/fate/ogg-theora-chained-size-frame-threads b/tests/ref/fate/ogg-theora-chained-size-frame-threads
index 3395566b5c..da50838554 100644
--- a/tests/ref/fate/ogg-theora-chained-size-frame-threads
+++ b/tests/ref/fate/ogg-theora-chained-size-frame-threads
@@ -13,3 +13,18 @@ pts=11|width=160|height=120
 pts=12|width=160|height=120
 pts=13|width=160|height=120
 pts=14|width=160|height=120
+pts=0|width=320|height=240
+pts=1|width=320|height=240
+pts=2|width=320|height=240
+pts=3|width=320|height=240
+pts=4|width=320|height=240
+pts=5|width=320|height=240
+pts=6|width=320|height=240
+pts=7|width=320|height=240
+pts=8|width=320|height=240
+pts=9|width=320|height=240
+pts=10|width=320|height=240
+pts=11|width=320|height=240
+pts=12|width=320|height=240
+pts=13|width=320|height=240
+pts=14|width=320|height=240