feat(packages)!: support media components as markup (#1883)

This commit is contained in:
Wesley Luyten
2026-07-30 17:18:58 -07:00
committed by GitHub
parent a5e73393fe
commit 68e96079e5
52 changed files with 1392 additions and 357 deletions
@@ -0,0 +1 @@
import '../../define/media/google-cast';
+1
View File
@@ -0,0 +1 @@
import '../../define/media/mux-data';
@@ -0,0 +1,12 @@
import { GoogleCastElement } from '../../media/google-cast';
import { safeDefine } from '../safe-define';
export { GoogleCastElement };
safeDefine(GoogleCastElement);
declare global {
interface HTMLElementTagNameMap {
[GoogleCastElement.tagName]: GoogleCastElement;
}
}
@@ -0,0 +1,12 @@
import { MuxDataElement } from '../../media/mux-data';
import { safeDefine } from '../safe-define';
export { MuxDataElement };
safeDefine(MuxDataElement);
declare global {
interface HTMLElementTagNameMap {
[MuxDataElement.tagName]: MuxDataElement;
}
}
+1 -8
View File
@@ -1,12 +1,5 @@
import { CustomMediaElement } from '@videojs/media/dom/custom-media-element';
import { DashMedia } from '@videojs/media/dom/dash';
import { GoogleCast } from '@videojs/media/dom/google-cast';
import { addMediaComponent } from '@videojs/media/dom/media-host';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
export class DashVideo extends MediaAttachMixin(CustomMediaElement('video', DashMedia)) {
constructor() {
super();
addMediaComponent(this.host, new GoogleCast());
}
}
export class DashVideo extends MediaAttachMixin(CustomMediaElement('video', DashMedia)) {}
@@ -0,0 +1,80 @@
import type { PropertyDeclarationMap } from '@videojs/element';
import { GoogleCast, type GoogleCastProps } from '@videojs/media/dom/google-cast';
import { MediaComponentElement } from '../media-component-element';
/**
* Adds Google Cast support to the surrounding player's media.
*
* Renders nothing — place it inside the player as a sibling of the media
* element and it registers a {@link GoogleCast} media component with the
* active media host.
*
* @example
* ```html
* <video-player>
* <hlsjs-video src="https://example.com/stream.m3u8"></hlsjs-video>
* <google-cast receiver="YOUR_APP_ID"></google-cast>
* </video-player>
* ```
*/
export class GoogleCastElement extends MediaComponentElement<GoogleCast> {
static readonly tagName = 'google-cast';
static override properties = {
src: { type: String },
contentType: { type: String, attribute: 'content-type' },
streamType: { type: String, attribute: 'stream-type' },
receiver: { type: String },
// `customData` takes an object, so it's a property-only prop.
} satisfies PropertyDeclarationMap<Exclude<keyof GoogleCastProps, 'customData'>>;
protected createComponent(): GoogleCast {
return new GoogleCast();
}
/** Source URL loaded on the Cast receiver. Falls back to the media's `src` / `currentSrc`. */
get src(): string {
return this.component.src ?? '';
}
set src(value: string | null | undefined) {
this.component.src = value ?? undefined;
}
/** MIME type of the Cast source. When unset, the receiver infers it from the URL. */
get contentType(): string | undefined {
return this.component.contentType;
}
set contentType(value: string | null | undefined) {
this.component.contentType = value ?? undefined;
}
/** Stream type used on the Cast receiver. Falls back to the media's `streamType`. */
get streamType(): GoogleCastProps['streamType'] {
return this.component.streamType;
}
set streamType(value: GoogleCastProps['streamType'] | null) {
this.component.streamType = value ?? undefined;
}
/** Cast receiver application ID. Defaults to Google's default media receiver. */
get receiver(): string | undefined {
return this.component.receiver;
}
set receiver(value: string | null | undefined) {
this.component.receiver = value ?? undefined;
}
/** Custom data sent to the Cast receiver with the load request. */
get customData(): GoogleCastProps['customData'] {
return this.component.customData;
}
set customData(value: GoogleCastProps['customData']) {
this.component.customData = value;
}
}
@@ -0,0 +1 @@
export * from './google-cast-element';
+1 -8
View File
@@ -1,12 +1,5 @@
import { CustomMediaElement } from '@videojs/media/dom/custom-media-element';
import { GoogleCast } from '@videojs/media/dom/google-cast';
import { HlsJsMedia } from '@videojs/media/dom/hls-js';
import { addMediaComponent } from '@videojs/media/dom/media-host';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
export class HlsJsVideo extends MediaAttachMixin(CustomMediaElement('video', HlsJsMedia)) {
constructor() {
super();
addMediaComponent(this.host, new GoogleCast());
}
}
export class HlsJsVideo extends MediaAttachMixin(CustomMediaElement('video', HlsJsMedia)) {}
@@ -0,0 +1,83 @@
import { ContextConsumer } from '@videojs/element/context';
import type { Media } from '@videojs/media/dom';
import {
addMediaComponent,
HTMLMediaElementHost,
type HTMLMediaTargetLike,
type MediaComponent,
} from '@videojs/media/dom/media-host';
import { mediaContext } from '../player/context';
import { MediaElement } from '../ui/media-element';
type MediaHost = HTMLMediaElementHost<HTMLMediaTargetLike, any>;
/** Resolve the media host from a context media value (a media custom element or the host itself). */
function resolveMediaHost(media: Media | null): MediaHost | null {
if (media instanceof HTMLMediaElementHost) return media;
const host = (media as { host?: unknown } | null)?.host;
return host instanceof HTMLMediaElementHost ? host : null;
}
/**
* Abstract base for elements that register a media component (e.g. Mux Data,
* Google Cast) with the media provided by the surrounding player.
*
* Place inside a player, as a sibling of the media element. The component is
* registered when a media host becomes available, follows the media when it
* changes, is removed when this element disconnects, and is destroyed with
* this element.
*/
export abstract class MediaComponentElement<Component extends MediaComponent> extends MediaElement {
#component: Component | null = null;
#host: MediaHost | null = null;
#removeComponent: (() => void) | null = null;
/**
* Create the media component this element registers. Called once, lazily.
*
* Must be a method rather than a field: upgrading an element that is already
* in the document runs its constructor while connected, so the media context
* callback below can fire before subclass field initializers have run.
*/
protected abstract createComponent(): Component;
/** The media component instance registered with the media host. */
protected get component(): Component {
return (this.#component ??= this.createComponent());
}
constructor() {
super();
// Registers itself as a controller on this host; re-requests on reconnect.
new ContextConsumer(this, {
context: mediaContext,
subscribe: true,
callback: (value) => this.#setHost(resolveMediaHost(value.media)),
});
}
override disconnectedCallback(): void {
// Remove the component while the media chain is still live so it can
// clean up against the real underlying target.
this.#setHost(null);
super.disconnectedCallback();
}
override destroyCallback(): void {
this.#setHost(null);
// Don't create a component just to destroy it.
this.#component?.destroy?.();
super.destroyCallback();
}
#setHost(host: MediaHost | null): void {
if (this.#host === host) return;
this.#removeComponent?.();
this.#removeComponent = null;
this.#host = host;
if (host) this.#removeComponent = addMediaComponent(host, this.component);
}
}
+1 -5
View File
@@ -1,7 +1,5 @@
import { CustomMediaElement } from '@videojs/media/dom/custom-media-element';
import { GoogleCast } from '@videojs/media/dom/google-cast';
import { addMediaComponent } from '@videojs/media/dom/media-host';
import { MuxData, MuxMedia } from '@videojs/media/dom/mux';
import { MuxMedia } from '@videojs/media/dom/mux';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
const MuxAudioBase = MediaAttachMixin(CustomMediaElement('audio', MuxMedia));
@@ -9,8 +7,6 @@ const MuxAudioBase = MediaAttachMixin(CustomMediaElement('audio', MuxMedia));
export class MuxAudio extends MuxAudioBase {
constructor() {
super();
addMediaComponent(this.host, new MuxData({ playerSoftwareName: 'mux-audio' }));
addMediaComponent(this.host, new GoogleCast());
this.host.addEventListener('sourcechange', () => this.#reflectSrc());
}
@@ -0,0 +1 @@
export * from './mux-data-element';
@@ -0,0 +1,124 @@
import type { PropertyDeclarationMap } from '@videojs/element';
import { MuxData, type MuxDataProps } from '@videojs/media/dom/mux';
import { MediaComponentElement } from '../media-component-element';
/**
* Adds [Mux Data](https://www.mux.com/data) monitoring to the surrounding
* player's media.
*
* Renders nothing — place it inside the player as a sibling of the media
* element and it registers a {@link MuxData} media component with the active
* media host.
*
* Mux-hosted playback needs no `env-key`: the view reports the Mux playback ID
* as its `video_id`, which Mux attributes to the owning environment. Set
* `env-key` to monitor sources Mux doesn't host.
*
* @example
* ```html
* <video-player>
* <mux-video src="https://stream.mux.com/abc123.m3u8"></mux-video>
* <mux-data player-software-name="mux-video"></mux-data>
* </video-player>
* ```
*/
export class MuxDataElement extends MediaComponentElement<MuxData> {
static readonly tagName = 'mux-data';
static override properties = {
envKey: { type: String, attribute: 'env-key' },
beaconCollectionDomain: { type: String, attribute: 'beacon-collection-domain' },
debug: { type: Boolean },
disableCookies: { type: Boolean, attribute: 'disable-cookies' },
playerSoftwareName: { type: String, attribute: 'player-software-name' },
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<Exclude<keyof MuxDataProps, 'metadata' | 'MuxDataSdk'>>;
protected createComponent(): MuxData {
return new MuxData();
}
/** Mux Data environment key for the beacons. Optional for Mux-hosted playback. */
get envKey(): string | undefined {
return this.component.envKey;
}
set envKey(value: string | null | undefined) {
this.component.envKey = value ?? undefined;
}
/** Custom domain beacons are sent to. */
get beaconCollectionDomain(): string | undefined {
return this.component.beaconCollectionDomain;
}
set beaconCollectionDomain(value: string | null | undefined) {
this.component.beaconCollectionDomain = value ?? undefined;
}
/** Enables Mux Data SDK debug logging. */
get debug(): boolean {
return this.component.debug;
}
set debug(value: boolean) {
this.component.debug = value;
}
/** Disables Mux Data SDK cookies. */
get disableCookies(): boolean {
return this.component.disableCookies;
}
set disableCookies(value: boolean) {
this.component.disableCookies = value;
}
/** Player software name reported to Mux Data (e.g. `mux-video`). */
get playerSoftwareName(): string | undefined {
return this.component.playerSoftwareName;
}
set playerSoftwareName(value: string | null | undefined) {
this.component.playerSoftwareName = value ?? undefined;
}
/** Player software version reported to Mux Data. Defaults to the Video.js version. */
get playerSoftwareVersion(): string | undefined {
return this.component.playerSoftwareVersion;
}
set playerSoftwareVersion(value: string | null | undefined) {
this.component.playerSoftwareVersion = value ?? undefined;
}
/** Epoch milliseconds the player was initialized. Defaults to the component's creation time. */
get playerInitTime(): number | undefined {
return this.component.playerInitTime;
}
set playerInitTime(value: number | null | undefined) {
this.component.playerInitTime = value ?? undefined;
}
/** Custom view metadata forwarded to the Mux Data SDK. */
get metadata(): MuxDataProps['metadata'] {
return this.component.metadata;
}
set metadata(value: MuxDataProps['metadata']) {
this.component.metadata = value;
}
/** Mux Data SDK used for monitoring. Set to `undefined` to disable monitoring. */
get MuxDataSdk(): MuxDataProps['MuxDataSdk'] {
return this.component.MuxDataSdk;
}
set MuxDataSdk(value: MuxDataProps['MuxDataSdk']) {
this.component.MuxDataSdk = value;
}
}
+1 -5
View File
@@ -1,8 +1,6 @@
import { CustomMediaElement } from '@videojs/media/dom/custom-media-element';
import { GoogleCast } from '@videojs/media/dom/google-cast';
import { StreamTypes } from '@videojs/media/dom/hls-js';
import { addMediaComponent } from '@videojs/media/dom/media-host';
import { MuxData, MuxMedia } from '@videojs/media/dom/mux';
import { MuxMedia } from '@videojs/media/dom/mux';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
const MuxVideoBase = MediaAttachMixin(CustomMediaElement('video', MuxMedia));
@@ -16,8 +14,6 @@ export class MuxVideo extends MuxVideoBase {
constructor() {
super();
addMediaComponent(this.host, new MuxData({ playerSoftwareName: 'mux-video' }));
addMediaComponent(this.host, new GoogleCast());
// Storyboards aren't generated for live streams; re-evaluate when the type is detected.
this.host.addEventListener('streamtypechange', () => this.#syncStoryboard());
// Covers both the `src` attribute and the `source` property (JS-only).
@@ -1,12 +1,5 @@
import { CustomMediaElement } from '@videojs/media/dom/custom-media-element';
import { GoogleCast } from '@videojs/media/dom/google-cast';
import { addMediaComponent } from '@videojs/media/dom/media-host';
import { NativeHlsMedia } from '@videojs/media/dom/native-hls';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
export class NativeHlsVideo extends MediaAttachMixin(CustomMediaElement('video', NativeHlsMedia)) {
constructor() {
super();
addMediaComponent(this.host, new GoogleCast());
}
}
export class NativeHlsVideo extends MediaAttachMixin(CustomMediaElement('video', NativeHlsMedia)) {}
@@ -0,0 +1,128 @@
import { ContextProvider } from '@videojs/element/context';
import type { Media } from '@videojs/media/dom';
import { GoogleCast } from '@videojs/media/dom/google-cast';
import { getMediaComponents } from '@videojs/media/dom/media-host';
import { HTMLVideoElementHost } from '@videojs/media/dom/video-host';
import { afterEach, describe, expect, it } from 'vitest';
import { mediaContext } from '../../player/context';
import { MediaElement } from '../../ui/media-element';
import { GoogleCastElement } from '../google-cast';
class TestMediaProvider extends MediaElement {
readonly #provider = new ContextProvider(this, {
context: mediaContext,
initialValue: { media: null, setMedia: () => {} },
});
setMedia(media: Media | null) {
this.#provider.setValue({ media, setMedia: () => {} });
}
}
customElements.define('test-cast-provider', TestMediaProvider);
customElements.define('test-google-cast', GoogleCastElement);
function setup() {
const host = new HTMLVideoElementHost();
const provider = new TestMediaProvider();
const el = new GoogleCastElement();
provider.append(el);
document.body.append(provider);
return { host, provider, el };
}
afterEach(() => {
document.body.innerHTML = '';
});
describe('GoogleCastElement', () => {
it('registers a GoogleCast 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);
});
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');
});
it('resolves the host from a media element host property', () => {
const { host, provider } = setup();
provider.setMedia({ host } as unknown as Media);
expect(getMediaComponents(host).get(GoogleCast)).toBeInstanceOf(GoogleCast);
});
it('ignores media that is not a media host', () => {
const { host, provider } = setup();
provider.setMedia(host as unknown as Media);
provider.setMedia(document.createElement('video') as unknown as Media);
expect(getMediaComponents(host).get(GoogleCast)).toBeUndefined();
});
it('forwards attributes to the component', () => {
const { host, provider, el } = setup();
provider.setMedia(host as unknown as Media);
el.setAttribute('receiver', 'APP_ID');
el.setAttribute('content-type', 'application/x-mpegURL');
el.setAttribute('stream-type', 'live');
el.setAttribute('src', 'https://example.com/stream.m3u8');
const component = getMediaComponents(host).get(GoogleCast)!;
expect(component.receiver).toBe('APP_ID');
expect(component.contentType).toBe('application/x-mpegURL');
expect(component.streamType).toBe('live');
expect(component.src).toBe('https://example.com/stream.m3u8');
// Properties read back from the component.
expect(el.receiver).toBe('APP_ID');
});
it('clears a component prop when its attribute is removed', () => {
const { el } = setup();
el.setAttribute('receiver', 'APP_ID');
el.removeAttribute('receiver');
expect(el.receiver).toBeUndefined();
});
it('moves the component when the media changes', () => {
const { host, provider } = setup();
const nextHost = new HTMLVideoElementHost();
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);
});
it('removes the component when the element disconnects', () => {
const { host, provider, el } = setup();
provider.setMedia(host as unknown as Media);
el.remove();
expect(getMediaComponents(host).get(GoogleCast)).toBeUndefined();
});
it('removes the component on destroy', () => {
const { host, provider, el } = setup();
provider.setMedia(host as unknown as Media);
el.destroy();
expect(getMediaComponents(host).get(GoogleCast)).toBeUndefined();
});
});
@@ -0,0 +1,110 @@
import { ContextProvider } from '@videojs/element/context';
import type { Media } from '@videojs/media/dom';
import { getMediaComponents, type MediaComponent } from '@videojs/media/dom/media-host';
import { HTMLVideoElementHost } from '@videojs/media/dom/video-host';
import { afterEach, describe, expect, it } from 'vitest';
import { mediaContext } from '../../player/context';
import { MediaElement } from '../../ui/media-element';
import { MediaComponentElement } from '../media-component-element';
class FakeComponent implements MediaComponent {
destroyed = false;
destroy() {
this.destroyed = true;
}
}
class TestMediaProvider extends MediaElement {
readonly #provider = new ContextProvider(this, {
context: mediaContext,
initialValue: { media: null, setMedia: () => {} },
});
setMedia(media: Media | null) {
this.#provider.setValue({ media, setMedia: () => {} });
}
}
class TestMediaComponentElement extends MediaComponentElement<FakeComponent> {
static readonly tagName = 'test-media-component';
/**
* Subclass field initializers run after the base constructor, which is the
* window the media context callback can fire in during a custom element
* upgrade. Reading the component here proves it resolves that early.
*/
readonly componentDuringFieldInit = this.component;
/** Exposes the protected component for assertions. */
get instance(): FakeComponent {
return this.component;
}
protected createComponent(): FakeComponent {
return new FakeComponent();
}
}
customElements.define('test-media-component-provider', TestMediaProvider);
customElements.define(TestMediaComponentElement.tagName, TestMediaComponentElement);
afterEach(() => {
document.body.innerHTML = '';
});
describe('MediaComponentElement', () => {
it('resolves the component before subclass fields initialize', () => {
const el = new TestMediaComponentElement();
expect(el.componentDuringFieldInit).toBeInstanceOf(FakeComponent);
// Created once and reused, not re-created per access.
expect(el.componentDuringFieldInit).toBe(el.instance);
});
it('registers the component with the media host from context', () => {
const host = new HTMLVideoElementHost();
const provider = new TestMediaProvider();
const el = new TestMediaComponentElement();
provider.append(el);
document.body.append(provider);
provider.setMedia(host as unknown as Media);
expect(getMediaComponents(host).get(FakeComponent)).toBe(el.instance);
});
it('destroys the component when the element is destroyed', () => {
const host = new HTMLVideoElementHost();
const provider = new TestMediaProvider();
const el = new TestMediaComponentElement();
provider.append(el);
document.body.append(provider);
provider.setMedia(host as unknown as Media);
el.destroy();
expect(el.instance.destroyed).toBe(true);
expect(getMediaComponents(host).get(FakeComponent)).toBeUndefined();
});
it('does not create a component when destroyed before use', () => {
const el = new TestMediaComponentElement();
// `componentDuringFieldInit` already forced creation, so assert through a
// subclass that never touches it.
class Untouched extends MediaComponentElement<FakeComponent> {
created = 0;
protected createComponent(): FakeComponent {
this.created++;
return new FakeComponent();
}
}
customElements.define('test-media-component-untouched', Untouched);
const untouched = new Untouched();
untouched.destroy();
expect(el.instance).toBeInstanceOf(FakeComponent);
expect(untouched.created).toBe(0);
});
});
@@ -0,0 +1,106 @@
import { ContextProvider } from '@videojs/element/context';
import type { Media } from '@videojs/media/dom';
import { getMediaComponents } from '@videojs/media/dom/media-host';
import { MuxData } from '@videojs/media/dom/mux';
import { HTMLVideoElementHost } from '@videojs/media/dom/video-host';
import { afterEach, describe, expect, it } from 'vitest';
import { mediaContext } from '../../player/context';
import { MediaElement } from '../../ui/media-element';
import { MuxDataElement } from '../mux-data';
class TestMediaProvider extends MediaElement {
readonly #provider = new ContextProvider(this, {
context: mediaContext,
initialValue: { media: null, setMedia: () => {} },
});
setMedia(media: Media | null) {
this.#provider.setValue({ media, setMedia: () => {} });
}
}
customElements.define('test-mux-data-provider', TestMediaProvider);
customElements.define('test-mux-data', MuxDataElement);
function setup() {
const host = new HTMLVideoElementHost();
const provider = new TestMediaProvider();
const el = new MuxDataElement();
// Prevent the real Mux SDK from initializing (and beaconing) in tests.
el.MuxDataSdk = undefined;
provider.append(el);
document.body.append(provider);
return { host, provider, el };
}
afterEach(() => {
document.body.innerHTML = '';
});
describe('MuxDataElement', () => {
it('registers when parsed into a connected player that already has media', () => {
const host = new HTMLVideoElementHost();
const provider = new TestMediaProvider();
document.body.append(provider);
provider.setMedia(host as unknown as Media);
provider.innerHTML = '<test-mux-data></test-mux-data>';
expect(getMediaComponents(host).get(MuxData)).toBeInstanceOf(MuxData);
});
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');
});
it('registers a MuxData 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);
});
it('forwards attributes to the component', () => {
const { host, provider, el } = setup();
provider.setMedia(host as unknown as Media);
el.setAttribute('env-key', 'test-key');
el.setAttribute('player-software-name', 'mux-video');
el.setAttribute('player-init-time', '1234');
el.setAttribute('debug', '');
el.setAttribute('disable-cookies', '');
const component = getMediaComponents(host).get(MuxData)!;
expect(component.envKey).toBe('test-key');
expect(component.playerSoftwareName).toBe('mux-video');
expect(component.playerInitTime).toBe(1234);
expect(component.debug).toBe(true);
expect(component.disableCookies).toBe(true);
// Properties read back from the component.
expect(el.envKey).toBe('test-key');
});
it('forwards the metadata property to the component', () => {
const { host, provider, el } = setup();
provider.setMedia(host as unknown as Media);
const metadata = { video_title: 'Test' };
el.metadata = metadata;
expect(getMediaComponents(host).get(MuxData)!.metadata).toEqual(metadata);
});
it('removes the component when the element disconnects', () => {
const { host, provider, el } = setup();
provider.setMedia(host as unknown as Media);
el.remove();
expect(getMediaComponents(host).get(MuxData)).toBeUndefined();
});
});
@@ -1,5 +1,3 @@
import { getMediaComponents } from '@videojs/media/dom/media-host';
import { MuxData } from '@videojs/media/dom/mux';
import { afterEach, describe, expect, it } from 'vitest';
import { MuxVideo } from '../mux-video';
@@ -7,8 +5,6 @@ customElements.define('test-mux-video', MuxVideo);
function createMuxVideo() {
const el = new MuxVideo();
// Prevent the real Mux SDK from initializing (and beaconing) in tests.
el.config = { muxData: { MuxDataSdk: undefined } };
document.body.appendChild(el);
return el;
}
@@ -18,32 +14,12 @@ afterEach(() => {
});
describe('MuxVideo', () => {
it('constructs the mux data component with the player software name', () => {
const el = createMuxVideo();
const muxData = getMediaComponents(el.host).get(MuxData);
expect(muxData).toBeInstanceOf(MuxData);
expect(muxData?.playerSoftwareName).toBe('mux-video');
});
it('exposes the element config as plain values, not the component instance', () => {
it('exposes the element config as a property, not an attribute', () => {
const el = createMuxVideo();
// `config` reflects exactly what was set — a plain namespace bag.
expect(el.config.muxData).toEqual({ MuxDataSdk: undefined });
expect(el.config.muxData).not.toBeInstanceOf(MuxData);
});
el.config = { preferPlayback: 'native' };
it('routes component config writes to the component', () => {
const el = createMuxVideo();
const muxData = getMediaComponents(el.host).get(MuxData);
el.config = { muxData: { envKey: 'test-key' } };
// The write reached the live component...
expect(muxData?.envKey).toBe('test-key');
// ...and config reads back the plain value, not the instance.
expect(el.config.muxData?.envKey).toBe('test-key');
expect(el.config.preferPlayback).toBe('native');
expect(el.hasAttribute('config')).toBe(false);
});
+2
View File
@@ -31,8 +31,10 @@ const presets = [
'background',
];
const media = [
'google-cast',
'hlsjs-video',
'mux-audio',
'mux-data',
'mux-video',
'native-hls-video',
'simple-hls-audio-only',