Skip to content

Commit

Permalink
Merge pull request #1157 from canalplus/misc/retry-session-on-expired
Browse files Browse the repository at this point in the history
[Proposal] DRM: Add `onKeyExpiration` `keySystems` option
  • Loading branch information
peaBerberian committed Oct 28, 2022
2 parents 33b3eb7 + b579777 commit 56c206f
Show file tree
Hide file tree
Showing 14 changed files with 489 additions and 136 deletions.
52 changes: 52 additions & 0 deletions doc/api/Decryption_Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,60 @@ session state).
This is very rarely needed.


### onKeyExpiration

_type_: `string | undefined`

`true` by default.

Behavior the RxPlayer should have when one of the key is known to be expired.

`onKeyExpiration` can be set to a string, each describing a different behavior,
the default one if not is defined being `"error"`:

- `"error"`: The RxPlayer will stop on an error when any key is expired.
This is the default behavior.

The error emiited in that case should be an
[EncryptedMediaError](./Player_Errors.md#encryptedmediaerror) with a
`KEY_STATUS_CHANGE_ERROR` `code` property with a set `keyStatuses`
property containing at least one string set to `"expired"`.

- `"continue"`: The RxPlayer will not do anything when a key expires.
This may lead in many cases to infinite rebuffering.

- `"fallback"`: The Representation(s) linked to the expired key(s) will
be fallbacked from, meaning the RxPlayer will switch to other
representation without expired keys.

If no Representation remain, a NO_PLAYABLE_REPRESENTATION error will
be thrown.

Note that when the "fallbacking" action is taken, the RxPlayer might
temporarily switch to the `"RELOADING"` state - which should thus be
properly handled.

- `"close-session"`: The RxPlayer will close and re-create a DRM session
(and thus re-download the corresponding license) if any of the key
associated to this session expired.

It will try to do so in an efficient manner, only reloading the license
when the corresponding content plays.

The RxPlayer might go through the `"RELOADING"` state after an expired
key and/or light decoding glitches can arise, depending on the
platform, for some seconds, under that mode.



### throwOnLicenseExpiration

<div class="warning">
This option is deprecated, it will disappear in the next major release
`v4.0.0` (see <a href="./Miscellaneous/Deprecated_APIs.md">Deprecated
APIs</a>).
</div>

_type_: `Boolean | undefined`

`true` by default.
Expand Down
41 changes: 41 additions & 0 deletions doc/api/Miscellaneous/Deprecated_APIs.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,47 @@ the true "native" subtitles to display them themselves in a better way.
However, this API seems to not be used anymore. Please open an issue if you need
it.

### keySystems[].throwOnLicenseExpiration

The `throwOnLicenseExpiration` property of the `keySystems` option has been
replaced by the more powerful `onKeyExpiration` property.

#### How to replace that option

If you set `throwOnLicenseExpiration` to `false` before, you can simply set
`onKeyExpiration` to `"continue"` instead, which reproduce the exact same
behavior:
```ts
// old way
rxPlayer.loadVideo({
// ...
keySystems: [
{
throwOnLicenseExpiration: false,
// ...
}
],
});

// new way
rxPlayer.loadVideo({
// ...
keySystems: [
{
onKeyExpiration: "continue",
// ...
}
],
});
```

You can have more information on the `onKeyExpiration` option [in the
correspnding API documentation](./Decryption_Options.md#onkeyexpiration).

If you previously set `throwOnLicenseExpiration` to `true` or `undefined`, you
can just remove this property as this still the default behavior.


## RxPlayer constructor options

The following RxPlayer constructor options are deprecated.
Expand Down
14 changes: 13 additions & 1 deletion doc/api/Player_Errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,21 @@ An EncryptedMediaError can have the following codes (`code` property):
of more than 10 seconds.

- `"KEY_STATUS_CHANGE_ERROR"`: An error was detected when the
`MediaKeySession` emitted a keyStatuseschange event (e.g. the key
`MediaKeySession` emitted a keyStatuseschange event (e.g. a key
became `"expired"`).

`EncryptedMediaError` having the `KEY_STATUS_CHANGE_ERROR` code will also have
a `keyStatuses` property, which is an array of objects - each describing a
problematic key status with the following properties:
- `keyId` (`BufferSource`): The key id concerned by the status change
indicated by `keyStatus`
- `keyStatus` ([`MediaKeyStatus`](https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus)):
The problematic key status encountered linked to the `keyId` of the same
object.

If multiple objects are found in the `keyStatuses` property, it means that
multiple keys changed to a problematic status roughly around the same time.

- `"KEY_UPDATE_ERROR"`: An error was detected after a message (like a
license was given to the CDM).

Expand Down
25 changes: 25 additions & 0 deletions doc/api/Typescript_Types.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,28 @@ rxPlayer.addEventListener("warning", (err : Error | IPlayerError) => {
});

```

### EncryptedMediaError's `keyStatuses` property

Some `EncryptedMediaError` error thrown by the RxPlayer, may have a
`keyStatuses` property set.
In that case, the type is described by the
`IEncryptedMediaErrorKeyStatusObject` type:
```ts
// the type wanted
import { IEncryptedMediaErrorKeyStatusObject } from "rx-player/types";

// hypothetical file exporting an RxPlayer instance
import rxPlayer from "./player";

rxPlayer.addEventListener("error", (err : Error | IPlayerError) => {
if (err.type === "ENCRYPTED_MEDIA_ERROR" && err.keyStatuses !== undefined) {
logKeyStatuses(err.keyStatuses);
}
});

function logKeyStatuses(keyStatuses: IEncryptedMediaErrorKeyStatusObject): void {
console.log(keyStatuses);
}

```
52 changes: 47 additions & 5 deletions src/core/decrypt/content_decryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
MediaKeySessionLoadingType,
IProcessedProtectionData,
} from "./types";
import { DecommissionedSessionError } from "./utils/check_key_statuses";
import cleanOldStoredPersistentInfo from "./utils/clean_old_stored_persistent_info";
import getDrmSystemId from "./utils/get_drm_system_id";
import InitDataValuesContainer from "./utils/init_data_values_container";
Expand Down Expand Up @@ -393,7 +394,7 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
.reduce((acc, kid) => `${acc}, ${bytesToHex(kid)}`, "");
log.debug("DRM: Blacklisting new key ids", hexKids);
}
updateDecipherability(initializationData.content.manifest, [], keyIds);
updateDecipherability(initializationData.content.manifest, [], keyIds, []);
}
return ;
}
Expand Down Expand Up @@ -423,7 +424,8 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
}
updateDecipherability(initializationData.content.manifest,
createdSess.keyStatuses.whitelisted,
createdSess.keyStatuses.blacklisted);
createdSess.keyStatuses.blacklisted,
[]);
return;
}
}
Expand Down Expand Up @@ -522,13 +524,42 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
if (initializationData.content !== undefined) {
updateDecipherability(initializationData.content.manifest,
linkedKeys.whitelisted,
linkedKeys.blacklisted);
linkedKeys.blacklisted,
[]);
}

this._unlockInitDataQueue();
},

error: (err) => {
if (err instanceof DecommissionedSessionError) {
log.warn("DRM: A session's closing condition has been triggered");
this._lockInitDataQueue();
const indexOf = this._currentSessions.indexOf(sessionInfo);
if (indexOf >= 0) {
this._currentSessions.splice(indexOf);
}
if (initializationData.content !== undefined) {
updateDecipherability(initializationData.content.manifest,
[],
[],
sessionInfo.record.getAssociatedKeyIds());
}
stores.persistentSessionsStore?.delete(mediaKeySession.sessionId);
stores.loadedSessionsStore.closeSession(mediaKeySession)
.catch(e => {
const closeError = e instanceof Error ? e :
"unknown error";
log.warn("DRM: failed to close expired session", closeError);
})
.then(() => this._unlockInitDataQueue())
.catch((retryError) => this._onFatalError(retryError));

if (!this._isStopped()) {
this.trigger("warning", err.reason);
}
return;
}
if (!(err instanceof BlacklistedSessionError)) {
this._onFatalError(err);
return ;
Expand Down Expand Up @@ -664,7 +695,8 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
"Marking Representations as not decipherable");
updateDecipherability(initializationData.content.manifest,
[],
initializationData.keyIds);
initializationData.keyIds,
[]);
return true;
}
}
Expand Down Expand Up @@ -801,14 +833,19 @@ function canCreatePersistentSession(
* - Those who have a key id listed in `blacklistedKeyIds` will have their
* decipherability updated to `false`
*
* - Those who have a key id listed in `delistedKeyIds` will have their
* decipherability updated to `undefined`.
*
* @param {Object} manifest
* @param {Array.<Uint8Array>} whitelistedKeyIds
* @param {Array.<Uint8Array>} blacklistedKeyIds
* @param {Array.<Uint8Array>} delistedKeyIds
*/
function updateDecipherability(
manifest : Manifest,
whitelistedKeyIds : Uint8Array[],
blacklistedKeyIds : Uint8Array[]
blacklistedKeyIds : Uint8Array[],
delistedKeyIds : Uint8Array[]
) : void {
manifest.updateRepresentationsDeciperability((representation) => {
if (representation.contentProtections === undefined) {
Expand All @@ -828,6 +865,11 @@ function updateDecipherability(
return true;
}
}
for (let j = 0; j < delistedKeyIds.length; j++) {
if (areKeyIdsEqual(delistedKeyIds[j], elt.keyId)) {
return undefined;
}
}
}
}
return representation.decipherable;
Expand Down
60 changes: 32 additions & 28 deletions src/core/decrypt/session_events_listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,17 @@ function getKeyStatusesEvents(
if (session.keyStatuses.size === 0) {
return EMPTY;
}
const { warnings, blacklistedKeyIds, whitelistedKeyIds } =
checkKeyStatuses(session, options, keySystem);

const warnings$ = warnings.length > 0 ? observableOf(...warnings) :
EMPTY;
const { warning,
blacklistedKeyIds,
whitelistedKeyIds } = checkKeyStatuses(session, options, keySystem);
const keysUpdate$ = observableOf({ type : "keys-update" as const,
value : { whitelistedKeyIds,
blacklistedKeyIds } });
return observableConcat(warnings$, keysUpdate$);
if (warning !== undefined) {
return observableConcat(observableOf({ type: "warning" as const, value: warning }),
keysUpdate$);
}
return keysUpdate$;
});
}

Expand Down Expand Up @@ -270,8 +272,9 @@ function updateSessionWithMessage(
}

/**
* @param {MediaKeySession}
* @param {Object} keySystem
* @param {MediaKeySession} session
* @param {Object} keySystemOptions
* @param {string} keySystem
* @param {Event} keyStatusesEvent
* @returns {Observable}
*/
Expand Down Expand Up @@ -334,25 +337,16 @@ function getLicenseBackoffOptions(
}

/**
* Some key ids have updated their status.
*
* We put them in two different list:
*
* - `blacklistedKeyIds`: Those key ids won't be used for decryption and the
* corresponding media it decrypts should not be pushed to the buffer
* Note that a blacklisted key id can become whitelisted in the future.
*
* - `whitelistedKeyIds`: Those key ids were found and their corresponding
* keys are now being considered for decryption.
* Note that a whitelisted key id can become blacklisted in the future.
* Some key ids related to the current MediaKeySession have updated their
* statuses.
*
* Note that each `IKeysUpdateEvent` is independent of any other.
*
* A new `IKeysUpdateEvent` does not completely replace a previously emitted
* one, as it can for example be linked to a whole other decryption session.
*
* However, if a key id is encountered in both an older and a newer
* `IKeysUpdateEvent`, only the older status should be considered.
* `IKeysUpdateEvent`, only the newer, updated, status should be considered.
*/
export interface IKeysUpdateEvent {
type: "keys-update";
Expand All @@ -362,18 +356,28 @@ export interface IKeysUpdateEvent {
/** Information on key ids linked to a MediaKeySession. */
export interface IKeyUpdateValue {
/**
* The list of key ids that are blacklisted.
* As such, their corresponding keys won't be used by that session, despite
* the fact that they were part of the pushed license.
* The list of key ids linked to the corresponding MediaKeySession that are
* now "blacklisted", i.e. the decryption keys they are linked to are blocked
* from ever being used anymore.
*
* Blacklisted key ids correspond to keys linked to a MediaKeySession that
* cannot and should not be used, due to various reasons, which mainly involve
* unmet output restrictions and CDM internal errors linked to that key.
*
* Content linked to key ids in `blacklistedKeyIds` should be refrained from
* being used.
*
* Reasons for blacklisting a keys depend on options, but mainly involve unmet
* output restrictions and CDM internal errors linked to that key id.
* Note that a key id may only be blacklisted temporarily.
*/
blacklistedKeyIds : Uint8Array[];
/*
* The list of key id linked to that session which are not blacklisted.
* Together with `blacklistedKeyIds` it regroups all key ids linked to the
* session.
* The list of key ids linked to the corresponding MediaKeySession that are
* now "whitelisted", i.e. the decryption keys they are linked to can be used
* to decrypt content.
*
* Content linked to key ids in `whitelistedKeyIds` should be safe to play.
*
* Note that a key id may only be whitelisted temporarily.
*/
whitelistedKeyIds : Uint8Array[];
}
Expand Down
Loading

0 comments on commit 56c206f

Please sign in to comment.