PlayerController
Reactive controller for accessing player store state in HTML custom elements
Import
import { PlayerController } from '@videojs/html';Usage
PlayerController is a reactive controller that consumes the player store from context. It has two overloads:
Without selector — returns the store instance directly. Does NOT subscribe to changes. Use this to access store actions.
import { PlayerController, MediaElement, playerContext } from '@videojs/html';
class VolumeControl extends MediaElement {
#player = new PlayerController(this, playerContext);
handleClick() {
this.#player.value?.setVolume(0.5);
}
}With selector — returns the selected value and subscribes to changes. Triggers a host update when the selected value changes (using shallow equality).
import { PlayerController, MediaElement, selectPlayback } from '@videojs/html';
class PlayButton extends MediaElement {
#playback = new PlayerController(this, context, selectPlayback);
render() {
const playback = this.#playback.value;
// playback is the selected state slice
}
} Access the current value via the .value getter, which returns undefined if the controller is not yet connected to a provider.
Examples
Basic Usage
<demo-ctrl-player class="html-player-controller-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
></video>
<div class="html-player-controller-basic__panel">
<demo-ctrl-actions class="html-player-controller-basic__actions">
<button class="action-play">Play</button>
<button class="action-pause">Pause</button>
<button class="action-volume">50% Volume</button>
</demo-ctrl-actions>
<demo-ctrl-state class="html-player-controller-basic__state">
<span class="state-text">Paused: Yes | Time: 0.0s | Volume: 100%</span>
</demo-ctrl-state>
</div>
</demo-ctrl-player>
.html-player-controller-basic {
position: relative;
}
.html-player-controller-basic video {
width: 100%;
}
.html-player-controller-basic__panel {
display: flex;
gap: 16px;
padding: 12px;
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid rgba(0, 0, 0, 0.1);
align-items: center;
}
.html-player-controller-basic__actions {
display: flex;
gap: 6px;
}
.html-player-controller-basic__actions button {
padding: 4px 12px;
border-radius: 6px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
font-size: 0.8125rem;
}
.html-player-controller-basic__state {
font-size: 0.8125rem;
color: #374151;
font-variant-numeric: tabular-nums;
}
import { applyElementProps, createButton, createPlayer, features, MediaElement } from '@videojs/html';
const { PlayerElement, PlayerController, context } = createPlayer({
features: [...features.video],
});
class DemoPlayer extends PlayerElement {
static readonly tagName = 'demo-ctrl-player';
}
class PlayerActions extends MediaElement {
static readonly tagName = 'demo-ctrl-actions';
readonly #player = new PlayerController(this, context);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const signal = this.#disconnect.signal;
const playBtn = this.querySelector<HTMLButtonElement>('.action-play')!;
const pauseBtn = this.querySelector<HTMLButtonElement>('.action-pause')!;
const volumeBtn = this.querySelector<HTMLButtonElement>('.action-volume')!;
const bind = (el: HTMLElement, action: () => void) => {
const props = createButton({ onActivate: action, isDisabled: () => !this.#player.value });
applyElementProps(el, props, signal);
};
bind(playBtn, () => this.#player.value?.play());
bind(pauseBtn, () => this.#player.value?.pause());
bind(volumeBtn, () => this.#player.value?.changeVolume(0.5));
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
}
class PlayerState extends MediaElement {
static readonly tagName = 'demo-ctrl-state';
readonly #state = new PlayerController(this, context, (s) => ({
paused: s.paused,
currentTime: s.currentTime,
volume: s.volume,
}));
protected override update(): void {
super.update();
const state = this.#state.value;
if (!state) return;
const el = this.querySelector('.state-text');
if (el) {
el.textContent = `Paused: ${state.paused ? 'Yes' : 'No'} | Time: ${state.currentTime.toFixed(1)}s | Volume: ${Math.round(state.volume * 100)}%`;
}
}
}
customElements.define(DemoPlayer.tagName, DemoPlayer);
customElements.define(PlayerActions.tagName, PlayerActions);
customElements.define(PlayerState.tagName, PlayerState);
API Reference
Overload 1
Parameters
| Parameter | Type | Default | |
|---|---|---|---|
host* | PlayerControllerHost | — | |
context* | PlayerContext<Store> | — |
Return Value
| Property | Type | |
|---|---|---|
value | Result | undefined |
Overload 2
Parameters
| Parameter | Type | Default | |
|---|---|---|---|
host* | PlayerControllerHost | — | |
context* | PlayerContext<Store> | — | |
selector* | Selector<InferStoreState<Store>, Result> | — |
Return Value
| Property | Type | |
|---|---|---|
value | Result | undefined |