diff --git a/internal/decisions/media/mux-data-monitor-lifecycle.md b/internal/decisions/media/mux-data-monitor-lifecycle.md index c2eebeeb87..79fe99d743 100644 --- a/internal/decisions/media/mux-data-monitor-lifecycle.md +++ b/internal/decisions/media/mux-data-monitor-lifecycle.md @@ -7,7 +7,7 @@ date: 2026-09-01 ## Decision -`MuxData` starts one `mux-embed` monitor per target and keeps it alive for the target's lifetime. A source change is reported to the live monitor as `videochange`, and an engine swap re-hooks engine telemetry on the live monitor. The monitor is destroyed and re-created only when the target changes or when an option baked into `monitor()` itself changes (SDK, beacon domain, debug, cookies). +`MuxDataExtension` starts one `mux-embed` monitor per target and keeps it alive for the target's lifetime. A source change is reported to the live monitor as `videochange`, and an engine swap re-hooks engine telemetry on the live monitor. The monitor is destroyed and re-created only when the target changes or when an option baked into `monitor()` itself changes (SDK, beacon domain, debug, cookies). ## Why diff --git a/packages/google-cast/src/google-cast-provider.ts b/packages/google-cast/src/google-cast-provider.ts index 6694f58291..1d96d70033 100644 --- a/packages/google-cast/src/google-cast-provider.ts +++ b/packages/google-cast/src/google-cast-provider.ts @@ -1,7 +1,7 @@ import type { HTMLMediaTargetLike } from '@videojs/media/dom'; import { isCaptionOrSubtitleTrack } from '@videojs/utils/dom'; -import type { GoogleCastProps } from './index'; +import type { GoogleCastExtensionProps } from './index'; import { castFramework, ensureCastFramework, googleCastInstances } from './registry'; import { RemotePlayback, type RemotePlaybackHooks } from './remote-playback'; import { @@ -20,14 +20,14 @@ import { type RemotePlayerListener = (event?: cast.framework.RemotePlayerChangedEvent) => void; -type GoogleCastConfig = GoogleCastProps; +type GoogleCastConfig = GoogleCastExtensionProps; /** - * Cast provider + lifecycle. Created by the {@link GoogleCast} component and installed as the host's `targetOverride` - * while a cast session is connected, so its getters/setters route through the cast receiver; when disconnected the host - * falls through to the attached target. Also owns the cast framework integration, the `RemotePlayback` instance exposed - * via {@link GoogleCastProvider#remote}, and dispatches media events on the attached target (forwarded by the host) - * while casting. + * Cast provider + lifecycle. Created by the {@link GoogleCastExtension} component and installed as the host's + * `targetOverride` while a cast session is connected, so its getters/setters route through the cast receiver; when + * disconnected the host falls through to the attached target. Also owns the cast framework integration, the + * `RemotePlayback` instance exposed via {@link GoogleCastProvider#remote}, and dispatches media events on the attached + * target (forwarded by the host) while casting. */ export class GoogleCastProvider { target: HTMLMediaTargetLike | null = null; diff --git a/packages/google-cast/src/media.ts b/packages/google-cast/src/google-cast.ts similarity index 91% rename from packages/google-cast/src/media.ts rename to packages/google-cast/src/google-cast.ts index 45956f9871..8c35b82aa9 100644 --- a/packages/google-cast/src/media.ts +++ b/packages/google-cast/src/google-cast.ts @@ -6,7 +6,7 @@ import { requiresCastFramework } from './utils'; type MediaHost = HTMLMediaElementHost; -export interface GoogleCastProps { +export interface GoogleCastExtensionProps { /** Source URL loaded on the Cast receiver. Falls back to the host's `src` / `currentSrc`. */ src?: string | undefined; /** MIME type of the Cast source. When unset, the receiver infers it from the URL. */ @@ -19,15 +19,15 @@ export interface GoogleCastProps { customData?: Record | null | undefined; } -export const googleCastDefaultProps: GoogleCastProps = { - src: undefined, - contentType: undefined, - streamType: undefined, - receiver: undefined, - customData: undefined, -}; +export class GoogleCastExtension implements GoogleCastExtensionProps, MediaComponent { + static defaultProps: GoogleCastExtensionProps = { + src: undefined, + contentType: undefined, + streamType: undefined, + receiver: undefined, + customData: undefined, + }; -export class GoogleCast implements GoogleCastProps, MediaComponent { #src: string | undefined; #contentType: string | undefined; #streamType: MediaStreamType | undefined; @@ -37,7 +37,7 @@ export class GoogleCast implements GoogleCastProps, MediaComponent { #provider: GoogleCastProvider | null = null; #override: Partial | null = null; - constructor(props: GoogleCastProps = {}) { + constructor(props: GoogleCastExtensionProps = {}) { Object.assign(this, props); } diff --git a/packages/google-cast/src/index.ts b/packages/google-cast/src/index.ts index 33d4737da1..a789856394 100644 --- a/packages/google-cast/src/index.ts +++ b/packages/google-cast/src/index.ts @@ -1 +1 @@ -export * from './media'; +export * from './google-cast'; diff --git a/packages/google-cast/src/tests/google-cast-provider.test.ts b/packages/google-cast/src/tests/google-cast-provider.test.ts index 87316b5244..e7cdd55986 100644 --- a/packages/google-cast/src/tests/google-cast-provider.test.ts +++ b/packages/google-cast/src/tests/google-cast-provider.test.ts @@ -2,7 +2,7 @@ import { addMediaComponent, type HTMLMediaTargetLike, HTMLVideoElementHost } fro import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'; import { GoogleCastProvider } from '../google-cast-provider'; -import { GoogleCast } from '../index'; +import { GoogleCastExtension } from '../index'; import { ensureCastFramework } from '../registry'; vi.mock('../registry', async (importOriginal) => { @@ -99,7 +99,7 @@ describe('GoogleCastProvider', () => { }); }); -describe('GoogleCast', () => { +describe('GoogleCastExtension', () => { it('loads the cast framework when the host remote is read while attached', () => { vi.stubGlobal('chrome', {}); @@ -108,7 +108,7 @@ describe('GoogleCast', () => { host.attach(target as Parameters[0]); - addMediaComponent(host, new GoogleCast()); + addMediaComponent(host, new GoogleCastExtension()); expect(ensureCastFramework).not.toHaveBeenCalled(); // The component's override must expose `remote` as an accessor so host @@ -123,7 +123,7 @@ describe('GoogleCast', () => { const host = new HTMLVideoElementHost(); - addMediaComponent(host, new GoogleCast()); + addMediaComponent(host, new GoogleCastExtension()); void host.remote; diff --git a/packages/google-cast/src/tests/media.test.ts b/packages/google-cast/src/tests/google-cast.test.ts similarity index 97% rename from packages/google-cast/src/tests/media.test.ts rename to packages/google-cast/src/tests/google-cast.test.ts index fd460ffbe8..1d0257ddbd 100644 --- a/packages/google-cast/src/tests/media.test.ts +++ b/packages/google-cast/src/tests/google-cast.test.ts @@ -1,7 +1,7 @@ import { addMediaComponent, HTMLVideoElementHost } from '@videojs/media/dom'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'; -import { GoogleCast } from '../index'; +import { GoogleCastExtension } from '../index'; const mocks = vi.hoisted(() => { class FakeRemote extends EventTarget { @@ -47,7 +47,7 @@ function setup() { host.attach(video); - const googleCast = new GoogleCast(); + const googleCast = new GoogleCastExtension(); addMediaComponent(host, googleCast); @@ -76,7 +76,7 @@ afterEach(() => { vi.unstubAllGlobals(); }); -describe('GoogleCast', () => { +describe('GoogleCastExtension', () => { it('registers remote state listeners only once across media changes', () => { const { googleCast, provider } = setup(); const nextHost = new HTMLVideoElementHost(); diff --git a/packages/html/src/define/extensions/google-cast.ts b/packages/html/src/define/extensions/google-cast.ts index 4efc5ee236..14767387a8 100644 --- a/packages/html/src/define/extensions/google-cast.ts +++ b/packages/html/src/define/extensions/google-cast.ts @@ -1,12 +1,12 @@ -import { GoogleCastElement } from '../../extensions/google-cast'; +import { GoogleCastExtension } from '../../extensions/google-cast'; import { safeDefine } from '../../registration/safe-define'; -export { GoogleCastElement }; +export { GoogleCastExtension }; -safeDefine(GoogleCastElement); +safeDefine(GoogleCastExtension); declare global { interface HTMLElementTagNameMap { - [GoogleCastElement.tagName]: GoogleCastElement; + [GoogleCastExtension.tagName]: GoogleCastExtension; } } diff --git a/packages/html/src/define/extensions/mux-data.ts b/packages/html/src/define/extensions/mux-data.ts index 415fe2bcc8..7d21d36d1d 100644 --- a/packages/html/src/define/extensions/mux-data.ts +++ b/packages/html/src/define/extensions/mux-data.ts @@ -1,12 +1,12 @@ -import { MuxDataElement } from '../../extensions/mux-data'; +import { MuxDataExtension } from '../../extensions/mux-data'; import { safeDefine } from '../../registration/safe-define'; -export { MuxDataElement }; +export { MuxDataExtension }; -safeDefine(MuxDataElement); +safeDefine(MuxDataExtension); declare global { interface HTMLElementTagNameMap { - [MuxDataElement.tagName]: MuxDataElement; + [MuxDataExtension.tagName]: MuxDataExtension; } } diff --git a/packages/html/src/extensions/google-cast/google-cast-element.ts b/packages/html/src/extensions/google-cast/extension.ts similarity index 75% rename from packages/html/src/extensions/google-cast/google-cast-element.ts rename to packages/html/src/extensions/google-cast/extension.ts index f9becb8288..494ab9acab 100644 --- a/packages/html/src/extensions/google-cast/google-cast-element.ts +++ b/packages/html/src/extensions/google-cast/extension.ts @@ -1,5 +1,5 @@ import type { PropertyDeclarationMap } from '@videojs/element'; -import { GoogleCast, type GoogleCastProps } from '@videojs/google-cast'; +import { GoogleCastExtension as GoogleCastExtensionBase, type GoogleCastExtensionProps } from '@videojs/google-cast'; import { MediaComponentElement } from '../media-component-element'; @@ -16,7 +16,7 @@ import { MediaComponentElement } from '../media-component-element'; * * ```; */ -export class GoogleCastElement extends MediaComponentElement { +export class GoogleCastExtension extends MediaComponentElement { static readonly tagName = 'google-cast'; static override properties = { @@ -25,10 +25,10 @@ export class GoogleCastElement extends MediaComponentElement { streamType: { type: String, attribute: 'stream-type' }, receiver: { type: String }, // `customData` takes an object, so it's a property-only prop. - } satisfies PropertyDeclarationMap>; + } satisfies PropertyDeclarationMap>; - protected createComponent(): GoogleCast { - return new GoogleCast(); + protected createComponent(): GoogleCastExtensionBase { + return new GoogleCastExtensionBase(); } /** Source URL loaded on the Cast receiver. Falls back to the media's `src` / `currentSrc`. */ @@ -50,11 +50,11 @@ export class GoogleCastElement extends MediaComponentElement { } /** Stream type used on the Cast receiver. Falls back to the media's `streamType`. */ - get streamType(): GoogleCastProps['streamType'] { + get streamType(): GoogleCastExtensionProps['streamType'] { return this.component.streamType; } - set streamType(value: GoogleCastProps['streamType'] | null) { + set streamType(value: GoogleCastExtensionProps['streamType'] | null) { this.component.streamType = value ?? undefined; } @@ -68,11 +68,11 @@ export class GoogleCastElement extends MediaComponentElement { } /** Custom data sent to the Cast receiver with the load request. */ - get customData(): GoogleCastProps['customData'] { + get customData(): GoogleCastExtensionProps['customData'] { return this.component.customData; } - set customData(value: GoogleCastProps['customData']) { + set customData(value: GoogleCastExtensionProps['customData']) { this.component.customData = value; } } diff --git a/packages/html/src/extensions/google-cast/index.ts b/packages/html/src/extensions/google-cast/index.ts index ed0be4a92b..f86dc29dd5 100644 --- a/packages/html/src/extensions/google-cast/index.ts +++ b/packages/html/src/extensions/google-cast/index.ts @@ -1 +1 @@ -export * from './google-cast-element'; +export * from './extension'; diff --git a/packages/html/src/extensions/mux-data/mux-data-element.ts b/packages/html/src/extensions/mux-data/extension.ts similarity index 85% rename from packages/html/src/extensions/mux-data/mux-data-element.ts rename to packages/html/src/extensions/mux-data/extension.ts index ba86415608..00061e0bfb 100644 --- a/packages/html/src/extensions/mux-data/mux-data-element.ts +++ b/packages/html/src/extensions/mux-data/extension.ts @@ -1,5 +1,5 @@ import type { PropertyDeclarationMap } from '@videojs/element'; -import { MuxData, type MuxDataProps } from '@videojs/mux-data'; +import { MuxDataExtension as MuxDataExtensionBase, type MuxDataExtensionProps } from '@videojs/mux-data'; import { MediaComponentElement } from '../media-component-element'; @@ -22,7 +22,7 @@ import { MediaComponentElement } from '../media-component-element'; * * ```; */ -export class MuxDataElement extends MediaComponentElement { +export class MuxDataExtension extends MediaComponentElement { static readonly tagName = 'mux-data'; static override properties = { @@ -34,10 +34,10 @@ export class MuxDataElement extends MediaComponentElement { playerSoftwareVersion: { type: String, attribute: 'player-software-version' }, playerInitTime: { type: Number, attribute: 'player-init-time' }, // `metadata` and `MuxDataSdk` take objects, so they're property-only props. - } satisfies PropertyDeclarationMap>; + } satisfies PropertyDeclarationMap>; - protected createComponent(): MuxData { - return new MuxData(); + protected createComponent(): MuxDataExtensionBase { + return new MuxDataExtensionBase(); } /** Mux Data environment key for the beacons. Optional for Mux-hosted playback. */ @@ -104,20 +104,20 @@ export class MuxDataElement extends MediaComponentElement { } /** Custom view metadata forwarded to the Mux Data SDK. */ - get metadata(): MuxDataProps['metadata'] { + get metadata(): MuxDataExtensionProps['metadata'] { return this.component.metadata; } - set metadata(value: MuxDataProps['metadata']) { + set metadata(value: MuxDataExtensionProps['metadata']) { this.component.metadata = value; } /** Mux Data SDK used for monitoring. Set to `undefined` to disable monitoring. */ - get MuxDataSdk(): MuxDataProps['MuxDataSdk'] { + get MuxDataSdk(): MuxDataExtensionProps['MuxDataSdk'] { return this.component.MuxDataSdk; } - set MuxDataSdk(value: MuxDataProps['MuxDataSdk']) { + set MuxDataSdk(value: MuxDataExtensionProps['MuxDataSdk']) { this.component.MuxDataSdk = value; } } diff --git a/packages/html/src/extensions/mux-data/index.ts b/packages/html/src/extensions/mux-data/index.ts index e31782287e..f86dc29dd5 100644 --- a/packages/html/src/extensions/mux-data/index.ts +++ b/packages/html/src/extensions/mux-data/index.ts @@ -1 +1 @@ -export * from './mux-data-element'; +export * from './extension'; diff --git a/packages/html/src/extensions/tests/google-cast.test.ts b/packages/html/src/extensions/tests/google-cast.test.ts index 605325e9cc..ece67a03c7 100644 --- a/packages/html/src/extensions/tests/google-cast.test.ts +++ b/packages/html/src/extensions/tests/google-cast.test.ts @@ -1,11 +1,11 @@ import { ContextProvider } from '@videojs/element/context'; -import { GoogleCast } from '@videojs/google-cast'; +import { GoogleCastExtension as GoogleCastExtensionBase } from '@videojs/google-cast'; import { getMediaComponents, HTMLVideoElementHost, type Media } from '@videojs/media/dom'; import { afterEach, describe, expect, it } from 'vite-plus/test'; import { mediaContext } from '../../player/context'; import { UIElement } from '../../ui/ui-element'; -import { GoogleCastElement } from '../google-cast'; +import { GoogleCastExtension } from '../google-cast'; class TestMediaProvider extends UIElement { readonly #provider = new ContextProvider(this, { @@ -19,12 +19,12 @@ class TestMediaProvider extends UIElement { } customElements.define('test-cast-provider', TestMediaProvider); -customElements.define('test-google-cast', GoogleCastElement); +customElements.define('test-google-cast', GoogleCastExtension); function setup() { const host = new HTMLVideoElementHost(); const provider = new TestMediaProvider(); - const el = new GoogleCastElement(); + const el = new GoogleCastExtension(); provider.append(el); document.body.append(provider); @@ -36,20 +36,20 @@ afterEach(() => { document.body.innerHTML = ''; }); -describe('GoogleCastElement', () => { - it('registers a GoogleCast component with the media host from context', () => { +describe('GoogleCastExtension', () => { + it('registers a GoogleCastExtension component with the media host from context', () => { const { host, provider } = setup(); provider.setMedia(host as unknown as Media); - expect(getMediaComponents(host).get(GoogleCast)).toBeInstanceOf(GoogleCast); + expect(getMediaComponents(host).get(GoogleCastExtensionBase)).toBeInstanceOf(GoogleCastExtensionBase); }); it('leaves the component to the base class lazy getter', () => { // An own `component` field would shadow the getter and be initialized after // the base constructor — too late for a connected upgrade, where the media // context callback registers the component from within that constructor. - expect(Object.getOwnPropertyNames(new GoogleCastElement())).not.toContain('component'); + expect(Object.getOwnPropertyNames(new GoogleCastExtension())).not.toContain('component'); }); it('resolves the host from a media element host property', () => { @@ -57,7 +57,7 @@ describe('GoogleCastElement', () => { provider.setMedia({ host } as unknown as Media); - expect(getMediaComponents(host).get(GoogleCast)).toBeInstanceOf(GoogleCast); + expect(getMediaComponents(host).get(GoogleCastExtensionBase)).toBeInstanceOf(GoogleCastExtensionBase); }); it('ignores media that is not a media host', () => { @@ -66,7 +66,7 @@ describe('GoogleCastElement', () => { provider.setMedia(host as unknown as Media); provider.setMedia(document.createElement('video') as unknown as Media); - expect(getMediaComponents(host).get(GoogleCast)).toBeUndefined(); + expect(getMediaComponents(host).get(GoogleCastExtensionBase)).toBeUndefined(); }); it('forwards attributes to the component', () => { @@ -79,7 +79,7 @@ describe('GoogleCastElement', () => { el.setAttribute('stream-type', 'live'); el.setAttribute('src', 'https://example.com/stream.m3u8'); - const component = getMediaComponents(host).get(GoogleCast)!; + const component = getMediaComponents(host).get(GoogleCastExtensionBase)!; expect(component.receiver).toBe('APP_ID'); expect(component.contentType).toBe('application/x-mpegURL'); @@ -105,8 +105,8 @@ describe('GoogleCastElement', () => { provider.setMedia(host as unknown as Media); provider.setMedia(nextHost as unknown as Media); - expect(getMediaComponents(host).get(GoogleCast)).toBeUndefined(); - expect(getMediaComponents(nextHost).get(GoogleCast)).toBeInstanceOf(GoogleCast); + expect(getMediaComponents(host).get(GoogleCastExtensionBase)).toBeUndefined(); + expect(getMediaComponents(nextHost).get(GoogleCastExtensionBase)).toBeInstanceOf(GoogleCastExtensionBase); }); it('removes the component when the element disconnects', () => { @@ -116,7 +116,7 @@ describe('GoogleCastElement', () => { el.remove(); - expect(getMediaComponents(host).get(GoogleCast)).toBeUndefined(); + expect(getMediaComponents(host).get(GoogleCastExtensionBase)).toBeUndefined(); }); it('removes the component on destroy', () => { @@ -126,6 +126,6 @@ describe('GoogleCastElement', () => { el.destroy(); - expect(getMediaComponents(host).get(GoogleCast)).toBeUndefined(); + expect(getMediaComponents(host).get(GoogleCastExtensionBase)).toBeUndefined(); }); }); diff --git a/packages/html/src/extensions/tests/mux-data.test.ts b/packages/html/src/extensions/tests/mux-data.test.ts index 9870dbfda3..7759a2fee4 100644 --- a/packages/html/src/extensions/tests/mux-data.test.ts +++ b/packages/html/src/extensions/tests/mux-data.test.ts @@ -1,11 +1,11 @@ import { ContextProvider } from '@videojs/element/context'; import { getMediaComponents, HTMLVideoElementHost, type Media } from '@videojs/media/dom'; -import { MuxData } from '@videojs/mux-data'; +import { MuxDataExtension as MuxDataExtensionBase } from '@videojs/mux-data'; import { afterEach, describe, expect, it } from 'vite-plus/test'; import { mediaContext } from '../../player/context'; import { UIElement } from '../../ui/ui-element'; -import { MuxDataElement } from '../mux-data'; +import { MuxDataExtension } from '../mux-data'; class TestMediaProvider extends UIElement { readonly #provider = new ContextProvider(this, { @@ -19,12 +19,12 @@ class TestMediaProvider extends UIElement { } customElements.define('test-mux-data-provider', TestMediaProvider); -customElements.define('test-mux-data', MuxDataElement); +customElements.define('test-mux-data', MuxDataExtension); function setup() { const host = new HTMLVideoElementHost(); const provider = new TestMediaProvider(); - const el = new MuxDataElement(); + const el = new MuxDataExtension(); // Prevent the real Mux SDK from initializing (and beaconing) in tests. el.MuxDataSdk = undefined; @@ -39,7 +39,7 @@ afterEach(() => { document.body.innerHTML = ''; }); -describe('MuxDataElement', () => { +describe('MuxDataExtension', () => { it('registers when parsed into a connected player that already has media', () => { const host = new HTMLVideoElementHost(); const provider = new TestMediaProvider(); @@ -49,21 +49,21 @@ describe('MuxDataElement', () => { provider.innerHTML = ''; - expect(getMediaComponents(host).get(MuxData)).toBeInstanceOf(MuxData); + expect(getMediaComponents(host).get(MuxDataExtensionBase)).toBeInstanceOf(MuxDataExtensionBase); }); it('leaves the component to the base class lazy getter', () => { // An own `component` field would shadow the getter and be initialized after // the base constructor — too late for a connected upgrade, where the media // context callback registers the component from within that constructor. - expect(Object.getOwnPropertyNames(new MuxDataElement())).not.toContain('component'); + expect(Object.getOwnPropertyNames(new MuxDataExtension())).not.toContain('component'); }); - it('registers a MuxData component with the media host from context', () => { + it('registers a MuxDataExtension component with the media host from context', () => { const { host, provider } = setup(); provider.setMedia(host as unknown as Media); - expect(getMediaComponents(host).get(MuxData)).toBeInstanceOf(MuxData); + expect(getMediaComponents(host).get(MuxDataExtensionBase)).toBeInstanceOf(MuxDataExtensionBase); }); it('forwards attributes to the component', () => { @@ -77,7 +77,7 @@ describe('MuxDataElement', () => { el.setAttribute('debug', ''); el.setAttribute('disable-cookies', ''); - const component = getMediaComponents(host).get(MuxData)!; + const component = getMediaComponents(host).get(MuxDataExtensionBase)!; expect(component.envKey).toBe('test-key'); expect(component.playerSoftwareName).toBe('mux-video'); @@ -97,7 +97,7 @@ describe('MuxDataElement', () => { el.metadata = metadata; - expect(getMediaComponents(host).get(MuxData)!.metadata).toEqual(metadata); + expect(getMediaComponents(host).get(MuxDataExtensionBase)!.metadata).toEqual(metadata); }); it('removes the component when the element disconnects', () => { @@ -107,6 +107,6 @@ describe('MuxDataElement', () => { el.remove(); - expect(getMediaComponents(host).get(MuxData)).toBeUndefined(); + expect(getMediaComponents(host).get(MuxDataExtensionBase)).toBeUndefined(); }); }); diff --git a/packages/media/src/dom/tests/media-host.test.ts b/packages/media/src/dom/tests/media-host.test.ts index ab6c606dda..72c95bdfca 100644 --- a/packages/media/src/dom/tests/media-host.test.ts +++ b/packages/media/src/dom/tests/media-host.test.ts @@ -202,7 +202,7 @@ describe('HTMLMediaElementHost', () => { host.destroy(); - // `` / `MuxData` own their component and may outlive the host. + // `` / `MuxDataExtension` own their component and may outlive the host. expect(component.destroy).not.toHaveBeenCalled(); }); diff --git a/packages/mux-data/README.md b/packages/mux-data/README.md index 1ea201cf88..846323dd0b 100644 --- a/packages/mux-data/README.md +++ b/packages/mux-data/README.md @@ -19,10 +19,10 @@ import '@videojs/html/extensions/mux-data'; ``` ```tsx -import { MuxData } from '@videojs/react/extensions/mux-data'; +import { MuxDataExtension } from '@videojs/react/extensions/mux-data'; ``` -Low-level consumers can import the framework-neutral `MuxData` extension from `@videojs/mux-data`. +Low-level consumers can import the framework-neutral `MuxDataExtension` extension from `@videojs/mux-data`. ## License diff --git a/packages/mux-data/src/index.ts b/packages/mux-data/src/index.ts index c3b3bafe66..f608e52252 100644 --- a/packages/mux-data/src/index.ts +++ b/packages/mux-data/src/index.ts @@ -1,3 +1,3 @@ -export { MuxData, type MuxDataMedia, type MuxDataProps, muxDataDefaultProps } from './mux-data'; +export { MuxDataExtension, type MuxDataMedia, type MuxDataExtensionProps } from './mux-data'; export { type MuxDataEngineOptions, toMuxDataEngineOptions } from './mux-data-engine'; export type { MuxDataOptions, MuxDataSdk } from './types'; diff --git a/packages/mux-data/src/mux-data.ts b/packages/mux-data/src/mux-data.ts index 7c1998b159..d34aa33bec 100644 --- a/packages/mux-data/src/mux-data.ts +++ b/packages/mux-data/src/mux-data.ts @@ -4,7 +4,7 @@ import { getPlayerVersion } from './env'; import { type MuxDataEngineOptions, toMuxDataEngineOptions } from './mux-data-engine'; import type { MuxDataOptions, MuxDataSdk } from './types'; -export interface MuxDataProps { +export interface MuxDataExtensionProps { MuxDataSdk: MuxDataSdk | undefined; beaconCollectionDomain: string | undefined; debug: boolean; @@ -16,19 +16,6 @@ export interface MuxDataProps { metadata: MuxDataOptions['data'] | undefined; } -export const muxDataDefaultProps: MuxDataProps = { - MuxDataSdk: Mux, - beaconCollectionDomain: undefined, - debug: false, - disableCookies: false, - envKey: undefined, - playerSoftwareName: undefined, - playerSoftwareVersion: getPlayerVersion(), - // Generated per instance; see `#generatePlayerInitTime()`. - playerInitTime: undefined, - metadata: undefined, -}; - const MUX_VIDEO_DOMAIN = 'mux.com'; /** The SDK's monitor handle on a video element, narrowed to a live (non-destroyed) one. */ @@ -48,16 +35,29 @@ export interface MuxDataMedia extends EventTarget { readonly src: string; } -export class MuxData implements MuxDataProps { - #MuxDataSdk: MuxDataSdk | undefined = muxDataDefaultProps.MuxDataSdk; +export class MuxDataExtension implements MuxDataExtensionProps { + static defaultProps: MuxDataExtensionProps = { + MuxDataSdk: Mux, + beaconCollectionDomain: undefined, + debug: false, + disableCookies: false, + envKey: undefined, + playerSoftwareName: undefined, + playerSoftwareVersion: getPlayerVersion(), + // Generated per instance; see `#generatePlayerInitTime()`. + playerInitTime: undefined, + metadata: undefined, + }; + + #MuxDataSdk: MuxDataSdk | undefined = MuxDataExtension.defaultProps.MuxDataSdk; #pendingSync: Promise | null = null; - #beaconCollectionDomain: string | undefined = muxDataDefaultProps.beaconCollectionDomain; - #debug = muxDataDefaultProps.debug; - #disableCookies = muxDataDefaultProps.disableCookies; - #metadata: MuxDataOptions['data'] | undefined = muxDataDefaultProps.metadata; - #envKey: string | undefined = muxDataDefaultProps.envKey; - #playerSoftwareName: string | undefined = muxDataDefaultProps.playerSoftwareName; - #playerSoftwareVersion: string | undefined = muxDataDefaultProps.playerSoftwareVersion; + #beaconCollectionDomain: string | undefined = MuxDataExtension.defaultProps.beaconCollectionDomain; + #debug = MuxDataExtension.defaultProps.debug; + #disableCookies = MuxDataExtension.defaultProps.disableCookies; + #metadata: MuxDataOptions['data'] | undefined = MuxDataExtension.defaultProps.metadata; + #envKey: string | undefined = MuxDataExtension.defaultProps.envKey; + #playerSoftwareName: string | undefined = MuxDataExtension.defaultProps.playerSoftwareName; + #playerSoftwareVersion: string | undefined = MuxDataExtension.defaultProps.playerSoftwareVersion; #playerInitTime: number | undefined = this.#generatePlayerInitTime(); #media: MuxDataMedia | null = null; #target: HTMLVideoElement | null = null; @@ -68,7 +68,7 @@ export class MuxData implements MuxDataProps { // Generated once per instance, so the views of one player group into one session. #viewSessionId: string | undefined; - constructor(props: Partial = {}) { + constructor(props: Partial = {}) { Object.assign(this, props); } diff --git a/packages/mux-data/src/tests/mux-data.test.ts b/packages/mux-data/src/tests/mux-data.test.ts index 2c773cb241..d0e79513d8 100644 --- a/packages/mux-data/src/tests/mux-data.test.ts +++ b/packages/mux-data/src/tests/mux-data.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vite-plus/test'; -import { MuxData } from '..'; +import { MuxDataExtension } from '..'; import type { MuxDataSdk } from '../types'; function createSdk() { @@ -66,14 +66,14 @@ afterEach(() => { document.body.innerHTML = ''; }); -describe('MuxData', () => { +describe('MuxDataExtension', () => { it('accepts a player software name', () => { - expect(new MuxData({ playerSoftwareName: 'mux-video' }).playerSoftwareName).toBe('mux-video'); + expect(new MuxDataExtension({ playerSoftwareName: 'mux-video' }).playerSoftwareName).toBe('mux-video'); }); it('monitors the attached target with the configured data', async () => { const { sdk, monitor } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key', playerSoftwareName: 'mux-video' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key', playerSoftwareName: 'mux-video' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -94,7 +94,7 @@ describe('MuxData', () => { it('does not monitor before a target is attached', async () => { const { sdk, monitor } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk }); + const data = new MuxDataExtension({ MuxDataSdk: sdk }); const media = new FakeMedia(); media.src = 'https://stream.mux.com/abc123.m3u8'; @@ -108,7 +108,7 @@ describe('MuxData', () => { it('keeps the monitor across a same-source loadstart', async () => { const { sdk, monitor, emit, destroy } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -129,7 +129,7 @@ describe('MuxData', () => { it('emits videochange on the live monitor when the source changes', async () => { const { sdk, monitor, emit, destroy } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -150,7 +150,7 @@ describe('MuxData', () => { it('names the pending view instead of changing videos when the first source arrives', async () => { const { sdk, monitor, emit, updateData } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -171,7 +171,7 @@ describe('MuxData', () => { it('emits videochange for a new video loaded after the source was cleared', async () => { const { sdk, monitor, emit } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -197,7 +197,7 @@ describe('MuxData', () => { it('hooks a new engine into the live monitor instead of re-monitoring', async () => { const { sdk, monitor, addHLSJS, removeHLSJS, destroy } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -230,7 +230,7 @@ describe('MuxData', () => { it('monitors a dash.js engine through the dash.js integration', async () => { const { sdk, monitor } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -251,7 +251,7 @@ describe('MuxData', () => { it('monitors media with no engine from the media element alone', async () => { const { sdk, monitor } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -271,7 +271,7 @@ describe('MuxData', () => { it('keeps one view session id across video changes', async () => { const { sdk, monitor, emit } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -296,7 +296,7 @@ describe('MuxData', () => { it('honors a caller-supplied view session id and never mutates caller metadata', async () => { const { sdk, monitor } = createSdk(); const metadata = { view_session_id: 'caller-session', video_title: 'Some Title' }; - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key', metadata }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key', metadata }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -315,7 +315,7 @@ describe('MuxData', () => { it('does not write a generated view session id into caller metadata', async () => { const { sdk } = createSdk(); const metadata = { video_title: 'Some Title' }; - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key', metadata }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key', metadata }); const video = document.createElement('video'); const media = new FakeMedia(); @@ -330,7 +330,7 @@ describe('MuxData', () => { it('destroys the old target monitor when attached to a new target', async () => { const { sdk, monitor, destroy } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const first = document.createElement('video'); const second = document.createElement('video'); const media = new FakeMedia(); @@ -352,7 +352,7 @@ describe('MuxData', () => { it('follows the media when registered with another host', async () => { const { sdk, monitor, emit } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk }); + const data = new MuxDataExtension({ MuxDataSdk: sdk }); const video = document.createElement('video'); const first = new FakeMedia(); const second = new FakeMedia(); @@ -378,7 +378,7 @@ describe('MuxData', () => { }); it('destroys active monitoring on destroy', () => { - const data = new MuxData(); + const data = new MuxDataExtension(); const video = document.createElement('video'); const destroy = vi.fn(); @@ -393,7 +393,7 @@ describe('MuxData', () => { it('stops syncing after destroy', async () => { const { sdk, monitor, emit } = createSdk(); - const data = new MuxData({ MuxDataSdk: sdk, envKey: 'key' }); + const data = new MuxDataExtension({ MuxDataSdk: sdk, envKey: 'key' }); const video = document.createElement('video'); const media = new FakeMedia(); diff --git a/packages/react/src/extensions/google-cast/google-cast.tsx b/packages/react/src/extensions/google-cast/extension.tsx similarity index 68% rename from packages/react/src/extensions/google-cast/google-cast.tsx rename to packages/react/src/extensions/google-cast/extension.tsx index 85888a3844..e88cabd880 100644 --- a/packages/react/src/extensions/google-cast/google-cast.tsx +++ b/packages/react/src/extensions/google-cast/extension.tsx @@ -1,16 +1,12 @@ 'use client'; -import { - GoogleCast as GoogleCastComponent, - googleCastDefaultProps, - type GoogleCastProps as GoogleCastComponentProps, -} from '@videojs/google-cast'; +import { GoogleCastExtension, type GoogleCastExtensionProps } from '@videojs/google-cast'; import type { ReactNode } from 'react'; import { useMediaComponent } from '../../utils/use-media-component'; import { useSyncProps } from '../../utils/use-sync-props'; -export type GoogleCastProps = Partial; +export type GoogleCastProps = Partial; /** * Adds the Google Cast extension to the surrounding player's media. @@ -27,9 +23,9 @@ export type GoogleCastProps = Partial; * ```; */ export function GoogleCast(props: GoogleCastProps): ReactNode { - const component = useMediaComponent(GoogleCastComponent); + const component = useMediaComponent(GoogleCastExtension); - useSyncProps(component, props, googleCastDefaultProps); + useSyncProps(component, props, GoogleCastExtension.defaultProps); return null; } diff --git a/packages/react/src/extensions/google-cast/index.ts b/packages/react/src/extensions/google-cast/index.ts index a789856394..f86dc29dd5 100644 --- a/packages/react/src/extensions/google-cast/index.ts +++ b/packages/react/src/extensions/google-cast/index.ts @@ -1 +1 @@ -export * from './google-cast'; +export * from './extension'; diff --git a/packages/react/src/extensions/mux-data/mux-data.tsx b/packages/react/src/extensions/mux-data/extension.tsx similarity index 80% rename from packages/react/src/extensions/mux-data/mux-data.tsx rename to packages/react/src/extensions/mux-data/extension.tsx index 796fd7a57f..ba116a04cc 100644 --- a/packages/react/src/extensions/mux-data/mux-data.tsx +++ b/packages/react/src/extensions/mux-data/extension.tsx @@ -1,16 +1,12 @@ 'use client'; -import { - MuxData as MuxDataComponent, - muxDataDefaultProps, - type MuxDataProps as MuxDataComponentProps, -} from '@videojs/mux-data'; +import { MuxDataExtension, type MuxDataExtensionProps } from '@videojs/mux-data'; import type { ReactNode } from 'react'; import { useMediaComponent } from '../../utils/use-media-component'; import { useSyncProps } from '../../utils/use-sync-props'; -export type MuxDataProps = Partial; +export type MuxDataProps = Partial; /** * Adds the [Mux Data](https://www.mux.com/data) extension to the surrounding player's media. @@ -33,18 +29,18 @@ export type MuxDataProps = Partial; * ```; */ export function MuxData(props: MuxDataProps): ReactNode { - const component = useMediaComponent(MuxDataComponent); + const component = useMediaComponent(MuxDataExtension); const { MuxDataSdk, ...rest } = props; // `useSyncProps` treats an `undefined` prop as "reset to the default", but // `MuxDataSdk={undefined}` is how consumers disable monitoring. Sync it here // instead: passing the prop wins even when its value is `undefined`, and only // omitting it falls back to the default SDK. - const sdk = 'MuxDataSdk' in props ? MuxDataSdk : muxDataDefaultProps.MuxDataSdk; + const sdk = 'MuxDataSdk' in props ? MuxDataSdk : MuxDataExtension.defaultProps.MuxDataSdk; if (component.MuxDataSdk !== sdk) component.MuxDataSdk = sdk; - useSyncProps(component, rest, muxDataDefaultProps); + useSyncProps(component, rest, MuxDataExtension.defaultProps); return null; } diff --git a/packages/react/src/extensions/mux-data/index.ts b/packages/react/src/extensions/mux-data/index.ts index d59d087c10..f86dc29dd5 100644 --- a/packages/react/src/extensions/mux-data/index.ts +++ b/packages/react/src/extensions/mux-data/index.ts @@ -1 +1 @@ -export * from './mux-data'; +export * from './extension'; diff --git a/packages/react/src/extensions/tests/google-cast.test.tsx b/packages/react/src/extensions/tests/google-cast.test.tsx index 66bf9208d9..765a2105a3 100644 --- a/packages/react/src/extensions/tests/google-cast.test.tsx +++ b/packages/react/src/extensions/tests/google-cast.test.tsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react'; -import { GoogleCast as GoogleCastComponent } from '@videojs/google-cast'; +import { GoogleCastExtension } from '@videojs/google-cast'; import { HlsJsMedia } from '@videojs/hlsjs-video'; import type { Media } from '@videojs/media'; import { getMediaComponents } from '@videojs/media/dom'; @@ -21,7 +21,7 @@ describe('GoogleCast', () => { render(, { wrapper: Wrapper }); - expect(getMediaComponents(media as HlsJsMedia).get(GoogleCastComponent)).toBeInstanceOf(GoogleCastComponent); + expect(getMediaComponents(media as HlsJsMedia).get(GoogleCastExtension)).toBeInstanceOf(GoogleCastExtension); }); it('syncs props to the component', () => { @@ -31,7 +31,7 @@ describe('GoogleCast', () => { wrapper: Wrapper, }); - const component = getMediaComponents(media as HlsJsMedia).get(GoogleCastComponent)!; + const component = getMediaComponents(media as HlsJsMedia).get(GoogleCastExtension)!; expect(component.receiver).toBe('APP_ID'); expect(component.contentType).toBe('application/x-mpegURL'); @@ -45,7 +45,7 @@ describe('GoogleCast', () => { rerender(); - expect(getMediaComponents(media as HlsJsMedia).get(GoogleCastComponent)!.receiver).toBeUndefined(); + expect(getMediaComponents(media as HlsJsMedia).get(GoogleCastExtension)!.receiver).toBeUndefined(); }); it('removes the component on unmount', () => { @@ -55,7 +55,7 @@ describe('GoogleCast', () => { unmount(); - expect(getMediaComponents(media as HlsJsMedia).get(GoogleCastComponent)).toBeUndefined(); + expect(getMediaComponents(media as HlsJsMedia).get(GoogleCastExtension)).toBeUndefined(); }); it('ignores media that is not a media host', () => { @@ -64,6 +64,6 @@ describe('GoogleCast', () => { render(, { wrapper: Wrapper }); - expect(getMediaComponents(video as any).get(GoogleCastComponent)).toBeUndefined(); + expect(getMediaComponents(video as any).get(GoogleCastExtension)).toBeUndefined(); }); }); diff --git a/packages/react/src/extensions/tests/mux-data.test.tsx b/packages/react/src/extensions/tests/mux-data.test.tsx index 9d3e7a3cf0..13d4953400 100644 --- a/packages/react/src/extensions/tests/mux-data.test.tsx +++ b/packages/react/src/extensions/tests/mux-data.test.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react'; import type { Media } from '@videojs/media'; import { addMediaComponent, getMediaComponents } from '@videojs/media/dom'; -import { MuxData as MuxDataComponent } from '@videojs/mux-data'; +import { MuxDataExtension } from '@videojs/mux-data'; import { MuxMedia } from '@videojs/mux-video'; import { describe, expect, it, vi } from 'vite-plus/test'; @@ -22,7 +22,7 @@ describe('MuxData', () => { render(, { wrapper: Wrapper }); - expect(getMediaComponents(media).get(MuxDataComponent)).toBeInstanceOf(MuxDataComponent); + expect(getMediaComponents(media).get(MuxDataExtension)).toBeInstanceOf(MuxDataExtension); }); it('syncs props to the component', () => { @@ -30,7 +30,7 @@ describe('MuxData', () => { render(, { wrapper: Wrapper }); - const component = getMediaComponents(media).get(MuxDataComponent)!; + const component = getMediaComponents(media).get(MuxDataExtension)!; expect(component.envKey).toBe('test-key'); expect(component.playerSoftwareName).toBe('mux-video'); @@ -42,10 +42,10 @@ describe('MuxData', () => { const MuxDataSdk = { monitor: vi.fn(), utils: { now: () => 0 }, - } as unknown as NonNullable; + } as unknown as NonNullable; const { rerender } = render(, { wrapper: Wrapper }); - const component = getMediaComponents(media).get(MuxDataComponent)!; + const component = getMediaComponents(media).get(MuxDataExtension)!; expect(component.MuxDataSdk).toBe(MuxDataSdk); @@ -63,27 +63,27 @@ describe('MuxData', () => { rerender(); - expect(getMediaComponents(media).get(MuxDataComponent)!.disableCookies).toBe(false); + expect(getMediaComponents(media).get(MuxDataExtension)!.disableCookies).toBe(false); }); it('keeps the component alive when the media host is destroyed while mounted', () => { const { media, Wrapper } = setup(); - const destroy = vi.spyOn(MuxDataComponent.prototype, 'destroy'); + const destroy = vi.spyOn(MuxDataExtension.prototype, 'destroy'); render(, { wrapper: Wrapper }); - const component = getMediaComponents(media).get(MuxDataComponent)!; + const component = getMediaComponents(media).get(MuxDataExtension)!; media.destroy(); // The host detaches and unregisters components it doesn't own; this one is // owned by the still-mounted `MuxData` and follows the next media. expect(destroy).not.toHaveBeenCalled(); - expect(getMediaComponents(media).get(MuxDataComponent)).toBeUndefined(); + expect(getMediaComponents(media).get(MuxDataExtension)).toBeUndefined(); const next = new MuxMedia(); addMediaComponent(next, component); - expect(getMediaComponents(next).get(MuxDataComponent)).toBe(component); + expect(getMediaComponents(next).get(MuxDataExtension)).toBe(component); destroy.mockRestore(); }); @@ -95,6 +95,6 @@ describe('MuxData', () => { unmount(); - expect(getMediaComponents(media).get(MuxDataComponent)).toBeUndefined(); + expect(getMediaComponents(media).get(MuxDataExtension)).toBeUndefined(); }); }); diff --git a/site/src/content/docs/reference/use-media-component.mdx b/site/src/content/docs/reference/use-media-component.mdx index 2edd9ace52..a7b86ffd82 100644 --- a/site/src/content/docs/reference/use-media-component.mdx +++ b/site/src/content/docs/reference/use-media-component.mdx @@ -17,11 +17,11 @@ import { useMediaComponent } from "@videojs/react"; Build a React extension by passing its media component class from a component rendered inside `Player`. ```tsx -import { MuxData as MuxDataComponent } from "@videojs/mux-data"; +import { MuxDataExtension } from "@videojs/mux-data"; import { useMediaComponent } from "@videojs/react"; function CustomMuxData() { - useMediaComponent(MuxDataComponent); + useMediaComponent(MuxDataExtension); return null; }