diff --git a/doc/api/Decryption_Options.md b/doc/api/Decryption_Options.md index c473d95ba9..9693a2d47a 100644 --- a/doc/api/Decryption_Options.md +++ b/doc/api/Decryption_Options.md @@ -597,3 +597,49 @@ The `type` property can be set to one of the three following values: you're setting the `audioCapabilitiesConfig` property) of the resulting [MediaKeySystemConfiguration](https://www.w3.org/TR/encrypted-media/#dom-mediakeysystemconfiguration) wanted by the RxPlayer. + +### wantedSessionTypes + +_type_: `Array. | undefined` + +Force a +[`sessionTypes`](https://www.w3.org/TR/encrypted-media-2/#dom-mediakeysystemconfiguration-sessiontypes) +value for the corresponding `MediaKeySystemConfiguration` asked when creating a +[`MediaKeySystemAccess`](https://www.w3.org/TR/encrypted-media-2/#dom-mediakeysystemaccess) +(the EME API concept). + +If not set, the RxPlayer will automatically ask for the most adapted `sessionTypes` based +on your configuration for the current content. As such, this option is only needed for +very specific usages. + +A case where you might want to set this option is if for example you want the ability to +load both temporary and persistent licenses, regardless of the configuration applied to +the current content. Setting in that case `wantedSessionTypes` to +`["temporary", "persistent-license"]` will lead, if compatible, to the creation of a +`MediaKeySystemAccess` able to handle both: + +- contents relying on temporary licenses, and: +- contents relying on persistent licenses + +The RxPlayer will then be able to keep that same `MediaKeySystemAccess` on future +`loadVideo` calls as long as they rely on either all or a subset of those session types - +and as long as the rest of the new wanted configuration is also considered compatible with +that `MediaKeySystemAccess`. + +Moreover, because our `MediaKeySession` cache (see +[`maxSessionCacheSize`](#maxsessioncachesize)) is linked to a `MediaKeySystemAccess`, +keeping the same one allows the RxPlayer to also keep the same cache (whereas changing +`MediaKeySystemAccess` when changing contents resets that cache). + +Note that the current device has to be compatible to _ALL_ `sessionTypes` for that +configuration to go through. + +#### Notes + +If this value is set to an array which does not contain `"persistent-license"`, we will +assume that no persistent license will be requested for the current content, regardless of +the [`persistentLicenseConfig`](#persistentlicenseconfig) option. + +If this value only contains `"persistent-license"` but the +[`persistentLicenseConfig`](#persistentlicenseconfig) option is not set, we will load +persistent licenses yet not persist them. diff --git a/doc/reference/API_Reference.md b/doc/reference/API_Reference.md index 67da85c357..e2b304f7b7 100644 --- a/doc/reference/API_Reference.md +++ b/doc/reference/API_Reference.md @@ -108,6 +108,11 @@ events and so on. [`videoCapabilities`](https://www.w3.org/TR/encrypted-media/#dom-mediakeysystemconfiguration-videocapabilities) property. + - [`keySystems[].wantedSessionTypes`](../api/Decryption_Options.md#wantedsessiontypes): + Allows the configuration of the + [`sessionTypes`](https://www.w3.org/TR/encrypted-media/#dom-mediakeysystemconfiguration-sessionTypes) + property. + - [`autoPlay`](../api/Loading_a_Content.md#autoplay): Allows to automatically play after a content is loaded. 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 6821016fc4..52a9e2a37e 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 @@ -367,6 +367,108 @@ describe("decrypt - global tests - media key system access", () => { ); }); + it("should want only persistent sessions if wantedSessionTypes is set to `['persistent-license']`", async () => { + const mockRequestMediaKeySystemAccess = vi + .fn() + .mockImplementation(() => Promise.reject("nope")); + mockCompat({ + requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess, + }); + await checkIncompatibleKeySystemsErrorMessage([ + { + type: "foo", + getLicense: neverCalledFn, + wantedSessionTypes: ["persistent-license"], + }, + ]); + expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); + + const expectedConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map((conf) => { + return { + ...conf, + sessionTypes: ["persistent-license"], + }; + }); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 1, + "foo", + expectedConfig, + ); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 2, + "foo", + removeCapabiltiesFromConfig(expectedConfig), + ); + }); + + it("should want only temporary sessions if wantedSessionTypes is set to `['temporary']`", async () => { + const mockRequestMediaKeySystemAccess = vi + .fn() + .mockImplementation(() => Promise.reject("nope")); + mockCompat({ + requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess, + }); + await checkIncompatibleKeySystemsErrorMessage([ + { + type: "foo", + getLicense: neverCalledFn, + wantedSessionTypes: ["temporary"], + }, + ]); + expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); + + const expectedConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map((conf) => { + return { + ...conf, + sessionTypes: ["temporary"], + }; + }); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 1, + "foo", + expectedConfig, + ); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 2, + "foo", + removeCapabiltiesFromConfig(expectedConfig), + ); + }); + + it("should want both temporary and persistent sessions if wantedSessionTypes is set to `['persistent-license', 'temporary']`", async () => { + const mockRequestMediaKeySystemAccess = vi + .fn() + .mockImplementation(() => Promise.reject("nope")); + mockCompat({ + requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess, + }); + await checkIncompatibleKeySystemsErrorMessage([ + { + type: "foo", + getLicense: neverCalledFn, + wantedSessionTypes: ["persistent-license", "temporary"], + }, + ]); + expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); + + const expectedConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map((conf) => { + return { + ...conf, + sessionTypes: ["persistent-license", "temporary"], + }; + }); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 1, + "foo", + expectedConfig, + ); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 2, + "foo", + removeCapabiltiesFromConfig(expectedConfig), + ); + }); + it("should want persistent sessions if persistentLicenseConfig is set", async () => { const mockRequestMediaKeySystemAccess = vi .fn() @@ -387,6 +489,137 @@ describe("decrypt - global tests - media key system access", () => { ]); expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); + const expectedConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map((conf) => { + return { + ...conf, + persistentState: "required", + sessionTypes: ["persistent-license"], + }; + }); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 1, + "foo", + expectedConfig, + ); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 2, + "foo", + removeCapabiltiesFromConfig(expectedConfig), + ); + }); + + it("should not want persistent sessions if persistentLicenseConfig is set but wantedSessionTypes only wants temporary licenses", async () => { + const mockRequestMediaKeySystemAccess = vi + .fn() + .mockImplementation(() => Promise.reject("nope")); + mockCompat({ + requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess, + }); + const persistentLicenseConfig = { + save() { + throw new Error("Should not save."); + }, + load() { + throw new Error("Should not load."); + }, + }; + await checkIncompatibleKeySystemsErrorMessage([ + { + type: "foo", + getLicense: neverCalledFn, + wantedSessionTypes: ["temporary"], + persistentLicenseConfig, + }, + ]); + expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); + + const expectedConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map((conf) => { + return { + ...conf, + sessionTypes: ["temporary"], + }; + }); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 1, + "foo", + expectedConfig, + ); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 2, + "foo", + removeCapabiltiesFromConfig(expectedConfig), + ); + }); + + it("should properly handle persistentLicenseConfig and wantedSessionTypes set to persistent-license", async () => { + const mockRequestMediaKeySystemAccess = vi + .fn() + .mockImplementation(() => Promise.reject("nope")); + mockCompat({ + requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess, + }); + const persistentLicenseConfig = { + save() { + throw new Error("Should not save."); + }, + load() { + throw new Error("Should not load."); + }, + }; + await checkIncompatibleKeySystemsErrorMessage([ + { + type: "foo", + getLicense: neverCalledFn, + wantedSessionTypes: ["persistent-license"], + persistentLicenseConfig, + }, + ]); + expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); + + const expectedConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map((conf) => { + return { + ...conf, + persistentState: "required", + sessionTypes: ["persistent-license"], + }; + }); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 1, + "foo", + expectedConfig, + ); + expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( + 2, + "foo", + removeCapabiltiesFromConfig(expectedConfig), + ); + }); + + it("should properly handle persistentLicenseConfig and wantedSessionTypes set to both temporary and persistent-license", async () => { + const mockRequestMediaKeySystemAccess = vi + .fn() + .mockImplementation(() => Promise.reject("nope")); + mockCompat({ + requestMediaKeySystemAccess: mockRequestMediaKeySystemAccess, + }); + const persistentLicenseConfig = { + save() { + throw new Error("Should not save."); + }, + load() { + throw new Error("Should not load."); + }, + }; + await checkIncompatibleKeySystemsErrorMessage([ + { + type: "foo", + getLicense: neverCalledFn, + wantedSessionTypes: ["temporary", "persistent-license"], + persistentLicenseConfig, + }, + ]); + expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); + const expectedConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map((conf) => { return { ...conf, @@ -680,7 +913,7 @@ describe("decrypt - global tests - media key system access", () => { return { ...conf, persistentState: "required", - sessionTypes: ["temporary", "persistent-license"], + sessionTypes: ["persistent-license"], }; }); const expectedPersistentConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map( @@ -688,7 +921,7 @@ describe("decrypt - global tests - media key system access", () => { return { ...conf, persistentState: "required", - sessionTypes: ["temporary", "persistent-license"], + sessionTypes: ["persistent-license"], }; }, ); @@ -786,7 +1019,7 @@ describe("decrypt - global tests - media key system access", () => { return { ...conf, persistentState: "required", - sessionTypes: ["temporary", "persistent-license"], + sessionTypes: ["persistent-license"], }; }); expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); @@ -833,7 +1066,7 @@ describe("decrypt - global tests - media key system access", () => { return { ...conf, persistentState: "required", - sessionTypes: ["temporary", "persistent-license"], + sessionTypes: ["persistent-license"], }; }, ); @@ -842,7 +1075,7 @@ describe("decrypt - global tests - media key system access", () => { return { ...conf, persistentState: "required", - sessionTypes: ["temporary", "persistent-license"], + sessionTypes: ["persistent-license"], }; }); const expectedIdentifierConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map( @@ -939,7 +1172,7 @@ describe("decrypt - global tests - media key system access", () => { return { ...conf, persistentState: "required", - sessionTypes: ["temporary", "persistent-license"], + sessionTypes: ["persistent-license"], }; }); expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); diff --git a/src/main_thread/decrypt/content_decryptor.ts b/src/main_thread/decrypt/content_decryptor.ts index f9b07fe5fe..591df4061c 100644 --- a/src/main_thread/decrypt/content_decryptor.ts +++ b/src/main_thread/decrypt/content_decryptor.ts @@ -530,13 +530,14 @@ export default class ContentDecryptor extends EventEmitter