feat(html): setup player api (#374)

This commit is contained in:
rahim
2026-02-02 21:11:44 +11:00
committed by GitHub
parent a1d0383d41
commit a419f5ef19
40 changed files with 678 additions and 1078 deletions
+16
View File
@@ -0,0 +1,16 @@
import { type Context, createContext } from '@lit/context';
import type { AnyPlayerStore, Media, PlayerStore } from '@videojs/core/dom';
export const PLAYER_CONTEXT_KEY = Symbol('@videojs/player');
export interface PlayerContextValue<Store extends PlayerStore = AnyPlayerStore> {
store: Store;
media: Media | null;
}
export type PlayerContext<Store extends PlayerStore = AnyPlayerStore> = Context<
typeof PLAYER_CONTEXT_KEY,
PlayerContextValue<Store>
>;
export const playerContext = createContext<PlayerContextValue, typeof PLAYER_CONTEXT_KEY>(PLAYER_CONTEXT_KEY);
+91
View File
@@ -0,0 +1,91 @@
import type { AnyPlayerFeature, PlayerStore, PlayerTarget } from '@videojs/core/dom';
import { combine, createStore } from '@videojs/store';
import { type ContainerMixin, createContainerMixin } from '../store/container-mixin';
import { createProviderMixin, type ProviderMixin } from '../store/provider-mixin';
import { type PlayerContext, playerContext } from './context';
import { PlayerController } from './player-controller';
import { createPlayerMixin, type PlayerMixin } from './player-mixin';
export interface CreatePlayerConfig<Features extends AnyPlayerFeature[]> {
features: Features;
}
export interface CreatePlayerResult<Store extends PlayerStore> {
/** Context for consuming player in controllers. */
context: PlayerContext<Store>;
/** Creates a store instance for imperative access. */
create: () => Store;
/** Player controller bound to this player's context. */
PlayerController: PlayerController.Constructor<Store>;
/** Mixin for a complete player element (provider + container). */
PlayerMixin: PlayerMixin<Store>;
/** Mixin that provides player context to descendants. */
ProviderMixin: ProviderMixin<Store>;
/** Mixin that consumes player context and auto-attaches media elements. */
ContainerMixin: ContainerMixin<Store>;
}
/**
* Creates a player factory with typed store, mixins, and controller.
*
* @example
* ```ts
* import { features } from '@videojs/core/dom';
*
* const {
* context,
* create,
* PlayerController,
* PlayerMixin,
* ProviderMixin,
* ContainerMixin,
* } = createPlayer({
* features: [...features.video],
* });
*
* // Complete player element
* class VideoPlayer extends PlayerMixin(ReactiveElement) {}
*
* // Or separate provider/container
* class MyPlayer extends ProviderMixin(ReactiveElement) {}
* class MyContainer extends ContainerMixin(ReactiveElement) {}
*
* // Control element
* class PlayButton extends ReactiveElement {
* #playback = new PlayerController(this, selectPlayback);
* }
* ```
*/
export function createPlayer<const Features extends AnyPlayerFeature[]>(
config: CreatePlayerConfig<Features>
): CreatePlayerResult<PlayerStore<Features>> {
type Store = PlayerStore<Features>;
const slice = combine<PlayerTarget, Features>(...config.features);
function create(): Store {
return createStore<PlayerTarget>()(slice);
}
const ctx = playerContext as PlayerContext<Store>;
const PlayerMixin = createPlayerMixin<Store>(ctx, create);
const ProviderMixin = createProviderMixin<Store>(ctx, create);
const ContainerMixin = createContainerMixin<Store>(ctx);
const BoundPlayerController = PlayerController as unknown as PlayerController.Constructor<Store>;
return {
context: ctx,
create,
PlayerController: BoundPlayerController,
PlayerMixin,
ProviderMixin,
ContainerMixin,
};
}
@@ -0,0 +1,101 @@
import { ContextConsumer } from '@lit/context';
import type { ReactiveController, ReactiveControllerHost } from '@lit/reactive-element';
import type { PlayerStore } from '@videojs/core/dom';
import type { InferStoreState, Selector } from '@videojs/store';
import { StoreController } from '@videojs/store/lit';
import type { Constructor as Ctor } from '@videojs/utils/types';
import type { PlayerContext, PlayerContextValue } from './context';
export type PlayerControllerHost = ReactiveControllerHost & HTMLElement;
/**
* Reactive controller for accessing player store state.
*
* Without selector: Returns the store, does NOT subscribe to changes.
* With selector: Returns selected state, subscribes with shallowEqual comparison.
*
* @example
* ```ts
* // Store access (no subscription)
* class Controls extends ReactiveElement {
* #player = new PlayerController(this, playerContext);
*
* handleClick() {
* this.#player.value.setVolume(0.5);
* }
* }
*
* // Selector-based subscription
* class PlayButton extends ReactiveElement {
* #playback = new PlayerController(this, playerContext, selectPlayback);
* }
* ```
*/
export class PlayerController<Store extends PlayerStore, Result = Store> implements ReactiveController {
readonly #host: PlayerControllerHost;
readonly #selector: Selector<InferStoreState<Store>, Result> | undefined;
#consumer: ContextConsumer<PlayerContext<Store>, PlayerControllerHost>;
#storeCtrl: StoreController<Store, Result> | null = null;
constructor(host: PlayerControllerHost, context: PlayerContext<Store>);
constructor(
host: PlayerControllerHost,
context: PlayerContext<Store>,
selector: Selector<InferStoreState<Store>, Result>
);
constructor(
host: PlayerControllerHost,
context: PlayerContext<Store>,
selector?: Selector<InferStoreState<Store>, Result>
) {
this.#host = host;
this.#selector = selector;
this.#consumer = new ContextConsumer(host, {
context,
callback: (ctx) => this.#connect(ctx),
subscribe: true,
});
host.addController(this);
}
get value(): Result {
const ctx = this.#consumer.value;
if (!ctx) throw new Error('Player context not available');
// Without selector: return store directly
if (!this.#selector) return ctx.store as unknown as Result;
// With selector: use StoreController
return this.#storeCtrl!.value;
}
hostConnected(): void {
const ctx = this.#consumer.value;
if (ctx) this.#connect(ctx);
}
hostDisconnected(): void {
this.#storeCtrl = null;
}
#connect(ctx: PlayerContextValue<Store> | undefined): void {
if (!ctx) return;
// Create StoreController with the store directly
if (!this.#storeCtrl && this.#selector) {
this.#storeCtrl = new StoreController(this.#host, ctx.store, this.#selector);
}
}
}
export namespace PlayerController {
export type Host = PlayerControllerHost;
export type Constructor<Store extends PlayerStore = PlayerStore, Result = Store> = Ctor<
typeof PlayerController<Store, Result>
>;
}
+33
View File
@@ -0,0 +1,33 @@
import type { ReactiveElement } from '@lit/reactive-element';
import type { PlayerStore } from '@videojs/core/dom';
import type { Constructor } from '@videojs/utils/types';
import { createContainerMixin } from '../store/container-mixin';
import { createProviderMixin } from '../store/provider-mixin';
import type { PlayerProvider } from '../store/types';
import type { PlayerContext } from './context';
export interface PlayerElement<Store extends PlayerStore> extends PlayerProvider<Store> {}
type Base = Constructor<ReactiveElement>;
type Result<Class extends Base, Store extends PlayerStore> = Class & PlayerElement<Store>;
export type PlayerMixin<Store extends PlayerStore> = <Class extends Base>(BaseClass: Class) => Result<Class, Store>;
/**
* Creates a mixin that combines provider and container functionality.
*
* Use for a complete player element that owns the store and attaches media.
*/
export function createPlayerMixin<Store extends PlayerStore>(
context: PlayerContext<Store>,
factory: () => Store
): PlayerMixin<Store> {
const ProviderMixin = createProviderMixin<Store>(context, factory);
const ContainerMixin = createContainerMixin<Store>(context);
return <Class extends Base>(BaseClass: Class) => {
return ContainerMixin(ProviderMixin(BaseClass)) as unknown as Result<Class, Store>;
};
}