Skip to content

Commit

Permalink
[Proposal] Forbid usage of the MediaKeys type and other EME TS types
Browse files Browse the repository at this point in the history
Based on #1397.

In the idea of including fake encrypted contents in our integration
tests by mocking MSE+EME+HTML 5 media API (not as complex as it sounds)
to increase by a lot the types of issues our CI is capable to catch, I
noticed an opportunity to even improve on the current code.

Like in #1397, the idea is there to provide typings subset of the
various EME API and to rely on them instead in the RxPlayer code (and
enforcing this through our linter).

This allows to:

  - much simplify EME API mocking by not having to implement the full
    extent of the EME API - though almost all of it is implemented
    instead (exceptions are the `EventTarget`'s third `options` optional
    parameter which we never use, `dispatchEvent`, and the `onevent`
    methods that we never rely on).

  - Allow the definition of environment-specific APIs

  - Be more aware of which EME API we currently use

The gain is here much more evident than in #1397 as we already had some
kind of sub-typings with the `ICustom...` types (e.g.
`ICustomMediaKeys`). By renaming those `I...` (e.g. `IMediaKeys`) and
ensuring they are actually compatible with the base type (e.g.
`MediaKeys`), we end up in my opinion with simpler code.
  • Loading branch information
peaBerberian committed Aug 1, 2024

Verified

This commit was signed with the committer’s verified signature.
1 parent 336dbd9 commit 96f4091
Showing 31 changed files with 250 additions and 300 deletions.
12 changes: 12 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -71,6 +71,18 @@ module.exports = {
message:
"Avoid relying on `SourceBufferList` directly unless it is API-facing. Prefer our more restricted `ISourceBufferList` type",
},
MediaKeySystemAccess: {
message:
"Avoid relying on `MediaKeySystemAccess` directly unless it is API-facing. Prefer our more restricted `IMediaKeySystemAccess` type",
},
MediaKeys: {
message:
"Avoid relying on `MediaKeys` directly unless it is API-facing. Prefer our more restricted `IMediaKeys` type",
},
MediaKeySession: {
message:
"Avoid relying on `MediaKeySession` directly unless it is API-facing. Prefer our more restricted `IMediaKeySession` type",
},
},
},
],
58 changes: 48 additions & 10 deletions src/compat/browser_compatibility_types.ts
Original file line number Diff line number Diff line change
@@ -18,12 +18,6 @@ import type { IListener } from "../utils/event_emitter";
import globalScope from "../utils/global_scope";
import isNullOrUndefined from "../utils/is_null_or_undefined";

/** Regular MediaKeys type + optional functions present in IE11. */
interface ICompatMediaKeysConstructor {
isTypeSupported?: (type: string) => boolean; // IE11 only
new (keyType?: string): MediaKeys; // argument for IE11 only
}

