Update the Implementation surface + live-edge phase row to attribute setLiveSeekableRange to syncLiveSeekableRange and the window derivation to the pure liveWindowFor helper, rather than the (now leaner) seekToLiveEdge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
20 KiB
status, date, definition
| status | date | definition |
|---|---|---|
| draft | 2026-06-23 | sketched |
Live stream support
The engine's foundation for playing live HLS sources: periodic
media-playlist refetch, sliding-window segment tracking, target-duration
pacing, Infinity duration semantics, live-edge seek, and termination
detection (transitioning out of live mode when the stream ends). Distinct from
sibling capabilities for low-latency live (LL-HLS) and DVR / event streams —
those are extensions on top of this foundation, tracked as separate features.
A Media-src feature in the framing from clusters.md § Feature classification axes: without it, live HLS sources don't play correctly.
Status
- Composition: implemented in
createSimpleHlsEngineat naive depth on branchfeat/spf-hls-live. The engine composes uniformly across VOD and live — there is no live-vs-VOD composition branch; live behaviors are inert for VOD via finite-duration guards (Number.isFinite(track.duration)). The reload loop, sliding-window tracking,Infinityduration semantics, live-edge seek +setLiveSeekableRange, and termination-on-#EXT-X-ENDLISTall land. The live-window playhead guard (reposition-on-window-exit) is now implemented inseek-to-live-edge. Not yet implemented: the edge-onlyon-resumereposition policy (a future use-case). Termination is#EXT-X-ENDLIST-based (the naive tier — sufficient for conformant content); the miss-counter fallback and reload jitter are deferred full-depth, andclearLiveSeekableRange()on termination is a low-risk verify-later item. - Definition depth: sketched — grounded in the implementation on
feat/spf-hls-live. Source material: SPF Epics Working Doc — Live Stream Support (epic #2) (cluster A foundation, eng size L, validation M). - Foundational for the manifest-reload-loop cluster — ll-hls-support and dvr-event-stream-support build on this feature.
Phases of complexity
Capability slices for the foundational live-stream-support feature. Each phase below is part of "live works (and terminates) at all"; richer live variants (LL-HLS, DVR) sit in sibling features.
| Phase | Status | What / how |
|---|---|---|
| Manifest reload loop | ✅ Implemented | Periodic media-playlist refetch keyed off #EXT-X-TARGETDURATION. Each resolveXTrack owns a RecurringRunner (resolve-track.ts) rescheduled by delayedReschedule(mediaPlaylistReloadDelay) (wired in engine.ts). Cadence in reload-policy.ts: full target-duration on window change / first reload, half target-duration on unchanged window; start-anchored per RFC 8216bis §6.3.4; returns null (stops) once duration is finite |
| Sliding-window segment tracking | ✅ Implemented | placeOnPreviousTimeline (parse-media-playlist.ts) carries the new window onto the established timeline via media-sequence overlap (PDT bridge on full turnover). Segment-loader planTasks re-evaluates the mutating track.segments on each load dispatch; back-buffer keeps last 2 segments. No explicit "no-longer-in-playlist" eviction signal — the keep-count heuristic + list shrink handle roll-off |
| Live duration semantics | ✅ Implemented | Parser sets Track.duration = Infinity for unended live; calculatePresentationDuration (default resolver getResolvedSelectedTrackDuration) writes it; updateMediaSourceDuration propagates mediaSource.duration = Infinity once MS is open and buffers idle |
Live edge tracking + setLiveSeekableRange |
✅ Implemented (option b) | The live window (or null for VOD/ended) is derived once by the pure liveWindowFor (media/live-window.ts), consumed by two separate behaviors: sync-live-seekable-range.ts declares setLiveSeekableRange(start, end) reactively on each window slide (runs while paused too), and seek-to-live-edge.ts does the one-time HOLD-BACK seek. Separate from the reload loop (option b). Inert for VOD via liveWindowFor returning null. Gap: no clearLiveSeekableRange() on termination |
| Live-window playhead guard | ✅ Implemented (window-exit) |
While playing (!paused && !seeking && readyState > 0), reposition currentTime to liveEdgeStart = max(windowStart, windowEnd − HOLD_BACK_TARGET_MULTIPLIER×targetDuration) when the playhead falls outside the sliding window — covering paused-too-long (Scenario A; fires on the playing resume) and fell-behind-on-poor-network (Scenario B; fires on the reload / window-update re-fire of the effect, since timeupdate stops during a stall). Within-window pause and scrub-back are left untouched (DVR model). Lives in seek-to-live-edge so the live playhead position has a single owner (the one-time initial seek + the ongoing guard); secondary triggers are playing / timeupdate / seeked listeners. Reposition policy is a seam (repositionPolicy, default 'window-exit'); the edge-only 'on-resume' branch is not yet implemented. Deferred: playback-rate latency catch-up, MSE gap-jumping |
| Reload jitter / backoff | ✅ Naive only | Target-duration cadence with unchanged-window throttle (half target). No thundering-herd jitter, no backoff on repeated identical-playlist responses (full depth, not implemented) |
| Per-type reload coordination | ✅ Independent | Audio / video / text each own their RecurringRunner and reload on their own #EXT-X-TARGETDURATION. Resolved as extended resolveXTrack (same setupTrackResolution handles one-shot VOD and recurring live via the RecurringRunner abstraction) — no separate reloadXTrack family |
| Termination detection | ✅ Naive depth | #EXT-X-ENDLIST recognized and surfaced (MediaPlaylistMetadata.endList); parser flips Track.duration finite (on ENDLIST or PLAYLIST-TYPE:VOD), which stops the reload loop. ENDLIST-only is the sanctioned naive tier (per clusters.md) and sufficient for conformant content (Mux always emits ENDLIST); the miss-counter fallback for non-conformant servers is deferred full-depth |
| Terminated state transition | ✅ Implemented (one verify-later item) | Reload stops (policy returns null); the endOfStream gate unblocks once Track.duration is finite and the last segment is appended; per-type independent (waits for all active tracks). clearLiveSeekableRange() is not called on the transition — low-risk (a terminated window's stale live range ~matches its buffered/duration-derived seekable); verify on a real terminating stream |
What's not implemented
Within this feature:
- Edge-only
on-resumereposition policy — therepositionPolicyseam exists (default'window-exit', implemented), but the'on-resume'(always-snap-to-edge / no-DVR) branch is inert. It lands as the[live-edge-only-mode]use-case, not here. clearLiveSeekableRange()on termination — not called; low-risk (a terminated window's stale live range ~matches its buffered/duration-derived seekable). Verify on a real terminating stream; not a blocker.
Naive depth (sufficient for conformant content; full depth deferred):
- Termination detection —
#EXT-X-ENDLIST/PLAYLIST-TYPE:VODonly, the sanctioned naive tier per clusters.md. Full depth adds a miss-counter fallback for non-conformant servers that stop updating withoutENDLIST(Mux always emits it). - Reload jitter / backoff — target-duration cadence only; full depth adds thundering-herd jitter + backoff on identical-playlist responses.
Out of scope (separate Media-src features):
- ll-hls-support — blocking reload, partial segments, delta playlists, preload hints. Builds on this feature's reload loop.
- dvr-event-stream-support — growing
(non-sliding) window, back-seek through history. Extension with different
windowing semantics; shares the reload loop and the
setLiveSeekableRangewriter shape. Sits at the maximal-back-seek end of the same seekable spectrum the guard'srepositionPolicyexposes (edge-only ↔ in-window scrub-back ↔ full-history back-seek). - non-zero-pts-support — live PTS advances from stream start, far from zero; the time-mapping primitive live consumes is a separate cluster B feature.
- buffer-stall-recovery — mid-stream stall detection + unstick-in-place recovery (nudge → flush → reset). Coordinates with this feature's planned guard: an in-window stall is buffer-stall-recovery's territory; the window-exit reposition is the live-specific terminal action.
Likely cross-cutting impact
The foundation is implemented, so most prior cross-cutting concerns are now realized. The remaining forward-looking impact is concentrated in the planned guard:
repositionPolicycomposition seam. The guard's reposition condition is a policy point:'window-exit'(default — DVR model; reposition only when the playhead is outside the window, in-window scrub-back preserved) vs'on-resume'(edge-only — always snap to the live edge on resume). Per clusters.md § Composition vs Policy vs middle pattern,'window-exit'ships as the default config consumed by the guard behavior; the edge-only variant (stop fetching segments while paused + always-snap on resume + narrowed seekable) is a future Composition (alternative-default-config + add + alternative-impl), tracked as a[live-edge-only-mode]use-case rather than a runtime branch. Leaving the seam in place costs only the policy indirection now and keeps the edge-only mode a composition rather than a rewrite later.- Single owner of the live playhead position. The guard writes
currentTime(a seek).seek-to-live-edgealready does the one-time initial seek; folding the guard into it keeps one writer of "where the live playhead belongs" (initial seek = first firing) rather than two behaviors racing seeks. No new state slot — the guard reads existingpresentation.duration(finiteness guard),selectedVideoTrackId, andtrack.segments(window), plus thecurrentTimemirror. - Primary trigger is the window-update signal, not
timeupdate. Scenario B's fall-behind happens during a stall, whentimeupdatehas stopped but the window keeps sliding via reloads — so the guard must re-evaluate on the reload / window-update signal.playing/timeupdate/seekedare secondary triggers (resume case, prompt in-playback detection, overran-edge).
Implementation surface
Behaviors:
| Behavior | File | Live responsibility |
|---|---|---|
liveWindowFor (pure helper) |
media/live-window.ts |
Derive the live window {start,end,targetDuration} from the selected video track, or null (VOD/ended/unresolved). The single source of truth consumed by the two behaviors below — centralizes all inertness so neither re-derives the window. |
syncLiveSeekableRange |
behaviors/dom/sync-live-seekable-range.ts |
Consume liveWindowFor; setLiveSeekableRange(start, end) (+ defensive duration = Infinity) reactively on each window slide, including while paused. Composed before seekToLiveEdge. |
seekToLiveEdge |
behaviors/dom/seek-to-live-edge.ts |
Consume liveWindowFor; one-time HOLD-BACK seek + the live-window playhead guard (reposition-on-window-exit, repositionPolicy seam). Gates on mediaSource open so the seek lands in the declared range. |
anchorLiveTracks |
behaviors/anchor-live-tracks.ts |
Pin live track timelines to the SourceBuffer's native-PTS ground truth (first appended segment) or manifest estimate; re-pin per reload as the window slides |
resolveVideoTrack / resolveAudioTrack / resolveTextTrack |
behaviors/resolve-track.ts |
Own the reload loop via RecurringRunner; reschedule defaults to mediaPlaylistReloadDelay; per-type independent |
calculatePresentationDuration |
behaviors/calculate-presentation-duration.ts |
Populate presentation.duration via the config resolver (Infinity for unended live) |
updateMediaSourceDuration |
behaviors/dom/update-mediasource-duration.ts |
Propagate presentation.duration to mediaSource.duration once per MediaSource (uniform across variants) |
endOfStream |
behaviors/dom/end-of-stream.ts |
Gate on Track.duration finiteness — inert for ongoing live, reachable once terminated |
Actors: SegmentLoaderActor (actors/dom/segment-loader.ts) —
planTasks / getSegmentsToLoad iterate the live-mutating segment list within
the forward/back-buffer windows; SourceBufferActor
(actors/dom/source-buffer.ts) — append/remove + tracks appended segment IDs
for the endOfStream gate.
State / derived: presentation.duration (Infinity for live; written by
calculatePresentationDuration, read by updateMediaSourceDuration /
seek-to-live-edge / endOfStream — distinct decision domains, not a
multi-writer conflict). presentation.streamType ('live'). Per-track
segments (live-mutating; written by reload parse, read by segment-loader).
Live edge and the planned liveEdgeStart are derived from
track.segments, not state slots.
Engine composition: engines/hls/engine.ts composes the live behaviors
unconditionally (anchorLiveTracks, calculatePresentationDuration,
updateMediaSourceDuration, the resolveXTrack family with
reschedule: delayedReschedule(mediaPlaylistReloadDelay), seekToLiveEdge,
endOfStream); VOD inertness comes from finite-duration guards, not a branch.
Config surface
| Constant | File | Value | Purpose |
|---|---|---|---|
HOLD_BACK_TARGET_MULTIPLIER |
behaviors/dom/seek-to-live-edge.ts |
3 |
Initial-seek + guard reposition target = live edge − 3×target-duration, clamped to window start |
REPOSITION_TOLERANCE |
behaviors/dom/seek-to-live-edge.ts |
0.1 |
Guard's boundary tolerance (s) — avoids jitter seeks at the window edges |
FALLBACK_TARGET_DURATION |
media/hls/reload-policy.ts |
6 |
Reload cadence (s) when no #EXT-X-TARGETDURATION |
DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration |
media/buffer/forward-buffer.ts |
30 |
Seconds ahead of playhead to load |
DEFAULT_BACK_BUFFER_CONFIG.keepSegments |
media/buffer/back-buffer.ts |
2 |
Segments kept behind playhead |
repositionPolicy |
seek-to-live-edge config (behavior-scoped) |
'window-exit' |
Reposition condition seam. 'window-exit' implemented; 'on-resume' (edge-only) is inert pending the [live-edge-only-mode] use-case, which adds the public engine-config surface |
Verification
Existing (implemented phases):
media/hls/tests/reload-policy.test.ts— cadence:nullfor finite duration, full/half target-duration, 6s fallback.media/hls/tests/parse-media-playlist.test.ts—Infinityfor unended live;endListon#EXT-X-ENDLIST; finite forPLAYLIST-TYPE:VOD; PDT capture + carry-forward.behaviors/tests/anchor-live-tracks.test.ts— pin to buffer ground truth; PDT carry-forward; sequence-origin bootstrap.behaviors/tests/resolve-track.test.ts— live reload re-resolves; stops on finite duration; source-change abort.behaviors/dom/tests/seek-to-live-edge.test.ts— declares window; seeks to HOLD-BACK; no-op for finite/absent tracks. Plus the live-window guard matrix (see below).behaviors/tests/calculate-presentation-duration.test.ts—Infinityfor live resolver.engines/hls/tests/engine.test.ts— end-to-end live setup / reload / window slide / termination.- Sandbox:
apps/sandbox/templates/live-hls-engine,SOURCES['hls-live'].
Live-window playhead guard — behaviors/dom/tests/seek-to-live-edge.test.ts
(describe('live-window playhead guard'), event-capable fake media element):
playing-inside-window → no seek; currentTime < windowStart (playing) → seek to
liveEdgeStart; currentTime > windowEnd (playing) → seek; paused-out-of-window
→ no seek, then seek on playing resume; DVR mid-window scrub-back → no yank;
seeking in flight → defer to seeked; sub-tolerance boundary → no jitter seek,
beyond-tolerance → seek; window slides past frozen paused playhead across reloads
→ no seek while paused, snaps in on resume; stalled-behind-window → repositions on
the window-update re-fire; on-resume policy → inert (not yet implemented).
Out of scope / deferred: E2E (Chromium) — a local synthetic sliding-window
stream (short target-duration) driving pause-beyond-window → resume-snaps-near-edge
and a CDP Network.emulateNetworkConditions buffer-drain → reposition-and-recover.
Deferred: needs new synthetic-sliding-window HLS fixture infra; the unit matrix
covers the guard logic deterministically.
Open questions
- Guard reposition target under truly-insufficient bandwidth. If the network can't sustain even the lowest rate, the guard can loop (reposition → stall → window slides → reposition). v1 lean: accept the loop (the stream is below its playable floor; the jumps are honest) and document it; cheap alternative is deepening the holdback on repeated exits rather than touching playback rate.
- Miss-counter threshold (if pursued). How many identical-manifest reloads constitute termination?
Resolved during guard implementation:
repositionPolicyseam shape → behavior-scoped config onseek-to-live-edge(default'window-exit'), not a publicSimpleHlsEngineConfigfield. Avoids a speculative engine-config surface with no current consumer (perconventions/config.md); the[live-edge-only-mode]use-case adds the public field when it implements'on-resume'.- Guard placement → extends
seek-to-live-edge(single owner of the live playhead position), not a sibling behavior.
Related features
- ll-hls-support — builds on this feature's reload loop; adds blocking reload, partial segments, delta playlists, preload hints.
- dvr-event-stream-support — growing
window + full-history back-seek on the same reload loop; the maximal-back-seek
end of the seekable spectrum the guard's
repositionPolicysits on (edge-only ↔ in-window scrub-back ↔ full history). - buffer-stall-recovery — in-window stall detection + unstick-in-place recovery; coordinates with this feature's planned guard (window-exit reposition is the live-specific terminal action).
- non-zero-pts-support — live PTS starts far
from zero; cluster B foundation live consumes for correct
currentTime/seekable. - mse-mms-pipeline —
Infinityduration viaconfig.resolveDuration; theendOfStreamgate uses segment + currentTime, not duration finiteness. - buffer-management — sliding-window tracking interacts with back-buffer eviction; the planner's currentTime-driven shape applies with the list mutating mid-flight.
- video-abr / audio-playback / subtitles — per-type consumers read the resolved track that now changes over time (segments append / roll off).
- source-replacement — orthogonal; live → live source change tears down and rebuilds the reload loop via the same in-place cascade.
[live-edge-only-mode](future use-case) — the edge-only composition therepositionPolicy: 'on-resume'seam enables.
See also
- clusters.md § Manifest reload loop — cluster A description; this feature is the foundation.
- clusters.md § Composition vs Policy vs middle pattern
— the
repositionPolicyseam / edge-only composition framing. - live-timeline-anchoring — PDT
anchor that places the sliding-window timeline
anchorLiveTracksconsumes. - mse-timestamp-offset — native-PTS
default,
setLiveSeekableRangein native coords, one-time seek into the window on load. - presentation-modeling.md — the reload loop
sits below
resolvePresentationand re-usesparseMediaPlaylistper cycle. - SPF Epics Working Doc — cluster A epic candidates.
- Mux Video Permutations Matrix — Stream Type section.