Skip to content

Commit

Permalink
[Proposal] DRM: Add keySystems[].wantedSessionTypes loadVideo option
Browse files Browse the repository at this point in the history
Problem
-------

We encountered a complex case with an application where they want to be
able to load some contents relying on temporary licenses (the decryption kind)
and others relying on persistent licenses, yet still keep our
"`MediaKeySession` cache" intact when doing that (which would in the end
contains both types).

The main issue with that usage was linked to our API:

  - When an application wants to load a content with a
    persistent license, it will set in its `loadVideo` call our
    `keySystems[].persistentLicenseConfig` option, which would set both:

      - the EME configuration's `persistentState` requirement to
        `"required"` - unless the `keySystems[].persistentState` option
        is explicitly set to something else, and

      - the `sessionTypes` configuration property to
        `["temporary", "persistent-license"]` (NOTE: so able to handle
        both kinds, weirdly, not sure why we ask for both, it seems to be
        a mistake here - maybe for another PR).

  - When an application wants to load a content with temporary licenses,
    it will not set the `keySystems[].persistentLicense` option, and
    consequently:

      - the `persistentState` would be set to `"optional"` -unless the
        `keySystems[].persistentState` option is explicitly set to
        something else, and

      - the `sessionTypes` configuration property is set to just
        `["temporary"]`

Those results lead to non-compatible `MediaKeySystemAccess` instances
which means that going from content with temporary licenses
to contents with hpersistent ones and vice-versa is going go lead to the
re-creation of a `MediaKeySystemAccess` behind the hood at content load.

In turn, this `MediaKeySystemAccess` change is going to internally reset
the `MediaKeySession` cache we keep to avoid doing license request if we
already requested it recently.

Note that this is the only known issue of switching
`MediaKeySystemAccess` here. For cases where that cache is not
important, there should be no much of a problem to create another
`MediaKeySystemAccess` (beside perhaps performance issue on really really
slow implementations).

Solution proposed here
----------------------

To fix that very specific issue, I propose here the
`keySystems[].wantedSessionTypes` `loadVideo` options, which would be
explicit about the `sessionTypes` we want the created
`MediaKeySystemAccess` to handle.

With that and `keySystems[].persistentState`, an application should be
able to ask for every capabilities it needs the `MediaKeySystemAccess`
to be able to do, without necessarily having to link it to the current
content.

As this is a very technical need, I had a lot of trouble in making the
API approachable in the documentation, I spent a lot more time writing
the few lines in the documentation than actually implementing this!

I'm also afraid that most applications won't understand the point of
that API, even at project at Canal+, as it is very linked to both the
EME recommendation and the RxPlayer internals (its `MediaKeySession`
cache).
  • Loading branch information
peaBerberian committed Dec 12, 2024
1 parent c564bdf commit bc8b6d6
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 9 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 Down Expand Up @@ -406,6 +508,137 @@ describe("decrypt - global tests - media key system access", () => {
);
});

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,
persistentState: "required",
sessionTypes: ["temporary", "persistent-license"],
};
});
expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith(
1,
"foo",
expectedConfig,
);
expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith(
2,
"foo",
removeCapabiltiesFromConfig(expectedConfig),
);
});

it("should do nothing if persistentLicenseConfig is set to null", async () => {
const mockRequestMediaKeySystemAccess = vi
.fn()
Expand Down
26 changes: 20 additions & 6 deletions src/main_thread/decrypt/content_decryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,14 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
this._lockInitDataQueue();

let wantedSessionType: MediaKeySessionType;
if (isNullOrUndefined(options.persistentLicenseConfig)) {
wantedSessionType = "temporary";
} else if (!canCreatePersistentSession(mediaKeySystemAccess)) {
log.warn('DRM: Cannot create "persistent-license" session: not supported');
wantedSessionType = "temporary";
} else {
if (
canCreatePersistentSession(mediaKeySystemAccess) &&
(!isNullOrUndefined(options.persistentLicenseConfig) ||
!canCreateTemporarySession(mediaKeySystemAccess))
) {
wantedSessionType = "persistent-license";
} else {
wantedSessionType = "temporary";
}

const {
Expand Down Expand Up @@ -997,6 +998,19 @@ function canCreatePersistentSession(
return sessionTypes !== undefined && arrayIncludes(sessionTypes, "persistent-license");
}

/**
* Returns `true` if the given MediaKeySystemAccess can create
* "temporary" MediaKeySessions.
* @param {MediaKeySystemAccess} mediaKeySystemAccess
* @returns {Boolean}
*/
function canCreateTemporarySession(
mediaKeySystemAccess: MediaKeySystemAccess | ICustomMediaKeySystemAccess,
): boolean {
const { sessionTypes } = mediaKeySystemAccess.getConfiguration();
return sessionTypes !== undefined && arrayIncludes(sessionTypes, "temporary");
}

/**
* Return the list of key IDs present in the `expectedKeyIds` array
* but that are not present in `actualKeyIds`.
Expand Down
17 changes: 14 additions & 3 deletions src/main_thread/decrypt/find_key_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,24 @@ function buildKeySystemConfigurations(
keySystemTypeInfo: IKeySystemType,
): MediaKeySystemConfiguration[] {
const { keyName, keyType, keySystemOptions: keySystem } = keySystemTypeInfo;
const sessionTypes = ["temporary"];
let sessionTypes: string[];
let persistentState: MediaKeysRequirement = "optional";
let distinctiveIdentifier: MediaKeysRequirement = "optional";

if (!isNullOrUndefined(keySystem.persistentLicenseConfig)) {
if (Array.isArray(keySystem.wantedSessionTypes)) {
sessionTypes = keySystem.wantedSessionTypes;
if (
arrayIncludes(keySystem.wantedSessionTypes, "persistent-license") &&
!isNullOrUndefined(keySystem.persistentLicenseConfig)
) {
persistentState = "required";
}
} else if (!isNullOrUndefined(keySystem.persistentLicenseConfig)) {
persistentState = "required";
sessionTypes.push("persistent-license");
// TODO: shouldn't it be just `["persistent-license"]` here?
sessionTypes = ["temporary", "persistent-license"];
} else {
sessionTypes = ["temporary"];
}

if (!isNullOrUndefined(keySystem.persistentState)) {
Expand Down
Loading

0 comments on commit bc8b6d6

Please sign in to comment.