From da87eff75d0decf17cda0c5e17613afdab451c62 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Wed, 3 Jul 2024 17:28:48 +0200 Subject: [PATCH 1/9] Revert "Remove FORCED_MEDIA_SOURCE" This reverts commit a7b5049d43435c2da4f9fb6b3085d8cb9bf394e9. --- src/compat/browser_compatibility_types.ts | 6 ++++++ src/main_thread/init/utils/create_media_source.ts | 5 ++++- src/mse/main_media_source_interface.ts | 5 +++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/compat/browser_compatibility_types.ts b/src/compat/browser_compatibility_types.ts index d4170fd108..b4a412b3a7 100644 --- a/src/compat/browser_compatibility_types.ts +++ b/src/compat/browser_compatibility_types.ts @@ -229,6 +229,12 @@ export interface IMediaElementEventMap { * implement it. */ export interface IMediaElement extends IEventTarget { + /** + * Optional property allowing to force a specific MSE Implementation when + * relying on a given `IMediaElement`. + */ + FORCED_MEDIA_SOURCE?: new () => IMediaSource; + /* From `HTMLMediaElement`: */ autoplay: boolean; buffered: TimeRanges; diff --git a/src/main_thread/init/utils/create_media_source.ts b/src/main_thread/init/utils/create_media_source.ts index 1baa39c876..86f7489d2d 100644 --- a/src/main_thread/init/utils/create_media_source.ts +++ b/src/main_thread/init/utils/create_media_source.ts @@ -114,7 +114,10 @@ function createMediaSource( // make sure the media has been correctly reset const oldSrc = isNonEmptyString(mediaElement.src) ? mediaElement.src : null; resetMediaElement(mediaElement, oldSrc); - const mediaSource = new MainMediaSourceInterface(generateMediaSourceId()); + const mediaSource = new MainMediaSourceInterface( + generateMediaSourceId(), + "FORCED_MEDIA_SOURCE" in mediaElement ? mediaElement.FORCED_MEDIA_SOURCE : undefined, + ); disableRemotePlaybackOnManagedMediaSource(mediaElement, unlinkSignal); unlinkSignal.register(() => { mediaSource.dispose(); diff --git a/src/mse/main_media_source_interface.ts b/src/mse/main_media_source_interface.ts index 87592f795b..aa45ae793d 100644 --- a/src/mse/main_media_source_interface.ts +++ b/src/mse/main_media_source_interface.ts @@ -81,7 +81,7 @@ export default class MainMediaSourceInterface * You can then obtain a link to that `MediaSource`, for example to link it * to an `HTMLMediaElement`, through the `handle` property. */ - constructor(id: string) { + constructor(id: string, forcedMediaSource?: new () => IMediaSource) { super(); this.id = id; this.sourceBuffers = []; @@ -95,7 +95,8 @@ export default class MainMediaSourceInterface } log.info("Init: Creating MediaSource"); - const mediaSource = new MediaSource_(); + const mediaSource = + forcedMediaSource !== undefined ? new forcedMediaSource() : new MediaSource_(); const handle = (mediaSource as unknown as { handle: MediaProvider }).handle; this.handle = isNullOrUndefined(handle) ? // eslint-disable-next-line @typescript-eslint/no-restricted-types From 7759ec3f71a124a355afaa531a3ee70f7b0d1720 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Mon, 8 Jul 2024 14:42:56 +0200 Subject: [PATCH 2/9] Provide EME implementation to the ContentDecryptor --- src/compat/eme/eme-api-implementation.ts | 24 +++------ src/compat/eme/index.ts | 4 +- .../__tests__/probers/DRMInfos.test.ts | 6 +-- .../__tests__/probers/HDCPPolicy.test.ts | 18 +++---- .../probers/DRMInfos.ts | 6 ++- .../probers/HDCPPolicy.ts | 6 ++- .../__global__/find_key_system.test.ts | 50 +++++++++++------ .../__tests__/__global__/get_license.test.ts | 6 ++- .../__tests__/__global__/init_data.test.ts | 46 ++++++++++++---- .../media_key_system_access.test.ts | 25 +++++++-- .../__tests__/__global__/media_keys.test.ts | 53 +++++++++++++++---- .../__global__/server_certificate.test.ts | 30 ++++++++--- .../decrypt/__tests__/__global__/utils.ts | 51 +++++++++--------- src/main_thread/decrypt/content_decryptor.ts | 32 +++++------ src/main_thread/decrypt/find_key_system.ts | 10 ++-- src/main_thread/decrypt/get_media_keys.ts | 4 ++ src/main_thread/decrypt/init_media_keys.ts | 4 ++ .../init/multi_thread_content_initializer.ts | 6 ++- .../utils/initialize_content_decryption.ts | 6 ++- .../containers/isobmff/drm/playready.ts | 2 +- tsconfig.unit-tests.json | 1 + 21 files changed, 259 insertions(+), 131 deletions(-) diff --git a/src/compat/eme/eme-api-implementation.ts b/src/compat/eme/eme-api-implementation.ts index 36c6afd4cf..690e9af4fb 100644 --- a/src/compat/eme/eme-api-implementation.ts +++ b/src/compat/eme/eme-api-implementation.ts @@ -28,16 +28,6 @@ import getOldKitWebKitMediaKeyCallbacks, { import getWebKitMediaKeysCallbacks from "./custom_media_keys/webkit_media_keys"; import { WebKitMediaKeysConstructor } from "./custom_media_keys/webkit_media_keys_constructor"; -/** - * Automatically detect and set which EME implementation should be used in the - * current platform. - * - * You can call `getEmeApiImplementation` for a different implementation. - */ -const defaultEmeImplementation = getEmeApiImplementation("auto"); - -export default defaultEmeImplementation; - /** * Generic interface harmonizing the structure of the different EME API * implementations the RxPlayer could use. @@ -126,19 +116,21 @@ export type IPreferredEmeApiType = "auto" | "standard" | "webkit"; * (@see IPreferredEmeApiType). * @returns {Object} */ -function getEmeApiImplementation( +export default function getEmeApiImplementation( preferredApiType: IPreferredEmeApiType, -): IEmeApiImplementation { +): IEmeApiImplementation | null { let requestMediaKeySystemAccess: IEmeApiImplementation["requestMediaKeySystemAccess"]; let onEncrypted: IEmeApiImplementation["onEncrypted"]; let setMediaKeys: IEmeApiImplementation["setMediaKeys"] = defaultSetMediaKeys; let implementation: IEmeApiImplementation["implementation"]; if ( - (preferredApiType === "standard" || - (preferredApiType === "auto" && !shouldFavourCustomSafariEME())) && - // eslint-disable-next-line @typescript-eslint/unbound-method - (isNode || !isNullOrUndefined(navigator.requestMediaKeySystemAccess)) + preferredApiType === "standard" || + (preferredApiType === "auto" && !shouldFavourCustomSafariEME()) ) { + // eslint-disable-next-line @typescript-eslint/unbound-method + if (isNode || isNullOrUndefined(navigator.requestMediaKeySystemAccess)) { + return null; + } requestMediaKeySystemAccess = (...args) => navigator.requestMediaKeySystemAccess(...args); onEncrypted = createCompatibleEventListener(["encrypted"]); diff --git a/src/compat/eme/index.ts b/src/compat/eme/index.ts index 5f529b9e75..eec18495e6 100644 --- a/src/compat/eme/index.ts +++ b/src/compat/eme/index.ts @@ -19,12 +19,12 @@ import type { IEmeApiImplementation, IPreferredEmeApiType, } from "./eme-api-implementation"; -import defaultEmeImplementation from "./eme-api-implementation"; +import getEmeApiImplementation from "./eme-api-implementation"; import generateKeyRequest from "./generate_key_request"; import type { IEncryptedEventData } from "./get_init_data"; import getInitData from "./get_init_data"; import loadSession from "./load_session"; -export default defaultEmeImplementation; +export default getEmeApiImplementation; export type { IEmeApiImplementation, IPreferredEmeApiType, IEncryptedEventData }; export { closeSession, generateKeyRequest, getInitData, loadSession }; diff --git a/src/experimental/tools/mediaCapabilitiesProber/__tests__/probers/DRMInfos.test.ts b/src/experimental/tools/mediaCapabilitiesProber/__tests__/probers/DRMInfos.test.ts index a38af90068..7921f76dd2 100644 --- a/src/experimental/tools/mediaCapabilitiesProber/__tests__/probers/DRMInfos.test.ts +++ b/src/experimental/tools/mediaCapabilitiesProber/__tests__/probers/DRMInfos.test.ts @@ -38,7 +38,7 @@ describe("MediaCapabilitiesProber probers - DRMInfos", () => { }, }; vi.doMock("../../../../../compat/eme", () => ({ - default: {}, + default: () => null, })); const probeDRMInfos = (await vi.importActual("../../probers/DRMInfos")) .default as typeof IProbeDRMInfos; @@ -61,7 +61,7 @@ describe("MediaCapabilitiesProber probers - DRMInfos", () => { }); }); vi.doMock("../../../../../compat/eme", () => ({ - default: { requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess }, + default: () => ({ requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess }), })); const probeDRMInfos = (await vi.importActual("../../probers/DRMInfos")) .default as typeof IProbeDRMInfos; @@ -88,7 +88,7 @@ describe("MediaCapabilitiesProber probers - DRMInfos", () => { return Promise.reject(new Error()); }); vi.doMock("../../../../../compat/eme", () => ({ - default: { requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess }, + default: () => ({ requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess }), })); const probeDRMInfos = (await vi.importActual("../../probers/DRMInfos")) diff --git a/src/experimental/tools/mediaCapabilitiesProber/__tests__/probers/HDCPPolicy.test.ts b/src/experimental/tools/mediaCapabilitiesProber/__tests__/probers/HDCPPolicy.test.ts index 2713a333da..fac84a728d 100644 --- a/src/experimental/tools/mediaCapabilitiesProber/__tests__/probers/HDCPPolicy.test.ts +++ b/src/experimental/tools/mediaCapabilitiesProber/__tests__/probers/HDCPPolicy.test.ts @@ -9,7 +9,7 @@ describe("MediaCapabilitiesProber probers - HDCPPolicy", () => { it("should throw if no requestMediaKeySystemAccess", async () => { vi.doMock("../../../../../compat/eme", () => ({ - default: vi.fn(), + default: () => null, })); const probeHDCPPolicy = (await vi.importActual("../../probers/HDCPPolicy")) .default as typeof IProbeHDCPPolicy; @@ -26,9 +26,9 @@ describe("MediaCapabilitiesProber probers - HDCPPolicy", () => { }); }); vi.doMock("../../../../../compat/eme", () => ({ - default: { + default: () => ({ requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess, - }, + }), })); const probeHDCPPolicy = (await vi.importActual("../../probers/HDCPPolicy")) .default as typeof IProbeHDCPPolicy; @@ -49,9 +49,9 @@ describe("MediaCapabilitiesProber probers - HDCPPolicy", () => { }); }); vi.doMock("../../../../../compat/eme", () => ({ - default: { + default: () => ({ requestMediaKeySystemAccess: mockRequestMediaKeySystemAcces, - }, + }), })); const probeHDCPPolicy = (await vi.importActual("../../probers/HDCPPolicy")) @@ -76,9 +76,9 @@ describe("MediaCapabilitiesProber probers - HDCPPolicy", () => { }); }); vi.doMock("../../../../../compat/eme", () => ({ - default: { + default: () => ({ requestMediaKeySystemAccess: mockRequestMediaKeySystemAcces, - }, + }), })); const probeHDCPPolicy = (await vi.importActual("../../probers/HDCPPolicy")) @@ -103,9 +103,9 @@ describe("MediaCapabilitiesProber probers - HDCPPolicy", () => { }); }); vi.doMock("../../../../../compat/eme", () => ({ - default: { + default: () => ({ requestMediaKeySystemAccess: mockRequestMediaKeySystemAcces, - }, + }), })); const probeHDCPPolicy = (await vi.importActual("../../probers/HDCPPolicy")) diff --git a/src/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.ts b/src/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.ts index 99a4e7a417..25afe55b9b 100644 --- a/src/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.ts +++ b/src/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.ts @@ -15,7 +15,7 @@ */ import { canRelyOnRequestMediaKeySystemAccess } from "../../../../compat/can_rely_on_request_media_key_system_access"; -import eme from "../../../../compat/eme"; +import getEmeApiImplementation from "../../../../compat/eme"; import { DUMMY_PLAY_READY_HEADER, generatePlayReadyInitData, @@ -50,7 +50,9 @@ export default function probeDRMInfos( keySystem.configuration === undefined ? {} : keySystem.configuration; const result: ICompatibleKeySystem = { type, configuration }; - if (isNullOrUndefined(eme.requestMediaKeySystemAccess)) { + const eme = getEmeApiImplementation("auto"); + + if (eme === null) { log.debug( "MediaCapabilitiesProber >>> API_CALL: " + "Your browser has no API to request a media key system access.", diff --git a/src/experimental/tools/mediaCapabilitiesProber/probers/HDCPPolicy.ts b/src/experimental/tools/mediaCapabilitiesProber/probers/HDCPPolicy.ts index 4b8af3a2fc..7de59859d7 100644 --- a/src/experimental/tools/mediaCapabilitiesProber/probers/HDCPPolicy.ts +++ b/src/experimental/tools/mediaCapabilitiesProber/probers/HDCPPolicy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import eme from "../../../../compat/eme"; +import getEmeApiImplementation from "../../../../compat/eme"; import isNullOrUndefined from "../../../../utils/is_null_or_undefined"; import type { IMediaConfiguration } from "../types"; import { ProberStatus } from "../types"; @@ -35,7 +35,9 @@ export type IMediaKeyStatus = export default function probeHDCPPolicy( config: IMediaConfiguration, ): Promise<[ProberStatus]> { - if (isNullOrUndefined(eme.requestMediaKeySystemAccess)) { + const eme = getEmeApiImplementation("auto"); + + if (eme === null) { return Promise.reject("MediaCapabilitiesProber >>> API_CALL: " + "API not available"); } if (isNullOrUndefined(config.hdcp)) { diff --git a/src/main_thread/decrypt/__tests__/__global__/find_key_system.test.ts b/src/main_thread/decrypt/__tests__/__global__/find_key_system.test.ts index aca5a9ea67..179eba8a5e 100644 --- a/src/main_thread/decrypt/__tests__/__global__/find_key_system.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/find_key_system.test.ts @@ -1,13 +1,25 @@ import type { MockInstance } from "vitest"; import { describe, beforeEach, it, expect, vi } from "vitest"; import * as compat from "../../../../compat/can_rely_on_request_media_key_system_access"; -import eme from "../../../../compat/eme"; import { testKeySystem } from "../../find_key_system"; +/* eslint-disable @typescript-eslint/no-explicit-any */ + describe("find_key_systems - ", () => { let requestMediaKeySystemAccessMock: MockInstance; let canRelyOnEMEMock: MockInstance; const keySystem = "com.microsoft.playready.recommendation"; + const eme = { + implementation: "standard", + requestMediaKeySystemAccess: ( + _type: string, + _config: MediaKeySystemConfiguration[], + ): Promise => Promise.reject(new Error("Unimplemented")), + onEncrypted: () => { + /* noop */ + }, + setMediaKeys: () => Promise.reject(new Error("Unimplemented")), + } as const; beforeEach(() => { vi.resetModules(); @@ -18,15 +30,17 @@ describe("find_key_systems - ", () => { it("should resolve if the keySystem is supported", async () => { /* mock implementation of requestMediaKeySystemAccess that support the keySystem */ - requestMediaKeySystemAccessMock.mockImplementation(() => ({ - createMediaKeys: () => ({ - createSession: () => ({ - // eslint-disable-next-line @typescript-eslint/no-empty-function - generateRequest: () => {}, + requestMediaKeySystemAccessMock.mockImplementation(() => + Promise.resolve({ + createMediaKeys: () => ({ + createSession: () => ({ + // eslint-disable-next-line @typescript-eslint/no-empty-function + generateRequest: () => {}, + }), }), }), - })); - await expect(testKeySystem(keySystem, [])).resolves.toBeTruthy(); + ); + await expect(testKeySystem(eme, keySystem, [])).resolves.toBeTruthy(); expect(requestMediaKeySystemAccessMock).toHaveBeenCalledTimes(1); }); @@ -35,7 +49,7 @@ describe("find_key_systems - ", () => { requestMediaKeySystemAccessMock.mockImplementation(() => { throw new Error(); }); - await expect(testKeySystem(keySystem, [])).rejects.toThrow(); + await expect(testKeySystem(eme, keySystem, [])).rejects.toThrow(); expect(requestMediaKeySystemAccessMock).toHaveBeenCalledTimes(1); }); @@ -45,16 +59,18 @@ describe("find_key_systems - ", () => { and generating a request. */ canRelyOnEMEMock.mockImplementation(() => false); - requestMediaKeySystemAccessMock.mockImplementation(() => ({ - createMediaKeys: () => ({ - createSession: () => ({ - generateRequest: () => { - throw new Error("generateRequest failed"); - }, + requestMediaKeySystemAccessMock.mockImplementation(() => + Promise.resolve({ + createMediaKeys: () => ({ + createSession: () => ({ + generateRequest: () => { + throw new Error("generateRequest failed"); + }, + }), }), }), - })); - await expect(testKeySystem(keySystem, [])).rejects.toThrow(); + ); + await expect(testKeySystem(eme, keySystem, [])).rejects.toThrow(); expect(requestMediaKeySystemAccessMock).toHaveBeenCalledTimes(1); }); }); diff --git a/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts b/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts index 216e74ae1b..a5d7983b4c 100644 --- a/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts @@ -1,5 +1,6 @@ import { describe, afterEach, it, expect, vi } from "vitest"; import type { IKeySystemOption, IPlayerError } from "../../../../public_types"; +import assert from "../../../../utils/assert"; import { concat } from "../../../../utils/byte_parsing"; import type IContentDecryptor from "../../content_decryptor"; import type { ContentDecryptorState as IContentDecryptorState } from "../../types"; @@ -339,6 +340,7 @@ async function checkGetLicense({ .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == /** Default keySystems configuration used in our tests. */ @@ -368,7 +370,9 @@ async function checkGetLicense({ } // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: number) => { if (newState !== ContentDecryptorState.WaitingForAttachment) { diff --git a/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts b/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts index 027b4e87ba..a9ae0375ef 100644 --- a/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts @@ -1,4 +1,5 @@ import { describe, afterEach, it, expect, vi } from "vitest"; +import assert from "../../../../utils/assert"; import type IContentDecryptor from "../../content_decryptor"; import type { ContentDecryptorState as IContentDecryptorState } from "../../types"; import { @@ -37,12 +38,15 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initData = new Uint8Array([54, 55, 75]); // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { @@ -93,12 +97,15 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initData = new Uint8Array([54, 55, 75]); // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { @@ -165,6 +172,7 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initDatas = [ @@ -174,7 +182,9 @@ describe("decrypt - global tests - init data", () => { ]; // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { @@ -273,12 +283,15 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initData = new Uint8Array([54, 55, 75]); // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { @@ -348,12 +361,15 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initData = new Uint8Array([54, 55, 75]); // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { @@ -408,12 +424,15 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initData = new Uint8Array([54, 55, 75]); // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { @@ -479,6 +498,7 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initDatas = [ @@ -495,7 +515,9 @@ describe("decrypt - global tests - init data", () => { ]; // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { @@ -585,6 +607,7 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initData = new Uint8Array([54, 55, 75]); @@ -595,7 +618,9 @@ describe("decrypt - global tests - init data", () => { ]; // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { @@ -667,6 +692,7 @@ describe("decrypt - global tests - init data", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { // == vars == const initDatas = [ @@ -683,7 +709,9 @@ describe("decrypt - global tests - init data", () => { ]; // == test == - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", (newState: IContentDecryptorState) => { diff --git a/src/main_thread/decrypt/__tests__/__global__/media_key_system_access.test.ts b/src/main_thread/decrypt/__tests__/__global__/media_key_system_access.test.ts index 05eb09c0b3..860a46a4e7 100644 --- a/src/main_thread/decrypt/__tests__/__global__/media_key_system_access.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/media_key_system_access.test.ts @@ -1,6 +1,7 @@ import { describe, beforeEach, afterEach, it, expect, vi } from "vitest"; import type { IMediaKeySystemAccess } from "../../../../compat/browser_compatibility_types"; import type { IKeySystemOption } from "../../../../public_types"; +import assert from "../../../../utils/assert"; import type IContentDecryptor from "../../content_decryptor"; import { defaultKSConfig, @@ -62,8 +63,12 @@ async function checkIncompatibleKeySystemsErrorMessage( const mediaElement = document.createElement("video"); const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); const error = await testContentDecryptorError( + eme, ContentDecryptor, mediaElement, keySystemsConfigs, @@ -966,11 +971,14 @@ describe("decrypt - global tests - media key system access", () => { }); const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { const config = [{ type: "com.widevine.alpha", getLicense: neverCalledFn }]; const mediaElement = document.createElement("video"); - const contentDecryptor = new ContentDecryptor(mediaElement, config); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, mediaElement, config); contentDecryptor.addEventListener("error", (error) => { rej(error); }); @@ -999,6 +1007,7 @@ describe("decrypt - global tests - media key system access", () => { }); const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { const config = [ { type: "com.widevine.alpha", getLicense: neverCalledFn }, @@ -1006,7 +1015,9 @@ describe("decrypt - global tests - media key system access", () => { ]; const mediaElement = document.createElement("video"); - const contentDecryptor = new ContentDecryptor(mediaElement, config); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, mediaElement, config); contentDecryptor.addEventListener("error", (error) => { rej(error); }); @@ -1047,6 +1058,7 @@ describe("decrypt - global tests - media key system access", () => { }); const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { const mediaElement = document.createElement("video"); @@ -1055,7 +1067,9 @@ describe("decrypt - global tests - media key system access", () => { { type: "bar", getLicense: neverCalledFn }, { type: "baz", getLicense: neverCalledFn }, ]; - contentDecryptor = new ContentDecryptor(mediaElement, config); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + contentDecryptor = new ContentDecryptor(eme, mediaElement, config); contentDecryptor.addEventListener("error", (error) => { rej(error); }); @@ -1083,11 +1097,14 @@ describe("decrypt - global tests - media key system access", () => { }); const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { const mediaElement = document.createElement("video"); const config = [{ type: "foo", getLicense: neverCalledFn }]; - const contentDecryptor = new ContentDecryptor(mediaElement, config); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, mediaElement, config); contentDecryptor.addEventListener("error", () => { expect(rmksHasBeenCalled).toEqual(true); res(); diff --git a/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts b/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts index e2c1f838e3..bc24cbaa4b 100644 --- a/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts @@ -1,5 +1,6 @@ import { describe, beforeEach, afterEach, it, expect, vi } from "vitest"; import type { IKeySystemOption } from "../../../../public_types"; +import assert from "../../../../utils/assert"; import type IContentDecryptor from "../../content_decryptor"; import type { ContentDecryptorState as IContentDecryptorState } from "../../types"; import { @@ -56,7 +57,15 @@ describe("decrypt - global tests - media key system access", () => { // == test == const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - const error = await testContentDecryptorError(ContentDecryptor, videoElt, ksConfig); + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const error = await testContentDecryptorError( + eme, + ContentDecryptor, + videoElt, + ksConfig, + ); expect(error).toBeInstanceOf(Error); expect(error.message).toEqual("CREATE_MEDIA_KEYS_ERROR: No non no"); expect(error.name).toEqual("EncryptedMediaError"); @@ -84,7 +93,15 @@ describe("decrypt - global tests - media key system access", () => { // == test == const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - const error = await testContentDecryptorError(ContentDecryptor, videoElt, ksConfig); + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const error = await testContentDecryptorError( + eme, + ContentDecryptor, + videoElt, + ksConfig, + ); expect(error).toBeInstanceOf(Error); expect(error.message).toEqual("CREATE_MEDIA_KEYS_ERROR: No non no"); expect(error.name).toEqual("EncryptedMediaError"); @@ -103,8 +120,11 @@ describe("decrypt - global tests - media key system access", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); return new Promise((res, rej) => { - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); let receivedStateChange = 0; contentDecryptor.addEventListener("stateChange", (newState) => { receivedStateChange++; @@ -140,8 +160,11 @@ describe("decrypt - global tests - media key system access", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); return new Promise((res, rej) => { - const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig); + const contentDecryptor1 = new ContentDecryptor(eme, videoElt, ksConfig); let receivedStateChange1 = 0; contentDecryptor1.addEventListener("error", rej); contentDecryptor1.addEventListener("stateChange", (state1) => { @@ -162,7 +185,7 @@ describe("decrypt - global tests - media key system access", () => { setTimeout(() => { contentDecryptor1.dispose(); - const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); + const contentDecryptor2 = new ContentDecryptor(eme, videoElt, ksConfig); let receivedStateChange2 = 0; contentDecryptor2.addEventListener("error", rej); contentDecryptor2.addEventListener("stateChange", (state2) => { @@ -207,8 +230,11 @@ describe("decrypt - global tests - media key system access", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; return new Promise((res, rej) => { - const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig); + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor1 = new ContentDecryptor(eme, videoElt, ksConfig); let receivedStateChange1 = 0; contentDecryptor1.addEventListener("error", rej); contentDecryptor1.addEventListener("stateChange", (state1) => { @@ -229,7 +255,7 @@ describe("decrypt - global tests - media key system access", () => { setTimeout(() => { contentDecryptor1.dispose(); - const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); + const contentDecryptor2 = new ContentDecryptor(eme, videoElt, ksConfig); let receivedStateChange2 = 0; contentDecryptor2.addEventListener("error", rej); contentDecryptor2.addEventListener("stateChange", (state2) => { @@ -274,9 +300,11 @@ describe("decrypt - global tests - media key system access", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); return new Promise((res, rej) => { - const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig); + const contentDecryptor1 = new ContentDecryptor(eme, videoElt, ksConfig); let receivedStateChange1 = 0; contentDecryptor1.addEventListener("error", rej); contentDecryptor1.addEventListener("stateChange", (state1) => { @@ -297,7 +325,7 @@ describe("decrypt - global tests - media key system access", () => { setTimeout(() => { contentDecryptor1.dispose(); - const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); + const contentDecryptor2 = new ContentDecryptor(eme, videoElt, ksConfig); let receivedStateChange2 = 0; contentDecryptor2.addEventListener("error", rej); contentDecryptor2.addEventListener("stateChange", (state2) => { @@ -341,7 +369,10 @@ describe("decrypt - global tests - media key system access", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfig); return new Promise((res) => { contentDecryptor.addEventListener("stateChange", (newState) => { if (newState === ContentDecryptorState.WaitingForAttachment) { diff --git a/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts b/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts index 62dd11e928..367cd0e145 100644 --- a/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts @@ -1,5 +1,6 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import type { IKeySystemOption } from "../../../../public_types"; +import assert from "../../../../utils/assert"; import type IContentDecryptor from "../../content_decryptor"; import type { ContentDecryptorState as IContentDecryptorState } from "../../types"; import { MediaKeysImpl, MediaKeySystemAccessImpl, mockCompat } from "./utils"; @@ -40,7 +41,7 @@ describe("decrypt - global tests - server certificate", () => { const mockCreateSession = vi.spyOn(MediaKeysImpl.prototype, "createSession"); const mockSetServerCertificate = vi .spyOn(MediaKeysImpl.prototype, "setServerCertificate") - .mockImplementation((_serverCertificate: BufferSource) => { + .mockImplementation((_serverCertificate: BufferSource): Promise => { expect(mockSetMediaKeys).toHaveBeenCalledTimes(1); expect(mockCreateSession).not.toHaveBeenCalled(); return Promise.resolve(true); @@ -50,7 +51,10 @@ describe("decrypt - global tests - server certificate", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfigCert); return new Promise((res) => { contentDecryptor.addEventListener("stateChange", (state) => { @@ -84,7 +88,7 @@ describe("decrypt - global tests - server certificate", () => { const mockCreateSession = vi.spyOn(MediaKeysImpl.prototype, "createSession"); const mockSetServerCertificate = vi .spyOn(MediaKeysImpl.prototype, "setServerCertificate") - .mockImplementation((_serverCertificate: BufferSource) => { + .mockImplementation((_serverCertificate: BufferSource): Promise => { expect(mockSetMediaKeys).toHaveBeenCalledTimes(1); expect(mockCreateSession).not.toHaveBeenCalled(); return Promise.resolve(true); @@ -94,7 +98,10 @@ describe("decrypt - global tests - server certificate", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfigCert); contentDecryptor.addEventListener("stateChange", (state) => { if (state === ContentDecryptorState.WaitingForAttachment) { @@ -136,7 +143,10 @@ describe("decrypt - global tests - server certificate", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfigCert); contentDecryptor.addEventListener("stateChange", (state) => { if (state === ContentDecryptorState.WaitingForAttachment) { @@ -176,7 +186,10 @@ describe("decrypt - global tests - server certificate", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfigCert); contentDecryptor.addEventListener("stateChange", (state) => { if (state === ContentDecryptorState.WaitingForAttachment) { @@ -227,7 +240,10 @@ describe("decrypt - global tests - server certificate", () => { .ContentDecryptorState as typeof IContentDecryptorState; const ContentDecryptor = (await vi.importActual("../../content_decryptor")) .default as typeof IContentDecryptor; - const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); + const getEmeApiImplementation = (await import("../../../../compat/eme")).default; + const eme = getEmeApiImplementation("auto"); + assert(eme !== null, "Expected to have an EME implementation"); + const contentDecryptor = new ContentDecryptor(eme, videoElt, ksConfigCert); contentDecryptor.addEventListener("stateChange", (state) => { if (state === ContentDecryptorState.WaitingForAttachment) { diff --git a/src/main_thread/decrypt/__tests__/__global__/utils.ts b/src/main_thread/decrypt/__tests__/__global__/utils.ts index 8d060a61ad..fb1d6dcc17 100644 --- a/src/main_thread/decrypt/__tests__/__global__/utils.ts +++ b/src/main_thread/decrypt/__tests__/__global__/utils.ts @@ -282,27 +282,6 @@ export function mockCompat( } = {}, ) { const ee = new MockedDecryptorEventEmitter(); - const onEncrypted = - presets.onEncrypted ?? - vi - .fn() - .mockImplementation( - (elt: IMediaElement, fn: (x: unknown) => void, signal: CancellationSignal) => { - elt.addEventListener("encrypted", fn); - signal.register(() => { - elt.removeEventListener("encrypted", fn); - }); - ee.addEventListener( - "encrypted", - (evt) => { - if (evt.elt === elt) { - fn(evt.value); - } - }, - signal, - ); - }, - ); const mockEvents: Record = { onKeyMessage: vi .fn() @@ -366,6 +345,27 @@ export function mockCompat( ), }; + const mockOnEncrypted = + presets.onEncrypted ?? + vi + .fn() + .mockImplementation( + (elt: IMediaElement, fn: (x: unknown) => void, signal: CancellationSignal) => { + elt.addEventListener("encrypted", fn); + signal.register(() => { + elt.removeEventListener("encrypted", fn); + }); + ee.addEventListener( + "encrypted", + (evt) => { + if (evt.elt === elt) { + fn(evt.value); + } + }, + signal, + ); + }, + ); const mockRmksa = presets.requestMediaKeySystemAccess ?? vi.fn().mockImplementation(requestMediaKeySystemAccessImpl); @@ -409,13 +409,13 @@ export function mockCompat( } const emeImplementation = { - onEncrypted, + onEncrypted: mockOnEncrypted, requestMediaKeySystemAccess: mockRmksa, setMediaKeys: mockSetMediaKeys, } as unknown as IEmeApiImplementation; vi.doMock("../../../../compat/eme", () => ({ - default: emeImplementation, + default: () => emeImplementation, getInitData: mockGetInitData, generateKeyRequest: mockGenerateKeyRequest, })); @@ -453,14 +453,15 @@ export function mockCompat( * @returns {Promise} */ export function testContentDecryptorError( + eme: IEmeApiImplementation, // eslint-disable-next-line @typescript-eslint/naming-convention ContentDecryptor: typeof IContentDecryptor, mediaElement: IMediaElement, keySystemsConfigs: IKeySystemOption[], ): Promise { return new Promise((res, rej) => { - const contentDecryptor = new ContentDecryptor(mediaElement, keySystemsConfigs); - contentDecryptor.addEventListener("error", (error) => { + const contentDecryptor = new ContentDecryptor(eme, mediaElement, keySystemsConfigs); + contentDecryptor.addEventListener("error", (error: Error) => { res(error); }); setTimeout(() => { diff --git a/src/main_thread/decrypt/content_decryptor.ts b/src/main_thread/decrypt/content_decryptor.ts index 35cf2a55cf..875119d2ab 100644 --- a/src/main_thread/decrypt/content_decryptor.ts +++ b/src/main_thread/decrypt/content_decryptor.ts @@ -19,7 +19,8 @@ import type { IMediaKeySystemAccess, IMediaKeys, } from "../../compat/browser_compatibility_types"; -import eme, { getInitData } from "../../compat/eme"; +import type { IEmeApiImplementation } from "../../compat/eme"; +import { getInitData } from "../../compat/eme"; import config from "../../config"; import { EncryptedMediaError, OtherError } from "../../errors"; import log from "../../log"; @@ -105,6 +106,11 @@ export default class ContentDecryptor extends EventEmitter} ksOptions - key system configuration. @@ -158,7 +155,11 @@ export default class ContentDecryptor extends EventEmitter { log.debug("DRM: Encrypted event received from media element."); @@ -188,7 +190,7 @@ export default class ContentDecryptor extends EventEmitter { const { options, mediaKeySystemAccess } = mediaKeysInfo; this._supportedCodecWhenEncrypted = mediaKeysInfo.codecSupport; @@ -281,7 +283,7 @@ export default class ContentDecryptor extends EventEmitter} keySystemsConfigs - The keySystems you want to test. * @param {Object} cancelSignal * @returns {Promise.} */ export default function getMediaKeySystemAccess( + eme: IEmeApiImplementation, mediaElement: IMediaElement, keySystemsConfigs: IKeySystemOption[], cancelSignal: CancellationSignal, @@ -503,7 +505,7 @@ export default function getMediaKeySystemAccess( } try { - keySystemAccess = await testKeySystem(keyType, [keySystemConfiguration]); + keySystemAccess = await testKeySystem(eme, keyType, [keySystemConfiguration]); log.info("DRM: Found compatible keysystem", keyType, index + 1); return { type: "create-media-key-system-access" as const, @@ -531,14 +533,16 @@ export default function getMediaKeySystemAccess( /** * Test a key system configuration, resolves with the MediaKeySystemAccess * or reject if the key system is unsupported. + * @param {Object} eme - current EME implementation * @param {string} keyType - The KeySystem string to test (ex: com.microsoft.playready.recommendation) * @param {Array.} keySystemConfigurations - Configurations for this keySystem * @returns Promise resolving with the MediaKeySystemAccess. Rejects if unsupported. */ export async function testKeySystem( + eme: IEmeApiImplementation, keyType: string, keySystemConfigurations: MediaKeySystemConfiguration[], -) { +): Promise { const keySystemAccess = await eme.requestMediaKeySystemAccess( keyType, keySystemConfigurations, diff --git a/src/main_thread/decrypt/get_media_keys.ts b/src/main_thread/decrypt/get_media_keys.ts index a845365643..43c6624cc6 100644 --- a/src/main_thread/decrypt/get_media_keys.ts +++ b/src/main_thread/decrypt/get_media_keys.ts @@ -20,6 +20,7 @@ import type { IMediaKeys, } from "../../compat/browser_compatibility_types"; import canReuseMediaKeys from "../../compat/can_reuse_media_keys"; +import type { IEmeApiImplementation } from "../../compat/eme"; import { EncryptedMediaError } from "../../errors"; import log from "../../log"; import type { IKeySystemOption } from "../../public_types"; @@ -72,6 +73,7 @@ export interface IMediaKeysInfos { /** * Create a MediaKeys instance and associated structures (or just return the * current ones if sufficient) based on a wanted configuration. + * @param {Object} eme - current EME implementation * @param {HTMLMediaElement} mediaElement - The HTMLMediaElement on which you * will attach the MediaKeys instance. * This Element is here only used to check if the current MediaKeys and @@ -83,11 +85,13 @@ export interface IMediaKeysInfos { * @returns {Promise.} */ export default async function getMediaKeysInfos( + eme: IEmeApiImplementation, mediaElement: IMediaElement, keySystemsConfigs: IKeySystemOption[], cancelSignal: CancellationSignal, ): Promise { const evt = await getMediaKeySystemAccess( + eme, mediaElement, keySystemsConfigs, cancelSignal, diff --git a/src/main_thread/decrypt/init_media_keys.ts b/src/main_thread/decrypt/init_media_keys.ts index a4546b0d6a..b81d717950 100644 --- a/src/main_thread/decrypt/init_media_keys.ts +++ b/src/main_thread/decrypt/init_media_keys.ts @@ -15,6 +15,7 @@ */ import type { IMediaElement } from "../../compat/browser_compatibility_types"; +import type { IEmeApiImplementation } from "../../compat/eme"; import log from "../../log"; import type { IKeySystemOption } from "../../public_types"; import type { CancellationSignal } from "../../utils/task_canceller"; @@ -24,17 +25,20 @@ import MediaKeysAttacher from "./utils/media_keys_attacher"; /** * Get media keys infos from key system configs then attach media keys to media element. + * @param {Object} eme - current EME implementation * @param {HTMLMediaElement} mediaElement * @param {Array.} keySystemsConfigs * @param {Object} cancelSignal * @returns {Promise.} */ export default async function initMediaKeys( + eme: IEmeApiImplementation, mediaElement: IMediaElement, keySystemsConfigs: IKeySystemOption[], cancelSignal: CancellationSignal, ): Promise { const mediaKeysInfo = await getMediaKeysInfos( + eme, mediaElement, keySystemsConfigs, cancelSignal, diff --git a/src/main_thread/init/multi_thread_content_initializer.ts b/src/main_thread/init/multi_thread_content_initializer.ts index 89ca5a1e56..71d355d2e7 100644 --- a/src/main_thread/init/multi_thread_content_initializer.ts +++ b/src/main_thread/init/multi_thread_content_initializer.ts @@ -1,4 +1,5 @@ import type { IMediaElement } from "../../compat/browser_compatibility_types"; +import getEmeApiImplementation from "../../compat/eme"; import mayMediaElementFailOnUndecipherableData from "../../compat/may_media_element_fail_on_undecipherable_data"; import shouldReloadMediaSourceOnDecipherabilityUpdate from "../../compat/should_reload_media_source_on_decipherability_update"; import type { ISegmentSinkMetrics } from "../../core/segment_sinks/segment_sinks_store"; @@ -1235,11 +1236,12 @@ export default class MultiThreadContentInitializer extends ContentInitializer { } const ContentDecryptor = features.decrypt; - if (!ContentDecryptor.hasEmeApis()) { + const emeApi = getEmeApiImplementation("auto"); + if (emeApi === null) { return createEmeDisabledReference("EME API not available on the current page."); } log.debug("MTCI: Creating ContentDecryptor"); - const contentDecryptor = new ContentDecryptor(mediaElement, keySystems); + const contentDecryptor = new ContentDecryptor(emeApi, mediaElement, keySystems); const drmStatusRef = new SharedReference( { initializationState: { type: "uninitialized", value: null }, diff --git a/src/main_thread/init/utils/initialize_content_decryption.ts b/src/main_thread/init/utils/initialize_content_decryption.ts index a4659946c0..a30317185e 100644 --- a/src/main_thread/init/utils/initialize_content_decryption.ts +++ b/src/main_thread/init/utils/initialize_content_decryption.ts @@ -1,4 +1,5 @@ import type { IMediaElement } from "../../../compat/browser_compatibility_types"; +import getEmeApiImplementation from "../../../compat/eme"; import { EncryptedMediaError } from "../../../errors"; import features from "../../../features"; import log from "../../../log"; @@ -77,12 +78,13 @@ export default function initializeContentDecryption( const ContentDecryptor = features.decrypt; - if (!ContentDecryptor.hasEmeApis()) { + const emeApi = getEmeApiImplementation("auto"); + if (emeApi === null) { return createEmeDisabledReference("EME API not available on the current page."); } log.debug("Init: Creating ContentDecryptor"); - const contentDecryptor = new ContentDecryptor(mediaElement, keySystems); + const contentDecryptor = new ContentDecryptor(emeApi, mediaElement, keySystems); const onStateChange = (state: ContentDecryptorState) => { if (state > ContentDecryptorState.Initializing) { diff --git a/src/parsers/containers/isobmff/drm/playready.ts b/src/parsers/containers/isobmff/drm/playready.ts index c0f14da1f3..dae0ddf3ab 100644 --- a/src/parsers/containers/isobmff/drm/playready.ts +++ b/src/parsers/containers/isobmff/drm/playready.ts @@ -20,7 +20,7 @@ import { bytesToHex, guidToUuid, utf16LEToStr } from "../../../../utils/string_p /** * Parse PlayReady privateData to get its Hexa-coded KeyID. - * @param {Uint8Array} privateData + * @param {Uint8Array} data * @returns {string} */ export function getPlayReadyKIDFromPrivateData(data: Uint8Array): string { diff --git a/tsconfig.unit-tests.json b/tsconfig.unit-tests.json index 0bb88b8fbe..5e488c1ae9 100644 --- a/tsconfig.unit-tests.json +++ b/tsconfig.unit-tests.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "module": "es2020", "moduleResolution": "Bundler", "esModuleInterop": true, "typeRoots": ["./src/globals.dev.d.ts", "./node_modules/@types"] From 9cc8dc8e0a30dadc4f397469641070b6acaf3237 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Fri, 5 Jul 2024 18:41:51 +0200 Subject: [PATCH 3/9] Implement Dummy media API --- demo/scripts/components/Options/Playback.tsx | 23 + demo/scripts/controllers/Player.tsx | 32 +- demo/scripts/controllers/Settings.tsx | 13 + demo/scripts/lib/parseDRMConfigurations.ts | 41 + demo/scripts/modules/player/index.ts | 14 + src/compat/browser_compatibility_types.ts | 6 +- .../tools/DummyMediaElement/eme.ts | 917 ++++++++++++++++++ .../tools/DummyMediaElement/html5.ts | 453 +++++++++ .../tools/DummyMediaElement/index.ts | 2 + .../tools/DummyMediaElement/mse.ts | 824 ++++++++++++++++ .../tools/DummyMediaElement/utils.ts | 515 ++++++++++ src/main_thread/api/public_api.ts | 1 + .../init/multi_thread_content_initializer.ts | 2 +- .../utils/initialize_content_decryption.ts | 2 +- .../isobmff/extract_complete_chunks.ts | 21 + src/parsers/containers/isobmff/index.ts | 3 +- 16 files changed, 2862 insertions(+), 7 deletions(-) create mode 100644 src/experimental/tools/DummyMediaElement/eme.ts create mode 100644 src/experimental/tools/DummyMediaElement/html5.ts create mode 100644 src/experimental/tools/DummyMediaElement/index.ts create mode 100644 src/experimental/tools/DummyMediaElement/mse.ts create mode 100644 src/experimental/tools/DummyMediaElement/utils.ts diff --git a/demo/scripts/components/Options/Playback.tsx b/demo/scripts/components/Options/Playback.tsx index 1d5c164544..b85d0b3048 100644 --- a/demo/scripts/components/Options/Playback.tsx +++ b/demo/scripts/components/Options/Playback.tsx @@ -10,11 +10,15 @@ function PlaybackConfig({ onAutoPlayChange, tryRelyOnWorker, onTryRelyOnWorkerChange, + useDummyMediaElement, + onUseDummyMediaElementChange, }: { autoPlay: boolean; onAutoPlayChange: (val: boolean) => void; tryRelyOnWorker: boolean; onTryRelyOnWorkerChange: (val: boolean) => void; + useDummyMediaElement: boolean; + onUseDummyMediaElementChange: (val: boolean) => void; }): JSX.Element { return ( <> @@ -51,6 +55,25 @@ function PlaybackConfig({ : "Currently running the RxPlayer's main logic only in main thread."} + +
  • + + Dummy Media API + + + {useDummyMediaElement + ? "Use mocked media API: The content will not really play but the RxPlayer " + + "will believe it does. Useful for debugging the RxPlayer's logic even on " + + "undecipherable or undecodable content." + : "Actually play the chosen content."} + +
  • ); } diff --git a/demo/scripts/controllers/Player.tsx b/demo/scripts/controllers/Player.tsx index 398d9d3f4e..3dbd5fc626 100644 --- a/demo/scripts/controllers/Player.tsx +++ b/demo/scripts/controllers/Player.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import DummyMediaElement from "../../../src/experimental/tools/DummyMediaElement"; import type { IAudioRepresentationsSwitchingMode, ILoadVideoOptions, @@ -17,6 +18,7 @@ import type { ILoadVideoSettings, IConstructorSettings, } from "../lib/defaultOptionsValues"; +import { toDummyDrmConfiguration } from "../lib/parseDRMConfigurations"; const { useCallback, useEffect, useRef, useState } = React; @@ -45,6 +47,7 @@ function Player(): JSX.Element { defaultOptionsValues.loadVideo, ); const [relyOnWorker, setRelyOnWorker] = useState(false); + const [useDummyMediaElement, setUseDummyMediaElement] = useState(false); const [hasUpdatedPlayerOptions, setHasUpdatedPlayerOptions] = useState(false); const displaySpinnerTimeoutRef = useRef(null); @@ -156,11 +159,14 @@ function Player(): JSX.Element { if (playerModule) { playerModule.destroy(); } + const videoElement = useDummyMediaElement + ? (new DummyMediaElement() as unknown as HTMLMediaElement) + : videoElementRef.current; const playerMod = new PlayerModule( Object.assign( {}, { - videoElement: videoElementRef.current, + videoElement, textTrackElement: textTrackElementRef.current, debugElement: debugElementRef.current, }, @@ -169,7 +175,24 @@ function Player(): JSX.Element { ); setPlayerModule(playerMod); return playerMod; - }, [playerOpts, playerModule]); + }, [useDummyMediaElement, playerOpts, playerModule]); + + useEffect(() => { + if (playerModule === null) { + return; + } + const mediaElement = playerModule.actions.getMediaElement(); + if (mediaElement === null) { + return; + } + if (useDummyMediaElement) { + if (!(mediaElement instanceof DummyMediaElement)) { + setHasUpdatedPlayerOptions(true); + } + } else if (mediaElement !== videoElementRef.current) { + setHasUpdatedPlayerOptions(true); + } + }, [setHasUpdatedPlayerOptions, useDummyMediaElement, playerModule]); const onVideoClick = useCallback(() => { if (playerModule === null) { @@ -202,6 +225,9 @@ function Player(): JSX.Element { created.actions.updateWorkerMode(relyOnWorker); playerMod = created; } + if (useDummyMediaElement && contentInfo.keySystems !== undefined) { + contentInfo.keySystems = toDummyDrmConfiguration(contentInfo.keySystems); + } loadContent(playerMod, contentInfo, loadVideoOpts); }, [ @@ -285,6 +311,8 @@ function Player(): JSX.Element { } tryRelyOnWorker={relyOnWorker} updateTryRelyOnWorker={setRelyOnWorker} + useDummyMediaElement={useDummyMediaElement} + updateUseDummyMediaElement={setUseDummyMediaElement} />
    diff --git a/demo/scripts/controllers/Settings.tsx b/demo/scripts/controllers/Settings.tsx index eb56e5e0f9..88efc2512e 100644 --- a/demo/scripts/controllers/Settings.tsx +++ b/demo/scripts/controllers/Settings.tsx @@ -31,6 +31,8 @@ function Settings({ updateDefaultVideoRepresentationsSwitchingMode, tryRelyOnWorker, updateTryRelyOnWorker, + useDummyMediaElement, + updateUseDummyMediaElement, }: { playerOptions: IConstructorSettings; updatePlayerOptions: ( @@ -50,6 +52,8 @@ function Settings({ ) => void; tryRelyOnWorker: boolean; updateTryRelyOnWorker: (tryRelyOnWorker: boolean) => void; + useDummyMediaElement: boolean; + updateUseDummyMediaElement: (useDummyMediaElement: boolean) => void; showOptions: boolean; }): JSX.Element | null { const { @@ -123,6 +127,13 @@ function Settings({ [updateLoadVideoOptions], ); + const onUseDummyMediaElementChange = useCallback( + (useDummyMediaElement: boolean) => { + updateUseDummyMediaElement(useDummyMediaElement); + }, + [updateUseDummyMediaElement], + ); + const onVideoResolutionLimitChange = useCallback( (videoResolutionLimitArg: { value: string }) => { updatePlayerOptions((prevOptions) => { @@ -366,6 +377,8 @@ function Settings({ onAutoPlayChange={onAutoPlayChange} tryRelyOnWorker={tryRelyOnWorker} onTryRelyOnWorkerChange={onTryRelyOnWorkerChange} + useDummyMediaElement={useDummyMediaElement} + onUseDummyMediaElementChange={onUseDummyMediaElementChange} />