mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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:
co-authored by
Claude Opus 4.8
parent
b32783fbd7
commit
e3c00f1db9
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user