Files
v10/internal/design/spf/features/live-stream-support.md
T

16 KiB

status, date, definition
status date definition
draft 2026-05-20 technical

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, 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 candidate 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: not implemented in createSimpleHlsEngine. Today's engine assumes VOD: resolvePresentation fetches the manifest once; media playlists are fetched once per resolved track; no reload loop.
  • Definition depth: technical — scope and constraints articulated; no implementation. 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 What Notes
Manifest reload loop Periodic media-playlist refetch keyed off #EXT-X-TARGETDURATION pacing per HLS spec. Each selected track's media playlist reloads independently as long as the source is live The core primitive; cluster A foundation
Sliding-window segment tracking Engine handles segments dropping off the start of the playlist as the window slides forward. Already-buffered segments past the window are still playable; un-fetched segments past the window are no longer fetchable Affects the segment-loader's planning + back-buffer policy
Live duration semantics presentation.duration = Infinity flows through config.resolveDuration (already pluggable). Downstream updateMediaSourceDuration propagates to mediaSource.duration = Infinity per MSE spec for live The pluggable resolveDuration hook from mse-mms-pipeline is the surface; no new state slot needed
Live edge tracking Engine tracks the latest segment available in the current playlist snapshot. Distinct from currentTime (the playhead); the gap between them is the buffer + the user's distance from live edge. DOM exposure via mediaSource.setLiveSeekableRange(start, end) so the browser's HTMLMediaElement.seekable reflects the live window (without it, seekable is empty under duration === Infinity) Likely a derived signal (computed) rather than a new state slot
Reload jitter / backoff Pacing variations under server delays or slow networks. Naive: poll on target-duration; full: jitter to avoid thundering herd, backoff on consecutive identical-playlist responses Naive depth matches what hls.js does; full depth adds vendor-specific tuning
Per-type reload coordination Audio / video / text media playlists each reload independently. Today the per-type resolveXTrack family is one-shot; live requires extending or replacing it with a reloading variant Open question: extend in place, add a sibling reloadXTrack behavior, or compose differently
Termination detection (manifest signal) Recognize when the reload loop should stop. Naive: #EXT-X-ENDLIST recognition only (assumes spec-compliant servers). Today's parser matches the literal #EXT-X-ENDLIST line but doesn't surface the value to the track output — the parser-side fix is part of this phase. Full: ENDLIST + unchanged-playlist miss-counter as a fallback for servers that stop updating without emitting ENDLIST Naive vs Full depth per clusters.md § Feature classification axes
Terminated state transition Engine flips out of live mode for the affected track. Reload loop stops scheduling that track's playlist. The track's segment list stops mutating, which makes the existing endOfStream gate naturally reachable (last segment now exists permanently). Per-type independence: audio / video can terminate at different times The state transition is the only new orchestration; endOfStream doesn't need new code, just the playlist to stabilize. clearLiveSeekableRange() pairs with the transition so the browser's seekable returns to buffer-derived semantics

What's in scope vs out of scope

In scope:

  • All phases above for HLS live VOD content with #EXT-X-TARGETDURATION pacing
  • Standard sliding-window behavior (segments roll off the start)
  • Infinity duration semantics through MSE
  • Naive reload pacing (target-duration interval; no jitter)
  • #EXT-X-ENDLIST recognition + unchanged-playlist miss-counter fallback for termination detection

Out of scope (separate Media-src candidate features):

  • ll-hls-support — blocking reload, partial segments, delta playlists, preload hints. Largest single live-related gap per the permutation matrix; builds directly on this feature's reload loop.
  • dvr-event-stream-support — DVR / event streams: growing playlist (non-sliding); user can seek backwards through history. Extension of this feature with different windowing semantics.

Out of scope (related but separate concerns):

  • non-zero-pts-support — live streams' PTS advances continuously from stream start, typically far from zero. Live needs this for correct currentTime / seekable semantics, but the time-mapping primitive itself is a separate cluster B feature.
  • [buffer-stall-recovery] — affects live more than VoD due to ingest variability, but is a separate borderline feature.
  • [viewer-rate-limiting-audit] — reload-loop pacing must respect server-side rate limiting; the audit itself is a separate borderline feature.

Likely cross-cutting impact

