diff --git a/src/compat/eme/custom_media_keys/types.ts b/src/compat/eme/custom_media_keys/types.ts index a3ccb95a6d..2ae6dc0466 100644 --- a/src/compat/eme/custom_media_keys/types.ts +++ b/src/compat/eme/custom_media_keys/types.ts @@ -58,10 +58,16 @@ export interface ICustomMediaKeyStatusMap { ) : boolean; } -export interface IMediaKeySessionEvents { [key : string] : MediaKeyMessageEvent|Event; - // "keymessage" - // "message" - // "keyadded" - // "ready" - // "keyerror" - /* "error" */ } +export interface IMediaKeySessionEvents { + [key: string]: MediaKeyMessageEvent | Event; + // "keymessage" + // "message" + // "keyadded" + // "ready" + // "keyerror" + /* "error" */ +} + +export interface ICustomMediaEncryptedEvent extends MediaEncryptedEvent { + forceSessionRecreation?: boolean | undefined; +} diff --git a/src/compat/eme/eme-api-implementation.ts b/src/compat/eme/eme-api-implementation.ts index 7d96421bc4..9b83c5b41c 100644 --- a/src/compat/eme/eme-api-implementation.ts +++ b/src/compat/eme/eme-api-implementation.ts @@ -1,6 +1,7 @@ import { MediaError } from "../../errors"; import assert from "../../utils/assert"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; +import objectAssign from "../../utils/object_assign"; import { CancellationSignal } from "../../utils/task_canceller"; import { ICompatHTMLMediaElement } from "../browser_compatibility_types"; import { isIE11 } from "../browser_detection"; @@ -18,6 +19,7 @@ import getOldKitWebKitMediaKeyCallbacks, { isOldWebkitMediaElement, } from "./custom_media_keys/old_webkit_media_keys"; import { + ICustomMediaEncryptedEvent, ICustomMediaKeys, } from "./custom_media_keys/types"; import getWebKitMediaKeysCallbacks from "./custom_media_keys/webkit_media_keys"; @@ -65,7 +67,7 @@ export interface IEmeApiImplementation { */ onEncrypted : ( target : IEventTargetLike, - listener : (evt : unknown) => void, + listener : (evt : ICustomMediaEncryptedEvent) => void, cancelSignal : CancellationSignal, ) => void; @@ -149,8 +151,8 @@ function getEmeApiImplementation( let createCustomMediaKeys: (keyType: string) => ICustomMediaKeys; if (preferredApiType === "webkit" && WebKitMediaKeysConstructor !== undefined) { - onEncrypted = createCompatibleEventListener(["needkey"]); const callbacks = getWebKitMediaKeysCallbacks(); + onEncrypted = createOnEncryptedForWebkit(); isTypeSupported = callbacks.isTypeSupported; createCustomMediaKeys = callbacks.createCustomMediaKeys; setMediaKeys = callbacks.setMediaKeys; @@ -166,7 +168,7 @@ function getEmeApiImplementation( implementation = "older-webkit"; // This is for WebKit with prefixed EME api } else if (WebKitMediaKeysConstructor !== undefined) { - onEncrypted = createCompatibleEventListener(["needkey"]); + onEncrypted = createOnEncryptedForWebkit(); const callbacks = getWebKitMediaKeysCallbacks(); isTypeSupported = callbacks.isTypeSupported; createCustomMediaKeys = callbacks.createCustomMediaKeys; @@ -271,6 +273,33 @@ function getEmeApiImplementation( setMediaKeys, implementation }; } +/** + * Create an event listener for the "webkitneedkey" event + * @returns + */ +function createOnEncryptedForWebkit(): IEmeApiImplementation["onEncrypted"] { + const compatibleEventListener = createCompatibleEventListener( + ["needkey"], + undefined /* prefixes */ + ); + const onEncrypted = ( + target: IEventTargetLike, + listener: (event: ICustomMediaEncryptedEvent) => void, + cancelSignal: CancellationSignal + ) => { + compatibleEventListener( + target, + (event?: Event) => { + const patchedEvent = objectAssign(event as MediaEncryptedEvent, { + forceSessionRecreation: true, + }); + listener(patchedEvent); + }, + cancelSignal + ); + }; + return onEncrypted; +} /** * Set the given MediaKeys on the given HTMLMediaElement. diff --git a/src/compat/eme/get_init_data.ts b/src/compat/eme/get_init_data.ts index e83f916226..5e804bcd8b 100644 --- a/src/compat/eme/get_init_data.ts +++ b/src/compat/eme/get_init_data.ts @@ -18,7 +18,9 @@ import log from "../../log"; import { getPsshSystemID } from "../../parsers/containers/isobmff"; import areArraysOfNumbersEqual from "../../utils/are_arrays_of_numbers_equal"; import { be4toi } from "../../utils/byte_parsing"; +import isNullOrUndefined from "../../utils/is_null_or_undefined"; import { PSSH_TO_INTEGER } from "./constants"; +import { ICustomMediaEncryptedEvent } from "./custom_media_keys/types"; /** Data recuperated from parsing the payload of an `encrypted` event. */ export interface IEncryptedEventData { @@ -49,6 +51,7 @@ export interface IEncryptedEventData { */ data: Uint8Array; }>; + forceSessionRecreation?: boolean | undefined; } /** @@ -145,15 +148,15 @@ function isPSSHAlreadyEncountered( * encountered in the given event. */ export default function getInitData( - encryptedEvent : MediaEncryptedEvent -) : IEncryptedEventData | null { - const { initData, initDataType } = encryptedEvent; - if (initData == null) { + encryptedEvent: ICustomMediaEncryptedEvent +): IEncryptedEventData | null { + const { initData, initDataType, forceSessionRecreation } = encryptedEvent; + if (isNullOrUndefined(initData)) { log.warn("Compat: No init data found on media encrypted event."); return null; } const initDataBytes = new Uint8Array(initData); const values = getInitializationDataValues(initDataBytes); - return { type: initDataType, values }; + return { type: initDataType, values, forceSessionRecreation }; } diff --git a/src/compat/event_listeners.ts b/src/compat/event_listeners.ts index 1c7b06b906..56138279a1 100644 --- a/src/compat/event_listeners.ts +++ b/src/compat/event_listeners.ts @@ -29,6 +29,7 @@ import { ICompatHTMLMediaElement, ICompatPictureInPictureWindow, } from "./browser_compatibility_types"; +import { ICustomMediaEncryptedEvent } from "./eme/custom_media_keys/types"; import isNode from "./is_node"; const BROWSER_PREFIXES = ["", "webkit", "moz", "ms"]; @@ -87,8 +88,14 @@ function eventPrefixed(eventNames : string[], prefixes? : string[]) : string[] { } export interface IEventEmitterLike { - addEventListener : (eventName: string, handler: () => void) => void; - removeEventListener: (eventName: string, handler: () => void) => void; + addEventListener: ( + eventName: string, + handler: EventListenerOrEventListenerObject, + ) => void; + removeEventListener: ( + eventName: string, + handler: EventListenerOrEventListenerObject, + ) => void; } export type IEventTargetLike = HTMLElement | @@ -106,28 +113,45 @@ export type IEventTargetLike = HTMLElement | * @returns {Function} - Returns function allowing to easily add a callback to * be triggered when that event is emitted on a given event target. */ + +function createCompatibleEventListener( + eventNames: Array<"needkey" | "encrypted">, + prefixes?: string[], +): ( + element: IEventTargetLike, + listener: (event: ICustomMediaEncryptedEvent) => void, + cancelSignal: CancellationSignal, +) => void; + function createCompatibleEventListener( - eventNames : string[], - prefixes? : string[] -) : + eventNames: string[], + prefixes?: string[], +): ( + element: IEventTargetLike, + listener: (event?: Event) => void, + cancelSignal: CancellationSignal, +) => void; + +function createCompatibleEventListener( + eventNames: string[] | Array<"needkey" | "encrypted">, + prefixes?: string[] +): ( - element : IEventTargetLike, - listener : (event? : unknown) => void, - cancelSignal: CancellationSignal - ) => void -{ - let mem : string|undefined; + element: IEventTargetLike, + listener: (event?: Event | MediaEncryptedEvent) => void, + cancelSignal: CancellationSignal, + ) => void { + let mem: string | undefined; const prefixedEvents = eventPrefixed(eventNames, prefixes); return ( - element : IEventTargetLike, - listener: (event? : unknown) => void, + element: IEventTargetLike, + listener: (event?: Event) => void, cancelSignal: CancellationSignal ) => { if (cancelSignal.isCancelled()) { return; } - // if the element is a HTMLElement we can detect // the supported event, and memoize it in `mem` if (element instanceof HTMLElement) { diff --git a/src/core/decrypt/content_decryptor.ts b/src/core/decrypt/content_decryptor.ts index 128ad252e5..918fc745c8 100644 --- a/src/core/decrypt/content_decryptor.ts +++ b/src/core/decrypt/content_decryptor.ts @@ -174,7 +174,7 @@ export default class ContentDecryptor extends EventEmitter { log.debug("DRM: Encrypted event received from media element."); - const initData = getInitData(evt as MediaEncryptedEvent); + const initData = getInitData(evt); if (initData !== null) { this.onInitializationData(initData); } @@ -636,6 +636,22 @@ export default class ContentDecryptor extends EventEmitter + log.error("DRM: Cannot close the session from the loaded session store") + ); + } + + /** + * If set, a currently-used key session is already compatible to this + * initialization data. + */ + const compatibleSessionInfo = arrayFind(this._currentSessions, (x) => + x.record.isCompatibleWith(initData) + ); + if (compatibleSessionInfo === undefined) { + return; + } + /** Remove the session from the currentSessions */ + const indexOf = this._currentSessions.indexOf(compatibleSessionInfo); + if (indexOf !== -1) { + log.debug( + "DRM: A session from a processed init is removed " + + "due to forceSessionRecreation policy." + ); + this._currentSessions.splice(indexOf, 1); + } + } + /** * Callback that should be called if an error that made the current * `ContentDecryptor` instance unusable arised. diff --git a/src/core/decrypt/types.ts b/src/core/decrypt/types.ts index 2b97fcf225..f9b757942b 100644 --- a/src/core/decrypt/types.ts +++ b/src/core/decrypt/types.ts @@ -118,6 +118,11 @@ export interface IProtectionData { /** Protection initialization data actually processed by the `ContentDecryptor`. */ export interface IProcessedProtectionData extends Omit { values: InitDataValuesContainer; + /** + * Enforce to recreate the media key session if there is already a session created + * with this init data + */ + forceSessionRecreation?: boolean | undefined; } /**