refactor(html): separate provider and container concerns in createPlayer (#635)

This commit is contained in:
rahim
2026-02-27 16:42:47 -08:00
committed by GitHub
parent 7bae887920
commit ed22730592
33 changed files with 348 additions and 315 deletions
+4 -2
View File
@@ -1,11 +1,13 @@
import { features } from '@videojs/core/dom';
import { createPlayer } from '../../player/create-player';
import { MediaElement } from '../../ui/media-element';
import '../media/container';
const { PlayerElement } = createPlayer({
const { ProviderMixin } = createPlayer({
features: features.audio,
});
export class AudioPlayerElement extends PlayerElement {
export class AudioPlayerElement extends ProviderMixin(MediaElement) {
static readonly tagName = 'audio-player';
}
@@ -1,11 +1,13 @@
import { features } from '@videojs/core/dom';
import { createPlayer } from '../../player/create-player';
import { MediaElement } from '../../ui/media-element';
import '../media/container';
const { PlayerElement } = createPlayer({
const { ProviderMixin } = createPlayer({
features: features.background,
});
export class BackgroundVideoPlayerElement extends PlayerElement {
export class BackgroundVideoPlayerElement extends ProviderMixin(MediaElement) {
static readonly tagName = 'background-video-player';
}
@@ -0,0 +1,17 @@
import { playerContext } from '../../player/context';
import { createContainerMixin } from '../../store/container-mixin';
import { MediaElement } from '../../ui/media-element';
const ContainerMixin = createContainerMixin(playerContext);
export class MediaContainerElement extends ContainerMixin(MediaElement) {
static readonly tagName = 'media-container';
}
customElements.define(MediaContainerElement.tagName, MediaContainerElement);
declare global {
interface HTMLElementTagNameMap {
[MediaContainerElement.tagName]: MediaContainerElement;
}
}
+4 -2
View File
@@ -1,11 +1,13 @@
import { features } from '@videojs/core/dom';
import { createPlayer } from '../../player/create-player';
import { MediaElement } from '../../ui/media-element';
import '../media/container';
const { PlayerElement } = createPlayer({
const { ProviderMixin } = createPlayer({
features: features.video,
});
export class VideoPlayerElement extends PlayerElement {
export class VideoPlayerElement extends ProviderMixin(MediaElement) {
static readonly tagName = 'video-player';
}
-1
View File
@@ -9,7 +9,6 @@ export { createSelector, shallowEqual } from '@videojs/store';
export * from './player/context';
export * from './player/create-player';
export * from './player/player-controller';
export * from './player/player-mixin';
export * from './store/container-mixin';
export * from './store/provider-mixin';
export * from './store/types';
+4 -19
View File
@@ -11,11 +11,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';
export interface CreatePlayerConfig<Features extends AnyPlayerFeature[]> {
features: Features;
@@ -31,12 +28,6 @@ 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>;
/** Mixin that provides player context to descendants. */
ProviderMixin: ProviderMixin<Store>;
@@ -52,15 +43,13 @@ export interface CreatePlayerResult<Store extends PlayerStore> {
* import { features } from '@videojs/core/dom';
* import { createPlayer, MediaElement } from '@videojs/html';
*
* const { PlayerElement, PlayerController, context } = createPlayer({
* const { ProviderMixin, ContainerMixin, PlayerController, context } = createPlayer({
* features: features.video,
* });
*
* // Simple: register pre-composed PlayerElement
* customElements.define('video-player', PlayerElement);
*
* // Custom: extend with PlayerMixin
* class MyPlayer extends PlayerMixin(MediaElement) {}
* // Provider element: owns the store, provides context to descendants
* class VideoPlayer extends ProviderMixin(MediaElement) {}
* customElements.define('video-player', VideoPlayer);
*
* // Control element with selector
* class PlayButton extends MediaElement {
@@ -98,8 +87,6 @@ export function createPlayer(config: CreatePlayerConfig<AnyPlayerFeature[]>): Cr
return createStore<PlayerTarget>()(slice);
}
const PlayerMixin = createPlayerMixin<PlayerStore>(playerContext, create);
const PlayerElement = PlayerMixin(MediaElement);
const ProviderMixin = createProviderMixin<PlayerStore>(playerContext, create);
const ContainerMixin = createContainerMixin<PlayerStore>(playerContext);
@@ -107,8 +94,6 @@ export function createPlayer(config: CreatePlayerConfig<AnyPlayerFeature[]>): Cr
context: playerContext,
create,
PlayerController,
PlayerElement,
PlayerMixin,
ProviderMixin,
ContainerMixin,
};
-33
View File
@@ -1,33 +0,0 @@
import type { PlayerStore } from '@videojs/core/dom';
import type { MediaElementConstructor } from '@/ui/media-element';
import { createContainerMixin } from '../store/container-mixin';
import { createProviderMixin } from '../store/provider-mixin';
import type { PlayerProviderConstructor } from '../store/types';
import type { PlayerContext } from './context';
type Result<Class extends MediaElementConstructor, Store extends PlayerStore> = Class &
PlayerProviderConstructor<Store>;
export type PlayerMixin<Store extends PlayerStore> = <Class extends MediaElementConstructor>(
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.
*
* @param context - Player context for descendant consumption.
* @param factory - Factory function that creates a store instance.
*/
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 MediaElementConstructor>(BaseClass: Class) => {
return ProviderMixin(ContainerMixin(BaseClass)) as unknown as Result<Class, Store>;
};
}
@@ -1,6 +1,7 @@
import { features } from '@videojs/core/dom';
import { describe, expect, it } from 'vitest';
import { MediaElement } from '../../ui/media-element';
import { createPlayer } from '../create-player';
describe('createPlayer', () => {
@@ -10,8 +11,6 @@ describe('createPlayer', () => {
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);
});
@@ -25,11 +24,20 @@ describe('createPlayer', () => {
expect(store.destroy).toBeInstanceOf(Function);
});
it('PlayerElement is a valid custom element class', () => {
const { PlayerElement } = createPlayer({ features: features.video });
it('ProviderMixin produces a valid custom element class', () => {
const { ProviderMixin } = createPlayer({ features: features.video });
const ProviderElement = ProviderMixin(MediaElement);
expect(typeof PlayerElement).toBe('function');
expect(PlayerElement.prototype).toBeDefined();
expect(typeof ProviderElement).toBe('function');
expect(ProviderElement.prototype).toBeDefined();
});
it('ContainerMixin produces a valid custom element class', () => {
const { ContainerMixin } = createPlayer({ features: features.video });
const ContainerElement = ContainerMixin(MediaElement);
expect(typeof ContainerElement).toBe('function');
expect(ContainerElement.prototype).toBeDefined();
});
it('creates audio player with expected exports', () => {
@@ -38,8 +46,6 @@ describe('createPlayer', () => {
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);
});
@@ -50,8 +56,6 @@ describe('createPlayer', () => {
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);
});
-8
View File
@@ -2,14 +2,6 @@ import type { PlayerStore } from '@videojs/core/dom';
import type { Constructor } from '@videojs/utils/types';
import type { MediaElement } from '@/ui/media-element';
// ----------------------------------------
// PlayerElement
// ----------------------------------------
export interface PlayerElement<Store extends PlayerStore> extends MediaElement, PlayerProvider<Store> {}
export interface PlayerElementConstructor<Store extends PlayerStore> extends Constructor<PlayerElement<Store>> {}
// ----------------------------------------
// PlayerProvider
// ----------------------------------------
+4 -8
View File
@@ -1,18 +1,14 @@
// HTML sandbox — Web player (DOM/Browser)
// http://localhost:5173/html/
import { createPlayer, features } from '@videojs/html';
const { PlayerElement } = createPlayer({
features: features.video,
});
customElements.define('video-player', PlayerElement);
import '@videojs/html/video/player';
const html = String.raw;
document.getElementById('root')!.innerHTML = html`
<video-player>
<video src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"></video>
<media-container>
<video src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"></video>
</media-container>
</video-player>
`;
@@ -1,12 +1,14 @@
<video-player class="html-buffering-indicator-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-buffering-indicator class="html-buffering-indicator-basic__overlay">
<div class="html-buffering-indicator-basic__spinner"></div>
</media-buffering-indicator>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-buffering-indicator class="html-buffering-indicator-basic__overlay">
<div class="html-buffering-indicator-basic__spinner"></div>
</media-buffering-indicator>
</media-container>
</video-player>
@@ -1,19 +1,21 @@
<video-player class="html-controls-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-controls class="html-controls-basic__root">
<media-controls-group class="html-controls-basic__bottom" aria-label="Playback controls">
<media-play-button class="html-controls-basic__button html-controls-basic__play">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
</media-play-button>
<media-time class="html-controls-basic__time" type="current"></media-time>
</media-controls-group>
</media-controls>
<media-controls class="html-controls-basic__root">
<media-controls-group class="html-controls-basic__bottom" aria-label="Playback controls">
<media-play-button class="html-controls-basic__button html-controls-basic__play">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
</media-play-button>
<media-time class="html-controls-basic__time" type="current"></media-time>
</media-controls-group>
</media-controls>
</media-container>
</video-player>
@@ -1,13 +1,15 @@
<video-player class="html-fullscreen-button-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-fullscreen-button class="html-fullscreen-button-basic__button">
<span class="show-when-fullscreen">Exit Fullscreen</span>
<span class="show-when-not-fullscreen">Fullscreen</span>
</media-fullscreen-button>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-fullscreen-button class="html-fullscreen-button-basic__button">
<span class="show-when-fullscreen">Exit Fullscreen</span>
<span class="show-when-not-fullscreen">Fullscreen</span>
</media-fullscreen-button>
</media-container>
</video-player>
@@ -1,12 +1,14 @@
<demo-video-player class="html-create-player-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
></video>
<demo-play-toggle class="html-create-player-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
</demo-play-toggle>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
></video>
<demo-play-toggle class="html-create-player-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
</demo-play-toggle>
</media-container>
</demo-video-player>
@@ -7,12 +7,13 @@ import {
MediaElement,
selectPlayback,
} from '@videojs/html';
import '@videojs/html/media/container';
const { PlayerElement, PlayerController, context } = createPlayer({
const { ProviderMixin, PlayerController, context } = createPlayer({
features: [...features.video],
});
class VideoPlayer extends PlayerElement {
class VideoPlayer extends ProviderMixin(MediaElement) {
static readonly tagName = 'demo-video-player';
}
@@ -1,13 +1,15 @@
<video-player class="html-mute-button-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-mute-button class="html-mute-button-basic__button">
<span class="show-when-muted">Unmute</span>
<span class="show-when-unmuted">Mute</span>
</media-mute-button>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-mute-button class="html-mute-button-basic__button">
<span class="show-when-muted">Unmute</span>
<span class="show-when-unmuted">Mute</span>
</media-mute-button>
</media-container>
</video-player>
@@ -1,15 +1,17 @@
<video-player class="html-mute-button-volume-levels">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-mute-button class="html-mute-button-volume-levels__button">
<span class="level-off">Off</span>
<span class="level-low">Low</span>
<span class="level-medium">Medium</span>
<span class="level-high">High</span>
</media-mute-button>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-mute-button class="html-mute-button-volume-levels__button">
<span class="level-off">Off</span>
<span class="level-low">Low</span>
<span class="level-medium">Medium</span>
<span class="level-high">High</span>
</media-mute-button>
</media-container>
</video-player>
@@ -1,13 +1,15 @@
<video-player class="html-pip-button-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-pip-button class="html-pip-button-basic__button">
<span class="show-when-pip">Exit PiP</span>
<span class="show-when-not-pip">Enter PiP</span>
</media-pip-button>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-pip-button class="html-pip-button-basic__button">
<span class="show-when-pip">Exit PiP</span>
<span class="show-when-not-pip">Enter PiP</span>
</media-pip-button>
</media-container>
</video-player>
@@ -1,13 +1,15 @@
<video-player class="html-play-button-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
></video>
<media-play-button class="html-play-button-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
<span class="show-when-ended">Replay</span>
</media-play-button>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
></video>
<media-play-button class="html-play-button-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
<span class="show-when-ended">Replay</span>
</media-play-button>
</media-container>
</video-player>
@@ -1,18 +1,20 @@
<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>
<media-container>
<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 type="button" class="action-play">Play</button>
<button type="button" class="action-pause">Pause</button>
<button type="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>
</media-container>
</demo-ctrl-player>
@@ -1,10 +1,11 @@
import { applyElementProps, createButton, createPlayer, features, MediaElement } from '@videojs/html';
import '@videojs/html/media/container';
const { PlayerElement, PlayerController, context } = createPlayer({
const { ProviderMixin, PlayerController, context } = createPlayer({
features: [...features.video],
});
class DemoPlayer extends PlayerElement {
class DemoPlayer extends ProviderMixin(MediaElement) {
static readonly tagName = 'demo-ctrl-player';
}
@@ -1,19 +1,21 @@
<!-- biome-ignore-all lint/a11y/useMediaCaption: TODO -->
<video-player class="html-poster-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
playsinline
></video>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
playsinline
></video>
<media-poster class="html-poster-basic__poster">
<img
src="https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/thumbnail.jpg"
alt="Video thumbnail"
/>
</media-poster>
<media-poster class="html-poster-basic__poster">
<img
src="https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/thumbnail.jpg"
alt="Video thumbnail"
/>
</media-poster>
<media-play-button class="html-poster-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
</media-play-button>
<media-play-button class="html-poster-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
</media-play-button>
</media-container>
</video-player>
@@ -1,17 +1,19 @@
<video-player class="html-seek-button-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<div class="html-seek-button-basic__buttons">
<media-seek-button seconds="-5" class="html-seek-button-basic__button">
<span class="show-when-backward">&#9194; 5s</span>
</media-seek-button>
<media-seek-button seconds="10" class="html-seek-button-basic__button">
<span class="show-when-forward">10s &#9193;</span>
</media-seek-button>
</div>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<div class="html-seek-button-basic__buttons">
<media-seek-button seconds="-5" class="html-seek-button-basic__button">
<span class="show-when-backward">&#9194; 5s</span>
</media-seek-button>
<media-seek-button seconds="10" class="html-seek-button-basic__button">
<span class="show-when-forward">10s &#9193;</span>
</media-seek-button>
</div>
</media-container>
</video-player>
@@ -1,14 +1,16 @@
<video-player class="html-time-current-duration">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time-group class="html-time-current-duration__group">
<media-time type="current"></media-time>
<media-time-separator></media-time-separator>
<media-time type="duration"></media-time>
</media-time-group>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time-group class="html-time-current-duration__group">
<media-time type="current"></media-time>
<media-time-separator></media-time-separator>
<media-time type="duration"></media-time>
</media-time-group>
</media-container>
</video-player>
@@ -1,10 +1,12 @@
<video-player class="html-time-current-time">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time class="html-time-current-time__value" type="current"></media-time>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time class="html-time-current-time__value" type="current"></media-time>
</media-container>
</video-player>
@@ -1,14 +1,16 @@
<video-player class="html-time-custom-negative-sign">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time
class="html-time-custom-negative-sign__value"
type="remaining"
negative-sign="~"
></media-time>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time
class="html-time-custom-negative-sign__value"
type="remaining"
negative-sign="~"
></media-time>
</media-container>
</video-player>
@@ -1,14 +1,16 @@
<video-player class="html-time-custom-separator">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time-group class="html-time-custom-separator__group">
<media-time type="current"></media-time>
<media-time-separator> of </media-time-separator>
<media-time type="duration"></media-time>
</media-time-group>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time-group class="html-time-custom-separator__group">
<media-time type="current"></media-time>
<media-time-separator> of </media-time-separator>
<media-time type="duration"></media-time>
</media-time-group>
</media-container>
</video-player>
@@ -1,10 +1,12 @@
<video-player class="html-time-remaining">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time class="html-time-remaining__value" type="remaining"></media-time>
<media-container>
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-time class="html-time-remaining__value" type="remaining"></media-time>
</media-container>
</video-player>
@@ -12,7 +12,13 @@ import DocsLink from "@/components/docs/DocsLink.astro";
Use `ContainerMixin` when the <DocsLink slug="reference/provider-mixin">provider</DocsLink> and container live in different elements. This is common when the store owner sits higher in the tree (e.g., a layout shell) while the media element sits deeper in a content area.
For a single element that both owns the store and attaches media, use <DocsLink slug="reference/player-mixin">`PlayerMixin`</DocsLink> instead.
For a single element that both owns the store and attaches media, compose both mixins on the same base class:
```ts
const { ProviderMixin, ContainerMixin } = createPlayer({ features: features.video });
class VideoPlayer extends ProviderMixin(ContainerMixin(MediaElement)) {}
```
### How media discovery works
@@ -6,33 +6,60 @@ description: Factory function that creates a player instance with typed store, m
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import Demo from "@/components/docs/demos/Demo.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
import StyleCase from "@/components/docs/StyleCase.astro";
import BasicUsageDemoHtml from "@/components/docs/demos/html-create-player/html/css/BasicUsage.astro";
import basicUsageHtml from "@/components/docs/demos/html-create-player/html/css/BasicUsage.html?raw";
import basicUsageHtmlCss from "@/components/docs/demos/html-create-player/html/css/BasicUsage.css?raw";
import basicUsageHtmlTs from "@/components/docs/demos/html-create-player/html/css/BasicUsage.ts?raw";
`createPlayer` is the entry point for setting up a Video.js player with HTML custom elements. It accepts a configuration object with a `features` array and returns everything needed to build a player: a pre-composed `PlayerElement`, typed <DocsLink slug="reference/player-controller">`PlayerController`</DocsLink>, `context`, individual mixins, and a `create` factory.
`createPlayer` is the entry point for setting up a Video.js player with HTML custom elements. It accepts a configuration object with a `features` array and returns a typed <DocsLink slug="reference/player-controller">`PlayerController`</DocsLink>, `context`, `ProviderMixin`, `ContainerMixin`, and a `create` store factory.
```ts
import { createPlayer, features, MediaElement } from '@videojs/html';
const {
PlayerElement,
PlayerController,
context,
PlayerMixin,
ProviderMixin,
ContainerMixin,
create,
} = createPlayer({ features: features.video });
// Register the pre-composed player element
customElements.define('video-player', PlayerElement);
// Provider element: owns and publishes the store
class VideoPlayer extends ProviderMixin(MediaElement) {}
// Container element: discovers <video>/<audio> and calls store.attach()
class MediaContainer extends ContainerMixin(MediaElement) {}
customElements.define('video-player', VideoPlayer);
customElements.define('media-container', MediaContainer);
```
The simplest approach is to register `PlayerElement` directly. For custom behavior, use the individual mixins to compose your own element classes.
### Composition patterns
Use split elements (recommended for reusable skins/layouts):
```ts
const {
PlayerController,
context,
ProviderMixin,
ContainerMixin,
} = createPlayer({ features: features.video });
class VideoPlayer extends ProviderMixin(MediaElement) {}
class MediaContainer extends ContainerMixin(MediaElement) {}
```
Use a single composed element when the same element should both own the store and attach media:
```ts
const { ProviderMixin, ContainerMixin } = createPlayer({ features: features.video });
class VideoPlayer extends ProviderMixin(ContainerMixin(MediaElement)) {}
```
When using the built-in defined elements (`@videojs/html/video/player`, etc.), `media-container` is expected as the first child and UI should live inside it.
## Examples
@@ -1,41 +1,35 @@
---
title: PlayerMixin
description: Composed mixin combining ProviderMixin and ContainerMixin for a complete player element
title: PlayerMixin (Removed)
description: Migration guide for replacing removed PlayerMixin/PlayerElement APIs
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
`PlayerMixin` combines <DocsLink slug="reference/provider-mixin">`ProviderMixin`</DocsLink> and <DocsLink slug="reference/container-mixin">`ContainerMixin`</DocsLink> into a single mixin. The resulting class both owns the player store and auto-attaches media elements, making it a complete player element. It is equivalent to `ProviderMixin(ContainerMixin(Base))` — it creates the store on connect, observes the DOM for `<video>` or `<audio>` children, and publishes the store to descendant elements via context.
`PlayerMixin` and `PlayerElement` were removed from HTML `createPlayer`. Use <DocsLink slug="reference/provider-mixin">`ProviderMixin`</DocsLink> and <DocsLink slug="reference/container-mixin">`ContainerMixin`</DocsLink> directly.
### Choose your approach
### Migration
| Approach | When to use |
|----------|-------------|
| `PlayerElement` | Simplest setup. Register and use directly -- no custom class needed. |
| `PlayerMixin(Base)` | Custom player element with additional lifecycle logic or methods. |
| <DocsLink slug="reference/provider-mixin">`ProviderMixin`</DocsLink> + <DocsLink slug="reference/container-mixin">`ContainerMixin`</DocsLink> | Provider and container are separate elements (e.g., store owner is a layout shell, media lives in a content region). |
### PlayerElement vs PlayerMixin
For the simplest setup, use the pre-composed `PlayerElement` from `createPlayer`:
Old:
```ts
const { PlayerElement } = createPlayer({ features: features.video });
customElements.define('video-player', PlayerElement);
const { PlayerElement, PlayerMixin } = createPlayer({ features: features.video });
```
Use `PlayerMixin` when you need to add custom behavior:
New:
```ts
class MyPlayer extends PlayerMixin(MediaElement) {
connectedCallback() {
super.connectedCallback();
this.store.subscribe(() => {
// Custom logic on state changes
});
}
}
const { ProviderMixin, ContainerMixin } = createPlayer({ features: features.video });
// Split elements
class VideoPlayer extends ProviderMixin(MediaElement) {}
class MediaContainer extends ContainerMixin(MediaElement) {}
// Single element equivalent of old PlayerMixin
class ComposedPlayer extends ProviderMixin(ContainerMixin(MediaElement)) {}
```
<UtilReference util="PlayerMixin" />
### Notes
- Built-in player elements (`@videojs/html/video/player`, `@videojs/html/audio/player`, etc.) now use a provider element plus `<media-container>` for media attachment.
- Barebones player markup should use `media-container` as first child, with UI inside the container.
- See <DocsLink slug="reference/html-create-player">`createPlayer`</DocsLink> for current API and examples.
@@ -28,6 +28,12 @@ class AppShell extends ProviderMixin(MediaElement) {}
class VideoRegion extends ContainerMixin(MediaElement) {}
```
When a single element should handle both, use <DocsLink slug="reference/player-mixin">`PlayerMixin`</DocsLink> instead.
When a single element should handle both responsibilities, compose the mixins directly:
```ts
const { ProviderMixin, ContainerMixin } = createPlayer({ features: features.video });
class VideoPlayer extends ProviderMixin(ContainerMixin(MediaElement)) {}
```
<UtilReference util="ProviderMixin" />
+1 -1
View File
@@ -70,7 +70,7 @@ export const sidebar: Sidebar = [
contents: [
{ slug: 'reference/html-create-player', sidebarLabel: 'createPlayer' },
{ slug: 'reference/player-controller' },
{ slug: 'reference/player-mixin' },
{ slug: 'reference/player-mixin', sidebarLabel: 'PlayerMixin (removed)' },
{
sidebarLabel: 'Advanced',
defaultOpen: false,