docs(spf): reconcile live-stream-support to implemented state, document live-window guard

Bump live-stream-support to sketched depth grounded in feat/spf-hls-live
(reload loop, sliding-window tracking, Infinity duration, live-edge seek,
ENDLIST termination all implemented); resolve its 5 open questions. Add the
planned live-window playhead guard (reposition on window-exit, covering
paused-too-long and fallen-behind) with a repositionPolicy seam for a future
edge-only composition.

Cascade: register [live-edge-only-mode] use-case candidate; cross-ref
buffer-stall-recovery (in-window stall vs window-exit reposition) and
dvr-event-stream-support (seekable spectrum).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 10:00:19 -07:00
co-authored by Claude Opus 4.8
parent b32783fbd7
commit e3c00f1db9
4 changed files with 235 additions and 211 deletions
@@ -196,7 +196,14 @@ Things this feature probably forces decisions on, not just additions:
lifecycle.
- **[live-stream-support](./live-stream-support.md)** — primary
consumer in the sense that live triggers stall detection more
often. Reload-loop interruption is one stall cause.
often. Reload-loop interruption is one stall cause. Division of
labor with that feature's live-window playhead guard: this feature
owns in-window stalls (nudge → flush → reset, the playhead is still
inside the sliding window); the guard owns the window-*exit*
reposition (the playhead fell behind the window start → seek to the
live edge). A Scenario-B poor-network stall may surface here first,
then escalate to the guard's reposition once the window slides past
the frozen playhead.
- **`[multi-signal-abr]`** — page visibility signal should suppress
stall detection during hidden state.
- **`[discontinuity-handling]`** *(deferred candidate)*
@@ -214,7 +214,11 @@ Things this feature probably forces decisions on, not just additions:
`Infinity` duration semantics, termination detection (`ENDLIST` +
miss-counter), and the `setLiveSeekableRange` writer-behavior
shape. DVR/event-stream supplies the windowing variance and the
variant-specific seekable-range start producer.
variant-specific seekable-range start producer. Its live-window
playhead guard's DVR `repositionPolicy` (preserve in-window
scrub-back on the sliding window) is the analog of this feature's
full-history back-seek; the guard's edge-only `repositionPolicy`
variant is the no-scrub-back opposite end of that spectrum.
- **[ll-hls-support](./ll-hls-support.md)** — orthogonal cluster A
extension. DVR + LL-HLS compose; both extensions sit atop the
same reload-loop foundation but address different concerns
@@ -1,18 +1,17 @@
---
status: draft
date: 2026-05-20
definition: technical
date: 2026-06-23
definition: 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, 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.
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](./clusters.md#feature-classification-axes):
@@ -20,232 +19,245 @@ 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)](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4)
- **Composition:** implemented in `createSimpleHlsEngine` at naive depth on
branch `feat/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, `Infinity` duration semantics,
live-edge seek + `setLiveSeekableRange`, and termination-on-`#EXT-X-ENDLIST`
all land. **Not yet implemented:** the live-window playhead guard (see the
planned phase below). Termination is `#EXT-X-ENDLIST`-based (the naive tier —
sufficient for conformant content); the miss-counter fallback and reload
jitter are deferred full-depth, and `clearLiveSeekableRange()` 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)](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4)
(cluster A foundation, eng size L, validation M).
- **Foundational** for the manifest-reload-loop cluster —
[ll-hls-support](./ll-hls-support.md) and
[dvr-event-stream-support](./dvr-event-stream-support.md) build on
this feature.
[dvr-event-stream-support](./dvr-event-stream-support.md) 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.
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 |
| Phase | Status | What / how |
|---|---|---|
| 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](./clusters.md#naive-vs-full-implementation-depth) |
| 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 |
| 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**) | Dedicated behavior `seek-to-live-edge.ts` reads the selected video track's timeline, calls `setLiveSeekableRange(windowStart, windowEnd)` reactively on each window slide, and does a one-time seek to the HOLD-BACK position. Separate from the reload loop (option b), so other consumers of the derived live-edge signal can plug in without coupling to polling. Inert for VOD via finite-duration guard. **Gap:** no `clearLiveSeekableRange()` on termination |
| Live-window playhead guard | 🔲 Planned | 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 signal, since `timeupdate` stops during a stall). Within-window pause and scrub-back are left untouched (DVR model). Extends `seek-to-live-edge` so the live playhead position has a single owner (the initial seek becomes this guard's first firing). Reposition policy is a seam — see Likely cross-cutting impact. **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](./clusters.md#naive-vs-full-implementation-depth)) 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 in scope vs out of scope
## What's not implemented
**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
**Within this feature:**
- **Live-window playhead guard** (planned phase above) — the substantive
unimplemented work. Today the playhead is never repositioned after the
one-time initial seek, so a paused-too-long or fallen-behind playhead simply
stalls outside the window.
- **`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:VOD` only, the
sanctioned naive tier per
[clusters.md](./clusters.md#naive-vs-full-implementation-depth). Full depth
adds a miss-counter fallback for non-conformant servers that stop updating
without `ENDLIST` (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):**
**Out of scope (separate Media-src candidate features):**
- **[ll-hls-support](./ll-hls-support.md)** — 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-stream-support.md)** —
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](./non-zero-pts-support.md)** — 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.
segments, delta playlists, preload hints. Builds on this feature's reload
loop.
- **[dvr-event-stream-support](./dvr-event-stream-support.md)** — growing
(non-sliding) window, back-seek through history. Extension with different
windowing semantics; shares the reload loop and the `setLiveSeekableRange`
writer shape. Sits at the maximal-back-seek end of the same seekable
spectrum the guard's `repositionPolicy` exposes (edge-only ↔ in-window
scrub-back ↔ full-history back-seek).
- **[non-zero-pts-support](./non-zero-pts-support.md)** — live PTS advances
from stream start, far from zero; the time-mapping primitive live consumes is
a separate cluster B feature.
- **[buffer-stall-recovery](./buffer-stall-recovery.md)** — 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
Things this feature probably forces decisions on, not just additions:
The foundation is implemented, so most prior cross-cutting concerns are now
realized. The remaining forward-looking impact is concentrated in the planned
guard:
- **`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](../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](./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 edge**`state.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 handling**`endOfStream` 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 surfacing**`parseMediaPlaylist` 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 — `setLiveSeekableRange`** —
`mediaSource.duration` already has two non-overlapping writers
(`updateMediaSourceDuration` for the initial `Infinity` write;
`endOfStream` for the deterministic final value — see
[mse-mms-pipeline.md](./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.md](../conventions/behaviors.md) → *Inverse:
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.
- **`repositionPolicy` composition 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](./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-edge` already 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 existing `presentation.duration`
(finiteness guard), `selectedVideoTrackId`, and `track.segments` (window),
plus the `currentTime` mirror.
- **Primary trigger is the window-update signal, not `timeupdate`.** Scenario
B's fall-behind happens *during a stall*, when `timeupdate` has stopped but
the window keeps sliding via reloads — so the guard must re-evaluate on the
reload / window-update signal. `playing` / `timeupdate` / `seeked` are
secondary triggers (resume case, prompt in-playback detection, overran-edge).
## Implementation surface
**Behaviors:**
| Behavior | File | Live responsibility |
|---|---|---|
| `seekToLiveEdge` | `behaviors/dom/seek-to-live-edge.ts` | Compute the live window from the selected video track, `setLiveSeekableRange(start, end)` reactively, one-time seek to HOLD-BACK. Inert for VOD. **(Planned)** host the live-window playhead guard |
| `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 (and planned guard) target = live edge 3×target-duration, clamped to window start |
| `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` *(planned)* | guard behavior | `'window-exit'` | Reposition condition seam; `'on-resume'` opt-in drives the edge-only composition |
## Verification
**Existing (implemented phases):**
- `media/hls/tests/reload-policy.test.ts` — cadence: `null` for finite
duration, full/half target-duration, 6s fallback.
- `media/hls/tests/parse-media-playlist.test.ts``Infinity` for unended
live; `endList` on `#EXT-X-ENDLIST`; finite for `PLAYLIST-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.
- `behaviors/tests/calculate-presentation-duration.test.ts``Infinity` for
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']`.
**Planned (live-window playhead guard):** unit (fake media element + injected
window signal) — within-window/playing → no seek; `currentTime < windowStart`
→ seek to `liveEdgeStart`; out-back while paused → no seek, then seek on
`playing`; `currentTime > windowEnd` → seek; DVR mid-window scrub-back →
**no** yank; target = `max(windowStart, windowEnd 3×td)`; boundary within
tolerance → no jitter; window slides past frozen paused playhead across N
reloads → single seek on resume; `seeking` in flight → defer to `seeked`. E2E
(Chromium) — local synthetic sliding-window stream (short target-duration):
pause-beyond-window → resume snaps near `seekable.end`; CDP
`Network.emulateNetworkConditions` to drain buffer → reposition + recover.
## 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.
- **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?
## Related features
- **[ll-hls-support](./ll-hls-support.md)** — builds on this feature's
reload loop. Adds blocking reload, partial segments, delta playlists,
preload hints. Largest live-related gap.
- **[dvr-event-stream-support](./dvr-event-stream-support.md)** —
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](./non-zero-pts-support.md)** — live PTS
starts far from zero. Live without non-zero PTS handling means
`currentTime` is wrong. Cluster B foundation that live consumes.
- **mse-mms-pipeline**`Infinity` 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.
- **[ll-hls-support](./ll-hls-support.md)** — builds on this feature's reload
loop; adds blocking reload, partial segments, delta playlists, preload hints.
- **[dvr-event-stream-support](./dvr-event-stream-support.md)** — growing
window + full-history back-seek on the same reload loop; the maximal-back-seek
end of the seekable spectrum the guard's `repositionPolicy` sits on (edge-only
↔ in-window scrub-back ↔ full history).
- **[buffer-stall-recovery](./buffer-stall-recovery.md)** — 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](./non-zero-pts-support.md)** — live PTS starts far
from zero; cluster B foundation live consumes for correct `currentTime` /
`seekable`.
- **mse-mms-pipeline**`Infinity` duration via `config.resolveDuration`; the
`endOfStream` gate 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
the `repositionPolicy: 'on-resume'` seam enables.
## See also
- [clusters.md § Manifest reload loop](./clusters.md#manifest-reload-loop)
cluster A description; this feature is the foundation
- [clusters.md § Feature classification axes](./clusters.md#feature-classification-axes)
— the Media-src feature framing this doc instantiates
- [presentation-modeling.md](../presentation-modeling.md) —
architectural deep-dive; the reload loop sits below
`resolvePresentation` and re-uses `parseMediaPlaylist` per cycle.
When `parseMediaPlaylist` pluggability arrives (see that doc's
Open questions), live + format support intersect
- [clusters.md § Manifest reload loop](./clusters.md#manifest-reload-loop)
cluster A description; this feature is the foundation.
- [clusters.md § Composition vs Policy vs middle pattern](./clusters.md#composition-vs-policy-vs-middle-pattern)
— the `repositionPolicy` seam / edge-only composition framing.
- [live-timeline-anchoring](../../../decisions/live-timeline-anchoring.md) — PDT
anchor that places the sliding-window timeline `anchorLiveTracks` consumes.
- [mse-timestamp-offset](../../../decisions/mse-timestamp-offset.md) — native-PTS
default, `setLiveSeekableRange` in native coords, one-time seek into the
window on load.
- [presentation-modeling.md](../presentation-modeling.md) — the reload loop
sits below `resolvePresentation` and re-uses `parseMediaPlaylist` per cycle.
- [SPF Epics Working Doc](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4)
source material; cluster A epic candidates and decompositions
— cluster A epic candidates.
- [Mux Video Permutations Matrix](https://www.notion.so/32c97a7f89d08191b84dd30f06685490)
— Stream Type section; SPF column shows ⚠️ for live + DVR (manifest
re-polling unverified), 🔲 for LL-HLS
— Stream Type section.
</content>
</invoke>
+1
View File
@@ -141,6 +141,7 @@ Initially empty; populated as docs land. Candidates flagged in source material (
- [`audio-only-mode-override`](./audio-only-mode-override.md) *(partial — Phase 1 landed)* — audio-only delivery. Covers both truly-audio-only HLS sources and mixed-manifest sources delivered as audio-only via the same shared engine factory (`createHlsAudioOnlyEngine`). Subsumes what Notion originally framed as separate epics #4a (Basic Audio-only) and #4b (Audio-only Mode Override).
- [`video-only-mode-override`](./video-only-mode-override.md) *(coarse)* — video-only delivery. Inverse-axis sibling of [`audio-only-mode-override`](./audio-only-mode-override.md); same shape. Subsumes Notion epics NEW-A (Basic Video-only) and NEW-B (Video-only Composition).
- `[background-video]` — Mux's background-video product scenario: loop + autoplay-muted + GPU/thermal-aware caps + likely silent-video delivery. **Distinct from `video-only-mode-override`** despite shared Mux consumer context; both may share constituent features but address different delivery scenarios. *[GitHub #873](https://github.com/videojs/v10/issues/873); [`mux-background-video`](https://github.com/muxinc/mux-background-video) prior art.*
- `[live-edge-only-mode]` — pure-live delivery with no DVR scrub-back: always snap the playhead to the live edge on resume rather than preserving in-window position, optionally stop fetching segments while paused, and narrow the declared seekable range to the edge. The opposite end of the seekable spectrum from [`dvr-event-stream-support`](../features/dvr-event-stream-support.md). Composes on [`live-stream-support`](../features/live-stream-support.md) via its `repositionPolicy: 'on-resume'` seam (alternative-default-config) + a pause-fetch-gate behavior (add) + a narrowed-seekable producer (alternative-impl). Scope it only when a concrete consumer asks for edge-only.
- Further candidates surfaced in source material but not yet scoped: picture-in-picture, short-form / shorts-player, audio-podcast mode, cast/remote-display compositions, ambient/decorative video.
## See also