--- status: draft date: 2026-05-20 definition: technical --- # DVR / event-stream support DVR (digital video recorder) and event-stream HLS support: engine handles HLS playlists with **growing-window** semantics (segments never roll off the start) instead of sliding-window (live). Adds back-seek capability through history, event-stream recognition via `#EXT-X-PLAYLIST-TYPE:EVENT`, and the transition from event-stream to VOD when termination commits. The third cluster A doc — sibling to [live-stream-support](./live-stream-support.md) (foundation) and [ll-hls-support](./ll-hls-support.md) (low-latency extension). A **Media-src feature** in the framing from [clusters.md § Feature classification axes](./clusters.md#feature-classification-axes): without it, DVR / event sources either don't play (engine treats the growing playlist as a malformed live stream) or back-seek doesn't work (buffer-management evicts history before the user reaches it). DVR and event-stream are documented together because they share the core mechanism — a growing playlist with no roll-off — and structural implementation. The HLS-spec-defined `PLAYLIST-TYPE:EVENT` is one producer-side construct; DVR is the capability that exposes back-seek through whatever history the server retains. Both consume the same client-side engine surface. ## Status - **Composition:** not implemented. Hard prerequisite [live-stream-support](./live-stream-support.md) is also not implemented. Today's `parseMediaPlaylist` recognizes `#EXT-X-PLAYLIST-TYPE:` as a known-tag pass-through but doesn't surface the value to the track output. - **Definition depth:** technical — scope and SPF touchpoints articulated against live-stream-support's structure; implementation specifics open. Source material: [SPF Epics Working Doc — epic #3 DVR / Event Stream Support](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4) (cluster A, Media-src, eng M, validation M; "Growing playlist; extension of #2 (live-stream-support)."). - **Hard prerequisite:** [live-stream-support](./live-stream-support.md). The reload loop, target-duration pacing, `Infinity` duration semantics, and `setLiveSeekableRange` writer-behavior shape all come from that feature; DVR/event-stream supplies the windowing variance. The "DVR / event boundary" open question in live-stream-support.md's Open questions section is **resolved by this doc's existence**: DVR is its own feature, not a phase of live-stream-support. ## Phases of complexity Content phases (capability slices). Each phase is a distinct capability slice of "DVR/event-stream supported." | Phase | What | Notes | |---|---|---| | Growing-window playlist semantics | Playlist grows from the recording / event start; no roll-off. Engine retains full history. `setLiveSeekableRange` start value derives from the playlist's first segment media time (or `0`) rather than the sliding-back-N derivation live uses. The reload-loop and target-duration-pacing machinery is unchanged from live | The core windowing difference. Variant-specific producer behavior (computes `seekableStart` from the playlist's first retained segment) feeds the variant-agnostic `setLiveSeekableRange` writer that live also uses | | Event-stream recognition (`PLAYLIST-TYPE:EVENT`) | Parser surfaces `#EXT-X-PLAYLIST-TYPE:EVENT` from the media playlist (today the tag is recognized but not extracted; track output gains `playlistType: 'EVENT' \| 'VOD' \| undefined`). Engine uses the value to distinguish event-stream from live-with-large-window — semantically distinct even if structurally similar | Parser-side change; the value isn't yet surfaced. `PLAYLIST-TYPE:VOD` is the orthogonal case (already-finished recording) that the parser would surface uniformly | | Back-seek through history | User can seek arbitrarily far back into the recorded portion. Buffer-management's seek phase already handles non-contiguous gap-fill (per [buffer-management.md](./buffer-management.md)'s "Seek handling" phase); DVR exercises this aggressively with large seek distances. No new behavior; existing seek code path under different load | The "DVR works" experience surface. Stress-tests the existing seek-from-arbitrary-position planning | | DVR-aware back-buffer policy | Default `backBuffer.keepSegments: 2` from buffer-management is too aggressive for DVR (history is evicted before user can seek back to it). Either configurable (large value for DVR variants) or a variant-specific policy ("retain N seconds back" or "retain indefinitely") | Variant-specific behavior. Affects buffer-management.md's back-buffer eviction phase. Open question: shape of the policy (configurable threshold vs subtractive composition) | | Event-stream termination → VOD transition | When `#EXT-X-ENDLIST` appears on an event-stream, live-stream-support's terminated-state-transition phase handles the mechanics (reload loop stops, last segment stabilizes, `mediaSource.duration` flips from `Infinity` to the finite value, `clearLiveSeekableRange()` paired with the transition). This phase notes the additional semantic that the stream now behaves equivalently to VOD: seekable to start, finite duration, normal `endOfStream` gating | Composes with live-stream-support's termination phases. No new termination behavior; this phase is the semantic note about what post-termination means for event-stream specifically | ## What's in scope vs out of scope **In scope:** - All five phases above for HLS event-stream and DVR-style growing- playlist sources - Parser surface for `EXT-X-PLAYLIST-TYPE:EVENT` (and `:VOD` for uniformity) - Variant-specific producer behavior for the seekable-range start signal - Variant-specific back-buffer policy - Composition with [live-stream-support](./live-stream-support.md)'s reload loop and termination phases **Out of scope (separate Media-src candidate features):** - **[ll-hls-support](./ll-hls-support.md)** — orthogonal sibling extension. DVR can compose with LL-HLS for low-latency live with back-seek; both extensions compose on the same reload-loop foundation but address different concerns (LL-HLS = latency reduction at the live edge; DVR = back-seek through history). The composition shape is "DVR + LL-HLS engine variant" with both feature's behaviors composed in. **Out of scope (different architectural layer):** - Adapter-layer customer-facing affordances ("seek back 30 seconds" buttons, scrub bar with history range, DVR window UI). Consume this feature's seekable-range signal; not SPF concerns themselves. - User-facing DVR window configuration (e.g., "only show last 24 hours"). Adapter-level policy; SPF respects what the server provides in the playlist. - Server-side DVR retention policy. The engine handles whatever segments the server lists; it doesn't enforce retention. ## Likely cross-cutting impact Things this feature probably forces decisions on, not just additions: - **Composition-variant placement for variant-specific behaviors.** Per the failure-mode catalog's composition-variant entry: behaviors with variant-specific value derivation (the seekable-range start producer, the back-buffer policy) compose into the DVR variant, not as runtime branches in always-on behaviors. Same shape as ll-hls-support's variant-composition analysis. The `live-stream- support` engine variant, the `ll-hls-support` extension variant, and the `dvr-event-stream-support` variant are three distinct compositions of the reload-loop foundation; each adds / replaces variant-specific behaviors. - **Seekable-range producer split.** live-stream-support documents `setLiveSeekableRange` as a new behavior reading derived live-edge data; for DVR, the *value derivation* differs (start = 0 / first- segment-time vs sliding-back-N). The cleanest split: a variant- specific *producer* behavior computes the `(seekableStart, seekableEnd)` signal; a variant-agnostic *writer* behavior consumes the signal and calls `mediaSource.setLiveSeekableRange()`. The writer doesn't branch; the producer is variant-specific. Same pattern as `updateMediaSourceDuration`'s uniform-across-variants shape, mirrored — uniform writer, variant-specific upstream. - **Back-buffer policy shape under DVR.** Today's `backBuffer. keepSegments: 2` is a single config knob. DVR may want one of: (a) a large configurable value, (b) a "retain N seconds back" alternative axis, (c) "retain indefinitely" (no eviction within the DVR window). The choice affects whether the back-buffer policy becomes variant-specific behavior (different policy composed for DVR variant) or stays uniform with a richer config surface. - **DVR-vs-live-with-large-window distinction.** A DVR stream *without* `PLAYLIST-TYPE:EVENT` is structurally indistinguishable from "live with a very large sliding window" until the engine observes that segments don't roll off. The engine has options: (a) configure DVR vs live at the adapter layer (consumer opts in via config), (b) detect from `PLAYLIST-TYPE:EVENT` when present, (c) observe the playlist's behavior over multiple reloads and infer (engine flips to "DVR mode" once it sees segments stay). (a) is the simplest; (c) is the most adaptive but adds detection complexity. - **`endOfStream` gate under DVR.** live-stream-support documents that `endOfStream` doesn't naturally fire for live (playlist keeps growing). For DVR-during-recording, same — `endOfStream` waits for termination. For DVR-post-termination, the gate becomes reachable the same way live-post-termination does (last segment stabilizes). No new gate semantics needed; the existing isLastSegmentAppended + currentTime gate logic from mse-mms-pipeline.md applies uniformly. - **Server-side retention edge case.** A server may stop serving older segments while still listing them in the playlist (e.g., "we keep 24 hours of history; older segments 404 on fetch"). The engine references segments via URI; if a back-seek targets a segment URI that 404s, the fetch fails. Whether this falls under this feature's scope (recover gracefully — surface a "history expired" error to the consumer) or under `[unsupported-case-error-mapping]` (generic fetch-failure mapping) is an open boundary. - **DVR + LL-HLS composition.** Both are cluster A extensions on the same reload loop. Composition order: LL-HLS replaces the standard reload-loop behavior with its variant; DVR replaces the back-buffer policy and seekable-range producer. The two extensions are orthogonal — neither's behaviors conflict with the other's. A "DVR + LL-HLS" engine variant composes both extension sets atop the live-stream-support foundation. ## Open questions - **Back-buffer policy shape.** Configurable `keepSegments` with large default for DVR vs "retain N seconds back" alternative axis vs "retain indefinitely" mode. Affects whether back-buffer becomes variant-specific behavior or stays uniform with richer config. - **DVR-vs-live distinction signal source.** Adapter-config opt-in vs `PLAYLIST-TYPE:EVENT` detection vs observe-over-reloads inference. (a) is simplest; (b) only covers spec-flagged event streams; (c) is the most adaptive but adds complexity. Likely combination: (b) when the tag is present; otherwise (a) until (c) matures. - **Seekable-range start derivation under server retention.** When the server keeps only the last N hours of segments and the playlist's first segment slides forward over time, the `seekableStart` value should track that. Same writer (variant- agnostic `setLiveSeekableRange` writer), but the variant-specific producer needs to read the playlist's current first-segment, not a constant `0`. - **Server-side retention error handling.** Segment 404 on back-seek fetch. Scope of this feature vs `[unsupported-case-error-mapping]`. - **`PLAYLIST-TYPE:VOD` semantics.** When a media playlist arrives with `PLAYLIST-TYPE:VOD` from the start (fully-resolved at first fetch, no reload needed), the engine should recognize this and skip reload-loop composition. Adjacent concern; may belong here (parser surfacing the value uniformly) or in live-stream-support (engine-side reload-loop activation gate). - **Event-stream-during-pre-event.** An event-stream playlist may exist with metadata but few or no segments before the event starts. Reload-loop pacing handles this naturally (target-duration reload until segments appear), but pre-roll segment-zero semantics are worth verifying. - **DVR + LL-HLS composition specifics.** Order of behavior composition, shared state between DVR and LL-HLS extensions. Verify when both extensions get implementation work. - **`liveSeekableRange` clearance on DVR termination.** For live → termination, live-stream-support's terminated-state-transition phase pairs `clearLiveSeekableRange()` with the transition (so the browser's `seekable` reverts to buffer-derived semantics). For event-stream → VOD transition, the seekable range stays a finite range from playlist start to last segment — but does it still go through `setLiveSeekableRange` (now with finite end) or through `clearLiveSeekableRange()` + `mediaSource.duration` setter? Spec allows either; spec-conformant browsers should behave equivalently. Verify when implementation arrives. ## Related features - **[live-stream-support](./live-stream-support.md)** *(hard prerequisite)* — provides the reload loop, target-duration pacing, `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. - **[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 (latency reduction vs back-seek). A DVR + LL-HLS engine variant is the cross-extension composition. - **[buffer-management](./buffer-management.md)** — back-buffer policy needs DVR-aware shape. Seek-from-arbitrary-position gap-fill is exercised aggressively under DVR. Both phases of buffer-management interact with this feature; the open question on back-buffer policy shape (variant-specific behavior vs configurable threshold) is shared. - **[mse-mms-pipeline](./mse-mms-pipeline.md)** — `Infinity` duration semantics during recording; finite duration on event- stream termination. Same MSE writer semantics as live; `endOfStream` gate logic applies uniformly. - **[non-zero-pts-support](./non-zero-pts-support.md)** — DVR and event streams typically have PTS far from zero (recording starts at wall-clock time, not at PTS 0). `currentTime` / `seekable` semantics require non-zero-PTS support to be correct. - **`[unsupported-case-error-mapping]`** *(candidate)* — server- side retention 404 errors on back-seek fall under this candidate's scope for consumer-facing mapping. ## See also - [live-stream-support.md](./live-stream-support.md) — cluster A foundation this feature extends; the doc's "DVR / event boundary" open question is resolved by this doc's existence (standalone, not absorbed) - [ll-hls-support.md](./ll-hls-support.md) — sibling cluster A extension; orthogonal composition - [clusters.md § Manifest reload loop](./clusters.md#manifest-reload-loop) — cluster A description - [clusters.md § Feature classification axes](./clusters.md#feature-classification-axes) — Media-src feature framing - [conventions/behaviors.md](../conventions/behaviors.md) → *Inverse: behaviors that operate uniformly across tracks* — the precedent for variant-agnostic writer + variant-specific producer split this feature applies to `setLiveSeekableRange` - [SPF Epics Working Doc](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4) — source material; epic #3 (DVR / Event Stream Support) - [Mux Video Permutations Matrix](https://www.notion.so/32c97a7f89d08191b84dd30f06685490) — Stream Type section; SPF column shows ⚠️ for DVR (manifest re- polling unverified) - [HLS Spec — `EXT-X-PLAYLIST-TYPE`](https://datatracker.ietf.org/doc/html/rfc8216bis) (§4.4.3.5)