Skip to content

Commit

Permalink
Merge pull request #1604 from canalplus/feat/wanted-session-types
Browse files Browse the repository at this point in the history
[Proposal] DRM: Add `keySystems[].wantedSessionTypes` `loadVideo` option
  • Loading branch information
peaBerberian authored Jan 10, 2025
2 parents a117437 + c76324a commit c4f1eb8
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 15 deletions.
46 changes: 46 additions & 0 deletions doc/api/Decryption_Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<string> | 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.
5 changes: 5 additions & 0 deletions doc/reference/API_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -680,15 +913,15 @@ describe("decrypt - global tests - media key system access", () => {
return {
...conf,
persistentState: "required",
sessionTypes: ["temporary", "persistent-license"],
sessionTypes: ["persistent-license"],
};
});
const expectedPersistentConfig: MediaKeySystemConfiguration[] = defaultKSConfig.map(
(conf) => {
return {
...conf,
persistentState: "required",
sessionTypes: ["temporary", "persistent-license"],
sessionTypes: ["persistent-license"],
};
},
);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -833,7 +1066,7 @@ describe("decrypt - global tests - media key system access", () => {
return {
...conf,
persistentState: "required",
sessionTypes: ["temporary", "persistent-license"],
sessionTypes: ["persistent-license"],
};
},
);
Expand All @@ -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(
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit c4f1eb8

Please sign in to comment.