mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
317 lines
8.6 KiB
Markdown
317 lines
8.6 KiB
Markdown
---
|
|
status: draft
|
|
---
|
|
|
|
# Player API Design
|
|
|
|
Unified API for Media and Container concerns. Two stores internally, one API for users.
|
|
|
|
## Contents
|
|
|
|
| Document | Purpose |
|
|
| ---------------------------------- | ---------------------------------------- |
|
|
| [index.md](index.md) | Overview, quick start, surface API |
|
|
| [primitives.md](primitives.md) | Library author API (`hasFeature`, types) |
|
|
| [architecture.md](architecture.md) | Two-store architecture, internals |
|
|
| [decisions.md](decisions.md) | Design decisions and rationale |
|
|
| [examples.md](examples.md) | Usage examples (React, HTML, Lit) |
|
|
|
|
## Problem
|
|
|
|
Two concerns, one player:
|
|
|
|
1. **Media** — play, pause, volume, time. Owned by `<video>`.
|
|
2. **Container** — fullscreen, keyboard, gestures, idle. Owned by the player UI wrapper.
|
|
|
|
Different targets, different lifecycles. Internally, they need separate stores.
|
|
|
|
But users want one API. This doc defines:
|
|
|
|
- How `createPlayer` exposes a unified API in React and HTML
|
|
- What it returns — hooks, controllers, providers
|
|
- How presets bundle features for common use cases
|
|
- How to extend with custom features
|
|
|
|
## Quick Start
|
|
|
|
### React
|
|
|
|
```tsx
|
|
import { createPlayer, presets } from '@videojs/react';
|
|
|
|
const { Provider, Container, usePlayer } = createPlayer(presets.website);
|
|
|
|
function App() {
|
|
return (
|
|
<Provider>
|
|
<Container>
|
|
<Video src="video.mp4" />
|
|
<Controls />
|
|
</Container>
|
|
</Provider>
|
|
);
|
|
}
|
|
|
|
function Controls() {
|
|
const player = usePlayer();
|
|
|
|
// Flat access — state and requests on same object
|
|
// Proxy-based tracking — accessing .paused subscribes automatically
|
|
return <button onClick={player.paused ? player.play : player.pause}>{player.paused ? 'Play' : 'Pause'}</button>;
|
|
}
|
|
```
|
|
|
|
### HTML / Lit
|
|
|
|
```ts
|
|
import { createPlayer, presets, VjsElement } from '@videojs/html';
|
|
|
|
const { ProviderElement, PlayerController } = createPlayer(presets.website);
|
|
|
|
customElements.define('vjs-website-provider', ProviderElement);
|
|
|
|
class VjsPlayButton extends VjsElement {
|
|
#player = new PlayerController(this);
|
|
|
|
render() {
|
|
// .value returns tracking proxy — accessing .paused subscribes
|
|
const { paused, play, pause } = this.#player.value;
|
|
|
|
return html` <button @click=${paused ? play : pause}>${paused ? 'Play' : 'Pause'}</button> `;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Declarative (Skin)
|
|
|
|
```html
|
|
<script type="module" src="@videojs/html/presets/website/skins/frosted.js"></script>
|
|
|
|
<vjs-website-provider>
|
|
<vjs-frosted-skin>
|
|
<video src="video.mp4"></video>
|
|
</vjs-frosted-skin>
|
|
</vjs-website-provider>
|
|
```
|
|
|
|
## Surface API
|
|
|
|
### createPlayer
|
|
|
|
```ts
|
|
// Shorthand — preset or feature array
|
|
createPlayer(presets.website);
|
|
createPlayer([features.playback, features.fullscreen]);
|
|
|
|
// Config object — extensible
|
|
createPlayer({
|
|
features: presets.website,
|
|
// future: devTools, middleware, etc.
|
|
});
|
|
```
|
|
|
|
### Returns (React)
|
|
|
|
```ts
|
|
const {
|
|
Provider, // Creates both stores
|
|
Container, // Attaches container to player store
|
|
usePlayer, // Player state + requests (flattened, typed to preset)
|
|
useMedia, // Media state + requests (escape hatch, typed to preset)
|
|
} = createPlayer(presets.website);
|
|
|
|
// Also exported from @videojs/react (for primitives):
|
|
// - usePlayer() → UnknownPlayer (loosely typed)
|
|
// - useMedia() → UnknownMedia (loosely typed)
|
|
// - hasFeature(player, feature) → type guard
|
|
// - getStore(player) → inferred store type
|
|
```
|
|
|
|
### Returns (HTML)
|
|
|
|
```ts
|
|
const {
|
|
// Ready-to-use elements
|
|
ProviderElement, // Both stores, auto-attaches media
|
|
ContainerElement, // Attaches container to player store
|
|
|
|
// Mixins (customization)
|
|
ProviderMixin, // Both stores
|
|
ContainerMixin, // Container attachment
|
|
MediaProviderMixin, // Media store only (escape hatch)
|
|
|
|
// Controllers (typed to preset)
|
|
PlayerController, // .value for state + requests, .store for store access
|
|
MediaController, // Media state + requests (escape hatch)
|
|
} = createPlayer(presets.website);
|
|
|
|
// Also exported from @videojs/html (for primitives):
|
|
// - PlayerController → .value as UnknownPlayer, .store as UnknownPlayerStore
|
|
// - MediaController → .value as UnknownMedia, .store as UnknownMediaStore
|
|
// - hasFeature(player, feature) → type guard
|
|
// - getStore(player) → inferred store type
|
|
```
|
|
|
|
### Returns (HTML) — createMedia
|
|
|
|
```ts
|
|
const {
|
|
ProviderElement, // Ready-to-use element
|
|
ProviderMixin, // Customization
|
|
MediaController, // Media state + requests
|
|
} = createMedia([features.playback, features.time]);
|
|
```
|
|
|
|
### usePlayer (React)
|
|
|
|
```tsx
|
|
function Controls() {
|
|
const player = usePlayer();
|
|
|
|
// State — proxy tracks access, subscribes automatically
|
|
player.paused;
|
|
player.volume;
|
|
player.isFullscreen;
|
|
|
|
// Requests — call directly
|
|
player.play();
|
|
player.pause();
|
|
player.setVolume(0.5);
|
|
player.toggleFullscreen();
|
|
}
|
|
```
|
|
|
|
### PlayerController (HTML/Lit)
|
|
|
|
```ts
|
|
class VjsControls extends VjsElement {
|
|
#player = new PlayerController(this);
|
|
|
|
render() {
|
|
const { paused, isFullscreen, play, pause, toggleFullscreen } = this.#player.value;
|
|
|
|
return html`
|
|
<button @click=${paused ? play : pause} />
|
|
<button @click=${toggleFullscreen} />
|
|
`;
|
|
}
|
|
}
|
|
```
|
|
|
|
### useMedia (Escape Hatch)
|
|
|
|
Direct media access. Rarely needed — use when player features don't expose what you need.
|
|
|
|
```tsx
|
|
function DebugPanel() {
|
|
const media = useMedia();
|
|
|
|
// Direct media state (bypasses player layer)
|
|
return <pre>{JSON.stringify({ readyState: media.readyState })}</pre>;
|
|
}
|
|
```
|
|
|
|
## Features
|
|
|
|
Features bundle related state and requests. Include only what you need.
|
|
|
|
```ts
|
|
import { features } from '@videojs/react';
|
|
|
|
features.playback; // play, pause, ended
|
|
features.volume; // volume, muted
|
|
features.time; // currentTime, duration, seeking
|
|
features.fullscreen; // isFullscreen, enter/exit
|
|
features.keyboard; // keyboard shortcuts
|
|
features.idle; // idle detection
|
|
features.gestures; // touch gestures
|
|
```
|
|
|
|
### Presets
|
|
|
|
Presets are curated feature collections. Pick one, get the right features.
|
|
|
|
```ts
|
|
createPlayer(presets.website); // full-featured
|
|
createPlayer(presets.background); // minimal (autoplay, loop)
|
|
```
|
|
|
|
| Preset | Use Case |
|
|
| -------------------- | ------------------------------------ |
|
|
| `presets.website` | Default website player **(default)** |
|
|
| `presets.background` | Background/hero video |
|
|
| `presets.news` | Article embeds |
|
|
| `presets.creator` | Creator platforms (YouTube) |
|
|
| `presets.swipe` | Short-form video (TikTok) |
|
|
| `presets.streaming` | Streaming apps (Netflix) |
|
|
| `presets.live` | Interactive live (Twitch) |
|
|
|
|
### Extending Presets
|
|
|
|
```ts
|
|
createPlayer({
|
|
features: [...presets.background, features.keyboard],
|
|
});
|
|
```
|
|
|
|
### Creating Features
|
|
|
|
```ts
|
|
import { createPlayerFeature } from '@videojs/react';
|
|
|
|
const analytics = createPlayerFeature({
|
|
initialState: { events: [] },
|
|
|
|
getSnapshot: ({ target, initialState }) => initialState,
|
|
|
|
subscribe: ({ target, update, signal }) => {
|
|
target.media.subscribe(
|
|
(s) => s.paused,
|
|
() => {
|
|
track(target.media.state.paused ? 'pause' : 'play');
|
|
update();
|
|
},
|
|
{ signal }
|
|
);
|
|
},
|
|
|
|
request: {
|
|
trackEvent: (event, { target }) => {
|
|
// Access media state and requests
|
|
target.media.state.paused;
|
|
target.media.request.play();
|
|
},
|
|
},
|
|
});
|
|
|
|
createPlayer({
|
|
features: [...presets.website, analytics],
|
|
});
|
|
```
|
|
|
|
## Naming Convention
|
|
|
|
State and requests share the same flat namespace. Follow this convention to avoid collisions:
|
|
|
|
| Type | Convention | Examples |
|
|
| -------- | ----------------------------------- | ------------------------------------------------------------- |
|
|
| State | Nouns, adjectives, past participles | `paused`, `volume`, `muted`, `isFullscreen`, `currentTime` |
|
|
| Requests | Verbs, imperative | `play`, `pause`, `setVolume`, `toggleMute`, `enterFullscreen` |
|
|
|
|
Runtime validation throws if duplicate keys are detected.
|
|
|
|
## Primitives API
|
|
|
|
Building reusable UI primitives? See [primitives.md](primitives.md) for:
|
|
|
|
- `hasFeature()` type guard for feature detection
|
|
- `UnknownPlayer` / `UnknownMedia` loosely typed proxies
|
|
- `getStore()` for direct store access
|
|
- Cross-framework patterns (React + ReactiveElement)
|
|
|
|
## Related Docs
|
|
|
|
- [decisions.md](decisions.md) — Why these choices were made
|
|
- [architecture.md](architecture.md) — Two-store internals, feature registry
|
|
- [examples.md](examples.md) — Full usage examples
|