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

This commit is contained in:
rahim
2026-02-02 21:11:44 +11:00
committed by GitHub
parent a1d0383d41
commit a419f5ef19
40 changed files with 678 additions and 1078 deletions
@@ -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;
};
}
+57
View File
@@ -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;
};
}
+10
View File
@@ -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;
}