Commit 09382db7f5 for ffmpeg

commit 09382db7f5791362dcaf0f4f26fad2bb18cb0dcb
Author: jiangjie <jiangjie@agora.io>
Date:   Tue Sep 15 19:14:18 2026 +0800

    avformat/movenc: fix global sidx fragment offsets

    Use moof-relative addressing when global sidx shifts fragmented data,
    unless absolute offsets are already avoided or no sidx is inserted,
    and test it in api-movenc-test.

    Fixes issue #24438.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Signed-off-by: Baptiste Coudurier <baptiste.coudurier@gmail.com>

diff --git a/doc/muxers.texi b/doc/muxers.texi
index ebca0971be..16a772818f 100644
--- a/doc/muxers.texi
+++ b/doc/muxers.texi
@@ -537,7 +537,9 @@ fragment at every frame
 start a new fragment at each video keyframe

 @item global_sidx
-write a global sidx index at the start of the file
+write a global sidx index at the start of the file. For fragmented output
+that would otherwise use absolute fragment offsets, this enables
+@samp{default_base_moof}.

 @item isml
 create a live smooth streaming feed (for pushing to a publishing point)
diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index c3fa66d816..27e1dd7718 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -8414,6 +8414,17 @@ static int mov_init(AVFormatContext *s)
         mov->flags |= FF_MOV_FLAG_FRAGMENT | FF_MOV_FLAG_EMPTY_MOOV |
                       FF_MOV_FLAG_DEFAULT_BASE_MOOF | FF_MOV_FLAG_NEGATIVE_CTS_OFFSETS;

