Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/media/src/core/predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ export function hasMetadata(media: MediaSourceCapability): boolean {
return media.readyState >= 1;
}

/**
* Get the playback engine exposed by a Media.
*
* Returns `undefined` when the value exposes no engine property. An explicit
* `null` is preserved because it means the Media supports an engine but none is
* currently active.
*
* @param media - Media value whose engine should be read.
*/
export function getMediaEngine(media: unknown): unknown {
return isObject(media) && 'engine' in media ? media.engine : undefined;
}

export function isMediaPauseCapable(value: unknown): value is MediaPauseCapability {
if (!isObject(value)) return false;
const media = value as Record<string, unknown>;
Expand Down
18 changes: 18 additions & 0 deletions packages/media/src/core/tests/predicate.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import { describe, expect, it } from 'vitest';
import { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from '../constants';
import {
getMediaEngine,
isMediaBufferCapable,
isMediaContentDataCapable,
isMediaRemotePlaybackCapable,
isMediaTextTrackCapable,
} from '../predicate';

describe('getMediaEngine', () => {
it('returns the exposed engine', () => {
const engine = {};

expect(getMediaEngine({ engine })).toBe(engine);
});

it('preserves an explicitly inactive engine', () => {
expect(getMediaEngine({ engine: null })).toBeNull();
});

it('returns undefined when no engine is exposed', () => {
expect(getMediaEngine({})).toBeUndefined();
expect(getMediaEngine(null)).toBeUndefined();
});
});

describe('isMediaContentDataCapable', () => {
it('uses undefined as the unsupported sentinel', () => {
expect(isMediaContentDataCapable({})).toBe(false);
Expand Down
1 change: 1 addition & 0 deletions packages/media/src/dom/dash/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './media';
export { isDashMedia } from './predicate';
2 changes: 2 additions & 0 deletions packages/media/src/dom/dash/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as dashjs from 'dashjs';
import { MediaTracksMixin } from '../../core/media-tracks';
import type { MediaEngineHost } from '../../core/types';
import { HTMLVideoElementHost } from '../video-host';
import { DASH_MEDIA } from './predicate';

/** Structured DASH source: which source to play, plus how to play it. */
export interface DashSource {
Expand Down Expand Up @@ -41,6 +42,7 @@ export class DashMedia
extends DashMediaBase
implements MediaEngineHost<dashjs.MediaPlayerClass, HTMLVideoElement>, DashMediaProps
{
readonly [DASH_MEDIA] = true;
#engine: dashjs.MediaPlayerClass;
#src = dashMediaDefaultProps.src;
#source: DashSource | null = dashMediaDefaultProps.source;
Expand Down
13 changes: 13 additions & 0 deletions packages/media/src/dom/dash/predicate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { isObject } from '@videojs/utils/predicate';
import type { DashMedia } from './media';

export const DASH_MEDIA = Symbol.for('@videojs/media/dash');

/**
* Check whether a value is a `DashMedia`.
*
* @param value - Value to identify.
*/
export function isDashMedia(value: unknown): value is DashMedia {
return isObject(value) && DASH_MEDIA in value;
}
12 changes: 12 additions & 0 deletions packages/media/src/dom/dash/tests/predicate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { DashMedia, isDashMedia } from '..';

describe('isDashMedia', () => {
it('recognizes DashMedia by its symbol marker', () => {
const media = new DashMedia();

expect(isDashMedia(media)).toBe(true);
expect(isDashMedia({})).toBe(false);
expect(isDashMedia(null)).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/media/src/dom/hls-js/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type { DrmSystemConfig, DrmSystemsConfig, KeySystem } from '../../core/drm';
export { KeySystems } from '../../core/drm';
export * from './media';
export { isHlsJsMedia } from './predicate';
2 changes: 2 additions & 0 deletions packages/media/src/dom/hls-js/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type MediaStreamType, MediaStreamTypes } from '../../core/types';
import { type NativeHlsConfig, NativeHlsMedia, type NativeHlsSource } from '../native-hls';
import { HTMLVideoElementHost } from '../video-host';
import { HlsJsOnlyMedia } from './hls-js-only';
import { HLS_JS_MEDIA } from './predicate';

export type PreloadType = '' | 'none' | 'metadata' | 'auto';

Expand Down Expand Up @@ -103,6 +104,7 @@ class HlsMediaEvent extends Event {}
* @fires targetlivewindowchange - Fired when the target live window changes. Read `targetLiveWindow` for the new value.
*/
export class HlsJsMedia extends HTMLVideoElementHost implements HlsMediaProps {
readonly [HLS_JS_MEDIA] = true;
#delegate: HlsJsOnlyMedia | NativeHlsMedia | null = null;
#mediaElement: HTMLVideoElement | null = null;
#src = hlsMediaDefaultProps.src;
Expand Down
13 changes: 13 additions & 0 deletions packages/media/src/dom/hls-js/predicate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { isObject } from '@videojs/utils/predicate';
import type { HlsJsMedia } from './media';

export const HLS_JS_MEDIA = Symbol.for('@videojs/media/hls-js');

/**
* Check whether a value is an `HlsJsMedia` or one of its subclasses.
*
* @param value - Value to identify.
*/
export function isHlsJsMedia(value: unknown): value is HlsJsMedia {
return isObject(value) && HLS_JS_MEDIA in value;
}
14 changes: 14 additions & 0 deletions packages/media/src/dom/hls-js/tests/predicate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { HlsJsMedia, isHlsJsMedia } from '..';

describe('isHlsJsMedia', () => {
it('recognizes HlsJsMedia by its symbol marker', () => {
const media = new HlsJsMedia();

expect(isHlsJsMedia(media)).toBe(true);
expect(isHlsJsMedia({})).toBe(false);
expect(isHlsJsMedia(null)).toBe(false);

media.destroy();
});
});
1 change: 1 addition & 0 deletions packages/media/src/dom/mux/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type { DrmSystemConfig, DrmSystemsConfig, KeySystem } from '../../core/dr
export { KeySystems } from '../../core/drm';
export * from './media';
export { MuxData, type MuxDataProps, muxDataDefaultProps } from './mux-data';
export { isMuxMedia } from './predicate';
// The engine-neutral source layer, re-exported so this stays the one import for
// the hls.js-backed Media. Its own entry point, `@videojs/media/dom/mux/source`,
// is what a Media on another engine imports — reaching it through here would
Expand Down
2 changes: 2 additions & 0 deletions packages/media/src/dom/mux/media.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HlsJsMedia, type HlsSource } from '../hls-js';
import { createMuxDrmSystems } from './drm';
import { MUX_MEDIA } from './predicate';
import {
createMuxPosterURL,
createMuxStoryboardURL,
Expand Down Expand Up @@ -36,6 +37,7 @@ export const muxMediaDefaultProps: MuxMediaProps = {
* @fires sourcechange - Fired when `source` changes, either directly or by parsing a new `src`. Read `source` for the new value.
*/
export class MuxMedia extends HlsJsMedia implements MuxMediaProps {
readonly [MUX_MEDIA] = true;
#source: MuxSource | null = muxMediaDefaultProps.source;

/**
Expand Down
49 changes: 17 additions & 32 deletions packages/media/src/dom/mux/mux-data-engine.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
import { hasMethods, isFunction, isObject, isString } from '@videojs/utils/predicate';
import { isFunction, isObject, isString } from '@videojs/utils/predicate';
import { getMediaEngine } from '../../core/predicate';
import { isDashMedia } from '../dash/predicate';
import { isHlsJsMedia } from '../hls-js/predicate';
import type { MuxDataOptions } from './types';

/** The `mux-embed` monitor options that hook a playback engine's own telemetry. */
export type MuxDataEngineOptions = Partial<Pick<MuxDataOptions, 'Hls' | 'hlsjs' | 'dashjs'>>;

type MuxDataHlsJsEngine = NonNullable<MuxDataOptions['hlsjs']>;
type MuxDataHlsJsClass = NonNullable<MuxDataOptions['Hls']>;
type MuxDataDashJsEngine = NonNullable<MuxDataOptions['dashjs']>;

const warnedEngines = new WeakSet<object>();

/**
* Pick the `mux-embed` integration for a media's playback engine.
* Pick the `mux-embed` integration for a Media's playback engine.
*
* `mux-embed` monitors the media element on its own, and an engine integration
* is what adds engine-level data on top: rendition switches, request timing and
* throughput, and engine errors. It ships two — hls.js and dash.js — and each
* one is wired through a different option, so handing a dash.js player to the
* hls.js option leaves the view with element-level data only.
*
* Engines are matched by shape rather than by class so this module imports
* neither hls.js nor dash.js. Mux Data can be registered with any media without
* pulling an engine it will never touch into the bundle (dash.js in particular
* reads `window` on import), and a media whose engine has no integration — a
* raw `<video>`, native HLS, or an SPF playback engine — is monitored from the
* element alone instead of through the wrong integration.
* Media are identified by their symbol-backed type guards, so this module
* imports neither hls.js nor dash.js. Mux Data can be registered with any media
* without pulling an engine it will never touch into the bundle (dash.js in
* particular reads `window` on import), and a media whose engine has no
* integration — a raw `<video>`, native HLS, or an SPF playback engine — is
* monitored from the element alone instead of through the wrong integration.
*
* @returns Options to spread into a `Mux.monitor()` call. Empty when the engine
* has no integration, which leaves element-level monitoring intact.
*/
export function toMuxDataEngineOptions(engine: unknown): MuxDataEngineOptions {
if (isDashJsEngine(engine)) return { dashjs: engine };
export function toMuxDataEngineOptions(media: unknown): MuxDataEngineOptions {
if (isDashMedia(media)) return { dashjs: media.engine };

if (isHlsJsEngine(engine)) {
if (isHlsJsMedia(media) && media.engine) {
// `mux-embed` reads hls.js's event names and error details off the class,
// falling back to `window.Hls` when it isn't given one. Take it from the
// instance so a bundled hls.js is always found, without importing it here.
const Hls = toHlsJsClass(engine);
if (Hls) return { hlsjs: engine, Hls };
const Hls = toHlsJsClass(media.engine);
if (Hls) return { hlsjs: media.engine, Hls };
}

const engine = getMediaEngine(media);
if (__DEV__ && isObject(engine) && !warnedEngines.has(engine)) {
warnedEngines.add(engine);
console.warn(
Expand All @@ -51,23 +53,6 @@ export function toMuxDataEngineOptions(engine: unknown): MuxDataEngineOptions {
return {};
}

// Each engine is recognized by what its `mux-embed` monitor reaches for, so a
// match means the integration has everything it needs. Both monitors subscribe
// through `on` / `off`; the rendition APIs are what tell the two engines apart.

/** hls.js: its monitor reads renditions from `levels`. */
function isHlsJsEngine(engine: unknown): engine is MuxDataHlsJsEngine {
return hasMethods(engine, ['on', 'off']) && Array.isArray((engine as { levels?: unknown }).levels);
}

/** dash.js: its monitor reads renditions through the track and rendition-list getters. */
function isDashJsEngine(engine: unknown): engine is MuxDataDashJsEngine {
if (!hasMethods(engine, ['on', 'off', 'getCurrentTrackFor'])) return false;
// dash.js v5 replaced `getBitrateInfoListFor` with `getRepresentationsByType`.
// `mux-embed` reads whichever the player has, so either one is a match.
return hasMethods(engine, ['getRepresentationsByType']) || hasMethods(engine, ['getBitrateInfoListFor']);
}

/** hls.js's own class, the only place its event names and error details are published. */
function toHlsJsClass(engine: object): MuxDataHlsJsClass | undefined {
const engineClass: unknown = engine.constructor;
Expand Down
8 changes: 4 additions & 4 deletions packages/media/src/dom/mux/mux-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ const MUX_VIDEO_DOMAIN = 'mux.com';
*
* `engine` is deliberately untyped. Every engine-backed media host exposes one,
* but they are unrelated types (an hls.js instance, a dash.js player, an SPF
* composition), and which of them Mux Data can hook is decided by
* {@link toMuxDataEngineOptions}, not by this contract. Media with no JS engine
* simply omit it.
* composition), and the symbol-backed Media guards in
* {@link toMuxDataEngineOptions} decide which of them Mux Data can hook. Media
* with no JS engine simply omit it.
*/
export interface MuxDataMedia extends EventTarget {
readonly engine?: unknown;
Expand Down Expand Up @@ -225,7 +225,7 @@ export class MuxData implements MuxDataProps {
debug,
...(beaconCollectionDomain ? { beaconCollectionDomain } : {}),
...(disableCookies ? { disableCookies } : {}),
...toMuxDataEngineOptions(media.engine),
...toMuxDataEngineOptions(media),
data: {
...(env_key ? { env_key } : {}),
...(player_software_name ? { player_software_name } : {}),
Expand Down
13 changes: 13 additions & 0 deletions packages/media/src/dom/mux/predicate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { isObject } from '@videojs/utils/predicate';
import type { MuxMedia } from './media';

export const MUX_MEDIA = Symbol.for('@videojs/media/mux');

/**
* Check whether a value is a `MuxMedia`.
*
* @param value - Value to identify.
*/
export function isMuxMedia(value: unknown): value is MuxMedia {
return isObject(value) && MUX_MEDIA in value;
}
Loading
Loading