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]
