mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(html): separate provider and container concerns in createPlayer (#635)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
// ----------------------------------------
|
||||
|
||||
@@ -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">⏪ 5s</span>
|
||||
</media-seek-button>
|
||||
<media-seek-button seconds="10" class="html-seek-button-basic__button">
|
||||
<span class="show-when-forward">10s ⏩</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">⏪ 5s</span>
|
||||
</media-seek-button>
|
||||
<media-seek-button seconds="10" class="html-seek-button-basic__button">
|
||||
<span class="show-when-forward">10s ⏩</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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user