mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
docs(spf): reflect the live-latency seam in live-stream-support
liveWindowFor is now {start, end}; the holdback rule lives in liveLatencyFor
and reaches seek-to-live-edge via the resolveLiveLatency seam + getLiveEdge.
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
62787a611d
commit
ada3100209
@@ -51,8 +51,8 @@ below is part of "live works (and terminates) at all"; richer live variants
|
||||
| Manifest reload loop | ✅ Implemented | Periodic media-playlist refetch keyed off `#EXT-X-TARGETDURATION`. Each `resolveXTrack` owns a `RecurringRunner` (`resolve-track.ts`) rescheduled by `delayedReschedule(mediaPlaylistReloadDelay)` (wired in `engine.ts`). Cadence in `reload-policy.ts`: full target-duration on window change / first reload, half target-duration on unchanged window; start-anchored per RFC 8216bis §6.3.4; returns `null` (stops) once duration is finite |
|
||||
| Sliding-window segment tracking | ✅ Implemented | `placeOnPreviousTimeline` (`parse-media-playlist.ts`) carries the new window onto the established timeline via media-sequence overlap (PDT bridge on full turnover). Segment-loader `planTasks` re-evaluates the mutating `track.segments` on each load dispatch; back-buffer keeps last 2 segments. No explicit "no-longer-in-playlist" eviction signal — the keep-count heuristic + list shrink handle roll-off |
|
||||
| Live duration semantics | ✅ Implemented | Parser sets `Track.duration = Infinity` for unended live; `calculatePresentationDuration` (default resolver `getResolvedSelectedTrackDuration`) writes it; `updateMediaSourceDuration` propagates `mediaSource.duration = Infinity` once MS is open and buffers idle |
|
||||
| Live edge tracking + `setLiveSeekableRange` | ✅ Implemented (option **b**) | The live window (or null for VOD/ended) is derived once by the pure `liveWindowFor` (`media/live-window.ts`), consumed by two separate behaviors: `sync-live-seekable-range.ts` declares `setLiveSeekableRange(start, end)` reactively on each window slide (runs while paused too), and `seek-to-live-edge.ts` does the one-time HOLD-BACK seek. Separate from the reload loop (option b). Inert for VOD via `liveWindowFor` returning null. No `clearLiveSeekableRange()` on termination — unnecessary: the UA consults the live range only while `duration === Infinity`, so once `endOfStream` sets a finite duration it's ignored |
|
||||
| Live-window playhead guard | ✅ Implemented (`window-exit`) | While playing (`!paused && !seeking && readyState > 0`), reposition `currentTime` to `liveEdgeStart = max(windowStart, windowEnd − HOLD_BACK_TARGET_MULTIPLIER×targetDuration)` when the playhead falls **outside** the sliding window — covering *paused-too-long* (Scenario A; fires on the `playing` resume) and *fell-behind-on-poor-network* (Scenario B; fires on the reload / window-update re-fire of the effect, since `timeupdate` stops during a stall). Within-window pause and scrub-back are left untouched (DVR model). Lives in `seek-to-live-edge` so the live playhead position has a single owner (the one-time initial seek + the ongoing guard); secondary triggers are `playing` / `timeupdate` / `seeked` listeners. Reposition policy is a seam (`repositionPolicy`, default `'window-exit'`); the edge-only `'on-resume'` branch is not yet implemented. **Deferred:** playback-rate latency catch-up, MSE gap-jumping |
|
||||
| Live edge tracking + `setLiveSeekableRange` | ✅ Implemented (option **b**) | The live window (or null for VOD/ended) is derived once by the pure `liveWindowFor` (`media/live-window.ts`), consumed by two separate behaviors: `sync-live-seekable-range.ts` declares `setLiveSeekableRange(start, end)` reactively on each window slide (runs while paused too), and `seek-to-live-edge.ts` does the one-time seek to `liveEdgeStart` (the live latency behind the edge, via the `getLiveEdge` primitive). Separate from the reload loop (option b). Inert for VOD via `liveWindowFor` returning null. No `clearLiveSeekableRange()` on termination — unnecessary: the UA consults the live range only while `duration === Infinity`, so once `endOfStream` sets a finite duration it's ignored |
|
||||
| Live-window playhead guard | ✅ Implemented (`window-exit`) | While playing (`!paused && !seeking && readyState > 0`), reposition `currentTime` to `liveEdgeStart = max(windowStart, windowEnd − liveLatency)` (the latency from the injected `resolveLiveLatency` seam — HLS `HOLD-BACK`, 3× target duration) when the playhead falls **outside** the sliding window — covering *paused-too-long* (Scenario A; fires on the `playing` resume) and *fell-behind-on-poor-network* (Scenario B; fires on the reload / window-update re-fire of the effect, since `timeupdate` stops during a stall). Within-window pause and scrub-back are left untouched (DVR model). Lives in `seek-to-live-edge` so the live playhead position has a single owner (the one-time initial seek + the ongoing guard); secondary triggers are `playing` / `timeupdate` / `seeked` listeners. Reposition policy is a seam (`repositionPolicy`, default `'window-exit'`); the edge-only `'on-resume'` branch is not yet implemented. **Deferred:** playback-rate latency catch-up, MSE gap-jumping |
|
||||
| Reload jitter / backoff | ✅ Naive only | Target-duration cadence with unchanged-window throttle (half target). No thundering-herd jitter, no backoff on repeated identical-playlist responses (full depth, not implemented) |
|
||||
| Per-type reload coordination | ✅ Independent | Audio / video / text each own their `RecurringRunner` and reload on their own `#EXT-X-TARGETDURATION`. Resolved as **extended `resolveXTrack`** (same `setupTrackResolution` handles one-shot VOD and recurring live via the `RecurringRunner` abstraction) — no separate `reloadXTrack` family |
|
||||
| Termination detection | ✅ Naive depth | `#EXT-X-ENDLIST` recognized and surfaced (`MediaPlaylistMetadata.endList`); parser flips `Track.duration` finite (on `ENDLIST` or `PLAYLIST-TYPE:VOD`), which stops the reload loop. ENDLIST-only is the sanctioned naive tier (per [clusters.md](./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 |
|
||||
@@ -134,10 +134,10 @@ guard:
|
||||
|
||||
| Behavior | File | Live responsibility |
|
||||
|---|---|---|
|
||||
| `liveWindowFor` *(pure helper)* | `media/live-window.ts` | Derive the live window `{start,end,targetDuration}` from the track with the given id (type-agnostic via `findTrackById`), or `null` (VOD/ended/unresolved). Centralizes all inertness so consumers don't re-derive the window. |
|
||||
| `liveWindowFromState` *(primitive)* | `playback/primitives/live-window.ts` | The single state-reading call site both live behaviors use. Picks the timeline-bearing track — `selectedVideoTrackId ?? selectedAudioTrackId` (video positions both A/V; audio-only falls back to audio) — and calls `liveWindowFor`. Reads signals lazily (call inside an effect). |
|
||||
| `liveWindowFor` *(pure helper)* | `media/live-window.ts` | Derive the live window `{start,end}` from the track with the given id (type-agnostic via `findTrackById`), or `null` (VOD/ended/unresolved). Purely geometric — no delivery-format metadata. Centralizes all inertness so consumers don't re-derive the window. |
|
||||
| `liveWindowFromState` / `getLiveEdge` *(primitives)* | `playback/primitives/live-window.ts` | The state-reading call sites the live behaviors use. `liveWindowFromState` picks the timeline-bearing track — `selectedVideoTrackId ?? selectedAudioTrackId` (video positions both A/V; audio-only falls back to audio) — and calls `liveWindowFor`. `getLiveEdge({state,config})` adds the target playhead position (`liveEdgeStart = end − live latency`, clamped to start), bundling window geometry with the format-specific `config.resolveLiveLatency` policy so the behavior consumes one edge. Reads signals lazily (call inside an effect). |
|
||||
| `syncLiveSeekableRange` | `behaviors/dom/sync-live-seekable-range.ts` | Consume `liveWindowFromState`; `setLiveSeekableRange(start, end)` reactively on each window slide, including while paused. Duration is owned solely by `updateMediaSourceDuration`. Composed before `seekToLiveEdge`. |
|
||||
| `seekToLiveEdge` | `behaviors/dom/seek-to-live-edge.ts` | Consume `liveWindowFromState`; one-time HOLD-BACK seek + the live-window playhead guard (reposition-on-window-exit, `repositionPolicy` seam). Gates on `mediaSource` open so the seek lands in the declared range. |
|
||||
| `seekToLiveEdge` | `behaviors/dom/seek-to-live-edge.ts` | Consume `getLiveEdge`; one-time seek to `liveEdgeStart` + the live-window playhead guard (reposition-on-window-exit, `repositionPolicy` seam). Format-neutral — the live latency comes from the injected `resolveLiveLatency` seam, never read here. Gates on `mediaSource` open so the seek lands in the declared range. |
|
||||
| `anchorLiveTracks` | `behaviors/anchor-live-tracks.ts` | Pin live track timelines to the SourceBuffer's native-PTS ground truth (first appended segment) or manifest estimate; re-pin per reload as the window slides |
|
||||
| `resolveVideoTrack` / `resolveAudioTrack` / `resolveTextTrack` | `behaviors/resolve-track.ts` | Own the reload loop via `RecurringRunner`; reschedule defaults to `mediaPlaylistReloadDelay`; per-type independent |
|
||||
| `calculatePresentationDuration` | `behaviors/calculate-presentation-duration.ts` | Populate `presentation.duration` via the config resolver (`Infinity` for unended live) |
|
||||
@@ -168,9 +168,10 @@ unconditionally (`anchorLiveTracks`, `calculatePresentationDuration`,
|
||||
|
||||
| Constant | File | Value | Purpose |
|
||||
|---|---|---|---|
|
||||
| `HOLD_BACK_TARGET_MULTIPLIER` | `behaviors/dom/seek-to-live-edge.ts` | `3` | Initial-seek + guard reposition target = live edge − 3×target-duration, clamped to window start |
|
||||
| `HOLD_BACK_TARGET_MULTIPLIER` | `media/hls/reload-policy.ts` | `3` | HLS default HOLD-BACK as a multiple of target duration. `liveLatencyFor(track)` applies it; the engine injects it as `seekToLiveEdge`'s format-neutral `resolveLiveLatency` seam. Initial-seek + guard reposition target = live edge − live latency, clamped to window start |
|
||||
| `REPOSITION_TOLERANCE` | `behaviors/dom/seek-to-live-edge.ts` | `0.1` | Guard's boundary tolerance (s) — avoids jitter seeks at the window edges |
|
||||
| `FALLBACK_TARGET_DURATION` | `media/hls/reload-policy.ts` | `6` | Reload cadence (s) when no `#EXT-X-TARGETDURATION` |
|
||||
| `FALLBACK_TARGET_DURATION` | `media/hls/reload-policy.ts` | `6` | Reload cadence (s) — and HOLD-BACK basis — when no `#EXT-X-TARGETDURATION` |
|
||||
| `resolveLiveLatency` | `seekToLiveEdge` config (engine-injected) | `liveLatencyFor` (HLS) | Format-neutral seam for the seconds the playhead trails the live edge. HLS injects `HOLD-BACK` (3× target duration); a DASH engine would inject `suggestedPresentationDelay`. Keeps `seek-to-live-edge` free of delivery-format specifics |
|
||||
| `DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration` | `media/buffer/forward-buffer.ts` | `30` | Seconds ahead of playhead to load |
|
||||
| `DEFAULT_BACK_BUFFER_CONFIG.keepSegments` | `media/buffer/back-buffer.ts` | `2` | Segments kept behind playhead |
|
||||
| `repositionPolicy` | `seek-to-live-edge` config (behavior-scoped) | `'window-exit'` | Reposition condition seam. `'window-exit'` implemented; `'on-resume'` (edge-only) is inert pending the `[live-edge-only-mode]` use-case, which adds the public engine-config surface |
|
||||
@@ -188,9 +189,13 @@ unconditionally (`anchorLiveTracks`, `calculatePresentationDuration`,
|
||||
PDT carry-forward; sequence-origin bootstrap.
|
||||
- `behaviors/tests/resolve-track.test.ts` — live reload re-resolves; stops on
|
||||
finite duration; source-change abort.
|
||||
- `behaviors/dom/tests/seek-to-live-edge.test.ts` — declares window; seeks to
|
||||
HOLD-BACK; no-op for finite/absent tracks. Plus the live-window guard matrix
|
||||
(see below).
|
||||
- `behaviors/dom/tests/seek-to-live-edge.test.ts` — seeks to `liveEdgeStart`
|
||||
(latency injected via `resolveLiveLatency`); no-op for finite/absent tracks.
|
||||
Plus the live-window guard matrix (see below).
|
||||
- `playback/primitives/tests/live-window.test.ts` — `liveWindowFromState` /
|
||||
`liveTrackId` track-pick; `getLiveEdge` latency placement + window-start clamp.
|
||||
- `media/hls/tests/reload-policy.test.ts` — also `liveLatencyFor` (3× target
|
||||
duration; fallback).
|
||||
- `behaviors/tests/calculate-presentation-duration.test.ts` — `Infinity` for
|
||||
live resolver.
|
||||
- `engines/hls/tests/engine.test.ts` — end-to-end live setup / reload / window
|
||||
|
||||
Reference in New Issue
Block a user