PR #25695 opened by Romain Beauxis (toots)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25695
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25695.patch

This PR fixes seeking with `-use_mfra_for dts` in fragmented MP4 files that 
have a chapter track. On such files, a seek to 8 s returns packets from 2 s, 
and the demuxer reads every fragment header from the start of the file, which 
means one range request per fragment over HTTP.

**tests/fate/mov: seek in a fragmented file with a chapter track**: video, 
audio and chapters in a fragmented MP4, seek to 8 s. The reference records the 
current behaviour so that the next commits show their effect.

**avformat/mov: ignore discarded streams when placing the fragment position on 
seek**: each stream's seek sets `next_root_atom`, and the last one wins. That 
is the chapter track, whose samples are all in the first fragment. Reading now 
resumes from the earliest fragment of the streams that are actually read.

**avformat/mov: keep the current sample when a fragment is inserted at it**: 
`mov_read_trun()` moves `current_sample` back when it inserts entries exactly 
at it (`<` instead of `<=`), so loading the fragment before the keyframe pulls 
video back by one fragment. With this fix, the packets after the seek match a 
read without the fragment index.



>From 3ae9ac80fdccbd49c6c82a976359d62a04d75673 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Mon, 2 Nov 2026 16:57:15 -0600
Subject: [PATCH 1/3] tests/fate/mov: seek in a fragmented file with a chapter
 track

Mux the chapters of vorbis_chapter_extension_demo.ogg with generated video and 
audio tracks into a fragmented MP4, then seek to 8 s with use_mfra_for=dts and 
read 40 packets across one fragment boundary.

The reference records the current behaviour, where the read starts at 2 s 
without audio because the chapter track's seek leaves the fragment index on the 
first fragment.
---
 tests/fate/mov.mak                        | 10 ++++
 tests/ref/fate/mov-mp4-frag-chapters-seek | 61 +++++++++++++++++++++++
 2 files changed, 71 insertions(+)
 create mode 100644 tests/ref/fate/mov-mp4-frag-chapters-seek

