Skip to content

Seek back to unbuffered start of VOD re-downloads first fragment in infinite loop (video track never appended) on 1.6.16 — appears fixed in 1.7.0-beta.2 #7932

Description

@damonshen17

What version of Hls.js are you using?

1.6.16 (reproduces). Not reproducible on 1.7.0-beta.2 — details below.

What browser (including version) are you using?

Chrome 149 (macOS 15 / Darwin 25.5.0)

What OS (including version) are you using?

macOS 15

Test stream

https://video.deeplearning.ai/JupyterAI/C1/L0/sc-JupyterAI-C1-L0-master.m3u8 (VOD, TS segments, 3 variants, CORS enabled, EXT-X-MEDIA-SEQUENCE:1, 10.000s segments). Segments are healthy: continuous PTS across the playlist (seg1 video starts 0.067s, seg2 10.077s, seg3 20.087s per ffprobe), identical characteristics across levels.

Configuration

{ startPosition: 28.8 }

plus a pinned quality level at startup (hls.currentLevel = 1 inside MANIFEST_PARSED). Setting hls.startLevel instead does not change the outcome as long as the first fragment is not buffered before seeking back.

Steps to reproduce

  1. Start playback mid-stream with a pinned level, so the beginning of the VOD is never buffered. E.g. new Hls({ startPosition: 28.8 }) and set hls.currentLevel = 1 at MANIFEST_PARSED. Buffered ranges become [20, …] — sn=1/sn=2 are (expectedly) not loaded.
  2. After a few seconds of playback, seek back: video.currentTime = 0.

Minimal self-contained repro page:

<video id="video" controls muted width="640"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.6.16/dist/hls.js"></script>
<script>
const video = document.getElementById('video');
const hls = new Hls({ startPosition: 28.8 });
hls.on(Hls.Events.FRAG_LOADING, (_e, d) => d.frag.type === 'main' &&
  console.log('FRAG_LOADING sn=' + d.frag.sn + ' level=' + d.frag.level));
hls.on(Hls.Events.BUFFER_APPENDED, (_e, d) => {
  const f = tr => { const o = []; for (let i = 0; i < (tr?.length || 0); i++)
    o.push('[' + tr.start(i).toFixed(2) + ',' + tr.end(i).toFixed(2) + ']'); return o.join(' ') || 'empty'; };
  console.log('BUFFER_APPENDED sn=' + d.frag?.sn, 'video=' + f(d.timeRanges?.video), 'audio=' + f(d.timeRanges?.audio));
});
hls.on(Hls.Events.MANIFEST_PARSED, () => { hls.currentLevel = 1; });
hls.loadSource('https://video.deeplearning.ai/JupyterAI/C1/L0/sc-JupyterAI-C1-L0-master.m3u8');
hls.attachMedia(video);
video.play();
setTimeout(() => { video.currentTime = 0; }, 6000);
</script>

Expected behaviour

The seek to 0 loads sn=1 once, appends both tracks, and playback resumes from 0 within roughly one fragment-load time.

What actually happened?

hls.js enters an infinite re-download loop of sn=1 (one request per ~100ms tick, indefinitely — hundreds of downloads of the same segment). Each cycle, FRAG_BUFFERED fires and the audio SourceBuffer gains [0.00, 9.99], but the video SourceBuffer never gains the range — so video.buffered (track intersection) never covers position 0, the stream controller sees the hole persist, re-requests sn=1, and the element stays at readyState=1 (HAVE_METADATA) forever. No fatal error is raised.

Event log excerpt (timestamps relative to page load; seek to 0 at +6000ms):

+6180ms FRAG_LOADING sn=1 level=1
+6199ms BUFFER_APPENDED sn=1 video=[20.00,80.06] audio=[0.00,9.99] [20.00,80.07]
+6201ms FRAG_BUFFERED  sn=1 level=1 [0.00-9.99]
+6207ms FRAG_LOADING sn=1 level=1          <-- 100ms later, same fragment again
+6228ms BUFFER_APPENDED sn=1 video=[20.00,80.06] audio=[0.00,9.99] [20.00,80.07]
... repeats every ~100ms indefinitely (observed >150 downloads over 15s)

The loop only breaks if an ABR level switch happens: with auto level, once the loader switched levels, sn=1 of the other level appended fine (both tracks) and playback recovered. With a manually pinned level there is no switch, so it never recovers.

Version matrix (same page, same stream, same steps)

Version Result
1.6.16 (latest stable) Infinite sn=1 reload loop, video track never appended, stuck at readyState=1
1.7.0-beta.2 Recovers in ~100ms: single sn=1 fetch, video [0.05,10.06] + audio [0.01,10.06] appended, then sn=2 backfills

Notes / possibly related

Ask

  1. Can you confirm which change in 1.7.0 fixed this (is it fixed issue with seeking backward leading to stalling OR elevated buffering #7577, or something else), so we can track the release that carries it?
  2. Is a backport to 1.6.x feasible? 1.6.16 is the latest stable and the loop is easy to hit for any player that pins a quality level and restores a saved playback position (we hit it in production behind Vidstack).

Checklist

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugConfirmedBug report confirmed or reproduced.Works in latestThe issue reported has already been fixed and works as expected in the latest release

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions