Skip to content

docs(design): add design doc for live presets#1395

Merged
luwes merged 3 commits intomainfrom
design/live-presets
May 1, 2026
Merged

docs(design): add design doc for live presets#1395
luwes merged 3 commits intomainfrom
design/live-presets

Conversation

@luwes
Copy link
Copy Markdown
Collaborator

@luwes luwes commented Apr 21, 2026

Summary

Records the decision to ship live playback as dedicated, live-only live-video / live-audio presets rather than branching the existing video / audio skins on streamType. Live skins render live UI unconditionally — no in-skin VOD fallback, no unknown-state placeholder. VOD-only apps keep their current store shape and bundle; live-only apps get a live-dedicated fork template with no dead branches.

Changes

  • New design doc internal/design/ui/live-presets.md covering the decision, alternatives, rationale, trade-offs, consequences, open questions, and prior art.

Key points captured in the doc

  • 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.
  • Live skin is live-only. Renders a live indicator, jump-to-live-edge button, and (where applicable) a DVR-window slider unconditionally. Does not read streamType and does not attempt to recover to a VOD layout.
  • Feature arrays start minimal. liveVideoFeatures / liveAudioFeatures initially alias videoFeatures / audioFeaturesstreamTypeFeature is intentionally not included yet. It's an additive, non-breaking change to append it once a live-only affordance actually needs stream-type state.
  • Base skins stay VOD-only. No change to VideoSkin / AudioSkin or to the derived store types.
  • Primary rationale: VOD players don't pay for live; explicit author opt-in for live capability; no unknown-state flash on first paint; clean fork templates in both directions; easy to layer dual-mode support on later, hard to retract once shipped.
  • Explicitly deferred: runtime mode switching within a single preset, and live-replay / post-stream-VOD flows — revisit when a concrete product need surfaces.

Testing

Docs-only; no code changes.

Made with Cursor


Note

Low Risk
Docs-only change that adds a design decision record; no runtime, API, or behavior changes.

Overview
Adds a new design doc internal/design/ui/live-presets.md that records the decision to ship separate, live-only live-video/live-audio presets (rather than branching existing VOD skins on streamType), outlines the intended feature/skin shape at a high level, and documents trade-offs, alternatives, and open questions for future implementation.

Reviewed by Cursor Bugbot for commit 463a462. Bugbot is set up for automated code reviews on this repo. Configure here.

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 21, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 463a462
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69e90a93a5da980008fef4c6
😎 Deploy Preview https://deploy-preview-1395--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Apr 22, 2026 5:51pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 28.70 kB
/video (default + hls) 161.34 kB
/video (minimal) 26.22 kB
/video (minimal + hls) 158.79 kB
/audio (default) 26.57 kB
/audio (minimal) 24.17 kB
/background 4.15 kB
Media (8)
Entry Size
/media/background-video 1.04 kB
/media/container 1.73 kB
/media/dash-video 236.54 kB
/media/hls-video 133.86 kB
/media/mux-audio 160.01 kB
/media/mux-video 159.87 kB
/media/native-hls-video 3.77 kB
/media/simple-hls-video 15.80 kB
Players (3)
Entry Size
/video/player 7.21 kB
/audio/player 5.27 kB
/background/player 3.86 kB
Skins (17)
Entry Type Size
/video/minimal-skin.css css 3.50 kB
/video/skin.css css 3.53 kB
/video/minimal-skin js 26.20 kB
/video/minimal-skin.tailwind js 26.42 kB
/video/skin js 28.70 kB
/video/skin.tailwind js 28.78 kB
/audio/minimal-skin.css css 2.54 kB
/audio/skin.css css 2.50 kB
/audio/minimal-skin js 24.17 kB
/audio/minimal-skin.tailwind js 24.35 kB
/audio/skin js 26.58 kB
/audio/skin.tailwind js 26.72 kB
/background/skin.css css 117 B
/background/skin js 1.14 kB
/base.css css 157 B
/shared.css css 88 B
/skin-element js 1.35 kB
UI Components (25)
Entry Size
/ui/alert-dialog 1022 B
/ui/alert-dialog-close 509 B
/ui/alert-dialog-description 380 B
/ui/alert-dialog-title 385 B
/ui/buffering-indicator 2.46 kB
/ui/captions-button 2.63 kB
/ui/cast-button 2.66 kB
/ui/compounds 4.14 kB
/ui/controls 2.31 kB
/ui/error-dialog 3.02 kB
/ui/fullscreen-button 2.62 kB
/ui/hotkey 1.90 kB
/ui/mute-button 2.69 kB
/ui/pip-button 2.64 kB
/ui/play-button 2.66 kB
/ui/playback-rate-button 2.68 kB
/ui/popover 1.83 kB
/ui/poster 2.26 kB
/ui/seek-button 2.65 kB
/ui/slider 1.52 kB
/ui/thumbnail 2.92 kB
/ui/time 2.52 kB
/ui/time-slider 3.92 kB
/ui/tooltip 2.02 kB
/ui/volume-slider 2.66 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 23.53 kB
/video (default + hls) 154.79 kB
/video (minimal) 21.11 kB
/video (minimal + hls) 152.52 kB
/audio (default) 19.07 kB
/audio (minimal) 17.58 kB
/background 755 B
Media (7)
Entry Size
/media/background-video 575 B
/media/dash-video 235.04 kB
/media/hls-video 132.64 kB
/media/mux-audio 158.59 kB
/media/mux-video 158.53 kB
/media/native-hls-video 2.26 kB
/media/simple-hls-video 14.36 kB
Skins (14)
Entry Type Size
/video/minimal-skin.css css 3.44 kB
/video/skin.css css 3.46 kB
/video/minimal-skin js 21.04 kB
/video/minimal-skin.tailwind js 24.53 kB
/video/skin js 23.40 kB
/video/skin.tailwind js 24.66 kB
/audio/minimal-skin.css css 2.44 kB
/audio/skin.css css 2.39 kB
/audio/minimal-skin js 17.52 kB
/audio/minimal-skin.tailwind js 20.04 kB
/audio/skin js 19.01 kB
/audio/skin.tailwind js 19.97 kB
/background/skin.css css 90 B
/background/skin js 272 B
UI Components (20)
Entry Size
/ui/alert-dialog 1.14 kB
/ui/buffering-indicator 1.83 kB
/ui/captions-button 2.07 kB
/ui/cast-button 2.07 kB
/ui/controls 1.81 kB
/ui/error-dialog 2.28 kB
/ui/fullscreen-button 2.09 kB
/ui/mute-button 2.11 kB
/ui/pip-button 2.01 kB
/ui/play-button 2.00 kB
/ui/playback-rate-button 1.94 kB
/ui/popover 1.86 kB
/ui/poster 1.69 kB
/ui/seek-button 2.14 kB
/ui/slider 2.67 kB
/ui/thumbnail 2.12 kB
/ui/time 2.11 kB
/ui/time-slider 2.43 kB
/ui/tooltip 2.18 kB
/ui/volume-slider 3.19 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (9)
Entry Size
. 4.96 kB
/dom 11.84 kB
/dom/media/custom-media-element 1.90 kB
/dom/media/dash 234.13 kB
/dom/media/google-cast 4.07 kB
/dom/media/hls 131.89 kB
/dom/media/mux 158.01 kB
/dom/media/native-hls 1.61 kB
/dom/media/simple-hls 13.73 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 999 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.39 kB
/html 695 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.92 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (3)
Entry Size
. 40 B
/dom 13.33 kB
/playback-engine 13.24 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

