# PlayerController

Reactive controller for accessing player store state in HTML custom elements

## Import

```ts
import { PlayerController } from "@videojs/html/video";
```

`PlayerController` is a reactive controller that consumes the player store from `playerContext`. Without a selector it returns the store instance directly (no subscription — use this for actions). With a selector it returns the selected value and subscribes to changes, triggering a host update on shallow-equal change. Access the current value via `.value`, which returns `undefined` until connected to a player.

The controller exported by each preset, or returned by [`createPlayer`](https://videojs.org/docs/framework/html/reference/api/html-create-player), is already bound to that player’s context, so pass only the host and optional selector. The lower-level `PlayerController` exported from `@videojs/html` accepts the context as its second argument; its constructor signatures are documented below.

```ts
import { UIElement, selectPlayback } from '@videojs/html';
import { PlayerController } from '@videojs/html/video';

class PlayButtonElement extends UIElement {
  readonly #playback = new PlayerController(this, selectPlayback);
}
```

## Examples

### Basic Usage

**index.html**

```html
<demo-ctrl-player class="demo-ctrl-player">
  <media-container>
    <video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted playsinline></video>
    <div class="panel">
      <demo-ctrl-actions class="actions">
        <button type="button" class="play">Play</button>
        <button type="button" class="pause">Pause</button>
        <button type="button" class="volume">50% Volume</button>
      </demo-ctrl-actions>
      <demo-ctrl-state class="state">
        <span class="text">Paused: Yes | Time: 0.0s | Volume: 100%</span>
      </demo-ctrl-state>
    </div>
  </media-container>
</demo-ctrl-player>
```

**index.css**

```css
.demo-ctrl-player media-container {
  position: relative;
}

.demo-ctrl-player media-container video {
  width: 100%;
}

.panel {
  display: flex;
  gap: 16px;
  align-items: center;
  padding: 12px;
  background: rgba(0, 0, 0, 0.05);
  border-top: 1px solid rgba(0, 0, 0, 0.1);
}

.actions {
  display: flex;
  gap: 6px;
}

.actions button {
  padding: 4px 12px;
  font-size: 0.8125rem;
  color: #111827;
  cursor: pointer;
  background: white;
  border: 1px solid #ccc;
  border-radius: 6px;
}

.state {
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  color: #374151;
}
```

**index.ts**

```ts
import {
  applyElementProps,
  createButton,
  createPlayer,
  selectPlayback,
  selectTime,
  selectVolume,
  UIElement,
} from '@videojs/html';
import { videoFeatures } from '@videojs/html/video';
import '@videojs/html/ui/container';

const { PlayerElement: DemoPlayerElement, PlayerController } = createPlayer({
  features: videoFeatures,
});

class PlayerActions extends UIElement {
  static readonly tagName = 'demo-ctrl-actions';

  readonly #player = new PlayerController(this);

  #disconnect: AbortController | null = null;

  override connectedCallback(): void {
    super.connectedCallback();
    this.#disconnect = new AbortController();
    const signal = this.#disconnect.signal;

    const playBtn = this.querySelector<HTMLButtonElement>('.play')!;
    const pauseBtn = this.querySelector<HTMLButtonElement>('.pause')!;
    const volumeBtn = this.querySelector<HTMLButtonElement>('.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?.setVolume(0.5));
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback();
    this.#disconnect?.abort();
    this.#disconnect = null;
  }
}

class PlayerState extends UIElement {
  static readonly tagName = 'demo-ctrl-state';

  readonly #playback = new PlayerController(this, selectPlayback);
  readonly #time = new PlayerController(this, selectTime);
  readonly #volume = new PlayerController(this, selectVolume);

  protected override update(changed: Map<string, unknown>): void {
    super.update(changed);
    const playback = this.#playback.value;
    const time = this.#time.value;
    const volume = this.#volume.value;

    if (!playback) return;

    const el = this.querySelector('.text');

    if (el) {
      el.textContent = `Paused: ${playback.paused ? 'Yes' : 'No'} | Time: ${(time?.currentTime ?? 0).toFixed(1)}s | Volume: ${Math.round((volume?.volume ?? 0) * 100)}%`;
    }
  }
}

customElements.define('demo-ctrl-player', DemoPlayerElement);
customElements.define(PlayerActions.tagName, PlayerActions);
customElements.define(PlayerState.tagName, PlayerState);
```

## API Reference

### Without Selector

`new PlayerController<Store extends PlayerStore, Result = Store>(host, context)`

#### Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `host` (required) | `{ addController(controller: ReactiveController): void; removeController(controller: ReactiveController): void; requestUpdate(): void; updateComplete: Promise<boolean> } & HTMLElement` | — | The host element that owns this controller. |
| `context` (required) | `Context<symbol, Store>` | — | Player context to resolve the store from. |

#### Return Value

| Property | Type |
| --- | --- |
| `value` | `Result \| undefined` |
| `displayName` | `string \| undefined` |

### With Selector

`new PlayerController<Store extends PlayerStore, Result = Store>(host, context, selector)`

#### Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `host` (required) | `{ addController(controller: ReactiveController): void; removeController(controller: ReactiveController): void; requestUpdate(): void; updateComplete: Promise<boolean> } & HTMLElement` | — | The host element that owns this controller. |
| `context` (required) | `Context<symbol, Store>` | — | Player context to resolve the store from. |
| `selector` (required) | `{ (state: Store['state']): Result; displayName?: string }` | — | Derives a value from the player store state. |

#### Return Value

| Property | Type |
| --- | --- |
| `value` | `Result \| undefined` |
| `displayName` | `string \| undefined` |

---

HTML documentation: https://videojs.org/docs/framework/html/llms.txt
All documentation: https://videojs.org/llms.txt