+    /* global_sidx is inserted in a second pass, shifting all fragments. Use
+     * moof-relative addressing so that tfhd offsets remain valid after that
+     * shift, unless absolute offsets are already avoided (omit_tfhd_offset,
+     * ISMV) or no sidx is inserted (hybrid_fragmented). */
+    if (mov->flags & FF_MOV_FLAG_GLOBAL_SIDX &&
+        mov->flags & FF_MOV_FLAG_FRAGMENT &&
+        !(mov->flags & (FF_MOV_FLAG_OMIT_TFHD_OFFSET |
+                        FF_MOV_FLAG_HYBRID_FRAGMENTED)) &&
+        mov->mode != MODE_ISM)
+        mov->flags |= FF_MOV_FLAG_DEFAULT_BASE_MOOF;
+
     if (mov->flags & FF_MOV_FLAG_EMPTY_MOOV && s->flags & AVFMT_FLAG_AUTO_BSF) {
         av_log(s, AV_LOG_VERBOSE, "Empty MOOV enabled; disabling automatic bitstream filtering\n");
         s->flags &= ~AVFMT_FLAG_AUTO_BSF;
diff --git a/tests/api/api-movenc-test.c b/tests/api/api-movenc-test.c
index 39494748fd..f4a78313f5 100644
--- a/tests/api/api-movenc-test.c
+++ b/tests/api/api-movenc-test.c
@@ -51,6 +51,7 @@ static uint8_t iobuf[32768];
 static AVDictionary *opts;

 static int write_file;
+static const char *out_path;
 static const char *cur_name;
 static FILE* out;
 static int out_size;
@@ -181,16 +182,18 @@ static void init_fps(int bf, int audio_preroll, int fps, int id3)
 {
     AVStream *st;
     int iobuf_size = force_iobuf_size ? force_iobuf_size : sizeof(iobuf);
-    ctx = avformat_alloc_context();
-    if (!ctx)
+    if (avformat_alloc_output_context2(&ctx, NULL, format, out_path) < 0)
         exit(1);
-    ctx->oformat = av_guess_format(format, NULL, NULL);
-    if (!ctx->oformat)
-        exit(1);
-    ctx->pb = avio_alloc_context(iobuf, iobuf_size, 1, NULL, NULL, io_write, NULL);
-    if (!ctx->pb)
-        exit(1);
-    ctx->pb->write_data_type = io_write_data_type;
+    if (out_path) {
+        // A real file, as global_sidx seeks and reopens the output
+        if (avio_open(&ctx->pb, out_path, AVIO_FLAG_WRITE) < 0)
+            exit(1);
+    } else {
+        ctx->pb = avio_alloc_context(iobuf, iobuf_size, 1, NULL, NULL, io_write, NULL);
+        if (!ctx->pb)
+            exit(1);
+        ctx->pb->write_data_type = io_write_data_type;
+    }
     ctx->flags |= AVFMT_FLAG_BITEXACT;

     st = avformat_new_stream(ctx, NULL);
@@ -373,16 +376,44 @@ static void signal_init_ts(void)

 static void finish(void)
 {
-    av_write_trailer(ctx);
-    avio_context_free(&ctx->pb);
+    check(av_write_trailer(ctx) >= 0, "Failed to write trailer");
+    if (out_path)
+        avio_closep(&ctx->pb);
+    else
+        avio_context_free(&ctx->pb);
     avformat_free_context(ctx);
     ctx = NULL;
 }

+// Inserting a global sidx shifts all fragments; check that every packet
+// still reads back the payload mux_frames() wrote, its pts.
+static void test_global_sidx(const char *movflags)
+{
+    AVFormatContext *ic = NULL;
+    int ret, count = 0, wrong = 0;
+
+    av_dict_set(&opts, "movflags", movflags, 0);
+    av_dict_set(&opts, "use_editlist", "0", 0);
+    init(0, 0);
+    mux_gops(2);
+    finish();
+
+    ret = avformat_open_input(&ic, out_path, NULL, NULL);
+    while (ret >= 0 && (ret = av_read_frame(ic, pkt)) >= 0) {
+        wrong += pkt->size != 8 || AV_RB32(pkt->data + 4) != pkt->pts;
+        count++;
+        av_packet_unref(pkt);
+    }
+    printf("%s: %d packets, %d wrong, eof=%d\n",
+           movflags, count, wrong, ret == AVERROR_EOF);
+    avformat_close_input(&ic);
+}
+
 static void help(void)
 {
-    printf("movenc-test [-w]\n"
-           "-w          write output into files\n");
+    printf("movenc-test [-w] [-s file]\n"
+           "-w          write output into files\n"
+           "-s file     check global_sidx output written to file\n");
 }

 int main(int argc, char **argv)
@@ -394,10 +425,13 @@ int main(int argc, char **argv)
     int prev_pos;

     for (;;) {
-        c = getopt(argc, argv, "wh");
+        c = getopt(argc, argv, "ws:h");
         if (c == -1)
             break;
         switch (c) {
+        case 's':
+            out_path = optarg;
+            break;
         case 'w':
             write_file = 1;
             break;
@@ -417,6 +451,12 @@ int main(int argc, char **argv)
         return 1;
     }

+    if (out_path) {
+        test_global_sidx("frag_keyframe+empty_moov+global_sidx");
+        test_global_sidx("frag_keyframe+delay_moov+global_sidx");
+        goto end;
+    }
+
     // Write a fragmented file with an initial moov and no samples.
     init_out("non-empty-moov-no-mdat");
     av_dict_set(&opts, "movflags", "+frag_keyframe", 0);
@@ -843,6 +883,7 @@ int main(int argc, char **argv)
     finish();
     close_out();

+end:
     av_free(md5);
     av_packet_free(&pkt);

diff --git a/tests/fate/api.mak b/tests/fate/api.mak
index 1659c7979c..1f3c1b43c7 100644
--- a/tests/fate/api.mak
+++ b/tests/fate/api.mak
@@ -54,6 +54,10 @@ FATE_API_LIBAVFORMAT-$(call ALLYES, MP4_MUXER ISMV_MUXER) += fate-api-movenc
 fate-api-movenc: $(APITESTSDIR)/api-movenc-test$(EXESUF)
 fate-api-movenc: CMD = run $(APITESTSDIR)/api-movenc-test$(EXESUF)

+FATE_API_LIBAVFORMAT-$(call ALLYES, MP4_MUXER MOV_DEMUXER FILE_PROTOCOL) += fate-api-movenc-global-sidx
+fate-api-movenc-global-sidx: $(APITESTSDIR)/api-movenc-test$(EXESUF)
+fate-api-movenc-global-sidx: CMD = run $(APITESTSDIR)/api-movenc-test$(EXESUF) -s $(TARGET_PATH)/tests/data/fate/api-movenc-global-sidx.mp4
+
 FATE_API-$(HAVE_THREADS) += fate-api-threadmessage
 fate-api-threadmessage: $(APITESTSDIR)/api-threadmessage-test$(EXESUF)
 fate-api-threadmessage: CMD = run $(APITESTSDIR)/api-threadmessage-test$(EXESUF) 3 10 30 50 2 20 40
diff --git a/tests/ref/fate/api-movenc-global-sidx b/tests/ref/fate/api-movenc-global-sidx
new file mode 100644
index 0000000000..5de4663a1b
--- /dev/null
+++ b/tests/ref/fate/api-movenc-global-sidx
@@ -0,0 +1,2 @@
+frag_keyframe+empty_moov+global_sidx: 147 packets, 0 wrong, eof=1
+frag_keyframe+delay_moov+global_sidx: 147 packets, 0 wrong, eof=1