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]

Reply via email to