Skip to content

Commit

Permalink
Merge pull request #1477 from canalplus/misc/no-eme-type
Browse files Browse the repository at this point in the history
[Proposal] Forbid usage of the `MediaKeys` type and other EME TS types
  • Loading branch information
peaBerberian authored Feb 25, 2025
2 parents 16d5190 + ebfab98 commit 5a614d9
Show file tree
Hide file tree
Showing 32 changed files with 245 additions and 337 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
87 changes: 45 additions & 42 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 @@ -308,45 +306,51 @@ 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;
}

// @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) {
assertCompatibleIMediaElement(x);
}
function assertCompatibleIMediaElement(_x: IMediaElement) {
// 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 testMediaSource(x: MediaSource) {
assertCompatibleIMediaSource(x);
export interface IMediaKeySystemAccess {
readonly keySystem: string;
getConfiguration(): MediaKeySystemConfiguration;
createMediaKeys(): Promise<IMediaKeys>;
}
function assertCompatibleIMediaSource(_x: IMediaSource) {
// 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 testSourceBuffer(x: SourceBuffer) {
assertCompatibleISourceBuffer(x);
}
function assertCompatibleISourceBuffer(_x: ISourceBuffer) {
// Noop

export interface IMediaKeys {
isTypeSupported?: (type: string) => boolean; // IE11 only
createSession(sessionType?: MediaKeySessionType): IMediaKeySession;
setServerCertificate(serverCertificate: BufferSource): Promise<boolean>;
}
// @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 testSourceBufferList(x: SourceBufferList) {
assertCompatibleISourceBufferList(x);

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>;
}
function assertCompatibleISourceBufferList(_x: ISourceBufferList) {
// Noop

// Trick to ensure our own types are compatible to TypeScript's
function assertTypeCompatibility<T, _U extends T>(): void {
// noop
}

/* eslint-disable @typescript-eslint/no-restricted-types */
assertTypeCompatibility<IMediaElement, HTMLMediaElement>();
assertTypeCompatibility<IMediaSource, MediaSource>();
assertTypeCompatibility<ISourceBuffer, SourceBuffer>();
assertTypeCompatibility<ISourceBufferList, SourceBufferList>();
assertTypeCompatibility<IMediaKeySystemAccess, MediaKeySystemAccess>();
assertTypeCompatibility<IMediaKeys, MediaKeys>();
assertTypeCompatibility<IMediaKeySession, MediaKeySession>();
/* eslint-enable @typescript-eslint/no-restricted-types */

/**
* AudioTrackList implementation (that TS forgot).
* Directly taken from the WHATG spec:
Expand Down Expand Up @@ -464,7 +468,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 5a614d9

Please sign in to comment.