HLS Seek Jumps Back or Live DVR Will Not Rewind: A Timeline-First Diagnosis
Diagnose HLS seek snap-back, expired live DVR positions, missing segments, keyframe alignment, Range responses, and player live-latency correction.
A viewer drags the scrubber backward, the control briefly shows the requested time, and playback jumps somewhere else. On video on demand, the same symptom may appear as a seek that stalls or resumes several seconds away. These outcomes are not one bug. The requested time may be outside the current live window, unavailable on the origin, adjusted to a decodable point, or deliberately corrected by the player.
This guide follows the timeline, playlist, requests, and player events in that order. Test only streams you own or are authorized to inspect. Remove signed URLs, cookies, tokens, viewer identifiers, and IP addresses before sharing evidence.
Verification method — September 22, 2026: We ran a Node.js 24.12.0 loopback fixture with two synthetic live-playlist snapshots. Both windows were 30 seconds long; EXT-X-MEDIA-SEQUENCE advanced from 100 to 103, so sequence 101 existed in the first snapshot but not the second. An EVENT fixture retained sequences 0–7. A VOD fixture ended with EXT-X-ENDLIST, and its controlled media endpoint returned 206 Partial Content, Content-Range: bytes 4-7/16, and the four requested bytes. This verifies only playlist-window arithmetic and the fixture's HTTP Range response. It does not run a browser media pipeline, decode media, inspect keyframes, reproduce a real player's snap-back, or test a phone, television, DRM system, or CDN. The fixture is stored at scripts/test-hls-seek-window-fixtures.cjs.
Define the symptom before changing the player
Record the requested time, the position actually reached, and the evidence between them. “Seeking is broken” hides several different checkpoints.
| Observation | What it establishes | What remains unknown |
|---|---|---|
| Scrubber moved to the requested point | The interface accepted an input | The media element or player accepted that time |
seeking event fired | The media element began a seek operation | The requested media is available or decodable |
| Playlist reloaded after the action | The player refreshed timeline information | The target still belongs to the advertised window |
| Target segment returned 200/206 | Some bytes were delivered | They contain the right timeline and a usable decode start |
seeked event fired | The seek operation ended | The result exactly equals the requested time |
| Playback moved near the live edge | A correction or fallback occurred | Whether the player, browser, or application caused it |
Capture the player version, browser or native playback path, stream type, requested currentTime, currentTime after seeked, seekable ranges, buffered ranges, live latency, selected rendition, and the first request after the action. Do not rely on the scrubber label alone.
Read seekable before reading duration
For a media element, duration and seekable answer different questions. duration describes the media timeline as the browser currently understands it. seekable is a TimeRanges object describing positions currently available for seeking. A live timeline can start above zero, and expired content may no longer be obtainable.
Record all ranges rather than assuming there is one:
function snapshotMedia(video) {
const ranges = (timeRanges) => Array.from(
{ length: timeRanges.length },
(_, index) => ({ start: timeRanges.start(index), end: timeRanges.end(index) })
);
return {
currentTime: video.currentTime,
duration: video.duration,
seekable: ranges(video.seekable),
buffered: ranges(video.buffered),
readyState: video.readyState,
};
}
Save this snapshot before assigning currentTime, when seeking fires, and after seeked. MDN notes that setting currentTime requests a seek only when that media time is available, and that live content can expire from the buffer. It also notes that the resulting position can be adjusted to a time the media supports.
If a requested time is below the first seekable.start(0) or beyond the last seekable end, clamp the user interface to the current ranges or explain that the position has expired. Do not present an hour-long progress bar merely because the event began an hour ago if the current DVR window contains only the last five minutes.
Identify LIVE, EVENT, and VOD behavior
The playlist type determines what history can remain available, but the tag alone does not prove the server still has every object.
| Playlist behavior | Expected mutation | Practical seek boundary |
|---|---|---|
Sliding live playlist, no ENDLIST | Old segment URIs may leave as new ones arrive | Current advertised window, subject to actual object availability |
EXT-X-PLAYLIST-TYPE:EVENT | New segments may be appended; existing playlist content is not removed | From retained event start through the current edge |
EXT-X-PLAYLIST-TYPE:VOD with ENDLIST | Playlist is static | Full declared presentation, if every referenced object remains available |
| Ended playlist without the expected history | Depends on how it was produced | Only the segments still referenced and served |
RFC 8216 requires a sliding live playlist that removes segments to advance EXT-X-MEDIA-SEQUENCE consistently. It also requires at least three target durations to remain when a non-ended playlist removes old entries. Apple documents EVENT playlists as append-only and useful when viewers need to seek back to the beginning of an event.
Our fixture's sequence 101 was a valid target in snapshot A and absent from snapshot B. That does not prove how a specific player reacts; it proves that a seek label derived from an earlier snapshot can point outside a later playlist window.
Compare consecutive playlist snapshots
Save the media playlist before the seek and again immediately after it. For each snapshot, record:
- Request time, final URL, cache status,
Age, and cache-control headers. EXT-X-MEDIA-SEQUENCE,EXT-X-TARGETDURATION, andEXT-X-ENDLIST.- First and last segment URI and the sum of
EXTINFdurations. EXT-X-PROGRAM-DATE-TIMEwhere the presentation uses it.- Discontinuity tags and
EXT-X-DISCONTINUITY-SEQUENCE. - Selected variant, alternate audio, subtitles, and whether their timelines cover the same target.
Do not compare two cached copies and call the window stable. Conversely, do not translate a sequence number directly into a wall-clock time. RFC 8216 says different renditions may have independent media sequence numbers; align them by relative playlist timeline, discontinuity information, or an unambiguous program-date mapping.
If old segments leave the playlist, the server still has retention obligations for in-progress clients under RFC 8216. An origin or CDN deleting them too early can interrupt playback even before a viewer attempts a new backward seek. The stale playlist versus missing segment guide explains how to distinguish an old manifest from a newly advertised object that returns 404.
Follow the first request after the seek
Preserve the Network log, perform one seek, and identify the first changed or failed request. Reloading the master playlist is less informative than the exact media playlist, initialization section, key, or segment selected after the action.
| First evidence after the seek | Likely investigation area | Next controlled check |
|---|---|---|
No request and currentTime is clamped | Target outside seekable, application guard, or browser adjustment | Log requested value and ranges at assignment time |
| Playlist refreshes and starts at a later sequence | Live window advanced | Compare both snapshots and recompute the UI range |
| Old segment returns 404/410 | Retention, origin cleanup, CDN policy, or expired URL | Check server retention and request time without exposing credentials |
| Segment returns 401/403 | Authorization or signed-child expiry | Compare expiry and credential scope with a working segment |
| Byte Range returns the wrong status or bytes | Origin/CDN Range handling or object mutation | Reissue the exact authorized Range request and inspect headers |
| Bytes arrive, then demux/decode fails | Container, initialization, timestamps, encryption, or codec | Capture player error details and inspect authorized media |
| Seek completes at a nearby time | Decode boundary or supported-position adjustment | Compare requested time, reached time, and keyframe layout |
A successful status does not prove the correct object arrived. Confirm content type, byte count, final URL, and permitted response sample. A CDN challenge or HTML login can return 200. Follow our developer-tools request guide for a privacy-safe capture workflow.
Separate byte ranges from timeline ranges
Range: bytes=... is an HTTP request for part of an object. video.seekable is a media timeline. They are not interchangeable. An HLS presentation may use separate segment files, EXT-X-BYTERANGE, fMP4 initialization sections, or server behavior that causes different HTTP access patterns.
Our VOD fixture confirmed a coherent 206 response for one synthetic byte request. For a real presentation, verify the actual request's Content-Range, total object length, returned byte count, content type, cache behavior, and object identity. A server that ignores Range may still work for some client paths, while an inconsistent partial response can break others; diagnose the observed path instead of requiring 206 universally.
If URLs are signed, confirm that a seek does not request an older object after its signature has expired. Never copy a live signed URL into a public report.
Account for keyframes and discontinuities
Video decoding cannot necessarily begin from every arbitrary frame. A player may start at or near a decodable random-access point, so an exact UI request can resolve to a nearby media time. Large or irregular keyframe spacing can make the adjustment visible. Inspect the actual encoded media with approved tools; the playlist's segment duration alone does not reveal every decode boundary.
Discontinuities add another timeline boundary. Compare discontinuity tags across video and alternate renditions, initialization changes, media timestamps, keys, and the target's position relative to an ad break or encoder restart. RFC 8216 uses EXT-X-DISCONTINUITY-SEQUENCE to help synchronize renditions when earlier discontinuities leave a live window.
Test just before and after each known discontinuity. If video seeks but alternate audio becomes silent, use the alternate-audio diagnosis rather than treating it as a generic scrubber issue.
Check player live-edge correction explicitly
Some players intentionally move playback forward when latency becomes too large. The hls.js API documents maxLatency as the distance from the live edge beyond which playback seeks forward to liveSyncPosition. That correction can look like an unwanted jump if the application exposes a target outside its supported latency policy.
Record hls.liveSyncPosition, estimated latency, configured live sync and maximum latency values, and the event sequence around the jump. Compare behavior with documented defaults and your explicit configuration. Do not disable correction blindly: first decide whether the product promises a DVR seek or low-latency live viewing, because those goals require different windows and controls.
Also check application code for its own “Go Live” logic, periodic currentTime assignments, state synchronization, or a stale React/UI value that overwrites the user's seek. A player-level correction and an application-level assignment need different fixes.
Test a small, reproducible matrix
Run the same authorized presentation through each officially supported path:
- Native HLS and JavaScript/MSE where applicable.
- VOD, sliding live, and EVENT/DVR fixtures with known retention.
- A target inside the current range, exactly at its beginning, and just outside it.
- A seek near a keyframe boundary and near a declared discontinuity.
- Low and high variants, alternate audio, and subtitles.
- Desktop and physical mobile or television devices you actually support.
- Normal network, delayed playlist refresh, and a controlled expired-segment case.
A narrow desktop viewport is not a phone media pipeline. Label untested devices as untested. If the stream buffers generally rather than only after a seek, use the broader HLS buffering diagnosis.
Write the report around one requested time
An actionable report might say: “At 09:14:22, the viewer requested 128.4 seconds. seekable was 141.0–171.0. The next playlist began at media sequence 103; the earlier snapshot contained sequence 101, but the new one did not. The application then set playback to the current live sync point.” That separates UI, availability, and correction.
Include the requested and reached times, all seekable ranges, playlist snapshots, first post-seek request, selected rendition, sequence and discontinuity information, keyframe evidence if inspected, player configuration, browser/player versions, and sanitized request IDs. Do not claim a keyframe or CDN cause without the corresponding evidence.
Primary references
- RFC 8216: HTTP Live Streaming
- Apple: Event playlist construction
- Apple: About HLS — live, event, and VOD playlists
- MDN: HTMLMediaElement currentTime
- MDN: seeking through media and seekable ranges
- hls.js API: liveSyncPosition and maxLatency
Start with the range the player can seek now, not the range the interface remembers. Then compare playlist snapshots, follow the first post-seek request, and distinguish decode alignment from deliberate live-edge correction. The first boundary that rejects or changes the target identifies the next owner.