---
status: draft
date: 2026-08-03
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. The hard prerequisite
  [live-stream-support](./live-stream-support.md) has since landed, and
  `parseMediaPlaylist` now surfaces `#EXT-X-PLAYLIST-TYPE` as
  `playlistType` in the media-playlist metadata — the adapter already
  maps `EVENT` to `targetLiveWindow: Infinity` (the media-ui-extensions
  DVR signal). The windowing semantics themselves are not implemented,
  and driving the engine at a real `EVENT` source fails (below).
- **Failures on a real `EVENT` source** (a Mux recording-during-live
  asset, `#EXT-X-PLAYLIST-TYPE:EVENT`, growing window, CMAF) — the
  concrete starting point for implementation, replacing the earlier
  "manifest re-polling unverified" guess. Observed against the
  **pre-re-landing** live implementation, so each item is tagged with
  what has since been re-confirmed:
  - The presentation **anchor never establishes**, leaving the model ~2s
    behind native. *Observed previously; not re-tested against current
    code.* Root cause undiagnosed — the same code path anchors correctly
    on the live playback id, so the divergence is specific to the EVENT /
    asset source shape. **This is the first task for DVR**, ahead of any
    windowing work.
  - Consequently `setLiveSeekableRange(negative, …)` **throws on every
    reload**. *Mechanism re-confirmed on current code* by
    `playback/primitives/tests/live-window.test.ts` → "yields a negative
    start when every selected window is negative": `liveWindowFromState`
    does not floor the start at 0 when the *reference* window is
    negative, and `syncLiveSeekableRange` writes it with no try/catch. So
    any mis-anchored source still throws, once per window update. See
    [live-stream-support § Still open](./live-stream-support.md#still-open).
  - **DVR back-seek stalls** in the resulting gap. *Observed previously.*
    Note `seekable` does not go empty when the range call throws — under
    `duration === Infinity` the UA falls back to the buffered-derived
    union — so the failure is a mismatched/narrow window, not an absent
    one.
  - The source **starts at the live edge**, because `seekToLiveEdge`
    commands a start position for any derivable live window regardless
    of `playlistType`. Start-in-place is an unimplemented seam. *Still
    true by construction on current code.*
- **Decision (2026-08-03): DVR starts at the live edge.** Treating DVR
  like sliding-window live for the initial position is the intended
  default — it matches viewer expectation for a stream still being
  produced, and it keeps `seekToLiveEdge` free of a live-vs-DVR branch.
  Start-in-place stays an unimplemented seam rather than a half-built
  conditional. Note this is currently inert on the EVENT source above:
  with the anchor failing, no window is derivable, so nothing is
  commanded and it starts at 0 regardless.
- **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)
  — **satisfied.** The reload loop, target-duration pacing, `Infinity`
  duration semantics, and the `syncLiveSeekableRange` writer behavior all
  landed there; 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` from the media playlist as `playlistType` in the media-playlist metadata. Engine uses the value to distinguish event-stream from live-with-large-window — semantically distinct even if structurally similar | **Implemented** (parser side) — landed with live-stream-support; the adapter reads it to map `EVENT` → `targetLiveWindow: Infinity`. No engine behavior branches on it yet |
| 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 retains almost no history. This does **not** make back-seek fail — an `EVENT` playlist never drops segments, so evicted history is still listed and re-fetchable, and the seek re-buffers rather than stalling. The cost is a re-buffer pause per deep scrub and repeated fetches for repeated scrubbing. Either configurable (larger value for DVR) or a variant-specific policy ("retain N seconds back") | Variant-specific behavior; a UX/bandwidth tune, not a correctness fix. Affects buffer-management.md's back-buffer eviction phase. Note the ceiling: retaining a multi-hour event outright risks `QuotaExceededError`, so "retain indefinitely" is not a safe target. 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 — no `clearLiveSeekableRange()` needed, since MSE ignores the live range once duration is finite). 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 [errors](./errors.md)
  (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 [errors](./errors.md).
- **`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.
- **[errors](./errors.md)** — server-side retention 404 errors on
  back-seek fall under that feature's scope for consumer-facing
  mapping. Its phase 5 also covers DVR-as-degraded-but-playable
  (SVTA 2039) while this feature is partially implemented.

## 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)