Narrow the live preset design to live-only skins that render live UI
unconditionally — no streamType branching, no in-skin VOD fallback,
no unknown-state flash on first paint. Drop streamTypeFeature from
liveVideoFeatures / liveAudioFeatures for now; add it back additively
when a live-only affordance actually needs it.

Made-with: Cursor
playbackFeature,
playbackRateFeature,
volumeFeature,
timeFeature,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Do we not need live-specific time handling capability?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I tried to keep it simpler downstream for now and updated the duration to seekable-end for live.
We can do the same for current time if needed. In media-chrome we had this separately and handled this in the UI itself but I'm don't see the need for it yet. Let me know if you can think of edge cases.

Comment thread internal/design/ui/live-presets.md Outdated
Comment thread internal/design/ui/live-presets.md Outdated
Comment thread internal/design/ui/live-presets.md Outdated

Live-only presets win on four axes:

1. **VOD players don't pay for live.** The dominant case is on-demand. Keeping `streamTypeFeature` and the live UI tree out of `videoFeatures` / `audioFeatures` means the VOD store has no `streamType` slice, the VOD skin has no live-branch code, and bundles for VOD-only apps omit both. A shared-skin approach forces every player to carry the stream-type event plumbing and the live-UI tree, even when the source is known to be VOD.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we could specifically mention file size. It's not a strong argument without answering another level of "why does this matter?". "carry the stream-type event plumbing" doesn't go far enough.

Comment thread internal/design/ui/live-presets.md Outdated
- **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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is combining the rational for both decisions and I'd love to see that split out.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/videojs/v10/pull/1395/changes#diff-8c4183f7c04c0f6fd650eb76f2b53ba4a351fc449b2ee9437db48e6fe43f39a5R147-R177

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.

Split the rationale into two sections — one for the preset split, one
for the live-only skin — and lead each with its actual driver.

- Split rationale: filesize. VOD is the dominant case; every byte the
  shared preset carries for live is paid by VOD-only apps that don't
  use it. Enumerates what a shared preset forces onto the VOD bundle
  (streamType slice, live UI tree, branching logic).
- Live-only rationale: smaller live bundle — no time slider, thumbnail
  previews, seek buttons, or remaining-time display. A dual-mode skin
  can't ship smaller, only differently shaped.
- Drop playbackRateFeature from liveVideoFeatures / liveAudioFeatures;
  speed controls aren't meaningful for live.
- Add a note that no live-specific timeFeature is needed — duration
  already resolves to seekable.end(last) for live.
- Flag textTrackFeature as the clearest follow-up feature split
  (captions vs chapters vs thumbnails) in Open Question #2.
- Tighten Context and cut AI-verbosity throughout.

Addresses review feedback from @heff on #1395.

Made-with: Cursor
@luwes luwes merged commit 6c81f2d into main May 1, 2026
26 checks passed
@luwes luwes deleted the design/live-presets branch May 1, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants