diff --git a/packages/core/src/dom/tsconfig.json b/packages/core/src/dom/tsconfig.json index 7525a101..44b4edee 100644 --- a/packages/core/src/dom/tsconfig.json +++ b/packages/core/src/dom/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "composite": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], - "declarationDir": "../../types/dom" + "declarationDir": "../../types/dom", + "useDefineForClassFields": false }, "references": [{ "path": "../.." }], "include": ["./**/*.ts"] diff --git a/packages/html/src/player/context.ts b/packages/html/src/player/context.ts index 35d4548b..b662127b 100644 --- a/packages/html/src/player/context.ts +++ b/packages/html/src/player/context.ts @@ -1,12 +1,9 @@ import { type Context, createContext } from '@lit/context'; -import type { AnyPlayerStore, Media, PlayerStore } from '@videojs/core/dom'; +import type { AnyPlayerStore, PlayerStore } from '@videojs/core/dom'; export const PLAYER_CONTEXT_KEY = Symbol('@videojs/player'); -export interface PlayerContextValue { - store: Store; - media: Media | null; -} +export type PlayerContextValue = Store; export type PlayerContext = Context< typeof PLAYER_CONTEXT_KEY, diff --git a/packages/html/src/player/create-player.ts b/packages/html/src/player/create-player.ts index 0c058f59..b5dc8b27 100644 --- a/packages/html/src/player/create-player.ts +++ b/packages/html/src/player/create-player.ts @@ -83,14 +83,13 @@ export function createPlayer(config: CreatePlayerConfig): Cr return createStore()(slice); } - const ctx = playerContext; - const PlayerMixin = createPlayerMixin(ctx, create); + const PlayerMixin = createPlayerMixin(playerContext, create); const PlayerElement = PlayerMixin(MediaElement); - const ProviderMixin = createProviderMixin(ctx, create); - const ContainerMixin = createContainerMixin(ctx); + const ProviderMixin = createProviderMixin(playerContext, create); + const ContainerMixin = createContainerMixin(playerContext); return { - context: ctx, + context: playerContext, create, PlayerController, PlayerElement, diff --git a/packages/html/src/player/player-controller.ts b/packages/html/src/player/player-controller.ts index 7d9c3116..03e404d5 100644 --- a/packages/html/src/player/player-controller.ts +++ b/packages/html/src/player/player-controller.ts @@ -4,7 +4,7 @@ import type { PlayerStore } from '@videojs/core/dom'; import type { InferStoreState, Selector } from '@videojs/store'; import { StoreController } from '@videojs/store/lit'; -import type { PlayerContext, PlayerContextValue } from './context'; +import type { PlayerContext } from './context'; export type PlayerControllerHost = ReactiveControllerHost & HTMLElement; @@ -62,31 +62,28 @@ export class PlayerController impleme } get value(): Result | undefined { - const ctx = this.#consumer.value; - if (!ctx) return undefined; + const store = this.#consumer.value; + if (!store) return undefined; // Without selector: return store directly - if (!this.#selector) return ctx.store as unknown as Result; + if (!this.#selector) return store as unknown as Result; // With selector: use StoreController return this.#store?.value; } hostConnected(): void { - const ctx = this.#consumer.value; - if (ctx) this.#connect(ctx); + const store = this.#consumer.value; + if (store) this.#connect(store); } hostDisconnected(): void { this.#store = null; } - #connect(ctx: PlayerContextValue | undefined): void { - if (!ctx) return; - - // Create StoreController with the store directly + #connect(store: Store): void { if (!this.#store && this.#selector) { - this.#store = new StoreController(this.#host, ctx.store, this.#selector); + this.#store = new StoreController(this.#host, store, this.#selector); } } } diff --git a/packages/html/src/player/player-mixin.ts b/packages/html/src/player/player-mixin.ts index dbd609b6..1b7025bf 100644 --- a/packages/html/src/player/player-mixin.ts +++ b/packages/html/src/player/player-mixin.ts @@ -25,6 +25,6 @@ export function createPlayerMixin( const ContainerMixin = createContainerMixin(context); return (BaseClass: Class) => { - return ContainerMixin(ProviderMixin(BaseClass)) as unknown as Result; + return ProviderMixin(ContainerMixin(BaseClass)) as unknown as Result; }; } diff --git a/packages/html/src/store/container-mixin.ts b/packages/html/src/store/container-mixin.ts index 8c4c2c35..4a1d4cbe 100644 --- a/packages/html/src/store/container-mixin.ts +++ b/packages/html/src/store/container-mixin.ts @@ -1,6 +1,5 @@ import { ContextConsumer } from '@lit/context'; -import type { MediaContainer, PlayerStore, PlayerTarget } from '@videojs/core/dom'; -import { listen, querySlot } from '@videojs/utils/dom'; +import type { Media, MediaContainer, PlayerStore, PlayerTarget } from '@videojs/core/dom'; import { noop } from '@videojs/utils/function'; import type { MediaElementConstructor } from '@/ui/media-element'; import type { PlayerContext } from '../player/context'; @@ -14,7 +13,7 @@ export function createContainerMixin(context: PlayerC return (BaseClass: Class) => { class PlayerContainerElement extends BaseClass implements PlayerConsumer, MediaContainer { #detach = noop; - #disconnect: AbortController | null = null; + #observer: MutationObserver | null = null; #consumer = new ContextConsumer(this, { context, @@ -23,44 +22,54 @@ export function createContainerMixin(context: PlayerC }); get store(): Store | null { - return (this.#consumer.value?.store as Store) ?? null; + return this.#consumer.value ?? null; + } + + protected createRenderRoot() { + return this; } override connectedCallback() { super.connectedCallback(); - this.#disconnect?.abort(); - this.#disconnect = new AbortController(); + this.#observer = new MutationObserver((records) => { + if (records.some(hasMediaNode)) this.#attachMedia(); + }); - if (this.shadowRoot) { - const slot = querySlot(this.shadowRoot, ''); - if (slot) listen(slot, 'slotchange', () => this.#attachMedia(), { signal: this.#disconnect.signal }); - } + this.#observer.observe(this, { childList: true, subtree: true }); this.#attachMedia(); } override disconnectedCallback() { super.disconnectedCallback(); - this.#disconnect?.abort(); - this.#disconnect = null; + this.#observer?.disconnect(); + this.#observer = null; this.#detach(); } #attachMedia() { - const ctx = this.#consumer.value; - if (!ctx) return; + // Store will be overridden and set by provider mixin if consumer is empty. + const store = this.#consumer.value ?? this.store; + if (!store) return; - const { store, media } = ctx; + const media = this.querySelector('video, audio'); - if (!media) return; + if (!media) { + this.#detach(); + this.#detach = noop; + return; + } const target: PlayerTarget = { media, container: this, }; - if (store.target?.media !== target.media || store.target?.container !== target.container) { + const hasMediaChanged = store.target?.media !== target.media, + hasContainerChanged = store.target?.container !== target.container; + + if (hasMediaChanged || hasContainerChanged) { this.#detach(); this.#detach = store.attach(target); } @@ -70,3 +79,19 @@ export function createContainerMixin(context: PlayerC return PlayerContainerElement; }; } + +function isMediaNode(node: Node): boolean { + return node instanceof HTMLMediaElement; +} + +function hasMediaNode(record: MutationRecord): boolean { + for (const node of record.addedNodes) { + if (isMediaNode(node)) return true; + } + + for (const node of record.removedNodes) { + if (isMediaNode(node)) return true; + } + + return false; +} diff --git a/packages/html/src/store/provider-mixin.ts b/packages/html/src/store/provider-mixin.ts index b7d4347d..e156ea71 100644 --- a/packages/html/src/store/provider-mixin.ts +++ b/packages/html/src/store/provider-mixin.ts @@ -1,5 +1,5 @@ import { ContextProvider } from '@lit/context'; -import type { Media, PlayerStore } from '@videojs/core/dom'; +import type { PlayerStore } from '@videojs/core/dom'; import { isNull } from '@videojs/utils/predicate'; import type { MediaElementConstructor } from '@/ui/media-element'; import type { PlayerContext } from '../player/context'; @@ -15,36 +15,34 @@ export function createProviderMixin( ): ProviderMixin { return (BaseClass: Class) => { class PlayerProviderElement extends BaseClass implements PlayerProvider { - #store: Store | null = null; - #media: Media | null = null; + #store: Store | null = factory(); #provider = new ContextProvider(this, { context, - initialValue: { store: this.store, media: null }, + initialValue: this.store, }); get store(): Store { if (isNull(this.#store)) { this.#store = factory(); } + return this.#store; } - get media(): Media | null { - return this.#media; + protected createRenderRoot() { + return this; } - set media(value: Media | null) { - this.#media = value; - this.#provider.setValue({ store: this.store, media: value }); + override connectedCallback() { + super.connectedCallback(); + this.#provider.setValue(this.store); } override disconnectedCallback() { super.disconnectedCallback(); - if (this.#store) { - this.#store.destroy(); - this.#store = null; - } + this.#store?.destroy(); + this.#store = null; } } diff --git a/packages/html/src/store/types.ts b/packages/html/src/store/types.ts index 59c59fb5..054f27f2 100644 --- a/packages/html/src/store/types.ts +++ b/packages/html/src/store/types.ts @@ -1,4 +1,4 @@ -import type { Media, PlayerStore } from '@videojs/core/dom'; +import type { PlayerStore } from '@videojs/core/dom'; import type { Constructor } from '@videojs/utils/types'; import type { MediaElement } from '@/ui/media-element'; @@ -16,7 +16,6 @@ export interface PlayerElementConstructor extends Con export interface PlayerProvider extends MediaElement { readonly store: Store; - media: Media | null; } export interface PlayerProviderConstructor extends Constructor> {} diff --git a/packages/html/src/ui/media-element.ts b/packages/html/src/ui/media-element.ts index 9f2763ed..1e441028 100644 --- a/packages/html/src/ui/media-element.ts +++ b/packages/html/src/ui/media-element.ts @@ -1,6 +1,10 @@ import { ReactiveElement } from '@lit/reactive-element'; import type { Constructor } from '@videojs/utils/types'; -export class MediaElement extends ReactiveElement {} +export class MediaElement extends ReactiveElement { + protected createRenderRoot() { + return this; + } +} export interface MediaElementConstructor extends Constructor {} diff --git a/packages/html/tsconfig.json b/packages/html/tsconfig.json index 4bb47b9d..fba333aa 100644 --- a/packages/html/tsconfig.json +++ b/packages/html/tsconfig.json @@ -6,7 +6,8 @@ "paths": { "@/*": ["src/*"] }, - "declarationDir": "types" + "declarationDir": "types", + "useDefineForClassFields": false }, "include": ["src"] } diff --git a/packages/react/src/ui/time/time-value.tsx b/packages/react/src/ui/time/time-value.tsx index 2eb8bb98..461294f7 100644 --- a/packages/react/src/ui/time/time-value.tsx +++ b/packages/react/src/ui/time/time-value.tsx @@ -57,7 +57,7 @@ export const Value = forwardRef(function Value( ref: [forwardedRef], props: [ { - datetime: state.datetime, + dateTime: state.datetime, children: content, ...core.getAttrs(state), },