6.2 KiB
Architecture
Internal structure of the Player API. Implementation detail for feature authors.
Overview
createPlayer()
│
creates Store<PlayerTarget>
│
▼
┌───────────────────────┐
│ Store │
│ target: PlayerTarget │
│ │
│ playback, volume, │
│ time, fullscreen... │
└───────────────────────┘
│
store.state
│
┌──────────┴──────────┐
▼ ▼
selectPlayback() selectVolume()
│ │
▼ ▼
PlaybackState VolumeState
Key insight: Single store with composite target. Features define slices that are combined. Selectors extract typed state for UI components.
PlayerTarget
All features receive a composite target containing both media element and container:
interface PlayerTarget {
media: Media; // HTMLMediaElement
container: MediaContainer | null; // Container element (optional)
}
This allows features to:
- Observe and control the media element directly
- Access the container for UI concerns (fullscreen, focus)
- Work with media-only scenarios (headless, audio)
Store Type
type Store<Target, State> = {
readonly target: Target | null;
readonly state: State;
attach(target: Target): () => void;
subscribe(callback: () => void): () => void;
destroy(): void;
} & State; // Direct state access via intersection
The store provides:
- Direct access —
store.pausedworks for quick reads - State snapshot —
store.statefor selectors - Attachment —
store.attach(target)connects to media element - Subscriptions —
store.subscribe(callback)for reactive updates
Slices and Combine
Features are defined as slices and combined into a single store:
import { defineSlice, combine, createStore } from '@videojs/store';
const playbackSlice = defineSlice<PlayerTarget>()({
state: () => ({ paused: true, ended: false }),
attach: ({ target, set, signal }) => {
const sync = () => set({ paused: target.media.paused, ended: target.media.ended });
target.media.addEventListener('play', sync, { signal });
target.media.addEventListener('pause', sync, { signal });
sync();
},
});
// Combine slices
const playerSlice = combine(playbackSlice, volumeSlice, timeSlice);
// Create store
const store = createStore<PlayerTarget>()(playerSlice);
Selectors
Selectors extract typed state from the store. They enable:
- Type-safe access — TypeScript knows the shape of selected state
- Subscription scoping — Only re-render when selected state changes
- Composition — Derive values from multiple state properties
import { createSelector } from '@videojs/store';
// Create selector from slice
const selectPlayback = createSelector(playbackSlice);
// Usage in React
const playback = usePlayer(selectPlayback);
// Type: { paused: boolean; ended: boolean; ... } | undefined
// Usage in HTML
const ctrl = new PlayerController(this, context, selectPlayback);
// ctrl.value: { paused: boolean; ended: boolean; ... } | undefined
Pre-built Selectors
Standard selectors are exported from @videojs/core/dom:
import {
selectPlayback,
selectVolume,
selectTime,
selectSource,
selectBuffer,
} from '@videojs/core/dom';
Reactive System
Selector-based subscriptions via useSyncExternalStore (React) or controller update cycle (HTML).
Subscription Flow
- Subscribe —
usePlayer(selector)subscribes to store - Snapshot — On change,
store.stateprovides current state - Select — Selector extracts relevant state
- Compare — Result compared with
shallowEqual - Update — If different, trigger re-render
Performance
Selectors with shallowEqual comparison prevent unnecessary re-renders:
// Only re-renders when paused OR ended changes
const playback = usePlayer(selectPlayback);
// currentTime updates (60fps during playback) don't affect this component
File Structure
| Path | Purpose |
|---|---|
packages/store/src/core/ |
Core store, slices, selectors |
packages/store/src/react/ |
React hooks (useStore) |
packages/store/src/html/ |
HTML controllers (StoreController) |
packages/core/src/dom/ |
PlayerTarget, features, selectors |
packages/html/src/ |
HTML player, mixins, elements |
packages/react/src/ |
React player, context, hooks |
Progressive Complexity
| Level | Example | Sees internals? |
|---|---|---|
| Use skin | <VideoSkin> |
No |
| Use features | createPlayer({ features: [...] }) |
No |
| Custom features | createPlayer({ features: [..., mySlice] }) |
No |
| Use hooks | usePlayer(selectPlayback) |
No |
| Write slice | defineSlice<PlayerTarget>()({ ... }) |
Yes (attach) |
Store internals are implementation details until you author slices.
Constraints
- Features target
PlayerTarget(composite of media + container) - Slices live in
@videojs/core/dom createPlayerlives in@videojs/htmland@videojs/react- Single store, selectors for typed access
shallowEqualcomparison in React hooks and HTML controllers