mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
docs(rfc): player api design v2 (#358)
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
# Features
|
||||
|
||||
Features are the primary abstraction. They bundle related state and requests.
|
||||
|
||||
## Feature Definition
|
||||
|
||||
### createFeature
|
||||
|
||||
```ts
|
||||
import { createFeature } from '@videojs/store';
|
||||
|
||||
const playbackFeature = createFeature<HTMLMediaElement>()({
|
||||
name: 'playback',
|
||||
|
||||
initialState: {
|
||||
paused: true,
|
||||
ended: false
|
||||
},
|
||||
|
||||
getSnapshot: ({ target }) => ({
|
||||
paused: target.paused,
|
||||
ended: target.ended,
|
||||
}),
|
||||
|
||||
subscribe: ({ target, update, signal }) => {
|
||||
target.addEventListener('play', update, { signal });
|
||||
target.addEventListener('pause', update, { signal });
|
||||
target.addEventListener('ended', update, { signal });
|
||||
},
|
||||
|
||||
request: {
|
||||
play: (_, { target }) => target.play(),
|
||||
pause: (_, { target }) => target.pause(),
|
||||
toggle: (_, { target }) => target.paused ? target.play() : target.pause(),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Config Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | ----------------------------- | ---------------------------------------- |
|
||||
| `key` | `symbol` (optional) | Unique identifier. Auto-generated if not provided. |
|
||||
| `name` | `string` | Human-readable name for debugging |
|
||||
| `initialState` | `object` | Initial state before target attached |
|
||||
| `getSnapshot` | `(ctx) => State` | Derive state from target |
|
||||
| `subscribe` | `(ctx) => void` | Set up subscriptions, call `update()` on changes |
|
||||
| `request` | `Record<string, RequestHandler>` | Actions that modify target |
|
||||
|
||||
### Context Object
|
||||
|
||||
Both `getSnapshot`, `subscribe`, and `request` handlers receive a context:
|
||||
|
||||
```ts
|
||||
interface FeatureContext {
|
||||
target: Target; // HTMLMediaElement or PlayerTarget
|
||||
store: Store; // Access other features via store.get()
|
||||
signal: AbortSignal; // For cleanup (subscribe only)
|
||||
update: () => void; // Trigger re-snapshot (subscribe only)
|
||||
}
|
||||
```
|
||||
|
||||
## Feature Keys
|
||||
|
||||
Features can define a custom key for identity and type-carrying.
|
||||
|
||||
### Pattern
|
||||
|
||||
```ts
|
||||
// Define key
|
||||
const PLAYBACK_KEY = Symbol.for('@videojs/playback');
|
||||
|
||||
// Create feature with key
|
||||
const playbackFeature = createFeature({
|
||||
key: PLAYBACK_KEY,
|
||||
name: 'playback',
|
||||
// ...
|
||||
});
|
||||
|
||||
// Export typed key
|
||||
export const playbackKey: FeatureKey<typeof playbackFeature> = PLAYBACK_KEY;
|
||||
|
||||
// Export feature
|
||||
export { playbackFeature };
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
// Import just the key (smaller bundle, no feature code)
|
||||
import { playbackKey } from '@videojs/core/features';
|
||||
|
||||
// Typed access
|
||||
const playback = store.get(playbackKey); // PlaybackSlice | undefined
|
||||
```
|
||||
|
||||
### FeatureKey Type
|
||||
|
||||
```ts
|
||||
// Type carrier — key holds the feature's type info
|
||||
type FeatureKey<F extends Feature> = symbol & { __feature?: F };
|
||||
```
|
||||
|
||||
### Why Keys?
|
||||
|
||||
1. **Smaller imports** — Import just the key when you don't need the feature definition
|
||||
2. **Cross-realm identity** — `Symbol.for()` works across module boundaries
|
||||
3. **Type inference** — `FeatureKey<F>` carries the feature type for `store.get()`
|
||||
## Feature Bundles
|
||||
|
||||
Bundles are sugar for common feature combinations.
|
||||
|
||||
### Base Bundles
|
||||
|
||||
```ts
|
||||
// Video player base
|
||||
features.video = [
|
||||
features.playback,
|
||||
features.volume,
|
||||
features.time,
|
||||
features.presentation,
|
||||
features.userActivity,
|
||||
];
|
||||
|
||||
// Audio player base
|
||||
features.audio = [
|
||||
features.playback,
|
||||
features.volume,
|
||||
features.time,
|
||||
];
|
||||
```
|
||||
|
||||
### Additional Bundles (Beta+)
|
||||
|
||||
```ts
|
||||
features.streaming = [
|
||||
features.qualitySelection,
|
||||
features.audioTracks,
|
||||
features.textTracks,
|
||||
];
|
||||
|
||||
features.ads = [
|
||||
features.adMarkers,
|
||||
features.adSkip,
|
||||
features.adCountdown,
|
||||
];
|
||||
|
||||
features.live = [
|
||||
features.liveIndicator,
|
||||
features.lowLatency,
|
||||
features.seekToLive,
|
||||
features.dvr,
|
||||
];
|
||||
```
|
||||
|
||||
### Granular vs Bundled
|
||||
|
||||
```ts
|
||||
// Granular — import individual features
|
||||
import '@videojs/html/feature/quality-selection';
|
||||
import '@videojs/html/feature/audio-tracks';
|
||||
|
||||
// Bundled — sugar for above + more
|
||||
import '@videojs/html/feature/streaming';
|
||||
```
|
||||
|
||||
### Extending Bundles
|
||||
|
||||
```ts
|
||||
createPlayer({
|
||||
features: [features.video, features.streaming, myCustomFeature]
|
||||
});
|
||||
```
|
||||
|
||||
## Media vs Player Features
|
||||
|
||||
### Media Features
|
||||
|
||||
Target `HTMLMediaElement`. Handle playback, volume, time, etc.
|
||||
|
||||
```ts
|
||||
const volumeFeature = createMediaFeature({
|
||||
name: 'volume',
|
||||
initialState: {
|
||||
volume: 1,
|
||||
muted: false
|
||||
},
|
||||
getSnapshot: ({ target }) => ({
|
||||
volume: target.volume,
|
||||
muted: target.muted,
|
||||
}),
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Player Features
|
||||
|
||||
Target the container element. Have access to media store via `store.get()`.
|
||||
|
||||
```ts
|
||||
createPlayerFeature({
|
||||
subscribe: ({ store, target, update, signal }) => {
|
||||
// Access media features from player store
|
||||
const playback = store.get(features.playback);
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user