feat(html): add PlayerElement to createPlayer (#376)

This commit is contained in:
rahim
2026-02-02 22:52:02 +11:00
committed by GitHub
parent 43cf39cbf3
commit 9c8d9cd8ed
9 changed files with 105 additions and 58 deletions
+2 -2
View File
@@ -4,6 +4,7 @@ export * from '@videojs/core/dom';
// Store
export type { Comparator, Selector } from '@videojs/store';
export { createSelector, shallowEqual } from '@videojs/store';
// Player
export * from './player/context';
export * from './player/create-player';
@@ -11,8 +12,7 @@ export * from './player/player-controller';
export * from './player/player-mixin';
export * from './store/container-mixin';
export * from './store/provider-mixin';
// Store bindings
export * from './store/types';
// Primitives
export { MediaElement } from './ui/media-element';
export * from './ui/media-element';
+17 -18
View File
@@ -3,6 +3,8 @@ import { combine, createStore } from '@videojs/store';
import { type ContainerMixin, createContainerMixin } from '../store/container-mixin';
import { createProviderMixin, type ProviderMixin } from '../store/provider-mixin';
import type { PlayerElementConstructor } from '../store/types';
import { MediaElement } from '../ui/media-element';
import { type PlayerContext, playerContext } from './context';
import { PlayerController } from './player-controller';
import { createPlayerMixin, type PlayerMixin } from './player-mixin';
@@ -21,6 +23,9 @@ export interface CreatePlayerResult<Store extends PlayerStore> {
/** Player controller bound to this player's context. */
PlayerController: PlayerController.Constructor<Store>;
/** Pre-composed player element ready for customElements.define(). */
PlayerElement: PlayerElementConstructor<Store>;
/** Mixin for a complete player element (provider + container). */
PlayerMixin: PlayerMixin<Store>;
@@ -37,28 +42,21 @@ export interface CreatePlayerResult<Store extends PlayerStore> {
* @example
* ```ts
* import { features } from '@videojs/core/dom';
* import { createPlayer, MediaElement } from '@videojs/html';
*
* const {
* context,
* create,
* PlayerController,
* PlayerMixin,
* ProviderMixin,
* ContainerMixin,
* } = createPlayer({
* const { PlayerElement, PlayerController, context } = createPlayer({
* features: [...features.video],
* });
*
* // Complete player element
* class VideoPlayer extends PlayerMixin(ReactiveElement) {}
* // Simple: register pre-composed PlayerElement
* customElements.define('video-player', PlayerElement);
*
* // Or separate provider/container
* class MyPlayer extends ProviderMixin(ReactiveElement) {}
* class MyContainer extends ContainerMixin(ReactiveElement) {}
* // Custom: extend with PlayerMixin
* class MyPlayer extends PlayerMixin(MediaElement) {}
*
* // Control element
* class PlayButton extends ReactiveElement {
* #playback = new PlayerController(this, selectPlayback);
* // Control element with selector
* class PlayButton extends MediaElement {
* #playback = new PlayerController(this, context, selectPlayback);
* }
* ```
*/
@@ -76,14 +74,15 @@ export function createPlayer<const Features extends AnyPlayerFeature[]>(
const ctx = playerContext as PlayerContext<Store>;
const PlayerMixin = createPlayerMixin<Store>(ctx, create);
const PlayerElement = PlayerMixin(MediaElement);
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,
PlayerController,
PlayerElement,
PlayerMixin,
ProviderMixin,
ContainerMixin,
@@ -3,7 +3,6 @@ import type { ReactiveController, ReactiveControllerHost } from '@lit/reactive-e
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';
@@ -18,7 +17,7 @@ export type PlayerControllerHost = ReactiveControllerHost & HTMLElement;
* @example
* ```ts
* // Store access (no subscription)
* class Controls extends ReactiveElement {
* class Controls extends MediaElement {
* #player = new PlayerController(this, playerContext);
*
* handleClick() {
@@ -27,7 +26,7 @@ export type PlayerControllerHost = ReactiveControllerHost & HTMLElement;
* }
*
* // Selector-based subscription
* class PlayButton extends ReactiveElement {
* class PlayButton extends MediaElement {
* #playback = new PlayerController(this, playerContext, selectPlayback);
* }
* ```
@@ -95,7 +94,8 @@ export class PlayerController<Store extends PlayerStore, Result = Store> impleme
export namespace PlayerController {
export type Host = PlayerControllerHost;
export type Constructor<Store extends PlayerStore = PlayerStore, Result = Store> = Ctor<
typeof PlayerController<Store, Result>
export type Constructor<Store extends PlayerStore = PlayerStore, Result = Store> = typeof PlayerController<
Store,
Result
>;
}
+8 -11
View File
@@ -1,19 +1,16 @@
import type { ReactiveElement } from '@lit/reactive-element';
import type { PlayerStore } from '@videojs/core/dom';
import type { Constructor } from '@videojs/utils/types';
import type { MediaElementConstructor } from '@/ui/media-element';
import { createContainerMixin } from '../store/container-mixin';
import { createProviderMixin } from '../store/provider-mixin';
import type { PlayerProvider } from '../store/types';
import type { PlayerProviderConstructor } from '../store/types';
import type { PlayerContext } from './context';
export interface PlayerElement<Store extends PlayerStore> extends PlayerProvider<Store> {}
type Result<Class extends MediaElementConstructor, Store extends PlayerStore> = Class &
PlayerProviderConstructor<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>;
export type PlayerMixin<Store extends PlayerStore> = <Class extends MediaElementConstructor>(
BaseClass: Class
) => Result<Class, Store>;
/**
* Creates a mixin that combines provider and container functionality.
@@ -27,7 +24,7 @@ export function createPlayerMixin<Store extends PlayerStore>(
const ProviderMixin = createProviderMixin<Store>(context, factory);
const ContainerMixin = createContainerMixin<Store>(context);
return <Class extends Base>(BaseClass: Class) => {
return <Class extends MediaElementConstructor>(BaseClass: Class) => {
return ContainerMixin(ProviderMixin(BaseClass)) as unknown as Result<Class, Store>;
};
}
@@ -0,0 +1,34 @@
import { features } from '@videojs/core/dom';
import { describe, expect, it } from 'vitest';
import { createPlayer } from '../create-player';
describe('createPlayer', () => {
it('returns expected exports', () => {
const result = createPlayer({ features: [...features.video] });
expect(result.context).toBeDefined();
expect(result.create).toBeInstanceOf(Function);
expect(result.PlayerController).toBeDefined();
expect(result.PlayerElement).toBeDefined();
expect(result.PlayerMixin).toBeInstanceOf(Function);
expect(result.ProviderMixin).toBeInstanceOf(Function);
expect(result.ContainerMixin).toBeInstanceOf(Function);
});
it('create() returns a store instance', () => {
const { create } = createPlayer({ features: [...features.video] });
const store = create();
expect(store.attach).toBeInstanceOf(Function);
expect(store.subscribe).toBeInstanceOf(Function);
expect(store.destroy).toBeInstanceOf(Function);
});
it('PlayerElement is a valid custom element class', () => {
const { PlayerElement } = createPlayer({ features: [...features.video] });
expect(typeof PlayerElement).toBe('function');
expect(PlayerElement.prototype).toBeDefined();
});
});
+6 -10
View File
@@ -1,22 +1,18 @@
import { ContextConsumer } from '@lit/context';
import type { ReactiveElement } from '@lit/reactive-element';
import type { MediaContainer, PlayerStore, PlayerTarget } from '@videojs/core/dom';
import { listen, querySlot } from '@videojs/utils/dom';
import { Disposer } from '@videojs/utils/events';
import { noop } from '@videojs/utils/function';
import type { Constructor } from '@videojs/utils/types';
import type { MediaElementConstructor } from '@/ui/media-element';
import type { PlayerContext } from '../player/context';
import type { PlayerConsumer } from './types';
import type { PlayerConsumer, PlayerConsumerConstructor } from './types';
type Base = Constructor<ReactiveElement>;
type Result<Class extends Base, Store extends PlayerStore> = Class & Constructor<PlayerConsumer<Store>>;
export type ContainerMixin<Store extends PlayerStore> = <Class extends Base>(BaseClass: Class) => Result<Class, Store>;
export type ContainerMixin<Store extends PlayerStore> = <Class extends MediaElementConstructor>(
BaseClass: Class
) => Class & PlayerConsumerConstructor<Store>;
export function createContainerMixin<Store extends PlayerStore>(context: PlayerContext<Store>): ContainerMixin<Store> {
return <Class extends Base>(BaseClass: Class) => {
return <Class extends MediaElementConstructor>(BaseClass: Class) => {
class PlayerContainerElement extends BaseClass implements PlayerConsumer<Store>, MediaContainer {
#detach = noop;
#disposer = new Disposer();
+6 -10
View File
@@ -1,23 +1,19 @@
import { ContextProvider } from '@lit/context';
import type { ReactiveElement } from '@lit/reactive-element';
import type { Media, PlayerStore } from '@videojs/core/dom';
import { isNull } from '@videojs/utils/predicate';
import type { Constructor } from '@videojs/utils/types';
import type { MediaElementConstructor } from '@/ui/media-element';
import type { PlayerContext } from '../player/context';
import type { PlayerProvider } from './types';
import type { PlayerProvider, PlayerProviderConstructor } from './types';
type Base = Constructor<ReactiveElement>;
type Result<Class extends Base, Store extends PlayerStore> = Class & Constructor<PlayerProvider<Store>>;
export type ProviderMixin<Store extends PlayerStore> = <Class extends Base>(BaseClass: Class) => Result<Class, Store>;
export type ProviderMixin<Store extends PlayerStore> = <Class extends MediaElementConstructor>(
BaseClass: Class
) => Class & PlayerProviderConstructor<Store>;
export function createProviderMixin<Store extends PlayerStore>(
context: PlayerContext<Store>,
factory: () => Store
): ProviderMixin<Store> {
return <Class extends Base>(BaseClass: Class) => {
return <Class extends MediaElementConstructor>(BaseClass: Class) => {
class PlayerProviderElement extends BaseClass implements PlayerProvider<Store> {
#store: Store | null = null;
#media: Media | null = null;
+24 -2
View File
@@ -1,10 +1,32 @@
import type { Media, PlayerStore } from '@videojs/core/dom';
import type { Constructor } from '@videojs/utils/types';
import type { MediaElement } from '@/ui/media-element';
export interface PlayerProvider<Store extends PlayerStore> {
// ----------------------------------------
// PlayerElement
// ----------------------------------------
export interface PlayerElement<Store extends PlayerStore> extends MediaElement, PlayerProvider<Store> {}
export interface PlayerElementConstructor<Store extends PlayerStore> extends Constructor<PlayerElement<Store>> {}
// ----------------------------------------
// PlayerProvider
// ----------------------------------------
export interface PlayerProvider<Store extends PlayerStore> extends MediaElement {
readonly store: Store;
media: Media | null;
}
export interface PlayerConsumer<Store extends PlayerStore> {
export interface PlayerProviderConstructor<Store extends PlayerStore> extends Constructor<PlayerProvider<Store>> {}
// ----------------------------------------
// PlayerConsumer
// ----------------------------------------
export interface PlayerConsumer<Store extends PlayerStore> extends MediaElement {
readonly store: Store | null;
}
export interface PlayerConsumerConstructor<Store extends PlayerStore> extends Constructor<PlayerConsumer<Store>> {}
+3
View File
@@ -1,3 +1,6 @@
import { ReactiveElement } from '@lit/reactive-element';
import type { Constructor } from '@videojs/utils/types';
export class MediaElement extends ReactiveElement {}
export interface MediaElementConstructor extends Constructor<MediaElement> {}