mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
docs(design): add design doc for live presets (#1395)
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
---
|
||||
status: decided
|
||||
date: 2026-04-21
|
||||
---
|
||||
|
||||
# Live Presets
|
||||
|
||||
Dedicated `live-video` and `live-audio` presets for **live-only** playback, alongside the existing `video` and `audio` presets for on-demand-only playback.
|
||||
|
||||
## Decision
|
||||
|
||||
Ship live playback as **separate, live-only presets** — `live-video` and `live-audio`. Each pairs a live-capable feature set with a skin that renders live UI unconditionally. We do **not**:
|
||||
|
||||
- Add live-capable state or UI to the existing `video` / `audio` presets.
|
||||
- Branch the live skins on `streamType` to cover VOD inside the same preset.
|
||||
|
||||
Two presets, two jobs: base presets play VOD, live presets play live. A preset is a commitment to one mode, not a runtime toggle between both. If a live preset ever needs to cover VOD sources too, we can layer that in later — but we're not paying the complexity up front before we've felt the friction.
|
||||
|
||||
## Context
|
||||
|
||||
Live playback needs visibly different UI from VOD:
|
||||
|
||||
- The duration display and standard scrubbing affordances disappear or change
|
||||
- A live indicator / "jump to live edge" button takes their place
|
||||
- The time slider, if shown at all, represents a DVR window, not `[0, duration]`
|
||||
- Live-only affordances are likely to grow (latency badge, "behind live" state, chat slot, etc.)
|
||||
|
||||
`streamTypeFeature` exposes `streamType: 'on-demand' | 'live' | 'unknown'` and could drive this at runtime inside one shared skin. We're choosing not to.
|
||||
|
||||
Authors almost always know at embed time whether their product plays live or VOD — an event page, a channel page, a DVR UI. "Live-aware" and "VOD-only" are two meaningfully different products, and the vast majority of `video` / `audio` integrations are VOD-only.
|
||||
|
||||
This doc records two decisions, with separate rationales below:
|
||||
|
||||
1. **Split** — live and VOD ship as separate presets.
|
||||
2. **Live-only** — the live preset's skin renders live UI unconditionally, with no in-skin VOD fallback.
|
||||
|
||||
## Shape
|
||||
|
||||
### Feature arrays
|
||||
|
||||
Base presets stay as today. Live presets start close in capability and will diverge further as live-only needs emerge. `playbackRateFeature` is dropped — speed controls aren't meaningful for live playback, where the user is always pinned to (or chasing) the live edge.
|
||||
|
||||
```ts
|
||||
// packages/core/src/dom/store/features/presets.ts
|
||||
export const videoFeatures: VideoFeatures = [
|
||||
playbackFeature,
|
||||
playbackRateFeature,
|
||||
volumeFeature,
|
||||
timeFeature,
|
||||
sourceFeature,
|
||||
bufferFeature,
|
||||
fullscreenFeature,
|
||||
pipFeature,
|
||||
remotePlaybackFeature,
|
||||
controlsFeature,
|
||||
textTrackFeature,
|
||||
errorFeature,
|
||||
];
|
||||
|
||||
export const liveVideoFeatures: VideoFeatures = [
|
||||
playbackFeature,
|
||||
volumeFeature,
|
||||
timeFeature,
|
||||
sourceFeature,
|
||||
bufferFeature,
|
||||
fullscreenFeature,
|
||||
pipFeature,
|
||||
remotePlaybackFeature,
|
||||
controlsFeature,
|
||||
textTrackFeature,
|
||||
errorFeature,
|
||||
];
|
||||
|
||||
export const audioFeatures: AudioFeatures = [
|
||||
playbackFeature,
|
||||
playbackRateFeature,
|
||||
volumeFeature,
|
||||
timeFeature,
|
||||
sourceFeature,
|
||||
bufferFeature,
|
||||
errorFeature,
|
||||
];
|
||||
|
||||
export const liveAudioFeatures: AudioFeatures = [
|
||||
playbackFeature,
|
||||
volumeFeature,
|
||||
timeFeature,
|
||||
sourceFeature,
|
||||
bufferFeature,
|
||||
errorFeature,
|
||||
];
|
||||
```
|
||||
|
||||
`streamTypeFeature` is intentionally **not** included. The live skin assumes live, so it doesn't need runtime detection. When a live-only affordance actually needs stream-type state (e.g., surface an error if a VOD source is loaded, or drive a "behind live" badge derived from seekable + streamType), it's an additive, non-breaking change to append it to `liveVideoFeatures` / `liveAudioFeatures`.
|
||||
|
||||
No live-specific time feature either. `timeFeature` already resolves `duration` to `seekable.end(last)` when the native duration is `Infinity`, which is all the live skin needs for now. If `currentTime` or other time values need live-specific handling later, that's also additive.
|
||||
|
||||
Beyond `playbackRateFeature`, the live feature arrays start as near-copies of their VOD counterparts. That's a conservative starting point, not a commitment that live needs every remaining feature. Further bundle wins for live-only are expected to come primarily from the skin (smaller DOM, fewer subscribers — see below). Trimming additional features from the live arrays is a follow-up once the skins land and we can measure.
|
||||
|
||||
### Preset packages
|
||||
|
||||
```ts
|
||||
// packages/react/src/presets/live-video/index.ts
|
||||
export { liveVideoFeatures } from '@videojs/core/dom';
|
||||
export { LiveVideo, type LiveVideoProps } from '@/media/live-video';
|
||||
export * from './skin';
|
||||
export * from './skin.tailwind';
|
||||
export * from './minimal-skin';
|
||||
export * from './minimal-skin.tailwind';
|
||||
```
|
||||
|
||||
```ts
|
||||
// packages/html/src/presets/live-video.ts
|
||||
export { liveVideoFeatures } from '@videojs/core/dom';
|
||||
export { LiveVideoSkinElement } from '../define/live-video/skin';
|
||||
export { LiveVideoSkinTailwindElement } from '../define/live-video/skin.tailwind';
|
||||
export { MinimalLiveVideoSkinElement } from '../define/live-video/minimal-skin';
|
||||
export { MinimalLiveVideoSkinTailwindElement } from '../define/live-video/minimal-skin.tailwind';
|
||||
```
|
||||
|
||||
`live-audio` mirrors this.
|
||||
|
||||
### Live skin is live-only
|
||||
|
||||
The live skin renders live controls unconditionally — a live indicator, a "jump to live edge" button, and (where applicable) a DVR-window slider derived from `seekable`. It does not read `streamType`, and it does not attempt to recover to a VOD layout when the source turns out to be on-demand:
|
||||
|
||||
```tsx
|
||||
// packages/react/src/presets/live-video/skin.tsx
|
||||
<div className="media-time-controls">
|
||||
<LiveEdgeButton />
|
||||
<LiveIndicator />
|
||||
</div>
|
||||
```
|
||||
|
||||
Pointing a `live-video` preset at a VOD source produces the live skin on top of a VOD stream — working playback, but with live affordances that don't match the content. That's an intentional degradation, not a supported configuration — the mirror of pointing `video` at a live source.
|
||||
|
||||
### Base skin is VOD-only
|
||||
|
||||
`VideoSkin` / `AudioSkin` never read `streamType` (and can't — the slice isn't in the store). They unconditionally render the on-demand time region. Pointing a `video` preset at a live source produces working playback with a duration of `Infinity` (or `seekable.end(last)` once `timeFeature` resolves it) and the standard scrubber on the seekable window, with no live-edge affordance. That's an intentional degradation, not a supported configuration — authors who expect live sources use a live preset.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Live skin branches on `streamType` to cover VOD too.** Ship `liveVideoFeatures = [...videoFeatures, streamTypeFeature]` and render live vs. on-demand time controls inside the live skin based on detection, with a neutral placeholder while `streamType === 'unknown'`. Supports runtime source switches between live and VOD within one preset.
|
||||
- **Branch the existing `video` / `audio` skins on `streamType`.** One preset per medium, one skin that covers both modes by reading `streamType`. Pattern already used for `volumeAvailability === 'unsupported'` (see `VolumePopover` in `packages/react/src/presets/video/skin.tsx:60`).
|
||||
- **Single preset, runtime-selected skin.** Keep one `video` / `audio` preset but have it mount a different skin tree based on `streamType`. Hides the split from the author but retains the unknown-state and dead-code problems.
|
||||
|
||||
## Rationale: Split live from VOD
|
||||
|
||||
The dominant case is VOD — the vast majority of `video` / `audio` integrations never play live. Every byte the VOD preset carries for a capability it doesn't use is a byte the dominant case pays for something it doesn't get.
|
||||
|
||||
A shared `video` preset covering both modes forces every VOD player to ship:
|
||||
|
||||
- The `streamType` state slice and the events, subscribers, and predicate helpers that maintain it.
|
||||
- The live UI tree — live indicator, jump-to-live-edge button, DVR slider — rendered conditionally but present in the bundle.
|
||||
- The branching logic itself (detection, unknown-state handling, transitions).
|
||||
|
||||
A separate `live-video` preset keeps that cost with the apps that actually use live. The VOD store has no `streamType` slice; `VideoSkin` has no live branches; nothing in the default import path references live code. The bundle-size report already separates `/video/skin` from a future `/live-video/skin` entry — the split is visible and measurable.
|
||||
|
||||
Secondary wins:
|
||||
|
||||
- **Explicit opt-in matches author intent.** Picking `live-video` is a one-word signal that live is the supported path — clearer than "use `video` and set some prop" and more discoverable than "`video` happens to work if the source is live."
|
||||
- **Smaller type surface in the common store.** `VideoPlayerStore` / `AudioPlayerStore` keep their current state shape. Adding `streamType` to the base ripples into every consumer of those types.
|
||||
- **Targetable visual regression tests.** Each skin snapshots independently; the VOD snapshot stays stable without a `streamType` harness.
|
||||
|
||||
## Rationale: Live preset renders live UI only
|
||||
|
||||
Given we're shipping a dedicated live preset, it could still support VOD internally — branch the live skin on `streamType` and fall back to VOD controls when the source isn't live. We're not doing that either.
|
||||
|
||||
1. **Smaller live bundle.** Live-only (especially non-DVR) unlocks a meaningfully smaller UI: no time slider, no thumbnail previews, no seek buttons, no remaining-time display. A dual-mode live skin has to ship all of those for the VOD branch, plus the branching. Live-only gets to be genuinely smaller, not just differently shaped.
|
||||
|
||||
2. **No unknown-state flash.** A dual-mode live skin starts every load in `streamType === 'unknown'` and resolves to `live` (or `on-demand`) after manifest detection. Either we render a neutral placeholder during that window — a third UI state to design and test — or we pick a default and flip visibly once detection completes. A live-only skin has one shape from first paint.
|
||||
|
||||
3. **Fork-template clarity.** Skins are reference implementations authors copy and mutate. A live-only skin gives live-app authors a starting point with only live concepts in the tree — no conditional helpers, no `unknown` placeholder, no dead VOD branches to delete. VOD authors forking `VideoSkin` get the same treatment from the other side.
|
||||
|
||||
4. **Headroom for divergence.** Live UI tends to accumulate bespoke affordances — latency indicator, "behind live" badge, DVR-aware scrubber interactions, live chat/reactions slot. Each lands naturally inside `LiveVideoSkin` without leaking concepts into the dual-mode branching.
|
||||
|
||||
5. **Easy to add, hard to remove.** If we later find authors commonly need one preset that handles live → VOD replay or cross-mode source swaps, we can add `streamTypeFeature` and branch the skin — additive. Shipping a dual-mode skin and later deciding the unknown-state handling and dead code aren't worth it is a breaking change for anyone on the VOD path.
|
||||
|
||||
### What we give up
|
||||
|
||||
- **Runtime mode switching.** Neither preset adapts if its source's stream type changes. A live preset pointed at a VOD replay shows live UI over a VOD stream; a VOD preset pointed at a live source shows VOD UI over a live stream. Authors needing both modes in one player either wait for a future iteration or compose their own.
|
||||
- **A story for live replay / DVR archive flows.** Some products want a single embed that starts live and keeps playing after the stream ends, or that plays back a recorded segment inside the same UI as the live stream. Deferred until a concrete product need surfaces.
|
||||
|
||||
### Trade-offs
|
||||
|
||||
| Gain | Cost |
|
||||
| -------------------------------------------------- | ------------------------------------------------------ |
|
||||
| VOD-only apps ship no stream-type state or live UI | Source-type switches at runtime aren't covered |
|
||||
| Live-only apps ship a smaller live-only UI | Live replay / post-stream VOD flows aren't served yet |
|
||||
| First paint has a single, correct UI shape | Two more React skins + four HTML skin elements to ship |
|
||||
| Easy to layer dual-mode support on later if needed | Shared skin idioms must be kept in sync across trees |
|
||||
| Room to add live-only affordances without churn | Authors must know their mode at preset-selection time |
|
||||
|
||||
## Consequences
|
||||
|
||||
- In `@videojs/core/dom`:
|
||||
- Add `liveVideoFeatures` and `liveAudioFeatures` exports in `packages/core/src/dom/store/features/presets.ts`. Initially they match `videoFeatures` / `audioFeatures` minus `playbackRateFeature`, and without `streamTypeFeature`.
|
||||
- `streamTypeFeature` stays a standalone export for custom compositions; it's not wired into any preset feature array.
|
||||
- No change to `VideoFeatures` / `AudioFeatures` or the derived store types.
|
||||
- In `@videojs/react`:
|
||||
- Add `packages/react/src/presets/live-video/` with `skin.tsx`, `skin.tailwind.tsx`, `minimal-skin.tsx`, `minimal-skin.tailwind.tsx`.
|
||||
- Add `packages/react/src/presets/live-audio/` mirroring the above.
|
||||
- Live skins render live controls unconditionally; they do not import `selectStreamType` and do not branch on stream type.
|
||||
- In `@videojs/html`:
|
||||
- Add `packages/html/src/presets/live-video.ts` and `packages/html/src/presets/live-audio.ts`, plus the corresponding `define/live-video/*` and `define/live-audio/*` custom element wrappers.
|
||||
- Site API reference pages for the new presets; update the presets overview to list live variants and clarify that the base presets are VOD-only and the live presets are live-only.
|
||||
- `selectStreamType` remains a core export; its existing contract (return `undefined` when the feature isn't configured) covers both presets naturally — neither preset configures `streamTypeFeature` today, so callers get `undefined`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **When to add `streamTypeFeature` to the live feature arrays.** What signal tells us we're paying for live-only's minimalism — authors writing the same "is this VOD?" guard themselves, Mux-specific mis-configurations we want to surface as an error, or something else? Worth revisiting after the first few live integrations.
|
||||
2. **Further trimming the live feature array.** `playbackRateFeature` is dropped on day one. The clearest next candidate is splitting `textTrackFeature` — it bundles captions (needed for live) with chapters and thumbnail cues (VOD-only). A `captionsFeature` / `chaptersFeature` / `thumbnailsFeature` split lets live take captions only and drop the cue iteration and `<track>` DOM-walking that exists to populate the others. Decide once the first live skins are in-tree and we can measure actual subscribers.
|
||||
3. **Live replay / post-stream transitions.** Some live flows end with a VOD archive of the stream. Whether that's served by a future dual-mode live preset, by swapping presets at transition time, or by a third preset entirely is explicitly deferred.
|
||||
|
||||
## Prior Art
|
||||
|
||||
- **Media Chrome** — No distinct live layout preset; authors compose `<media-live-button>` alongside or instead of `<media-time-display>` / `<media-time-range>` in their own control bar. Effectively forces every integrator to solve this composition problem themselves.
|
||||
- **Vidstack** — Ships a `LiveIndicator` / `LiveButton` primitive and expects the author-provided layout to swap them in for the time display. Similar to Media Chrome — no separate live layout preset.
|
||||
- **Video.js v8** — Adds a `vjs-live` body class and hides `.vjs-remaining-time` / `.vjs-duration` via CSS when the stream is live; shows `<LiveDisplay>` instead. Single skin, CSS-driven branching — one preset serving both modes.
|
||||
- **Plyr** — Hides the progress/time controls and shows a "Live" badge when duration is not finite. Single skin, JS-driven branching.
|
||||
|
||||
Our choice is the least common in the ecosystem but aligns with the fork-template philosophy of this project: presets are full reference implementations authors copy and mutate, and the split reflects a real product distinction (VOD-only vs. live-only) rather than a runtime-only toggle. We're also biased toward starting narrower than the alternatives — a live skin that covers live only is an easy thing to extend if demand proves us wrong, and a hard thing to retract once shipped.
|
||||
Reference in New Issue
Block a user