Things this feature probably forces decisions on, not just additions:

  • resolvePresentation reload variant — today's behavior is one-shot: parse manifest → write resolved presentation. Live requires re-fetching the media playlists (not the multivariant), so the reload loop sits below resolvePresentation rather than replacing it. Most likely a per-type reloadXTrack family alongside the existing resolveXTrack family, or an extension to the existing resolve-track.ts shape. Touches the parseMediaPlaylist direct import (see presentation-modeling.md) — same parser, but called repeatedly per track.
  • Segment-loader sliding-window awareness — today's planner iterates over all segments in track.segments. Live needs the planner to handle the segment list mutating mid-flight (segments appended at the live edge, segments removed from the start). The existing back-buffer eviction policy may need extension to honor "no longer in the playlist" as an eviction signal independent of keepSegments count.
  • bandwidthState resume semantics — already preserved across source resets (see video-abr.md). For live, bandwidth-aware ABR continues to function, but bandwidth measurement during live is more variable (network conditions matter more without the full-buffer-ahead cushion).
  • Time mapping for live edgestate.currentTime is the playhead; the live edge is a derived value computed from the playlist's last segment. Tools like "seek to live edge" or "is at live edge" would consume this derived signal. Doesn't necessarily need its own state slot.
  • End-of-stream handlingendOfStream today gates on isLastSegmentAppended + currentTime >= lastSegStart, not on presentation.duration finiteness. For live, the gate naturally doesn't fire because the playlist keeps growing — no segment is permanently "the last." Once termination commits via this feature's termination-detection phases, the last segment stabilizes and the gate becomes reachable for normal reasons. Subtlety: there's a possible race if reload pacing lags playhead consumption — the current last segment could meet the gate before the next reload appends a new one, firing endOfStream spuriously. Whether this happens in practice depends on reload pacing relative to forward-buffer depth.
  • Parser-side ENDLIST surfacingparseMediaPlaylist currently recognizes #EXT-X-ENDLIST (skips the line) but doesn't extract the value. The MediaPlaylistInfo.endList: boolean type field exists but is orphaned (the parser returns a Track, not MediaPlaylistInfo). The termination-detection phases need the parser to surface the value to the track output.
  • mediaSource.* third-writer pattern — setLiveSeekableRangemediaSource.duration already has two non-overlapping writers (updateMediaSourceDuration for the initial Infinity write; endOfStream for the deterministic final value — see mse-mms-pipeline.md on DOM-property multi-writer). Live introduces a structurally different third writer on the same mediaSource resource: ongoing reactive setLiveSeekableRange(start, end) calls keyed off live-edge updates, plus clearLiveSeekableRange() paired with the terminated-state transition. Distinct from the existing two writers along all three characterization axes — decision domain (derived from playlist snapshot vs. presentation / buffered), trigger (ongoing reactive vs. one-shot transitions), and method (range setter rather than .duration assignment). start = earliest still-fetchable segment (sliding-window-aware); end = live edge. Lives as a new live-only behavior composed into the live engine variant, not as a runtime branch inside an existing MSE behavior — live vs VoD is a composition-time distinction, and updateMediaSourceDuration is deliberately uniform-across-variants (see conventions/behaviors.mdInverse: behaviors that operate uniformly across tracks and the updateMediaSourceDuration worked example). Two SPF-shaped options for placement within the live variant: (a) baked into the (yet-to-exist) live track-polling / reload-loop behavior that produces edge data, or (b) a separate behavior reading presentation / segment state and writing to DOM. Current lean: (b) — single-purpose composition keeps other consumers of the same derived live-edge signal (above-engine "seek to live edge," ABR live-edge-distance if ever introduced) pluggable on the same data without coupling to the polling behavior. Gates on MediaSource 'open'; buffers-idle gating is an implementation detail for the behavior itself.

Open questions

  • Per-type reload coordination. Audio / video / text media playlists each have their own EXT-X-TARGETDURATION. Reload them independently per their own pacing, or coordinate (e.g., reload all on the shortest target duration)? The Epics doc doesn't take a position; HLS spec allows independent. Practical question: does the engine want symmetric coordination for stalls (one playlist behind → block segment-loader from advancing) or independent (each playlist paces itself)?
  • Reload behavior extension vs new behavior. Extending the existing resolveXTrack family to keep reloading vs adding a sibling reloadXTrack family vs composing differently. Affects cleanup-cascade semantics — the existing family ties cancellation to source identity via reactor state-exit; a reload variant needs to honor the same contract.
  • Default defaultResolveDuration already handles VoD; does live need a different wired default in createSimpleHlsEngine, or do live consumers wire their own? The resolveDuration hook is already pluggable; the only question is whether the default engine variant covers live or requires opt-in.
  • Miss-counter threshold. Heuristic feature — how many identical- manifest reloads constitute termination? hls.js uses some count; SPF needs its own choice. Threshold affects false-positive vs false-negative rate.
  • Per-type termination semantics. When audio terminates before video, what's the engine's consumer-facing surface? "Live until all tracks terminate" or "terminated when any track terminates"? Likely the former, but worth confirming the precedent. Aligns with the per-type reload-coordination question above.
  • endOfStream race under live reload pacing. Could the gate fire spuriously between reloads on a heavily-buffered live source? Likely not in practice (reload pace beats consumption pace) but worth verifying.
  • setLiveSeekableRange behavior shape within the live variant. Two SPF-shaped options — (a) call from inside the live track-polling / reload-loop behavior that produces edge data, or (b) a separate behavior reacting to presentation / track state changes. Lean: (b), for single-purpose composition and so other consumers of the same derived live-edge signal can plug in alongside without coupling to the polling behavior. Revisit once the polling behavior's shape lands.
  • ll-hls-support — builds on this feature's reload loop. Adds blocking reload, partial segments, delta playlists, preload hints. Largest live-related gap.
  • dvr-event-stream-support — different windowing semantics on top of the same reload loop. Growing playlist + back-seek through history; resolves this doc's prior "DVR / event boundary" decomposition question (DVR is its own feature, not a phase here).
  • non-zero-pts-support — live PTS starts far from zero. Live without non-zero PTS handling means currentTime is wrong. Cluster B foundation that live consumes.
  • mse-mms-pipelineInfinity duration via config.resolveDuration is already supported there; live writes the value, MSE pipeline propagates it. The endOfStream gate uses segment + currentTime, not duration finiteness; it naturally doesn't fire for live (growing playlist) and becomes reachable once termination commits.
  • buffer-management — sliding-window segment tracking interacts with back-buffer eviction. The planner's currentTime-driven plan shape applies; the playlist itself mutating mid-flight is new.
  • video-abr / audio-playback / subtitles — all per-type consumers continue to work in live, but each reads the resolved track which now changes over time (segments append / roll off). Quality-aware buffer planning preserves buffered higher-quality content; for live this still applies but the windowing changes the buffered-set turnover rate.
  • source-replacement — orthogonal; live and VoD source changes use the same in-place cascade. Live → live source change tears down and rebuilds the reload loop along with everything else.

See also