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 Dec 12, 2024
1 parent acd7b46 commit 109494f
Show file tree
Hide file tree
Showing 32 changed files with 260 additions and 308 deletions.
12 changes: 12 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
},
],
Expand Down
73 changes: 59 additions & 14 deletions src/compat/browser_compatibility_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@
import type { IListener } from "../utils/event_emitter";
import globalScope from "../utils/global_scope";

/** 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?
Expand Down Expand Up @@ -191,18 +185,22 @@ export interface ISourceBuffer extends IEventTarget<ISourceBufferEventMap> {
onupdatestart: ((evt: Event) => void) | null;
}

export interface IMediaEncryptedEvent extends MediaEncryptedEvent {
forceSessionRecreation?: boolean;
}

/** Events potentially dispatched by an `IMediaElement` */
export interface IMediaElementEventMap {
canplay: Event;
canplaythrough: Event;
encrypted: MediaEncryptedEvent;
encrypted: IMediaEncryptedEvent;
ended: Event;
enterpictureinpicture: Event;
error: Event;
leavepictureinpicture: Event;
loadeddata: Event;
loadedmetadata: Event;
needkey: MediaEncryptedEvent;
needkey: IMediaEncryptedEvent;
pause: Event;
play: Event;
playing: Event;
Expand All @@ -214,7 +212,7 @@ export interface IMediaElementEventMap {
visibilitychange: Event;
volumechange: Event;
waiting: Event;
webkitneedkey: MediaEncryptedEvent;
webkitneedkey: IMediaEncryptedEvent;
}

/**
Expand All @@ -241,7 +239,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;
Expand All @@ -264,9 +262,9 @@ 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;
onencrypted: ((evt: IMediaEncryptedEvent) => void) | null;
oncanplay: ((evt: Event) => void) | null;
oncanplaythrough: ((evt: Event) => void) | null;
onended: ((evt: Event) => void) | null;
Expand Down Expand Up @@ -296,12 +294,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>;
}

// @ts-expect-error unused function, just used for compile-time typechecking
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-restricted-types
function testMediaElement(x: HTMLMediaElement) {
Expand Down Expand Up @@ -334,6 +356,30 @@ function testSourceBufferList(x: SourceBufferList) {
function assertCompatibleISourceBufferList(_x: ISourceBufferList) {
// Noop
}
// @ts-expect-error unused function, just used for compile-time typechecking
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-restricted-types
function testMediaKeySystemAccess(x: MediaKeySystemAccess) {
assertCompatibleIMediaKeySystemAccess(x);
}
function assertCompatibleIMediaKeySystemAccess(_x: IMediaKeySystemAccess) {
// Noop
}
// @ts-expect-error unused function, just used for compile-time typechecking
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-restricted-types
function testMediaKeys(x: MediaKeys) {
assertCompatibleIMediaKeys(x);
}
function assertCompatibleIMediaKeys(_x: IMediaKeys) {
// Noop
}
// @ts-expect-error unused function, just used for compile-time typechecking
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-restricted-types
function testMediaKeySession(x: MediaKeySession) {
assertCompatibleIMediaKeySession(x);
}
function assertCompatibleIMediaKeySession(_x: IMediaKeySession) {
// Noop
}

/**
* AudioTrackList implementation (that TS forgot).
Expand Down Expand Up @@ -446,7 +492,6 @@ export type {
ICompatVideoTrackList,
ICompatAudioTrack,
ICompatVideoTrack,
ICompatMediaKeysConstructor,
ICompatTextTrack,
ICompatVTTCue,
ICompatVTTCueConstructor,
Expand Down
6 changes: 2 additions & 4 deletions src/compat/eme/close_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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([
Expand Down
15 changes: 4 additions & 11 deletions src/compat/eme/custom_key_system_access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand All @@ -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,
) {}

Expand All @@ -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));
}

Expand Down
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
Expand Up @@ -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;
Expand All @@ -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) => {
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -123,7 +132,7 @@ class IE11MediaKeySession
}
}

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

Expand All @@ -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) {
Expand All @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions src/compat/eme/custom_media_keys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 109494f

Please sign in to comment.