/**
* Browser implementation of a VTTCue constructor.
* TODO open TypeScript issue about it?
@@ -239,7 +233,7 @@ export interface IMediaElement extends IEventTarget<IMediaElementEventMap> {
duration: number;
ended: boolean;
error: MediaError | null;
mediaKeys: null | MediaKeys;
mediaKeys: null | IMediaKeys;
muted: boolean;
nodeName: string;
paused: boolean;
@@ -261,7 +255,7 @@ export interface IMediaElement extends IEventTarget<IMediaElementEventMap> {
play(): Promise<void>;
removeAttribute(attr: string): void;
removeChild(x: unknown): void;
setMediaKeys(x: MediaKeys | null): Promise<void>;
setMediaKeys(x: IMediaKeys | null): Promise<void>;

onencrypted: ((evt: MediaEncryptedEvent) => void) | null;
oncanplay: ((evt: Event) => void) | null;
@@ -293,12 +287,36 @@ export interface IMediaElement extends IEventTarget<IMediaElementEventMap> {
msSetMediaKeys?: (mediaKeys: unknown) => void;
webkitSetMediaKeys?: (mediaKeys: unknown) => void;
webkitKeys?: {
createSession?: (mimeType: string, initData: BufferSource) => MediaKeySession;
createSession?: (mimeType: string, initData: BufferSource) => IMediaKeySession;
};
audioTracks?: ICompatAudioTrackList;
videoTracks?: ICompatVideoTrackList;
}

export interface IMediaKeySystemAccess {
readonly keySystem: string;
getConfiguration(): MediaKeySystemConfiguration;
createMediaKeys(): Promise<IMediaKeys>;
}

export interface IMediaKeys {
isTypeSupported?: (type: string) => boolean; // IE11 only
createSession(sessionType?: MediaKeySessionType): IMediaKeySession;
setServerCertificate(serverCertificate: BufferSource): Promise<boolean>;
}

export interface IMediaKeySession extends IEventTarget<MediaKeySessionEventMap> {
readonly closed: Promise<MediaKeySessionClosedReason>;
readonly expiration: number;
readonly keyStatuses: MediaKeyStatusMap;
readonly sessionId: string;
close(): Promise<void>;
generateRequest(_initDataType: string, _initData: BufferSource): Promise<void>;
load(sessionId: string): Promise<boolean>;
remove(): Promise<void>;
update(response: BufferSource): Promise<void>;
}

/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-restricted-types */
// @ts-expect-error unused function, just used for compile-time typechecking
function testMediaElement(x: HTMLMediaElement) {
@@ -328,6 +346,27 @@ function testSourceBufferList(x: SourceBufferList) {
function assertCompatibleISourceBufferList(_x: ISourceBufferList) {
// Noop
}
// @ts-expect-error unused function, just used for compile-time typechecking
function testMediaKeySystemAccess(x: MediaKeySystemAccess) {
assertCompatibleIMediaKeySystemAccess(x);
}
function assertCompatibleIMediaKeySystemAccess(_x: IMediaKeySystemAccess) {
// Noop
}
// @ts-expect-error unused function, just used for compile-time typechecking
function testMediaKeys(x: MediaKeys) {
assertCompatibleIMediaKeys(x);
}
function assertCompatibleIMediaKeys(_x: IMediaKeys) {
// Noop
}
// @ts-expect-error unused function, just used for compile-time typechecking
function testMediaKeySession(x: MediaKeySession) {
assertCompatibleIMediaKeySession(x);
}
function assertCompatibleIMediaKeySession(_x: IMediaKeySession) {
// Noop
}
/* eslint-enable @typescript-eslint/no-unused-vars, @typescript-eslint/no-restricted-types */

/**
@@ -441,7 +480,6 @@ export type {
ICompatVideoTrackList,
ICompatAudioTrack,
ICompatVideoTrack,
ICompatMediaKeysConstructor,
ICompatTextTrack,
ICompatVTTCue,
ICompatVTTCueConstructor,
6 changes: 2 additions & 4 deletions src/compat/eme/close_session.ts
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
import log from "../../log";
import cancellableSleep from "../../utils/cancellable_sleep";
import TaskCanceller, { CancellationError } from "../../utils/task_canceller";
import type { ICustomMediaKeySession } from "./custom_media_keys";
import type { IMediaKeySession } from "../browser_compatibility_types";

/**
* Close the given `MediaKeySession` and returns a Promise resolving when the
@@ -32,9 +32,7 @@ import type { ICustomMediaKeySession } from "./custom_media_keys";
* @param {MediaKeySession|Object} session
* @returns {Promise.<undefined>}
*/
export default function closeSession(
session: MediaKeySession | ICustomMediaKeySession,
): Promise<void> {
export default function closeSession(session: IMediaKeySession): Promise<void> {
const timeoutCanceller = new TaskCanceller();

return Promise.race([
15 changes: 4 additions & 11 deletions src/compat/eme/custom_key_system_access.ts
Original file line number Diff line number Diff line change
@@ -13,22 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICustomMediaKeys } from "./custom_media_keys";

// MediaKeySystemAccess implementation
export interface ICustomMediaKeySystemAccess {
readonly keySystem: string;
getConfiguration(): MediaKeySystemConfiguration;
createMediaKeys(): Promise<MediaKeys | ICustomMediaKeys>;
}
import type { IMediaKeySystemAccess, IMediaKeys } from "../browser_compatibility_types";

/**
* Simple implementation of the MediaKeySystemAccess EME API.
*
* All needed arguments are given to the constructor
* @class CustomMediaKeySystemAccess
*/
export default class CustomMediaKeySystemAccess implements ICustomMediaKeySystemAccess {
export default class CustomMediaKeySystemAccess implements IMediaKeySystemAccess {
/**
* @param {string} _keyType - type of key system (e.g. "widevine" or
* "com.widevine.alpha").
@@ -38,7 +31,7 @@ export default class CustomMediaKeySystemAccess implements ICustomMediaKeySystem
*/
constructor(
private readonly _keyType: string,
private readonly _mediaKeys: ICustomMediaKeys | MediaKeys,
private readonly _mediaKeys: IMediaKeys,
private readonly _configuration: MediaKeySystemConfiguration,
) {}

@@ -54,7 +47,7 @@ export default class CustomMediaKeySystemAccess implements ICustomMediaKeySystem
* @returns {Promise.<Object>} - Promise wrapping the MediaKeys for this
* MediaKeySystemAccess. Never rejects.
*/
public createMediaKeys(): Promise<ICustomMediaKeys | MediaKeys> {
public createMediaKeys(): Promise<IMediaKeys> {
return new Promise((res) => res(this._mediaKeys));
}

52 changes: 29 additions & 23 deletions src/compat/eme/custom_media_keys/ie11_media_keys.ts
Original file line number Diff line number Diff line change
@@ -18,25 +18,23 @@ import EventEmitter from "../../../utils/event_emitter";
import isNullOrUndefined from "../../../utils/is_null_or_undefined";
import TaskCanceller from "../../../utils/task_canceller";
import wrapInPromise from "../../../utils/wrapInPromise";
import type { IMediaElement } from "../../browser_compatibility_types";
import type {
IMediaElement,
IMediaKeySession,
IMediaKeys,
} from "../../browser_compatibility_types";
import * as events from "../../event_listeners";
import type { MSMediaKeys, MSMediaKeySession } from "./ms_media_keys_constructor";
import { MSMediaKeysConstructor } from "./ms_media_keys_constructor";
import type {
ICustomMediaKeys,
ICustomMediaKeySession,
ICustomMediaKeyStatusMap,
IMediaKeySessionEvents,
} from "./types";

class IE11MediaKeySession
extends EventEmitter<IMediaKeySessionEvents>
implements ICustomMediaKeySession
extends EventEmitter<MediaKeySessionEventMap>
implements IMediaKeySession
{
public readonly update: (license: Uint8Array) => Promise<void>;
public readonly closed: Promise<void>;
public readonly closed: Promise<MediaKeySessionClosedReason>;
public expiration: number;
public keyStatuses: ICustomMediaKeyStatusMap;
public keyStatuses: MediaKeyStatusMap;
private readonly _mk: MSMediaKeys;
private readonly _sessionClosingCanceller: TaskCanceller;
private _ss: MSMediaKeySession | undefined;
@@ -47,7 +45,9 @@ class IE11MediaKeySession
this._mk = mk;
this._sessionClosingCanceller = new TaskCanceller();
this.closed = new Promise((resolve) => {
this._sessionClosingCanceller.signal.register(() => resolve());
this._sessionClosingCanceller.signal.register(() =>
resolve("closed-by-application"),
);
});
this.update = (license: Uint8Array) => {
return new Promise((resolve, reject) => {
@@ -81,21 +81,30 @@ class IE11MediaKeySession
events.onKeyMessage(
this._ss,
(evt) => {
this.trigger((evt as Event).type ?? "message", evt as Event);
this.trigger(
((evt as Event).type ?? "message") as keyof MediaKeySessionEventMap,
evt as Event,
);
},
this._sessionClosingCanceller.signal,
);
events.onKeyAdded(
this._ss,
(evt) => {
this.trigger((evt as Event).type ?? "keyadded", evt as Event);
this.trigger(
((evt as Event).type ?? "keyadded") as keyof MediaKeySessionEventMap,
evt as Event,
);
},
this._sessionClosingCanceller.signal,
);
events.onKeyError(
this._ss,
(evt) => {
this.trigger((evt as Event).type ?? "keyerror", evt as Event);
this.trigger(
((evt as Event).type ?? "keyerror") as keyof MediaKeySessionEventMap,
evt as Event,
);
},
this._sessionClosingCanceller.signal,
);
@@ -123,7 +132,7 @@ class IE11MediaKeySession
}
}

class IE11CustomMediaKeys implements ICustomMediaKeys {
class IE11CustomMediaKeys implements IMediaKeys {
private _videoElement?: IMediaElement;
private _mediaKeys?: MSMediaKeys;

@@ -143,25 +152,22 @@ class IE11CustomMediaKeys implements ICustomMediaKeys {
});
}

createSession(/* sessionType */): ICustomMediaKeySession {
createSession(/* sessionType */): IMediaKeySession {
if (this._videoElement === undefined || this._mediaKeys === undefined) {
throw new Error("Video not attached to the MediaKeys");
}
return new IE11MediaKeySession(this._mediaKeys);
}

setServerCertificate(): Promise<void> {
setServerCertificate(): Promise<boolean> {
throw new Error("Server certificate is not implemented in your browser");
}
}

export default function getIE11MediaKeysCallbacks(): {
isTypeSupported: (keyType: string) => boolean;
createCustomMediaKeys: (keyType: string) => IE11CustomMediaKeys;
setMediaKeys: (
elt: IMediaElement,
mediaKeys: MediaKeys | ICustomMediaKeys | null,
) => Promise<unknown>;
setMediaKeys: (elt: IMediaElement, mediaKeys: IMediaKeys | null) => Promise<unknown>;
} {
const isTypeSupported = (keySystem: string, type?: string | null) => {
if (MSMediaKeysConstructor === undefined) {
@@ -175,7 +181,7 @@ export default function getIE11MediaKeysCallbacks(): {
const createCustomMediaKeys = (keyType: string) => new IE11CustomMediaKeys(keyType);
const setMediaKeys = (
elt: IMediaElement,
mediaKeys: MediaKeys | ICustomMediaKeys | null,
mediaKeys: IMediaKeys | null,
): Promise<unknown> => {
if (mediaKeys === null) {
// msSetMediaKeys only accepts native MSMediaKeys as argument.
2 changes: 0 additions & 2 deletions src/compat/eme/custom_media_keys/index.ts
Original file line number Diff line number Diff line change
@@ -5,11 +5,9 @@ import getMozMediaKeysCallbacks, {
import getOldKitWebKitMediaKeyCallbacks, {
isOldWebkitMediaElement,
} from "./old_webkit_media_keys";
import type { ICustomMediaKeys, ICustomMediaKeySession } from "./types";
import getWebKitMediaKeysCallbacks from "./webkit_media_keys";
import { WebKitMediaKeysConstructor } from "./webkit_media_keys_constructor";

export type { ICustomMediaKeys, ICustomMediaKeySession };
export {
getIE11MediaKeysCallbacks,
MSMediaKeysConstructor,
14 changes: 5 additions & 9 deletions src/compat/eme/custom_media_keys/moz_media_keys_constructor.ts
Original file line number Diff line number Diff line change
@@ -16,11 +16,10 @@

import globalScope from "../../../utils/global_scope";
import wrapInPromise from "../../../utils/wrapInPromise";
import type { IMediaElement } from "../../browser_compatibility_types";
import type { ICustomMediaKeys } from "./types";
import type { IMediaElement, IMediaKeys } from "../../browser_compatibility_types";

interface IMozMediaKeysConstructor {
new (keySystem: string): ICustomMediaKeys;
new (keySystem: string): IMediaKeys;
isTypeSupported(keySystem: string, type?: string | null): boolean;
}

@@ -42,11 +41,8 @@ export { MozMediaKeysConstructor };

export default function getMozMediaKeysCallbacks(): {
isTypeSupported: (keyType: string) => boolean;
createCustomMediaKeys: (keyType: string) => ICustomMediaKeys;
setMediaKeys: (
elt: IMediaElement,
mediaKeys: MediaKeys | ICustomMediaKeys | null,
) => Promise<unknown>;
createCustomMediaKeys: (keyType: string) => IMediaKeys;
setMediaKeys: (elt: IMediaElement, mediaKeys: IMediaKeys | null) => Promise<unknown>;
} {
const isTypeSupported = (keySystem: string, type?: string | null) => {
if (MozMediaKeysConstructor === undefined) {
@@ -65,7 +61,7 @@ export default function getMozMediaKeysCallbacks(): {
};
const setMediaKeys = (
elt: IMediaElement,
mediaKeys: MediaKeys | ICustomMediaKeys | null,
mediaKeys: IMediaKeys | null,
): Promise<unknown> => {
return wrapInPromise(() => {
if (
Loading

0 comments on commit 96f4091

Please sign in to comment.