mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add Mux video component (#1036)
This commit is contained in:
@@ -7,6 +7,21 @@ export interface Delegate {
|
||||
detach?(): void;
|
||||
}
|
||||
|
||||
// Detects readonly vs writable properties via conditional type identity check.
|
||||
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
|
||||
|
||||
type WritableKeys<T> = {
|
||||
[K in keyof T]-?: IfEquals<{ [Q in K]: T[K] }, { -readonly [Q in K]: T[K] }, K, never>;
|
||||
}[keyof T];
|
||||
|
||||
type SettableKeys<T> = {
|
||||
[K in WritableKeys<T>]: T[K] extends (...args: any[]) => any ? never : K;
|
||||
}[WritableKeys<T>];
|
||||
|
||||
export type InferDelegateProps<D extends abstract new (...args: any[]) => any> = Partial<
|
||||
Pick<InstanceType<D>, SettableKeys<InstanceType<D>>>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Mixin that intercepts `get`, `set`, and `call` to delegate property access
|
||||
* and method calls to an instance of `DelegateClass` before falling through
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as dashjs from 'dashjs';
|
||||
|
||||
import { type Delegate, DelegateMixin } from '../../../core/media/delegate';
|
||||
import { CustomMediaMixin } from '../custom-media-element';
|
||||
import { MediaProxyMixin } from '../proxy';
|
||||
import { CustomVideoElement } from '../custom-media-element';
|
||||
import { VideoProxy } from '../proxy';
|
||||
|
||||
export class DashMediaDelegateBase implements Delegate {
|
||||
export class DashMediaDelegate implements Delegate {
|
||||
#engine: dashjs.MediaPlayerClass;
|
||||
|
||||
constructor() {
|
||||
@@ -39,10 +39,7 @@ export class DashMediaDelegateBase implements Delegate {
|
||||
}
|
||||
|
||||
// This is used by the web component because it needs to extend HTMLElement!
|
||||
export class DashCustomMedia extends DelegateMixin(
|
||||
CustomMediaMixin(globalThis.HTMLElement ?? class {}, { tag: 'video' }),
|
||||
DashMediaDelegateBase
|
||||
) {}
|
||||
export class DashCustomMedia extends DelegateMixin(CustomVideoElement, DashMediaDelegate) {}
|
||||
|
||||
// This is used by the React component.
|
||||
export class DashMedia extends DelegateMixin(MediaProxyMixin, DashMediaDelegateBase) {}
|
||||
export class DashMedia extends DelegateMixin(VideoProxy, DashMediaDelegate) {}
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import Hls from 'hls.js';
|
||||
|
||||
import { type Delegate, DelegateMixin } from '../../../core/media/delegate';
|
||||
import { CustomMediaMixin } from '../custom-media-element';
|
||||
import { MediaProxyMixin } from '../proxy';
|
||||
import { CustomVideoElement } from '../custom-media-element';
|
||||
import { VideoProxy } from '../proxy';
|
||||
import { HlsMediaTextTracksMixin } from './text-tracks';
|
||||
|
||||
export { Hls };
|
||||
|
||||
export type PlaybackType = (typeof PlaybackTypes)[keyof typeof PlaybackTypes];
|
||||
export type SourceType = (typeof SourceTypes)[keyof typeof SourceTypes];
|
||||
|
||||
export const PlaybackTypes = {
|
||||
MSE: 'mse',
|
||||
NATIVE: 'native',
|
||||
};
|
||||
|
||||
export const SourceTypes = {
|
||||
M3U8: 'application/vnd.apple.mpegurl',
|
||||
MP4: 'video/mp4',
|
||||
};
|
||||
|
||||
const defaultConfig = {
|
||||
backBufferLength: 30,
|
||||
renderTextTracksNatively: false,
|
||||
@@ -14,40 +29,138 @@ const defaultConfig = {
|
||||
};
|
||||
|
||||
export class HlsMediaDelegateBase implements Delegate {
|
||||
#engine = Hls.isSupported() ? new Hls(defaultConfig) : null;
|
||||
#target: EventTarget | null = null;
|
||||
#engine: Hls | null = null;
|
||||
#loadRequested?: Promise<void> | null;
|
||||
#src: string = '';
|
||||
#debug: boolean = false;
|
||||
#type: SourceType | undefined;
|
||||
#preferPlayback: PlaybackType | undefined = 'mse';
|
||||
|
||||
constructor() {
|
||||
this.#initialize();
|
||||
}
|
||||
|
||||
#initialize(): void {
|
||||
this.#engine?.destroy();
|
||||
this.#engine = null;
|
||||
|
||||
if (this.type !== SourceTypes.M3U8) return;
|
||||
if (this.#preferPlayback === PlaybackTypes.NATIVE) return;
|
||||
if (!Hls.isSupported()) return;
|
||||
|
||||
this.#engine = new Hls({
|
||||
...defaultConfig,
|
||||
debug: this.#debug,
|
||||
});
|
||||
|
||||
if (this.#target) {
|
||||
this.#engine.attachMedia(this.#target as HTMLMediaElement);
|
||||
}
|
||||
|
||||
if (this.#src) {
|
||||
this.#requestLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/** The target element, or `null` when not attached. */
|
||||
get target(): EventTarget | null | undefined {
|
||||
return this.#target ?? null;
|
||||
}
|
||||
|
||||
/** The underlying hls.js instance, or `null` when using native playback. */
|
||||
get engine(): Hls | null {
|
||||
return this.#engine;
|
||||
}
|
||||
|
||||
/** Explicit source type. When unset, inferred from the source URL extension. */
|
||||
get type(): SourceType | undefined {
|
||||
return this.#type ?? inferSourceType(this.#src);
|
||||
}
|
||||
|
||||
set type(value: SourceType | undefined) {
|
||||
if (this.#type === value) return;
|
||||
this.#type = value;
|
||||
this.#initialize();
|
||||
}
|
||||
|
||||
/** Enable hls.js debug logging. Re-initializes the engine when changed. */
|
||||
get debug(): boolean {
|
||||
return this.#debug;
|
||||
}
|
||||
|
||||
set debug(value: boolean) {
|
||||
if (this.#debug === value) return;
|
||||
this.#debug = value;
|
||||
this.#initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to prefer `'mse'` (hls.js) or `'native'` (browser-built-in) HLS
|
||||
* playback. Changing this re-initializes the delegate.
|
||||
*/
|
||||
get preferPlayback(): PlaybackType | undefined {
|
||||
return this.#preferPlayback;
|
||||
}
|
||||
|
||||
set preferPlayback(value: PlaybackType | undefined) {
|
||||
if (this.#preferPlayback === value) return;
|
||||
this.#preferPlayback = value;
|
||||
this.#initialize();
|
||||
}
|
||||
|
||||
/** The HLS source URL to load. */
|
||||
set src(src: string) {
|
||||
this.#src = src;
|
||||
this.#requestLoad();
|
||||
}
|
||||
|
||||
get src(): string {
|
||||
return this.#src;
|
||||
}
|
||||
|
||||
async #requestLoad() {
|
||||
if (this.#loadRequested) return;
|
||||
await (this.#loadRequested = Promise.resolve());
|
||||
this.#loadRequested = null;
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
if (this.#engine) {
|
||||
this.#engine.loadSource(this.#src);
|
||||
} else if (this.#target) {
|
||||
(this.#target as HTMLMediaElement).src = this.#src;
|
||||
}
|
||||
}
|
||||
|
||||
attach(target: EventTarget): void {
|
||||
this.#target = target;
|
||||
this.#engine?.attachMedia(target as HTMLMediaElement);
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
this.#engine?.detachMedia();
|
||||
this.#target = null;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.#engine?.destroy();
|
||||
}
|
||||
|
||||
set src(src: string) {
|
||||
this.#engine?.loadSource(src);
|
||||
}
|
||||
|
||||
get src(): string {
|
||||
return this.#engine?.url ?? '';
|
||||
this.#engine = null;
|
||||
this.#target = null;
|
||||
}
|
||||
}
|
||||
|
||||
const HlsMediaDelegate = HlsMediaTextTracksMixin(HlsMediaDelegateBase);
|
||||
function inferSourceType(src: string): SourceType {
|
||||
const path = src.split(/[?#]/)[0] ?? '';
|
||||
if (path.endsWith('.mp4')) return SourceTypes.MP4;
|
||||
return SourceTypes.M3U8;
|
||||
}
|
||||
|
||||
export const HlsMediaDelegate = HlsMediaTextTracksMixin(HlsMediaDelegateBase);
|
||||
|
||||
// This is used by the web component because it needs to extend HTMLElement!
|
||||
export class HlsCustomMedia extends DelegateMixin(
|
||||
CustomMediaMixin(globalThis.HTMLElement ?? class {}, { tag: 'video' }),
|
||||
HlsMediaDelegate
|
||||
) {}
|
||||
export class HlsCustomMedia extends DelegateMixin(CustomVideoElement, HlsMediaDelegate) {}
|
||||
|
||||
// This is used by the React component.
|
||||
export class HlsMedia extends DelegateMixin(MediaProxyMixin, HlsMediaDelegate) {}
|
||||
export class HlsMedia extends DelegateMixin(VideoProxy, HlsMediaDelegate) {}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const getEnvPlayerVersion = () => {
|
||||
try {
|
||||
// @ts-expect-error
|
||||
return __PLAYER_VERSION__ as string;
|
||||
} catch {}
|
||||
return 'UNKNOWN';
|
||||
};
|
||||
|
||||
const player_version: string = getEnvPlayerVersion();
|
||||
|
||||
export const getPlayerVersion = () => player_version;
|
||||
@@ -0,0 +1,224 @@
|
||||
import Mux from 'mux-embed';
|
||||
|
||||
import { type Delegate, DelegateMixin } from '../../../core/media/delegate';
|
||||
import { CustomVideoElement } from '../custom-media-element';
|
||||
import { Hls, HlsMediaDelegate } from '../hls';
|
||||
import { VideoProxy } from '../proxy';
|
||||
import { getPlayerVersion } from './env';
|
||||
import type { MuxDataSdk } from './types';
|
||||
|
||||
const MUX_VIDEO_DOMAIN = 'mux.com';
|
||||
|
||||
export class MuxMediaDelegate extends HlsMediaDelegate implements Delegate {
|
||||
static PLAYER_SOFTWARE_NAME = '';
|
||||
|
||||
#playbackId: string | null = null;
|
||||
#customDomain: string = MUX_VIDEO_DOMAIN;
|
||||
#MuxDataSdk: MuxDataSdk | undefined = Mux;
|
||||
#beaconCollectionDomain: string | undefined;
|
||||
#disableCookies: boolean = false;
|
||||
#metadata: Record<string, any> | undefined;
|
||||
#envKey: string | undefined;
|
||||
#playerSoftwareName: string | undefined = (this.constructor as typeof MuxMediaDelegate).PLAYER_SOFTWARE_NAME;
|
||||
#playerSoftwareVersion: string | undefined = getPlayerVersion();
|
||||
#playerInitTime: number | undefined = this.#generatePlayerInitTime();
|
||||
|
||||
get playbackId() {
|
||||
return this.#playbackId;
|
||||
}
|
||||
|
||||
set playbackId(value: string | null) {
|
||||
if (this.#playbackId === value) return;
|
||||
this.#playbackId = value;
|
||||
this.#syncSrc();
|
||||
}
|
||||
|
||||
get customDomain(): string {
|
||||
return this.#customDomain;
|
||||
}
|
||||
|
||||
set customDomain(value: string) {
|
||||
const normalized = value || MUX_VIDEO_DOMAIN;
|
||||
if (this.#customDomain === normalized) return;
|
||||
this.#customDomain = normalized;
|
||||
this.#syncSrc();
|
||||
}
|
||||
|
||||
get MuxDataSdk() {
|
||||
return this.#MuxDataSdk;
|
||||
}
|
||||
|
||||
set MuxDataSdk(value) {
|
||||
this.#MuxDataSdk = value;
|
||||
}
|
||||
|
||||
get beaconCollectionDomain(): string | undefined {
|
||||
return this.#beaconCollectionDomain;
|
||||
}
|
||||
|
||||
set beaconCollectionDomain(value: string | undefined) {
|
||||
this.#beaconCollectionDomain = value;
|
||||
}
|
||||
|
||||
get disableCookies(): boolean {
|
||||
return this.#disableCookies;
|
||||
}
|
||||
|
||||
set disableCookies(value: boolean) {
|
||||
this.#disableCookies = value;
|
||||
}
|
||||
|
||||
get envKey(): string | undefined {
|
||||
return this.#envKey;
|
||||
}
|
||||
|
||||
set envKey(value: string | undefined) {
|
||||
this.#envKey = value;
|
||||
}
|
||||
|
||||
get playerSoftwareName(): string | undefined {
|
||||
return this.#playerSoftwareName;
|
||||
}
|
||||
|
||||
set playerSoftwareName(value: string | undefined) {
|
||||
this.#playerSoftwareName = value;
|
||||
}
|
||||
|
||||
get playerSoftwareVersion(): string | undefined {
|
||||
return this.#playerSoftwareVersion;
|
||||
}
|
||||
|
||||
set playerSoftwareVersion(value: string | undefined) {
|
||||
this.#playerSoftwareVersion = value;
|
||||
}
|
||||
|
||||
get playerInitTime(): number | undefined {
|
||||
return this.#playerInitTime;
|
||||
}
|
||||
|
||||
set playerInitTime(value: number | undefined) {
|
||||
this.#playerInitTime = value;
|
||||
}
|
||||
|
||||
get metadata(): Record<string, any> | undefined {
|
||||
return this.#metadata;
|
||||
}
|
||||
|
||||
set metadata(value: Record<string, any> | undefined) {
|
||||
this.#metadata = value;
|
||||
}
|
||||
|
||||
attach(target: EventTarget): void {
|
||||
super.attach(target);
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
this.#MuxDataSdk?.destroyMonitor(this.target as HTMLMediaElement);
|
||||
|
||||
super.detach();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.#initializeMuxDataSdk();
|
||||
super.load();
|
||||
}
|
||||
|
||||
#syncSrc(): void {
|
||||
this.src = this.#playbackId ? toSrc(this.#playbackId, this.#customDomain) : '';
|
||||
}
|
||||
|
||||
#initializeMuxDataSdk(): void {
|
||||
const target = this.target as HTMLMediaElement;
|
||||
if (!this.#MuxDataSdk || !target || target.mux) return;
|
||||
|
||||
const {
|
||||
debug,
|
||||
beaconCollectionDomain,
|
||||
disableCookies,
|
||||
engine: hlsjs,
|
||||
envKey: env_key,
|
||||
playerSoftwareName: player_software_name,
|
||||
playerSoftwareVersion: player_software_version,
|
||||
playerInitTime: player_init_time,
|
||||
metadata = {},
|
||||
} = this;
|
||||
|
||||
const { view_session_id = this.#MuxDataSdk?.utils.generateUUID() } = metadata;
|
||||
const video_id = toVideoId(this);
|
||||
metadata.view_session_id = view_session_id;
|
||||
metadata.video_id = video_id;
|
||||
|
||||
this.#MuxDataSdk?.monitor(this.target as HTMLMediaElement, {
|
||||
debug,
|
||||
...(beaconCollectionDomain ? { beaconCollectionDomain } : {}),
|
||||
...(disableCookies ? { disableCookies } : {}),
|
||||
...(hlsjs ? { hlsjs } : {}),
|
||||
Hls,
|
||||
data: {
|
||||
...(env_key ? { env_key } : {}),
|
||||
...(player_software_name ? { player_software_name } : {}),
|
||||
// NOTE: Adding this because there appears to be some instability on whether
|
||||
// player_software_name or player_software "wins" for Mux Data (CJP)
|
||||
...(player_software_name ? { player_software: player_software_name } : {}),
|
||||
...(player_software_version ? { player_software_version } : {}),
|
||||
...(player_init_time ? { player_init_time } : {}),
|
||||
// Use any metadata passed in programmatically (which may override the defaults above)
|
||||
...metadata,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
#generatePlayerInitTime(): number | undefined {
|
||||
if (!this.#MuxDataSdk) return undefined;
|
||||
return this.#MuxDataSdk.utils.now();
|
||||
}
|
||||
}
|
||||
|
||||
function toSrc(playbackId: string, customDomain: string): string {
|
||||
return `https://stream.${customDomain}/${playbackId}.m3u8`;
|
||||
}
|
||||
|
||||
type MuxSrcProps = Pick<MuxMediaDelegate, 'playbackId' | 'src' | 'customDomain'>;
|
||||
|
||||
export function toVideoId(props: MuxSrcProps & Pick<MuxMediaDelegate, 'metadata'>): string | undefined {
|
||||
if (props.metadata?.video_id) return props.metadata.video_id;
|
||||
if (!isMuxVideoSrc(props)) return props.src;
|
||||
return toPlaybackIdFromParameterized(props.playbackId) ?? toPlaybackIdFromSrc(props.src) ?? props.src;
|
||||
}
|
||||
|
||||
function toPlaybackIdFromParameterized(playbackId: MuxMediaDelegate['playbackId']): string | undefined {
|
||||
if (!playbackId) return undefined;
|
||||
const [id] = playbackId.split('?');
|
||||
return id || undefined;
|
||||
}
|
||||
|
||||
export function toPlaybackIdFromSrc(src: MuxMediaDelegate['src']): string | undefined {
|
||||
if (!src || !src.startsWith('https://stream.')) return undefined;
|
||||
const [playbackId] = new URL(src).pathname.slice(1).split(/\.m3u8|\//);
|
||||
return playbackId || undefined;
|
||||
}
|
||||
|
||||
export function isMuxVideoSrc({ playbackId, src, customDomain }: MuxSrcProps): boolean {
|
||||
if (playbackId) return true;
|
||||
if (typeof src !== 'string') return false;
|
||||
const base = window?.location.href;
|
||||
const hostname = new URL(src, base).hostname.toLocaleLowerCase();
|
||||
|
||||
return hostname.includes(MUX_VIDEO_DOMAIN) || (!!customDomain && hostname.includes(customDomain.toLocaleLowerCase()));
|
||||
}
|
||||
|
||||
export class MuxVideoDelegate extends MuxMediaDelegate {
|
||||
static PLAYER_SOFTWARE_NAME = 'mux-video';
|
||||
}
|
||||
|
||||
export class MuxAudioDelegate extends MuxMediaDelegate {
|
||||
static PLAYER_SOFTWARE_NAME = 'mux-audio';
|
||||
}
|
||||
|
||||
export class MuxCustomMedia extends DelegateMixin(CustomVideoElement, MuxMediaDelegate) {}
|
||||
|
||||
export class MuxMedia extends DelegateMixin(VideoProxy, MuxMediaDelegate) {}
|
||||
|
||||
export class MuxCustomVideo extends DelegateMixin(CustomVideoElement, MuxVideoDelegate) {}
|
||||
|
||||
export class MuxVideo extends DelegateMixin(VideoProxy, MuxVideoDelegate) {}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MuxMediaDelegate } from '..';
|
||||
|
||||
describe('MuxMediaDelegate', () => {
|
||||
it('defaults playbackId to null', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
expect(delegate.playbackId).toBeNull();
|
||||
});
|
||||
|
||||
it('defaults customDomain to mux.com', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
expect(delegate.customDomain).toBe('mux.com');
|
||||
});
|
||||
|
||||
it('sets src when playbackId is set', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
delegate.playbackId = 'abc123';
|
||||
|
||||
expect(delegate.src).toBe('https://stream.mux.com/abc123.m3u8');
|
||||
});
|
||||
|
||||
it('uses customDomain in the generated src', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
delegate.customDomain = 'example.com';
|
||||
delegate.playbackId = 'abc123';
|
||||
|
||||
expect(delegate.src).toBe('https://stream.example.com/abc123.m3u8');
|
||||
});
|
||||
|
||||
it('updates src when customDomain changes after playbackId', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
delegate.playbackId = 'abc123';
|
||||
expect(delegate.src).toBe('https://stream.mux.com/abc123.m3u8');
|
||||
|
||||
delegate.customDomain = 'custom.tv';
|
||||
expect(delegate.src).toBe('https://stream.custom.tv/abc123.m3u8');
|
||||
});
|
||||
|
||||
it('falls back to default domain when customDomain is set to empty', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
delegate.customDomain = 'custom.tv';
|
||||
delegate.playbackId = 'abc123';
|
||||
expect(delegate.src).toBe('https://stream.custom.tv/abc123.m3u8');
|
||||
|
||||
delegate.customDomain = '';
|
||||
expect(delegate.src).toBe('https://stream.mux.com/abc123.m3u8');
|
||||
});
|
||||
|
||||
it('does not update src when playbackId is set to the same value', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
delegate.playbackId = 'abc123';
|
||||
expect(delegate.src).toBe('https://stream.mux.com/abc123.m3u8');
|
||||
|
||||
delegate.src = 'https://override.example.com/video.m3u8';
|
||||
delegate.playbackId = 'abc123';
|
||||
expect(delegate.src).toBe('https://override.example.com/video.m3u8');
|
||||
});
|
||||
|
||||
it('does not trigger syncSrc when customDomain is set to empty while already default', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
delegate.playbackId = 'abc123';
|
||||
expect(delegate.src).toBe('https://stream.mux.com/abc123.m3u8');
|
||||
|
||||
delegate.src = 'https://override.example.com/video.m3u8';
|
||||
delegate.customDomain = '';
|
||||
expect(delegate.src).toBe('https://override.example.com/video.m3u8');
|
||||
});
|
||||
|
||||
it('clears src when playbackId is null and customDomain changes', () => {
|
||||
const delegate = new MuxMediaDelegate();
|
||||
delegate.src = 'https://manual.example.com/video.m3u8';
|
||||
delegate.customDomain = 'custom.tv';
|
||||
|
||||
expect(delegate.src).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
/// <reference path="../../../../node_modules/mux-embed/dist/types/mux-embed.d.ts" preserve="true" />
|
||||
export type { Mux as MuxDataSdk } from 'mux-embed';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ProxyMixin } from '../../core/media/proxy';
|
||||
|
||||
export const MediaProxyMixin = ProxyMixin(
|
||||
export const VideoProxy = ProxyMixin(
|
||||
globalThis.HTMLVideoElement ?? class {},
|
||||
globalThis.HTMLMediaElement ?? class {},
|
||||
globalThis.EventTarget ?? class {}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { SpfMedia as SpfMediaDelegate } from '@videojs/spf/dom';
|
||||
import { DelegateMixin } from '../../../core/media/delegate';
|
||||
import { CustomMediaMixin } from '../custom-media-element';
|
||||
import { MediaProxyMixin } from '../proxy';
|
||||
import { CustomVideoElement } from '../custom-media-element';
|
||||
import { VideoProxy } from '../proxy';
|
||||
|
||||
// This is used by the web component because it needs to extend HTMLElement!
|
||||
export class SimpleHlsCustomMedia extends DelegateMixin(
|
||||
CustomMediaMixin(globalThis.HTMLElement ?? class {}, { tag: 'video' }),
|
||||
SpfMediaDelegate
|
||||
) {}
|
||||
export class SimpleHlsCustomMedia extends DelegateMixin(CustomVideoElement, SpfMediaDelegate) {}
|
||||
|
||||
// This is used by the React component.
|
||||
export class SimpleHlsMedia extends DelegateMixin(MediaProxyMixin, SpfMediaDelegate) {}
|
||||
export class SimpleHlsMedia extends DelegateMixin(VideoProxy, SpfMediaDelegate) {}
|
||||
|
||||
Reference in New Issue
Block a user