mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(html): setup player api (#374)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
// TODO: register video features here when registrations are added
|
||||
@@ -0,0 +1 @@
|
||||
// TODO: Register HLS video element
|
||||
@@ -0,0 +1,17 @@
|
||||
import { features } from '@videojs/core/dom';
|
||||
import { MediaElement } from '@/ui/media-element';
|
||||
import { createPlayer } from '../../player/create-player';
|
||||
|
||||
const { PlayerMixin } = createPlayer({
|
||||
features: [...features.video],
|
||||
});
|
||||
|
||||
export class VideoPlayer extends PlayerMixin(MediaElement) {}
|
||||
|
||||
customElements.define('video-player', VideoPlayer);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'video-player': VideoPlayer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// TODO: Implement VideoSkinElement and then register it here
|
||||
@@ -0,0 +1 @@
|
||||
// TODO: Implement PlayButtonElement then register it here
|
||||
+15
-23
@@ -1,26 +1,18 @@
|
||||
// Re-exports from core/dom
|
||||
export {
|
||||
type AnyPlayerFeature,
|
||||
type BufferState,
|
||||
definePlayerFeature,
|
||||
type FeatureAvailability,
|
||||
features,
|
||||
type Media,
|
||||
type MediaContainer,
|
||||
type PlaybackState,
|
||||
type PlayerFeature,
|
||||
type PlayerStore,
|
||||
type PlayerTarget,
|
||||
type SourceState,
|
||||
selectBuffer,
|
||||
selectPlayback,
|
||||
selectSource,
|
||||
selectTime,
|
||||
selectVolume,
|
||||
type TimeState,
|
||||
type VolumeState,
|
||||
} from '@videojs/core/dom';
|
||||
// Core
|
||||
export * from '@videojs/core/dom';
|
||||
|
||||
// Re-exports from store (utilities)
|
||||
// Store
|
||||
export type { Comparator, Selector } from '@videojs/store';
|
||||
export { createSelector, shallowEqual } from '@videojs/store';
|
||||
// Player
|
||||
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';
|
||||
// Store bindings
|
||||
export * from './store/types';
|
||||
|
||||
// Primitives
|
||||
export { MediaElement } from './ui/media-element';
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
>;
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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 { PlayerContext } from '../player/context';
|
||||
import type { PlayerConsumer } 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 function createContainerMixin<Store extends PlayerStore>(context: PlayerContext<Store>): ContainerMixin<Store> {
|
||||
return <Class extends Base>(BaseClass: Class) => {
|
||||
class PlayerContainerElement extends BaseClass implements PlayerConsumer<Store>, MediaContainer {
|
||||
#detach = noop;
|
||||
#disposer = new Disposer();
|
||||
|
||||
#consumer = new ContextConsumer(this, {
|
||||
context,
|
||||
callback: () => this.#attachMedia(),
|
||||
subscribe: true,
|
||||
});
|
||||
|
||||
get store(): Store | null {
|
||||
return (this.#consumer.value?.store as Store) ?? null;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (this.shadowRoot) {
|
||||
const slot = querySlot(this.shadowRoot, '');
|
||||
if (slot) this.#disposer.add(listen(slot, 'slotchange', () => this.#attachMedia()));
|
||||
}
|
||||
|
||||
this.#attachMedia();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.#disposer.dispose();
|
||||
this.#detach();
|
||||
}
|
||||
|
||||
#attachMedia() {
|
||||
const ctx = this.#consumer.value;
|
||||
if (!ctx) return;
|
||||
|
||||
const { store, media } = ctx;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
const target: PlayerTarget = {
|
||||
media,
|
||||
container: this,
|
||||
};
|
||||
|
||||
if (store.target?.media !== target.media || store.target?.container !== target.container) {
|
||||
this.#detach();
|
||||
this.#detach = store.attach(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PlayerContainerElement;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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 { PlayerContext } from '../player/context';
|
||||
import type { PlayerProvider } 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 function createProviderMixin<Store extends PlayerStore>(
|
||||
context: PlayerContext<Store>,
|
||||
factory: () => Store
|
||||
): ProviderMixin<Store> {
|
||||
return <Class extends Base>(BaseClass: Class) => {
|
||||
class PlayerProviderElement extends BaseClass implements PlayerProvider<Store> {
|
||||
#store: Store | null = null;
|
||||
#media: Media | null = null;
|
||||
|
||||
#provider = new ContextProvider(this, {
|
||||
context,
|
||||
initialValue: { store: this.store, media: null },
|
||||
});
|
||||
|
||||
get store(): Store {
|
||||
if (isNull(this.#store)) {
|
||||
this.#store = factory();
|
||||
}
|
||||
return this.#store;
|
||||
}
|
||||
|
||||
get media(): Media | null {
|
||||
return this.#media;
|
||||
}
|
||||
|
||||
set media(value: Media | null) {
|
||||
this.#media = value;
|
||||
this.#provider.setValue({ store: this.store, media: value });
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.#store) {
|
||||
this.#store.destroy();
|
||||
this.#store = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PlayerProviderElement;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Media, PlayerStore } from '@videojs/core/dom';
|
||||
|
||||
export interface PlayerProvider<Store extends PlayerStore> {
|
||||
readonly store: Store;
|
||||
media: Media | null;
|
||||
}
|
||||
|
||||
export interface PlayerConsumer<Store extends PlayerStore> {
|
||||
readonly store: Store | null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ReactiveElement } from '@lit/reactive-element';
|
||||
|
||||
export class MediaElement extends ReactiveElement {}
|
||||
Reference in New Issue
Block a user