PR #24645 opened by Tonghuaaaa URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24645 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24645.patch
mov_seek_fragment() only assigns next_root_atom when a following fragment exists, leaving a stale value from a previous seek when the target lands in the last fragment. mov_read_packet() treats next_root_atom as the right edge of the current fragment window: it clamps sample->pos/size to it and calls mov_switch_root() whenever sample->pos exceeds it. With a stale value pointing at an earlier part of the file, a seek into the last fragment silently rewinds to that old offset. avformat_seek_file() still returns success, and the demuxer then reads sequentially from the wrong position. Reproducible with a fragmented MP4 carrying a global sidx by seeking into the last fragment, reading to EOF, seeking backwards (e.g. a player looping to 0), and then seeking into the last fragment again. The second seek returns 0 but delivers packets from near the earlier seek position instead. Set next_root_atom to 0 in the else branch, matching mov_switch_root(), which already clears it before conditionally assigning the next fragment offset. Signed-off-by: Tonghuaaaa <[email protected]> # Summary of changes ## Summary `mov_seek_fragment()` leaves a stale `next_root_atom` when the seek target lands in the last fragment, so a later seek silently rewinds to an earlier part of the file while still reporting success. ## How to reproduce Create a fragmented MP4 with a global sidx (320 fragments, 5 s each): ffmpeg -f lavfi -i testsrc2=size=320x240:rate=30 -t 300 -c:v mpeg4 \ -movflags +frag_keyframe+empty_moov+default_base_moof+global_sidx \ -frag_duration 5000000 fragmented.mp4 Then drive the demuxer through this sequence (what a looping player does when the user jumps to a bookmark near the end): 1. `avformat_seek_file()` to a timestamp **inside the last fragment** (for the file above: 300.0 s; the last sidx entry starts at 300.000 s) 2. `av_read_frame()` until EOF 3. `avformat_seek_file()` backwards — e.g. to 0, as an auto-looping player does — and read a few packets forward 4. `avformat_seek_file()` to the same timestamp as step 1 ### Expected Step 4 returns 0 and delivers packets from the last fragment (~300.0 s). ### Actual Step 4 returns 0, but the first packet read back is at **5.033 s**. The demuxer then reads sequentially from there; no error is ever reported. ## Necessary conditions All three must hold, which is why this is rarely hit: - the file has a complete global sidx — otherwise `mov_seek_fragment()` returns early at `!mov->frag_index.complete` - the seek target resolves to the **last** fragment, so `index + 1 < nb_items` is false and `next_root_atom` is never updated - that fragment already has `headers_read` set, which step 1 itself causes via `mov_switch_root()` ## Fingerprint Step 4 lands on the fragment *after* the furthest one read in step 3, i.e. exactly where the stale `next_root_atom` points. Varying how far step 3 reads moves the landing point in lockstep (30 packets → 5.03 s, 150 → 10.03 s, 900 → 35.03 s), which pins the cause to the stale value rather than the index search. <!-- If this PR requires new FATE test samples, attach them to the PR and list their target paths below (relative to the fate-suite root). Attached filenames must match the sample's filename: ```fate-samples # e.g. vorbis/new-sample.ogg ``` --> From 2459eff5fc2b4f43cd8b1f353681f6f876db6eb6 Mon Sep 17 00:00:00 2001 From: Tonghuaaaa <[email protected]> Date: Wed, 23 Sep 2026 16:12:35 +0800 Subject: [PATCH] avformat/mov: reset next_root_atom when seeking to the last fragment mov_seek_fragment() only assigns next_root_atom when a following fragment exists, leaving a stale value from a previous seek when the target lands in the last fragment. mov_read_packet() treats next_root_atom as the right edge of the current fragment window: it clamps sample->pos/size to it and calls mov_switch_root() whenever sample->pos exceeds it. With a stale value pointing at an earlier part of the file, a seek into the last fragment silently rewinds to that old offset. avformat_seek_file() still returns success, and the demuxer then reads sequentially from the wrong position. Reproducible with a fragmented MP4 carrying a global sidx by seeking into the last fragment, reading to EOF, seeking backwards (e.g. a player looping to 0), and then seeking into the last fragment again. The second seek returns 0 but delivers packets from near the earlier seek position instead. Set next_root_atom to 0 in the else branch, matching mov_switch_root(), which already clears it before conditionally assigning the next fragment offset. Signed-off-by: Tonghuaaaa <[email protected]> --- libavformat/mov.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libavformat/mov.c b/libavformat/mov.c index 8ddedf269b..9f9fcf1e63 100644 --- a/libavformat/mov.c +++ b/libavformat/mov.c @@ -12304,6 +12304,8 @@ static int mov_seek_fragment(AVFormatContext *s, AVStream *st, int64_t timestamp return mov_switch_root(s, -1, index); if (index + 1 < mov->frag_index.nb_items) mov->next_root_atom = mov->frag_index.item[index + 1].moof_offset; + else + mov->next_root_atom = 0; return 0; } -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
