From b8414f0a6b82243ea78c5ff2835fd2fbe92481b6 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Fri, 5 Jul 2024 16:00:53 +0200 Subject: [PATCH] [Proposal] Forbid usage of the `MediaKeys` type and other EME TS types 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. --- .eslintrc.js | 12 +++ src/compat/browser_compatibility_types.ts | 73 +++++++++++++++---- src/compat/eme/close_session.ts | 6 +- src/compat/eme/custom_key_system_access.ts | 15 +--- .../eme/custom_media_keys/ie11_media_keys.ts | 52 +++++++------ src/compat/eme/custom_media_keys/index.ts | 2 - .../moz_media_keys_constructor.ts | 14 ++-- .../old_webkit_media_keys.ts | 35 ++++----- src/compat/eme/custom_media_keys/types.ts | 66 ----------------- .../custom_media_keys/webkit_media_keys.ts | 64 +++++++--------- src/compat/eme/eme-api-implementation.ts | 30 ++++---- src/compat/eme/generate_key_request.ts | 4 +- src/compat/eme/get_init_data.ts | 4 +- src/compat/eme/index.ts | 11 +-- src/compat/eme/load_session.ts | 14 +--- src/compat/eme/set_media_keys.ts | 5 +- src/compat/event_listeners.ts | 4 +- .../media_key_system_access.test.ts | 4 +- src/main_thread/decrypt/attach_media_keys.ts | 14 ++-- src/main_thread/decrypt/content_decryptor.ts | 13 ++-- .../decrypt/create_or_load_session.ts | 6 +- src/main_thread/decrypt/create_session.ts | 6 +- src/main_thread/decrypt/find_key_system.ts | 12 +-- src/main_thread/decrypt/get_media_keys.ts | 15 ++-- .../decrypt/session_events_listener.ts | 6 +- .../decrypt/set_server_certificate.ts | 8 +- .../decrypt/utils/check_key_statuses.ts | 4 +- .../decrypt/utils/is_session_usable.ts | 6 +- .../decrypt/utils/loaded_sessions_store.ts | 31 ++++---- .../decrypt/utils/media_keys_infos_store.ts | 14 ++-- .../utils/persistent_sessions_store.ts | 12 ++- .../decrypt/utils/server_certificate_store.ts | 18 ++--- .../isobmff/extract_complete_chunks.ts | 2 +- 33 files changed, 267 insertions(+), 315 deletions(-) delete mode 100644 src/compat/eme/custom_media_keys/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index 473df4d2eb..907cfcb3e5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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", + }, }, }, ], diff --git a/src/compat/browser_compatibility_types.ts b/src/compat/browser_compatibility_types.ts index 3bfd5c90a0..8c9037e585 100644 --- a/src/compat/browser_compatibility_types.ts +++ b/src/compat/browser_compatibility_types.ts @@ -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? @@ -189,18 +183,22 @@ export interface ISourceBuffer extends IEventTarget { 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; @@ -212,7 +210,7 @@ export interface IMediaElementEventMap { visibilitychange: Event; volumechange: Event; waiting: Event; - webkitneedkey: MediaEncryptedEvent; + webkitneedkey: IMediaEncryptedEvent; } /** @@ -239,7 +237,7 @@ export interface IMediaElement extends IEventTarget { duration: number; ended: boolean; error: MediaError | null; - mediaKeys: null | MediaKeys; + mediaKeys: null | IMediaKeys; muted: boolean; nodeName: string; paused: boolean; @@ -261,9 +259,9 @@ export interface IMediaElement extends IEventTarget { play(): Promise; removeAttribute(attr: string): void; removeChild(x: unknown): void; - setMediaKeys(x: MediaKeys | null): Promise; + setMediaKeys(x: IMediaKeys | null): Promise; - 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; @@ -293,12 +291,36 @@ export interface IMediaElement extends IEventTarget { 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; +} + +export interface IMediaKeys { + isTypeSupported?: (type: string) => boolean; // IE11 only + createSession(sessionType?: MediaKeySessionType): IMediaKeySession; + setServerCertificate(serverCertificate: BufferSource): Promise; +} + +export interface IMediaKeySession extends IEventTarget { + readonly closed: Promise; + readonly expiration: number; + readonly keyStatuses: MediaKeyStatusMap; + readonly sessionId: string; + close(): Promise; + generateRequest(_initDataType: string, _initData: BufferSource): Promise; + load(sessionId: string): Promise; + remove(): Promise; + update(response: BufferSource): Promise; +} + // @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) { @@ -331,6 +353,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). @@ -443,7 +489,6 @@ export type { ICompatVideoTrackList, ICompatAudioTrack, ICompatVideoTrack, - ICompatMediaKeysConstructor, ICompatTextTrack, ICompatVTTCue, ICompatVTTCueConstructor, diff --git a/src/compat/eme/close_session.ts b/src/compat/eme/close_session.ts index 92a42d6f81..d0babfa7f3 100644 --- a/src/compat/eme/close_session.ts +++ b/src/compat/eme/close_session.ts @@ -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.} */ -export default function closeSession( - session: MediaKeySession | ICustomMediaKeySession, -): Promise { +export default function closeSession(session: IMediaKeySession): Promise { const timeoutCanceller = new TaskCanceller(); return Promise.race([ diff --git a/src/compat/eme/custom_key_system_access.ts b/src/compat/eme/custom_key_system_access.ts index af888b79f1..da27d29e4a 100644 --- a/src/compat/eme/custom_key_system_access.ts +++ b/src/compat/eme/custom_key_system_access.ts @@ -13,14 +13,7 @@ * 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; -} +import type { IMediaKeySystemAccess, IMediaKeys } from "../browser_compatibility_types"; /** * Simple implementation of the MediaKeySystemAccess EME API. @@ -28,7 +21,7 @@ export interface ICustomMediaKeySystemAccess { * 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.} - Promise wrapping the MediaKeys for this * MediaKeySystemAccess. Never rejects. */ - public createMediaKeys(): Promise { + public createMediaKeys(): Promise { return new Promise((res) => res(this._mediaKeys)); } diff --git a/src/compat/eme/custom_media_keys/ie11_media_keys.ts b/src/compat/eme/custom_media_keys/ie11_media_keys.ts index 9cd20c8aca..e10108b611 100644 --- a/src/compat/eme/custom_media_keys/ie11_media_keys.ts +++ b/src/compat/eme/custom_media_keys/ie11_media_keys.ts @@ -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 - implements ICustomMediaKeySession + extends EventEmitter + implements IMediaKeySession { public readonly update: (license: Uint8Array) => Promise; - public readonly closed: Promise; + public readonly closed: Promise; 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,14 +152,14 @@ 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 { + setServerCertificate(): Promise { throw new Error("Server certificate is not implemented in your browser"); } } @@ -158,10 +167,7 @@ class IE11CustomMediaKeys implements ICustomMediaKeys { export default function getIE11MediaKeysCallbacks(): { isTypeSupported: (keyType: string) => boolean; createCustomMediaKeys: (keyType: string) => IE11CustomMediaKeys; - setMediaKeys: ( - elt: IMediaElement, - mediaKeys: MediaKeys | ICustomMediaKeys | null, - ) => Promise; + setMediaKeys: (elt: IMediaElement, mediaKeys: IMediaKeys | null) => Promise; } { 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 => { if (mediaKeys === null) { // msSetMediaKeys only accepts native MSMediaKeys as argument. diff --git a/src/compat/eme/custom_media_keys/index.ts b/src/compat/eme/custom_media_keys/index.ts index 19ae83bb8e..21eff0060c 100644 --- a/src/compat/eme/custom_media_keys/index.ts +++ b/src/compat/eme/custom_media_keys/index.ts @@ -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, diff --git a/src/compat/eme/custom_media_keys/moz_media_keys_constructor.ts b/src/compat/eme/custom_media_keys/moz_media_keys_constructor.ts index e615f17279..203af1c121 100644 --- a/src/compat/eme/custom_media_keys/moz_media_keys_constructor.ts +++ b/src/compat/eme/custom_media_keys/moz_media_keys_constructor.ts @@ -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; } @@ -41,11 +40,8 @@ export { MozMediaKeysConstructor }; export default function getMozMediaKeysCallbacks(): { isTypeSupported: (keyType: string) => boolean; - createCustomMediaKeys: (keyType: string) => ICustomMediaKeys; - setMediaKeys: ( - elt: IMediaElement, - mediaKeys: MediaKeys | ICustomMediaKeys | null, - ) => Promise; + createCustomMediaKeys: (keyType: string) => IMediaKeys; + setMediaKeys: (elt: IMediaElement, mediaKeys: IMediaKeys | null) => Promise; } { const isTypeSupported = (keySystem: string, type?: string | null) => { if (MozMediaKeysConstructor === undefined) { @@ -64,7 +60,7 @@ export default function getMozMediaKeysCallbacks(): { }; const setMediaKeys = ( elt: IMediaElement, - mediaKeys: MediaKeys | ICustomMediaKeys | null, + mediaKeys: IMediaKeys | null, ): Promise => { return wrapInPromise(() => { if ( diff --git a/src/compat/eme/custom_media_keys/old_webkit_media_keys.ts b/src/compat/eme/custom_media_keys/old_webkit_media_keys.ts index 92dce51f13..134d93d9a3 100644 --- a/src/compat/eme/custom_media_keys/old_webkit_media_keys.ts +++ b/src/compat/eme/custom_media_keys/old_webkit_media_keys.ts @@ -20,13 +20,11 @@ import isNullOrUndefined from "../../../utils/is_null_or_undefined"; import noop from "../../../utils/noop"; import { utf8ToStr } from "../../../utils/string_parsing"; import wrapInPromise from "../../../utils/wrapInPromise"; -import type { IMediaElement } from "../../browser_compatibility_types"; import type { - ICustomMediaKeys, - ICustomMediaKeySession, - ICustomMediaKeyStatusMap, - IMediaKeySessionEvents, -} from "./types"; + IMediaElement, + IMediaKeySession, + IMediaKeys, +} from "../../browser_compatibility_types"; export interface IOldWebkitHTMLMediaElement extends HTMLVideoElement { webkitGenerateKeyRequest: (keyType: string, initData: ArrayBuffer) => void; @@ -60,12 +58,12 @@ export function isOldWebkitMediaElement( * @class OldWebkitMediaKeySession */ class OldWebkitMediaKeySession - extends EventEmitter - implements ICustomMediaKeySession + extends EventEmitter + implements IMediaKeySession { - public readonly closed: Promise; + public readonly closed: Promise; public expiration: number; - public keyStatuses: ICustomMediaKeyStatusMap; + public keyStatuses: MediaKeyStatusMap; public sessionId: string; private readonly _vid: IOldWebkitHTMLMediaElement; @@ -84,7 +82,7 @@ class OldWebkitMediaKeySession this.expiration = NaN; const onSessionRelatedEvent = (evt: Event) => { - this.trigger(evt.type, evt); + this.trigger(evt.type as keyof MediaKeySessionEventMap, evt); }; this.closed = new Promise((resolve) => { this._closeSession = () => { @@ -94,7 +92,7 @@ class OldWebkitMediaKeySession mediaElement.removeEventListener(`webkit${evt}`, onSessionRelatedEvent); }, ); - resolve(); + resolve("closed-by-application"); }; }); @@ -155,7 +153,7 @@ class OldWebkitMediaKeySession } } -class OldWebKitCustomMediaKeys implements ICustomMediaKeys { +class OldWebKitCustomMediaKeys implements IMediaKeys { private readonly _keySystem: string; private _videoElement?: IOldWebkitHTMLMediaElement; @@ -172,14 +170,14 @@ class OldWebKitCustomMediaKeys implements ICustomMediaKeys { }); } - createSession(/* sessionType */): ICustomMediaKeySession { + createSession(/* sessionType */): IMediaKeySession { if (isNullOrUndefined(this._videoElement)) { throw new Error("Video not attached to the MediaKeys"); } return new OldWebkitMediaKeySession(this._videoElement, this._keySystem); } - setServerCertificate(): Promise { + setServerCertificate(): Promise { throw new Error("Server certificate is not implemented in your browser"); } } @@ -187,10 +185,7 @@ class OldWebKitCustomMediaKeys implements ICustomMediaKeys { export default function getOldWebKitMediaKeysCallbacks(): { isTypeSupported: (keyType: string) => boolean; createCustomMediaKeys: (keyType: string) => OldWebKitCustomMediaKeys; - setMediaKeys: ( - elt: IMediaElement, - mediaKeys: MediaKeys | ICustomMediaKeys | null, - ) => Promise; + setMediaKeys: (elt: IMediaElement, mediaKeys: IMediaKeys | null) => Promise; } { const isTypeSupported = function (keyType: string): boolean { // get any