From d081db9507eeb92abbc1e3da65c2104e90472e43 Mon Sep 17 00:00:00 2001 From: rahim Date: Fri, 30 Jan 2026 22:39:51 +1100 Subject: [PATCH] docs(rfc): player api design v2 (#358) --- .../design/feature-availability-design.md | 2 +- internal/design/feature-slice-design.md | 171 ------ rfc/player-api/api.md | 240 ++++++++ rfc/player-api/architecture.md | 223 +++---- rfc/player-api/decisions.md | 439 ++++++-------- rfc/player-api/examples.md | 545 ++++++++++++------ rfc/player-api/features.md | 206 +++++++ rfc/player-api/feedback.md | 68 +++ rfc/player-api/html.md | 323 +++++++++++ rfc/player-api/index.md | 363 +++--------- rfc/player-api/primitives.md | 292 ++++++---- 11 files changed, 1719 insertions(+), 1153 deletions(-) delete mode 100644 internal/design/feature-slice-design.md create mode 100644 rfc/player-api/api.md create mode 100644 rfc/player-api/features.md create mode 100644 rfc/player-api/feedback.md create mode 100644 rfc/player-api/html.md diff --git a/internal/design/feature-availability-design.md b/internal/design/feature-availability-design.md index 81756f59..ebc1f9fd 100644 --- a/internal/design/feature-availability-design.md +++ b/internal/design/feature-availability-design.md @@ -38,7 +38,7 @@ type FeatureAvailability = 'available' | 'unavailable' | 'unsupported'; ## Related: Missing Feature vs Unavailable Capability -See [feature-slice-design](feature-slice-design.md) for feature access patterns. +See [rfc/player-api](../../rfc/player-api/primitives.md) for feature access patterns. | Concept | Cause | Detection | | ---------------------- | ------------------- | --------------------------------- | diff --git a/internal/design/feature-slice-design.md b/internal/design/feature-slice-design.md deleted file mode 100644 index 4a619512..00000000 --- a/internal/design/feature-slice-design.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -status: draft -date: 2025-01-29 ---- - -# Feature Slice Design - -## Problem - -Primitives don't know which preset the user chose: - -```tsx -// Inside @videojs/react - shipped to users -export function PlayButton() { - const player = usePlayer(); // What type is this? - // User will use custom feature configuration - // We don't know if `playbackFeature` is included -} -``` - -They need: - -1. Loosely typed access to the player -2. A way to check if a feature exists -3. Type narrowing when the feature is present - -## Solution - -A `FeatureSlice` provides typed, scoped access to a feature's state and requests. Two API layers: - -1. **Primitive** — generic, explicit store/feature -2. **Player-specific** — typed by registry, uses context - -## Design - -### Feature Definition - -Features have a typed `name` (literal type preserved): - -```ts -const playbackFeature = createFeature()({ - name: 'playback', // Type: 'playback' (literal) - initialState: { paused: true }, - request: { play, pause }, -}); -``` - -### Store API - -```ts -// By reference — typed from feature -store.getFeature(playbackFeature); - -// By name — typed from store's features array -store.getFeature('playback'); -``` - -Returns `FeatureSlice | undefined`. Flat access to state and requests: - -```ts -const playback = store.getFeature(playbackFeature); - -if (!playback) return null; - -playback.paused; // State (flat) -playback.play(); // Request (flat) -playback.subscribe(cb); // Scoped subscription -``` - -### Primitive Layer (`@videojs/store`) - -Generic APIs that require explicit store/feature: - -```ts -// React — explicit store + feature -const playback = useFeature(store, playbackFeature); - -// Lit — explicit store + feature -#playback = new FeatureController(host, store, playbackFeature); -``` - -### Player Layer (`@videojs/core/dom`, `@videojs/react`, `@videojs/html`) - -Typed by `PlayerFeatureRegistry`, uses context: - -```ts -// Registry (manually maintained) -interface PlayerFeatureRegistry { - playback: typeof playbackFeature; - volume: typeof volumeFeature; - time: typeof timeFeature; -} - -// Utility -getPlayerFeature(store, 'playback'); // Typed - -// React — uses player context -usePlayerFeature('playback'); // Typed, store from context - -// Lit — uses player context -new PlayerFeatureController(host, 'playback'); // Typed -``` - -### Usage Examples - -**React primitive (custom feature):** - -```tsx -function CustomControl() { - const store = useStore(); - const custom = useFeature(store, myCustomFeature); - // ... -} -``` - -**React player-specific:** - -```tsx -function PlayButton() { - const playback = usePlayerFeature('playback'); - - if (!playback) return null; - - // ... -} -``` - -**Lit player-specific:** - -```ts -class PlayButton extends ReactiveElement { - #core = new PlayButtonCore(); - #playback = new PlayerFeatureController(this, 'playback'); - - protected override update(changed: PropertyValues) { - super.update(changed); - applyElementProps(this, this.#core.getProps(this.#playback.value)); - } -} -``` - -## FeatureSlice - -A thin lens over the store — flat access, scoped subscription: - -- State keys exposed directly (not `.state.paused`) -- Request keys exposed directly (not `.request.play()`) -- `subscribe()` for scoped updates (reserved name) - -## Rationale - -1. **Type safety** — Types flow from feature reference or registry -2. **No collisions** — Features keyed by symbol (unique) -3. **Layered API** — Primitive layer is generic, player layer is typed and convenient -4. **Familiar pattern** — Similar to Jotai atoms, Vue InjectionKey - -## Related - -See [feature-availability-design](feature-availability-design.md) for capability detection patterns. - -## Files - -| Package | File | Change | -|---------|------|--------| -| `store` | `src/core/feature-slice.ts` | New — `FeatureSlice` class | -| `store` | `src/core/store.ts` | Add `getFeature()` | -| `store` | `src/react/use-feature.ts` | New — `useFeature` hook | -| `store` | `src/lit/feature-controller.ts` | New — `FeatureController` | -| `core/dom` | `src/store/registry.ts` | New — `PlayerFeatureRegistry` | -| `react` | `src/use-player-feature.ts` | New — `usePlayerFeature` hook | -| `html` | `src/player-feature-controller.ts` | New — `PlayerFeatureController` | diff --git a/rfc/player-api/api.md b/rfc/player-api/api.md new file mode 100644 index 00000000..42da787f --- /dev/null +++ b/rfc/player-api/api.md @@ -0,0 +1,240 @@ +# API + +Surface API for React and HTML. + +## createPlayer + +### React + +```ts +import { createPlayer, features } from '@videojs/react'; + +const { Provider, Container, usePlayer } = createPlayer({ + features: [features.video] +}); +``` + +**Returns:** + +| Export | Purpose | +| ----------- | ---------------------------------------- | +| `Provider` | Creates stores, provides context | +| `Container` | Attaches container element to player store | +| `usePlayer` | Access player state (typed to features) | + +### HTML + +```ts +import { createPlayer, features } from '@videojs/html'; + +const { PlayerElement, PlayerController } = createPlayer({ + features: [features.video] +}); + +customElements.define('my-video-player', PlayerElement); +``` + +**Returns:** + +| Export | Purpose | +| ------------------ | --------------------------------------------------- | +| `PlayerElement` | Combined provider + container element (common case) | +| `PlayerController` | Reactive controller for accessing player state | +| `ProviderElement` | Provider-only element (advanced, split cases) | +| `ContainerElement` | Container-only element (advanced, split cases) | +| `ProviderMixin` | Mixin for custom provider elements | +| `ContainerMixin` | Mixin for custom container elements | + +### Config + +```ts +// Individual features +createPlayer({ + features: [features.playback, features.volume, features.fullscreen] +}); + +// Feature bundles (sugar) +createPlayer({ + features: [features.video] +}); + +// Extended bundle +createPlayer({ + features: [features.video, features.streaming] +}); +``` + +## usePlayer (React) + +Access player state with selector-based subscriptions. + +### Overloads + +```ts +// 1. Feature only — returns full feature slice +usePlayer(feature): FeatureSlice | undefined + +// 2. Feature + selector — returns selected value from feature +usePlayer(feature, selector): R | undefined + +// 3. Global selector — returns selected value from all state +usePlayer(selector): R +``` + +### Examples + +```tsx +// Get full playback feature slice +const playback = usePlayer(features.playback); +if (!playback) return null; +playback.paused; +playback.play(); + +// Get specific value from feature +const paused = usePlayer(features.playback, s => s.paused); + +// Derive value from feature +const isPlaying = usePlayer(features.playback, s => !s.paused && !s.ended); + +// Select across multiple features (global selector) +const state = usePlayer(s => ({ + paused: s.paused, + volume: s.volume +})); +``` + +### Performance + +> **Warning:** Global selectors without feature scoping subscribe to all state changes. During playback, `currentTime` updates frequently (4-60 times/sec). Always scope to features or use specific selectors. + +```tsx +// Bad — re-renders on every currentTime update +const state = usePlayer(s => s); + +// Good — only subscribes to playback feature +const playback = usePlayer(features.playback); + +// Good — only subscribes to paused +const paused = usePlayer(features.playback, s => s.paused); +``` + +### Selector Comparison + +Selectors returning objects use `shallowEqual` comparison: + +```tsx +// Re-renders only when paused OR volume changes +const state = usePlayer(s => ({ + paused: s.paused, + volume: s.volume +})); +``` + +`shallowEqual` is exported from `@videojs/store` for custom use. +## store.get / store.has + +Access features within feature context (subscribe/request handlers). + +### store.get(feature | key | name) + +Returns typed feature slice or `undefined`. + +```ts +// By feature reference +store.get(features.playback) // PlaybackSlice | undefined + +// By feature key (Symbol) +store.get(playbackKey) // PlaybackSlice | undefined + +// By name (string) +store.get('playback') // Slice | undefined (loose typing) +``` + +### store.has(feature | key | name) + +Returns `boolean`. + +```ts +store.has(features.playback) // boolean +store.has('playback') // boolean +``` + +### Usage in Features + +```ts +const keyboardFeature = createPlayerFeature({ + subscribe: ({ store, update, signal }) => { + const playback = store.get(features.playback); + + document.addEventListener('keydown', (e) => { + if (e.key === ' ') playback?.toggle(); + }, { signal }); + } +}); +``` + +## PlayerController (HTML) + +Reactive controller for accessing player state in custom elements. + +```ts +import { createPlayer, features, MediaElement } from '@videojs/html'; + +const { PlayerController } = createPlayer({ + features: [features.video] +}); + +class MediaPlayButton extends MediaElement { + #playback = new PlayerController(this, features.playback); + + override connectedCallback() { + super.connectedCallback(); + this.addEventListener('click', this.#handleClick); + } + + #handleClick = () => { + this.#playback.value?.toggle(); + }; + + override update() { + const playback = this.#playback.value; + if (!playback) return; + // ... + } +} +``` + +### Controller API + +| Property | Returns | Description | +| -------- | -------------------- | ----------------------------------------------- | +| `value` | `FeatureSlice \| undefined` | Feature slice, triggers update on change | +## Type Exports + +### From `@videojs/store` + +```ts +import { shallowEqual } from '@videojs/store'; +``` + +| Export | Purpose | +| -------------- | -------------------------------- | +| `shallowEqual` | Shallow comparison for selectors | + +### From `@videojs/react` + +```ts +import { createPlayer, features, usePlayer } from '@videojs/react'; +``` + +### From `@videojs/html` + +```ts +import { createPlayer, features, MediaElement } from '@videojs/html'; +``` + +| Export | Purpose | +| --------------- | --------------------------------- | +| `createPlayer` | Factory for player infrastructure | +| `features` | Feature definitions and bundles | +| `MediaElement` | Base class for UI primitives | diff --git a/rfc/player-api/architecture.md b/rfc/player-api/architecture.md index 4c62db3b..a3fae373 100644 --- a/rfc/player-api/architecture.md +++ b/rfc/player-api/architecture.md @@ -1,75 +1,43 @@ # Architecture -Internal structure of the Player API. +Internal structure of the Player API. Implementation detail for feature authors. ## Overview ``` createPlayer() - config: presets.website | { features: [...] } - │ - filters by feature.type - │ - ┌───────────────┴───────────────┐ - ▼ ▼ - ┌────────────────────────┐ ┌────────────────────────┐ - │ createStore() │ │ createStore() │ - │ type: 'media' │ │ type: 'player' │ - └────────────────────────┘ └────────────────────────┘ - │ │ - ▼ ▼ - ┌────────────────────────┐ ┌────────────────────────┐ - │ Media Store │◄─────│ Player Store │ - │ target: MediaTarget │ │ target: PlayerTarget │ - │ │ │ │ - │ state: paused, volume │ │ state: isFullscreen │ - │ request: play, pause │ │ request: toggleFS │ - └────────────────────────┘ └────────────────────────┘ - │ - getFeature(target.media, f) - │ - ┌───────────┴───────────┐ - ▼ ▼ - Read media state Call media requests - (iOS fallback) (keyboard shortcuts) + │ + filters by feature.type + │ + ┌───────────┴───────────┐ + ▼ ▼ + ┌───────────────────┐ ┌───────────────────┐ + │ Media Store │ │ Player Store │ + │ target: