Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Add hidden "Dummy media API" experimental feature and add integration tests linked to DRM #1478

Open
wants to merge 9 commits into
base: misc/no-eme-type
Choose a base branch
from
23 changes: 23 additions & 0 deletions demo/scripts/components/Options/Playback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ function PlaybackConfig({
onAutoPlayChange,
tryRelyOnWorker,
onTryRelyOnWorkerChange,
useDummyMediaElement,
onUseDummyMediaElementChange,
}: {
autoPlay: boolean;
onAutoPlayChange: (val: boolean) => void;
tryRelyOnWorker: boolean;
onTryRelyOnWorkerChange: (val: boolean) => void;
useDummyMediaElement: boolean;
onUseDummyMediaElementChange: (val: boolean) => void;
}): JSX.Element {
return (
<>
Expand Down Expand Up @@ -51,6 +55,25 @@ function PlaybackConfig({
: "Currently running the RxPlayer's main logic only in main thread."}
</span>
</li>

<li>
<Checkbox
className="playerOptionsCheckBox playerOptionsCheckBoxTitle"
name="useDummyMediaElement"
ariaLabel="Rely in a WebWorker when possible"
checked={useDummyMediaElement}
onChange={onUseDummyMediaElementChange}
>
Dummy Media API
</Checkbox>
<span className="option-desc">
{useDummyMediaElement
? "Use mocked media API: The content will not really play but the RxPlayer " +
"will believe it does. Useful for debugging the RxPlayer's logic even on " +
"undecipherable or undecodable content."
: "Actually play the chosen content."}
</span>
</li>
</>
);
}
Expand Down
32 changes: 30 additions & 2 deletions demo/scripts/controllers/Player.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import DummyMediaElement from "../../../src/experimental/tools/DummyMediaElement";
import type {
IAudioRepresentationsSwitchingMode,
ILoadVideoOptions,
Expand All @@ -17,6 +18,7 @@ import type {
ILoadVideoSettings,
IConstructorSettings,
} from "../lib/defaultOptionsValues";
import { toDummyDrmConfiguration } from "../lib/parseDRMConfigurations";

const { useCallback, useEffect, useRef, useState } = React;

Expand Down Expand Up @@ -45,6 +47,7 @@ function Player(): JSX.Element {
defaultOptionsValues.loadVideo,
);
const [relyOnWorker, setRelyOnWorker] = useState(false);
const [useDummyMediaElement, setUseDummyMediaElement] = useState(false);
const [hasUpdatedPlayerOptions, setHasUpdatedPlayerOptions] = useState(false);
const displaySpinnerTimeoutRef = useRef<number | null>(null);

Expand Down Expand Up @@ -156,11 +159,14 @@ function Player(): JSX.Element {
if (playerModule) {
playerModule.destroy();
}
const videoElement = useDummyMediaElement
? (new DummyMediaElement() as unknown as HTMLMediaElement)
: videoElementRef.current;
const playerMod = new PlayerModule(
Object.assign(
{},
{
videoElement: videoElementRef.current,
videoElement,
textTrackElement: textTrackElementRef.current,
debugElement: debugElementRef.current,
},
Expand All @@ -169,7 +175,24 @@ function Player(): JSX.Element {
);
setPlayerModule(playerMod);
return playerMod;
}, [playerOpts, playerModule]);
}, [useDummyMediaElement, playerOpts, playerModule]);

useEffect(() => {
if (playerModule === null) {
return;
}
const mediaElement = playerModule.actions.getMediaElement();
if (mediaElement === null) {
return;
}
if (useDummyMediaElement) {
if (!(mediaElement instanceof DummyMediaElement)) {
setHasUpdatedPlayerOptions(true);
}
} else if (mediaElement !== videoElementRef.current) {
setHasUpdatedPlayerOptions(true);
}
}, [setHasUpdatedPlayerOptions, useDummyMediaElement, playerModule]);

const onVideoClick = useCallback(() => {
if (playerModule === null) {
Expand Down Expand Up @@ -202,6 +225,9 @@ function Player(): JSX.Element {
created.actions.updateWorkerMode(relyOnWorker);
playerMod = created;
}
if (useDummyMediaElement && contentInfo.keySystems !== undefined) {
contentInfo.keySystems = toDummyDrmConfiguration(contentInfo.keySystems);
}
loadContent(playerMod, contentInfo, loadVideoOpts);
},
[
Expand Down Expand Up @@ -285,6 +311,8 @@ function Player(): JSX.Element {
}
tryRelyOnWorker={relyOnWorker}
updateTryRelyOnWorker={setRelyOnWorker}
useDummyMediaElement={useDummyMediaElement}
updateUseDummyMediaElement={setUseDummyMediaElement}
/>
<div className="video-player-wrapper" ref={playerWrapperElementRef}>
<div className="video-screen-parent">
Expand Down
13 changes: 13 additions & 0 deletions demo/scripts/controllers/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ function Settings({
updateDefaultVideoRepresentationsSwitchingMode,
tryRelyOnWorker,
updateTryRelyOnWorker,
useDummyMediaElement,
updateUseDummyMediaElement,
}: {
playerOptions: IConstructorSettings;
updatePlayerOptions: (
Expand All @@ -50,6 +52,8 @@ function Settings({
) => void;
tryRelyOnWorker: boolean;
updateTryRelyOnWorker: (tryRelyOnWorker: boolean) => void;
useDummyMediaElement: boolean;
updateUseDummyMediaElement: (useDummyMediaElement: boolean) => void;
showOptions: boolean;
}): JSX.Element | null {
const {
Expand Down Expand Up @@ -123,6 +127,13 @@ function Settings({
[updateLoadVideoOptions],
);

const onUseDummyMediaElementChange = useCallback(
(useDummyMediaElement: boolean) => {
updateUseDummyMediaElement(useDummyMediaElement);
},
[updateUseDummyMediaElement],
);

const onVideoResolutionLimitChange = useCallback(
(videoResolutionLimitArg: { value: string }) => {
updatePlayerOptions((prevOptions) => {
Expand Down Expand Up @@ -366,6 +377,8 @@ function Settings({
onAutoPlayChange={onAutoPlayChange}
tryRelyOnWorker={tryRelyOnWorker}
onTryRelyOnWorkerChange={onTryRelyOnWorkerChange}
useDummyMediaElement={useDummyMediaElement}
onUseDummyMediaElementChange={onUseDummyMediaElementChange}
/>
</Option>
<Option title="Video adaptive settings">
Expand Down
41 changes: 41 additions & 0 deletions demo/scripts/lib/parseDRMConfigurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,47 @@ export default async function parseDRMConfigurations(
return keySystems.filter((ks): ks is IKeySystemOption => ks !== undefined);
}

export function toDummyDrmConfiguration(
baseOptions: IKeySystemOption[],
): IKeySystemOption[] {
return baseOptions.map((ks) => {
return {
...ks,
getLicense(...args: Parameters<IKeySystemOption["getLicense"]>) {
try {
const challenge = args[0];
const challengeStr = utf8ToStr(challenge);
const challengeObj = JSON.parse(challengeStr) as {
certificate: string | null;
persistent: boolean;
keyIds: string[];
};
const keys: Record<
string,
{
policyLevel: number;
}
> = {};
challengeObj.keyIds.forEach((kid) => {
keys[kid] = {
policyLevel: 50,
};
});
const license = {
type: "license",
persistent: false,
keys,
};
const licenseU8 = strToUtf8(JSON.stringify(license));
return licenseU8.buffer;
} catch (e) {
return ks.getLicense(...args);
}
},
};
});
}

function getServerCertificate(url: string): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
Expand Down
14 changes: 14 additions & 0 deletions demo/scripts/modules/player/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ const PlayerModule = declareModule(
player.setVolume(volume);
},

getMediaElement(): HTMLMediaElement | null {
return player.getVideoElement();
},

updateWorkerMode(enabled: boolean) {
if (enabled && !hasAttachedMultithread) {
attachMultithread(player);
Expand Down Expand Up @@ -310,6 +314,11 @@ const PlayerModule = declareModule(
if (!isStopped && !hasEnded) {
state.update("isPaused", false);
}
setTimeout(() => {
if (!isStopped && !hasEnded && player.isPaused() && !state.get("isPaused")) {
state.update("isPaused", true);
}
}, 100);
},

pause() {
Expand All @@ -320,6 +329,11 @@ const PlayerModule = declareModule(
if (!isStopped && !hasEnded) {
state.update("isPaused", true);
}
setTimeout(() => {
if (!isStopped && !hasEnded && !player.isPaused() && state.get("isPaused")) {
state.update("isPaused", false);
}
}, 100);
},

stop() {
Expand Down
12 changes: 10 additions & 2 deletions src/compat/browser_compatibility_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import type { IListener } from "../utils/event_emitter";
import globalScope from "../utils/global_scope";
import type { IEmeApiImplementation } from "./eme";

/**
* Browser implementation of a VTTCue constructor.
Expand Down Expand Up @@ -229,6 +230,14 @@ export interface IMediaElementEventMap {
* implement it.
*/
export interface IMediaElement extends IEventTarget<IMediaElementEventMap> {
/**
* Optional property allowing to force a specific MSE Implementation when
* relying on a given `IMediaElement`.
*/
FORCED_MEDIA_SOURCE?: new () => IMediaSource;

FORCED_EME_API?: IEmeApiImplementation;

/* From `HTMLMediaElement`: */
autoplay: boolean;
buffered: TimeRanges;
Expand Down Expand Up @@ -256,11 +265,10 @@ export interface IMediaElement extends IEventTarget<IMediaElementEventMap> {

addTextTrack: (kind: TextTrackKind) => TextTrack;
appendChild<T extends Node>(x: T): void;
hasAttribute(attr: string): boolean;
hasChildNodes(): boolean;
pause(): void;
play(): Promise<void>;
removeAttribute(attr: string): void;
removeAttribute(attr: "src"): void;
removeChild(x: unknown): void;
setMediaKeys(x: IMediaKeys | null): Promise<void>;

Expand Down
26 changes: 9 additions & 17 deletions src/compat/eme/eme-api-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,6 @@ import getOldKitWebKitMediaKeyCallbacks, {
import getWebKitMediaKeysCallbacks from "./custom_media_keys/webkit_media_keys";
import { WebKitMediaKeysConstructor } from "./custom_media_keys/webkit_media_keys_constructor";

/**
* Automatically detect and set which EME implementation should be used in the
* current platform.
*
* You can call `getEmeApiImplementation` for a different implementation.
*/
const defaultEmeImplementation = getEmeApiImplementation("auto");

export default defaultEmeImplementation;

/**
* Generic interface harmonizing the structure of the different EME API
* implementations the RxPlayer could use.
Expand All @@ -54,7 +44,7 @@ export interface IEmeApiImplementation {
requestMediaKeySystemAccess: (
keyType: string,
config: MediaKeySystemConfiguration[],
) => Promise<IMediaKeySystemAccess | CustomMediaKeySystemAccess>;
) => Promise<IMediaKeySystemAccess>;

/**
* API allowing to listen for `"encrypted"` events, presumably sent by the
Expand Down Expand Up @@ -126,19 +116,21 @@ export type IPreferredEmeApiType = "auto" | "standard" | "webkit";
* (@see IPreferredEmeApiType).
* @returns {Object}
*/
function getEmeApiImplementation(
export default function getEmeApiImplementation(
preferredApiType: IPreferredEmeApiType,
): IEmeApiImplementation {
): IEmeApiImplementation | null {
let requestMediaKeySystemAccess: IEmeApiImplementation["requestMediaKeySystemAccess"];
let onEncrypted: IEmeApiImplementation["onEncrypted"];
let setMediaKeys: IEmeApiImplementation["setMediaKeys"] = defaultSetMediaKeys;
let implementation: IEmeApiImplementation["implementation"];
if (
(preferredApiType === "standard" ||
(preferredApiType === "auto" && !shouldFavourCustomSafariEME())) &&
// eslint-disable-next-line @typescript-eslint/unbound-method
(isNode || !isNullOrUndefined(navigator.requestMediaKeySystemAccess))
preferredApiType === "standard" ||
(preferredApiType === "auto" && !shouldFavourCustomSafariEME())
) {
// eslint-disable-next-line @typescript-eslint/unbound-method
if (isNode || isNullOrUndefined(navigator.requestMediaKeySystemAccess)) {
return null;
}
requestMediaKeySystemAccess = (...args) =>
navigator.requestMediaKeySystemAccess(...args);
onEncrypted = createCompatibleEventListener(["encrypted"]);
Expand Down
4 changes: 2 additions & 2 deletions src/compat/eme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import type {
IEmeApiImplementation,
IPreferredEmeApiType,
} from "./eme-api-implementation";
import defaultEmeImplementation from "./eme-api-implementation";
import getEmeApiImplementation from "./eme-api-implementation";
import generateKeyRequest from "./generate_key_request";
import type { IEncryptedEventData } from "./get_init_data";
import getInitData from "./get_init_data";
import loadSession from "./load_session";

export default defaultEmeImplementation;
export default getEmeApiImplementation;
export type { IEmeApiImplementation, IPreferredEmeApiType, IEncryptedEventData };
export { closeSession, generateKeyRequest, getInitData, loadSession };
Loading
Loading