diff --git a/tests/fate/mov.mak b/tests/fate/mov.mak
index e467acdafbd..7d65d4599e7 100644
--- a/tests/fate/mov.mak
+++ b/tests/fate/mov.mak
@@ -203,6 +203,16 @@ FATE_MOV_FFMPEG_FFPROBE_SAMPLES-$(call REMUX, MP4 MOV, 
OGG_DEMUXER VORBIS_DECODE
                           += fate-mov-mp4-chapters
 fate-mov-mp4-chapters: CMD = transcode ogg 
$(TARGET_SAMPLES)/vorbis/vorbis_chapter_extension_demo.ogg mp4 "-c copy" "-c 
copy -t 0.1" "-show_chapters"
 
+# Seek in a fragmented file with a chapter track: the chapter track's samples
+# all sit in the first fragment, and its own seek must not send the read
+# position of the seeked video stream back there.
+FATE_MOV_FFMPEG_FFPROBE_SAMPLES-$(call TRANSCODE, MPEG4, MP4 MOV, OGG_DEMUXER 
VORBIS_DECODER TESTSRC_FILTER SCALE_FILTER FORMAT_FILTER SINE_FILTER) \
+                          += fate-mov-mp4-frag-chapters-seek
+fate-mov-mp4-frag-chapters-seek: CMD = transcode ogg 
$(TARGET_SAMPLES)/vorbis/vorbis_chapter_extension_demo.ogg mp4 \
+  "-filter_complex 
testsrc=size=64x64:rate=25:duration=12,scale,format=yuv420p[v] -filter_complex 
sine=d=12[a] -map [v] -map [a] -map_chapters 0 -c:v mpeg4 -c:a pcm_s16le -g 25 
-movflags frag_keyframe -frag_duration 1000000" \
+  "-c copy -t 0.1" \
+  "-use_mfra_for dts -read_intervals 8%+\#40 -show_entries 
packet=stream_index,pts,pos -print_format compact"
+
 FATE_MOV_FFMPEG_FFPROBE_SAMPLES-$(call TRANSCODE, PNG, MP4 MOV, MJPEG_DECODER 
SCALE_FILTER) \
                           += fate-mov-cover-image
 fate-mov-cover-image: CMD = transcode mov 
$(TARGET_SAMPLES)/cover_art/Owner-iTunes_9.0.3.15.m4a mp4 "-map 0 -map 0:v -c:a 
copy -c:v:0 copy -filter:v:1 scale -c:v:1 png" "-map 0 -t 0.1 -c copy" 
"-show_entries stream_disposition=attached_pic:stream=index,codec_name"
diff --git a/tests/ref/fate/mov-mp4-frag-chapters-seek 
b/tests/ref/fate/mov-mp4-frag-chapters-seek
new file mode 100644
index 00000000000..39a00be4e1f
--- /dev/null
+++ b/tests/ref/fate/mov-mp4-frag-chapters-seek
@@ -0,0 +1,61 @@
+94392fb6cce206d1ebf5ceaae1d2dff6 
*tests/data/fate/mov-mp4-frag-chapters-seek.mp4
+1139622 tests/data/fate/mov-mp4-frag-chapters-seek.mp4
+#extradata 0:       30, 0x472a0551
+#tb 0: 1/12800
+#media_type 0: video
+#codec_id 0: mpeg4
+#dimensions 0: 64x64
+#sar 0: 1/1
+#tb 1: 1/44100
+#media_type 1: audio
+#codec_id 1: pcm_s16le
+#sample_rate 1: 44100
+#channel_layout_name 1: mono
+0,          0,          0,      512,     1813, 0x909a351b
+1,          0,          0,     1024,     2048, 0x2096f45b
+1,       1024,       1024,     1024,     2048, 0x2262f6ec
+0,        512,        512,      512,      327, 0x420a91ce, F=0x0
+1,       2048,       2048,     1024,     2048, 0xaa83fe05
+1,       3072,       3072,     1024,     2048, 0x487e06b5
+0,       1024,       1024,      512,      127, 0x11d63930, F=0x0
+1,       4096,       4096,     1024,     2048, 0xb0abfcca
+packet|stream_index=0|pts=25600|pos=193518
+packet|stream_index=0|pts=26112|pos=195708
+packet|stream_index=0|pts=26624|pos=195862
+packet|stream_index=0|pts=27136|pos=195958
+packet|stream_index=0|pts=27648|pos=196147
+packet|stream_index=0|pts=28160|pos=196252
+packet|stream_index=0|pts=28672|pos=196451
+packet|stream_index=0|pts=29184|pos=196551
+packet|stream_index=0|pts=29696|pos=196738
+packet|stream_index=0|pts=30208|pos=196835
+packet|stream_index=0|pts=30720|pos=197026
+packet|stream_index=0|pts=31232|pos=197143
+packet|stream_index=0|pts=31744|pos=197247
+packet|stream_index=0|pts=32256|pos=197364
+packet|stream_index=0|pts=32768|pos=197590
+packet|stream_index=0|pts=33280|pos=197695
+packet|stream_index=0|pts=33792|pos=197922
+packet|stream_index=0|pts=34304|pos=198025
+packet|stream_index=0|pts=34816|pos=198360
+packet|stream_index=0|pts=35328|pos=198461
+packet|stream_index=0|pts=35840|pos=198688
+packet|stream_index=0|pts=36352|pos=198895
+packet|stream_index=0|pts=36864|pos=199117
+packet|stream_index=0|pts=37376|pos=199226
+packet|stream_index=0|pts=37888|pos=199454
+packet|stream_index=0|pts=38400|pos=287927
+packet|stream_index=0|pts=38912|pos=290114
+packet|stream_index=0|pts=39424|pos=290304
+packet|stream_index=0|pts=39936|pos=290389
+packet|stream_index=0|pts=40448|pos=290608
+packet|stream_index=0|pts=40960|pos=290833
+packet|stream_index=0|pts=41472|pos=291158
+packet|stream_index=0|pts=41984|pos=291271
+packet|stream_index=0|pts=42496|pos=291374
+packet|stream_index=0|pts=43008|pos=291486
+packet|stream_index=0|pts=43520|pos=291825
+packet|stream_index=0|pts=44032|pos=291924
+packet|stream_index=0|pts=44544|pos=292040
+packet|stream_index=0|pts=45056|pos=292263
+packet|stream_index=0|pts=45568|pos=292505
-- 
2.54.0


>From 5bf82383a5e206ae71fc52a9f22b99087e848b45 Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Mon, 2 Nov 2026 16:57:47 -0600
Subject: [PATCH 2/3] avformat/mov: ignore discarded streams when placing the
 fragment position on seek

With seek_individually, the last stream seeked sets next_root_atom, so a 
chapter track whose samples all sit in the first fragment makes 
mov_read_packet() read every fragment from the start of the file before it 
reaches the seeked sample.

Resume reading from the earliest fragment any stream that is read landed in, 
which keeps the fragments that a lagging audio track still needs and ignores 
the chapter track.

The seek in fate-mov-mp4-frag-chapters-seek now starts at the keyframe before 8 
s, as it does for the same file without chapters.
---
 libavformat/mov.c                         | 10 +++
 tests/ref/fate/mov-mp4-frag-chapters-seek | 80 +++++++++++------------
 2 files changed, 50 insertions(+), 40 deletions(-)

diff --git a/libavformat/mov.c b/libavformat/mov.c
index 786994699e7..c7e6164d5e1 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -13196,21 +13196,31 @@ static int mov_read_seek(AVFormatContext *s, int 
stream_index, int64_t sample_ti
     if (mc->seek_individually) {
         /* adjust seek timestamp to found sample timestamp */
         int64_t seek_timestamp = sti->index_entries[sample].timestamp;
+        int64_t next_root_atom = mc->next_root_atom;
         sti->skip_samples = mov_get_skip_samples(st, sample);
 
         for (i = 0; i < s->nb_streams; i++) {
             AVStream *const st  = s->streams[i];
             FFStream *const sti = ffstream(st);
+            const MOVStreamContext *sc = st->priv_data;
             int64_t timestamp;
 
             if (stream_index == i)
                 continue;
 
+            mc->next_root_atom = 0;
             timestamp = av_rescale_q(seek_timestamp, 
s->streams[stream_index]->time_base, st->time_base);
             sample = mov_seek_stream(s, st, timestamp, flags);
             if (sample >= 0)
                 sti->skip_samples = mov_get_skip_samples(st, sample);
+
+            /* Reading resumes from the earliest fragment a read stream landed
+             * in; a discarded chapter track would send it back to the start. 
*/
+            if ((st->discard != AVDISCARD_ALL || sc->iamf) && 
mc->next_root_atom &&
+                (!next_root_atom || mc->next_root_atom < next_root_atom))
+                next_root_atom = mc->next_root_atom;
         }
+        mc->next_root_atom = next_root_atom;
     } else {
         for (i = 0; i < s->nb_streams; i++) {
             MOVStreamContext *sc;
diff --git a/tests/ref/fate/mov-mp4-frag-chapters-seek 
b/tests/ref/fate/mov-mp4-frag-chapters-seek
index 39a00be4e1f..932434ccf65 100644
--- a/tests/ref/fate/mov-mp4-frag-chapters-seek
+++ b/tests/ref/fate/mov-mp4-frag-chapters-seek
@@ -19,43 +19,43 @@
 1,       3072,       3072,     1024,     2048, 0x487e06b5
 0,       1024,       1024,      512,      127, 0x11d63930, F=0x0
 1,       4096,       4096,     1024,     2048, 0xb0abfcca
-packet|stream_index=0|pts=25600|pos=193518
-packet|stream_index=0|pts=26112|pos=195708
-packet|stream_index=0|pts=26624|pos=195862
-packet|stream_index=0|pts=27136|pos=195958
-packet|stream_index=0|pts=27648|pos=196147
-packet|stream_index=0|pts=28160|pos=196252
-packet|stream_index=0|pts=28672|pos=196451
-packet|stream_index=0|pts=29184|pos=196551
-packet|stream_index=0|pts=29696|pos=196738
-packet|stream_index=0|pts=30208|pos=196835
-packet|stream_index=0|pts=30720|pos=197026
-packet|stream_index=0|pts=31232|pos=197143
-packet|stream_index=0|pts=31744|pos=197247
-packet|stream_index=0|pts=32256|pos=197364
-packet|stream_index=0|pts=32768|pos=197590
-packet|stream_index=0|pts=33280|pos=197695
-packet|stream_index=0|pts=33792|pos=197922
-packet|stream_index=0|pts=34304|pos=198025
-packet|stream_index=0|pts=34816|pos=198360
-packet|stream_index=0|pts=35328|pos=198461
-packet|stream_index=0|pts=35840|pos=198688
-packet|stream_index=0|pts=36352|pos=198895
-packet|stream_index=0|pts=36864|pos=199117
-packet|stream_index=0|pts=37376|pos=199226
-packet|stream_index=0|pts=37888|pos=199454
-packet|stream_index=0|pts=38400|pos=287927
-packet|stream_index=0|pts=38912|pos=290114
-packet|stream_index=0|pts=39424|pos=290304
-packet|stream_index=0|pts=39936|pos=290389
-packet|stream_index=0|pts=40448|pos=290608
-packet|stream_index=0|pts=40960|pos=290833
-packet|stream_index=0|pts=41472|pos=291158
-packet|stream_index=0|pts=41984|pos=291271
-packet|stream_index=0|pts=42496|pos=291374
-packet|stream_index=0|pts=43008|pos=291486
-packet|stream_index=0|pts=43520|pos=291825
-packet|stream_index=0|pts=44032|pos=291924
-packet|stream_index=0|pts=44544|pos=292040
-packet|stream_index=0|pts=45056|pos=292263
-packet|stream_index=0|pts=45568|pos=292505
+packet|stream_index=0|pts=89600|pos=666470
+packet|stream_index=0|pts=90112|pos=668682
+packet|stream_index=0|pts=90624|pos=668750
+packet|stream_index=0|pts=91136|pos=668926
+packet|stream_index=0|pts=91648|pos=669026
+packet|stream_index=0|pts=92160|pos=669214
+packet|stream_index=0|pts=92672|pos=669411
+packet|stream_index=0|pts=93184|pos=669518
+packet|stream_index=0|pts=93696|pos=669708
+packet|stream_index=0|pts=94208|pos=669901
+packet|stream_index=0|pts=94720|pos=670098
+packet|stream_index=0|pts=95232|pos=670198
+packet|stream_index=0|pts=95744|pos=670393
+packet|stream_index=0|pts=96256|pos=670586
+packet|stream_index=0|pts=96768|pos=670778
+packet|stream_index=0|pts=97280|pos=670977
+packet|stream_index=0|pts=97792|pos=671171
+packet|stream_index=0|pts=98304|pos=671364
+packet|stream_index=0|pts=98816|pos=671568
+packet|stream_index=0|pts=99328|pos=671765
+packet|stream_index=0|pts=99840|pos=671963
+packet|stream_index=0|pts=100352|pos=672155
+packet|stream_index=0|pts=100864|pos=672355
+packet|stream_index=0|pts=101376|pos=672560
+packet|stream_index=0|pts=101888|pos=672750
+packet|stream_index=1|pts=352256|pos=758872
+packet|stream_index=0|pts=102400|pos=761224
+packet|stream_index=0|pts=102912|pos=763414
+packet|stream_index=0|pts=103424|pos=763568
+packet|stream_index=0|pts=103936|pos=763664
+packet|stream_index=0|pts=104448|pos=763853
+packet|stream_index=0|pts=104960|pos=763958
+packet|stream_index=0|pts=105472|pos=764157
+packet|stream_index=0|pts=105984|pos=764257
+packet|stream_index=0|pts=106496|pos=764444
+packet|stream_index=0|pts=107008|pos=764541
+packet|stream_index=0|pts=107520|pos=764732
+packet|stream_index=0|pts=108032|pos=764849
+packet|stream_index=0|pts=108544|pos=764953
+packet|stream_index=0|pts=109056|pos=765070
-- 
2.54.0


>From e4c8b48912d5d0d148f960669f37847563d72ecf Mon Sep 17 00:00:00 2001
From: Romain Beauxis <[email protected]>
Date: Mon, 2 Nov 2026 17:23:50 -0600
Subject: [PATCH 3/3] avformat/mov: keep the current sample when a fragment is
 inserted at it

When a fragment's samples are inserted into the index exactly at a stream's 
current sample, current_sample is left on the first inserted entry, so loading 
an earlier fragment after a seek moves the stream back to the start of that 
fragment.

Advance current_sample past the inserted entries in that case too, as is 
already done for insertions before it.

The seek in fate-mov-mp4-frag-chapters-seek now starts at the 8 s keyframe, 
with the same packets as a read without the fragment index.
---
 libavformat/mov.c                         |  2 +-
 tests/ref/fate/mov-mp4-frag-chapters-seek | 50 +++++++++++------------
 2 files changed, 26 insertions(+), 26 deletions(-)

diff --git a/libavformat/mov.c b/libavformat/mov.c
index c7e6164d5e1..834f4804c44 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -6404,7 +6404,7 @@ static int mov_read_trun(MOVContext *c, AVIOContext *pb, 
MOVAtom atom)
         memmove(sc->tts_data + index_entry_pos + entries,
                 sc->tts_data + index_entry_pos,
                 sizeof(*sc->tts_data) * (sti->nb_index_entries - 
index_entry_pos));
-        if (index_entry_pos < sc->current_sample) {
+        if (index_entry_pos <= sc->current_sample) {
             sc->current_sample += entries;
         }
     }
diff --git a/tests/ref/fate/mov-mp4-frag-chapters-seek 
b/tests/ref/fate/mov-mp4-frag-chapters-seek
index 932434ccf65..31d9a14a4e5 100644
--- a/tests/ref/fate/mov-mp4-frag-chapters-seek
+++ b/tests/ref/fate/mov-mp4-frag-chapters-seek
@@ -19,31 +19,6 @@
 1,       3072,       3072,     1024,     2048, 0x487e06b5
 0,       1024,       1024,      512,      127, 0x11d63930, F=0x0
 1,       4096,       4096,     1024,     2048, 0xb0abfcca
-packet|stream_index=0|pts=89600|pos=666470
-packet|stream_index=0|pts=90112|pos=668682
-packet|stream_index=0|pts=90624|pos=668750
-packet|stream_index=0|pts=91136|pos=668926
-packet|stream_index=0|pts=91648|pos=669026
-packet|stream_index=0|pts=92160|pos=669214
-packet|stream_index=0|pts=92672|pos=669411
-packet|stream_index=0|pts=93184|pos=669518
-packet|stream_index=0|pts=93696|pos=669708
-packet|stream_index=0|pts=94208|pos=669901
-packet|stream_index=0|pts=94720|pos=670098
-packet|stream_index=0|pts=95232|pos=670198
-packet|stream_index=0|pts=95744|pos=670393
-packet|stream_index=0|pts=96256|pos=670586
-packet|stream_index=0|pts=96768|pos=670778
-packet|stream_index=0|pts=97280|pos=670977
-packet|stream_index=0|pts=97792|pos=671171
-packet|stream_index=0|pts=98304|pos=671364
-packet|stream_index=0|pts=98816|pos=671568
-packet|stream_index=0|pts=99328|pos=671765
-packet|stream_index=0|pts=99840|pos=671963
-packet|stream_index=0|pts=100352|pos=672155
-packet|stream_index=0|pts=100864|pos=672355
-packet|stream_index=0|pts=101376|pos=672560
-packet|stream_index=0|pts=101888|pos=672750
 packet|stream_index=1|pts=352256|pos=758872
 packet|stream_index=0|pts=102400|pos=761224
 packet|stream_index=0|pts=102912|pos=763414
@@ -59,3 +34,28 @@ packet|stream_index=0|pts=107520|pos=764732
 packet|stream_index=0|pts=108032|pos=764849
 packet|stream_index=0|pts=108544|pos=764953
 packet|stream_index=0|pts=109056|pos=765070
+packet|stream_index=0|pts=109568|pos=765296
+packet|stream_index=0|pts=110080|pos=765401
+packet|stream_index=0|pts=110592|pos=765628
+packet|stream_index=0|pts=111104|pos=765731
+packet|stream_index=0|pts=111616|pos=766066
+packet|stream_index=0|pts=112128|pos=766167
+packet|stream_index=0|pts=112640|pos=766394
+packet|stream_index=0|pts=113152|pos=766601
+packet|stream_index=0|pts=113664|pos=766823
+packet|stream_index=0|pts=114176|pos=766932
+packet|stream_index=0|pts=114688|pos=767160
+packet|stream_index=1|pts=353280|pos=767265
+packet|stream_index=1|pts=354304|pos=769313
+packet|stream_index=1|pts=355328|pos=771361
+packet|stream_index=1|pts=356352|pos=773409
+packet|stream_index=1|pts=357376|pos=775457
+packet|stream_index=1|pts=358400|pos=777505
+packet|stream_index=1|pts=359424|pos=779553
+packet|stream_index=1|pts=360448|pos=781601
+packet|stream_index=1|pts=361472|pos=783649
+packet|stream_index=1|pts=362496|pos=785697
+packet|stream_index=1|pts=363520|pos=787745
+packet|stream_index=1|pts=364544|pos=789793
+packet|stream_index=1|pts=365568|pos=791841
+packet|stream_index=1|pts=366592|pos=793889
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to