diff --git a/doc/api/Decryption_Options.md b/doc/api/Decryption_Options.md
index 0b382ca7e5..41a644ed47 100644
--- a/doc/api/Decryption_Options.md
+++ b/doc/api/Decryption_Options.md
@@ -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
+
+This option is deprecated, it will disappear in the next major release
+`v4.0.0` (see Deprecated
+APIs).
+
+
_type_: `Boolean | undefined`
`true` by default.
diff --git a/doc/api/Miscellaneous/Deprecated_APIs.md b/doc/api/Miscellaneous/Deprecated_APIs.md
index 69e624941b..a95d4d15bb 100644
--- a/doc/api/Miscellaneous/Deprecated_APIs.md
+++ b/doc/api/Miscellaneous/Deprecated_APIs.md
@@ -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.
diff --git a/doc/api/Player_Errors.md b/doc/api/Player_Errors.md
index db67d1e770..602169a6c4 100644
--- a/doc/api/Player_Errors.md
+++ b/doc/api/Player_Errors.md
@@ -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).
diff --git a/doc/api/Typescript_Types.md b/doc/api/Typescript_Types.md
index 7c0e251098..3c4f88541d 100644
--- a/doc/api/Typescript_Types.md
+++ b/doc/api/Typescript_Types.md
@@ -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);
+}
+
+```
diff --git a/src/core/decrypt/content_decryptor.ts b/src/core/decrypt/content_decryptor.ts
index c0fefb4e05..3879fe0657 100644
--- a/src/core/decrypt/content_decryptor.ts
+++ b/src/core/decrypt/content_decryptor.ts
@@ -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";
@@ -393,7 +394,7 @@ export default class ContentDecryptor extends EventEmitter `${acc}, ${bytesToHex(kid)}`, "");
log.debug("DRM: Blacklisting new key ids", hexKids);
}
- updateDecipherability(initializationData.content.manifest, [], keyIds);
+ updateDecipherability(initializationData.content.manifest, [], keyIds, []);
}
return ;
}
@@ -423,7 +424,8 @@ export default class ContentDecryptor extends EventEmitter {
+ 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 ;
@@ -664,7 +695,8 @@ export default class ContentDecryptor extends EventEmitter} whitelistedKeyIds
* @param {Array.} blacklistedKeyIds
+ * @param {Array.} delistedKeyIds
*/
function updateDecipherability(
manifest : Manifest,
whitelistedKeyIds : Uint8Array[],
- blacklistedKeyIds : Uint8Array[]
+ blacklistedKeyIds : Uint8Array[],
+ delistedKeyIds : Uint8Array[]
) : void {
manifest.updateRepresentationsDeciperability((representation) => {
if (representation.contentProtections === undefined) {
@@ -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;
diff --git a/src/core/decrypt/session_events_listener.ts b/src/core/decrypt/session_events_listener.ts
index 3f45aaac83..9a8226a135 100644
--- a/src/core/decrypt/session_events_listener.ts
+++ b/src/core/decrypt/session_events_listener.ts
@@ -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$;
});
}
@@ -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}
*/
@@ -334,17 +337,8 @@ 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.
*
@@ -352,7 +346,7 @@ function getLicenseBackoffOptions(
* 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";
@@ -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[];
}
diff --git a/src/core/decrypt/utils/check_key_statuses.ts b/src/core/decrypt/utils/check_key_statuses.ts
index 571886705f..0b4580d012 100644
--- a/src/core/decrypt/utils/check_key_statuses.ts
+++ b/src/core/decrypt/utils/check_key_statuses.ts
@@ -18,36 +18,45 @@ import { ICustomMediaKeySession } from "../../../compat";
/* eslint-disable-next-line max-len */
import getUUIDKidFromKeyStatusKID from "../../../compat/eme/get_uuid_kid_from_keystatus_kid";
import { EncryptedMediaError } from "../../../errors";
+import {
+ IEncryptedMediaErrorKeyStatusObject,
+ IKeySystemOption,
+ IPlayerError,
+} from "../../../public_types";
+import assertUnreachable from "../../../utils/assert_unreachable";
import { bytesToHex } from "../../../utils/string_parsing";
-import { IEMEWarningEvent } from "../types";
-const KEY_STATUSES = { EXPIRED: "expired",
- INTERNAL_ERROR: "internal-error",
- OUTPUT_RESTRICTED: "output-restricted" };
+/**
+ * Error thrown when the MediaKeySession has to be closed due to a trigger
+ * specified by user configuration.
+ * Such MediaKeySession should be closed immediately and may be re-created if
+ * needed again.
+ * @class DecommissionedSessionError
+ * @extends Error
+ */
+export class DecommissionedSessionError extends Error {
+ public reason : IPlayerError;
-export interface IKeyStatusesCheckingOptions {
/**
- * If explicitely set to `false`, we won't throw on error when a used license
- * is expired.
+ * Creates a new `DecommissionedSessionError`.
+ * @param {Error} reason - Error that led to the decision to close the
+ * current MediaKeySession. Should be used for reporting purposes.
*/
- throwOnLicenseExpiration? : boolean;
- /** Avoid throwing when invalid key statuses are encountered. */
- fallbackOn? : {
- /**
- * If set to `true`, we won't throw when an "internal-error" key status is
- * encountered but just add a warning and the corresponding key id to the list
- * of blacklisted key ids.
- */
- keyInternalError? : boolean;
- /**
- * If set to `true`, we won't throw when an "output-restricted" key status is
- * encountered but just add a warning and the corresponding key id to the list
- * of blacklisted key ids.
- */
- keyOutputRestricted? : boolean;
- };
+ constructor(reason : IPlayerError) {
+ super();
+ // @see https://stackoverflow.com/questions/41102060/typescript-extending-error-class
+ Object.setPrototypeOf(this, DecommissionedSessionError.prototype);
+ this.reason = reason;
+ }
}
+const KEY_STATUSES = { EXPIRED: "expired",
+ INTERNAL_ERROR: "internal-error",
+ OUTPUT_RESTRICTED: "output-restricted" };
+
+export type IKeyStatusesCheckingOptions =
+ Pick;
+
/**
* MediaKeyStatusMap's iterator seems to be quite peculiar and wrongly defined
* by TypeScript.
@@ -74,14 +83,16 @@ export default function checkKeyStatuses(
session : MediaKeySession | ICustomMediaKeySession,
options: IKeyStatusesCheckingOptions,
keySystem: string
-) : { warnings : IEMEWarningEvent[];
+) : { warning : EncryptedMediaError | undefined;
blacklistedKeyIds : Uint8Array[];
whitelistedKeyIds : Uint8Array[]; }
{
- const warnings : IEMEWarningEvent[] = [];
+ const { fallbackOn = {},
+ throwOnLicenseExpiration,
+ onKeyExpiration } = options;
const blacklistedKeyIds : Uint8Array[] = [];
const whitelistedKeyIds : Uint8Array[] = [];
- const { fallbackOn = {}, throwOnLicenseExpiration } = options;
+ const badKeyStatuses: IEncryptedMediaErrorKeyStatusObject[] = [];
(session.keyStatuses.forEach as IKeyStatusesForEach)((
_arg1 : unknown,
@@ -96,40 +107,63 @@ export default function checkKeyStatuses(
const keyId = getUUIDKidFromKeyStatusKID(keySystem,
new Uint8Array(keyStatusKeyId));
+
+ const keyStatusObj = { keyId: keyId.buffer, keyStatus };
switch (keyStatus) {
case KEY_STATUSES.EXPIRED: {
const error = new EncryptedMediaError(
"KEY_STATUS_CHANGE_ERROR",
- `A decryption key expired (${bytesToHex(keyId)})`);
+ `A decryption key expired (${bytesToHex(keyId)})`,
+ { keyStatuses: [keyStatusObj, ...badKeyStatuses] });
- if (throwOnLicenseExpiration !== false) {
+ if (onKeyExpiration === "error" ||
+ (onKeyExpiration === undefined && throwOnLicenseExpiration === false))
+ {
throw error;
}
- warnings.push({ type: "warning", value: error });
- whitelistedKeyIds.push(keyId);
+
+ switch (onKeyExpiration) {
+ case "close-session":
+ throw new DecommissionedSessionError(error);
+ case "fallback":
+ blacklistedKeyIds.push(keyId);
+ break;
+ default:
+ // I weirdly stopped relying on switch-cases here due to some TypeScript
+ // issue, not checking properly `case undefined` (bug?)
+ if (onKeyExpiration === "continue" || onKeyExpiration === undefined) {
+ whitelistedKeyIds.push(keyId);
+ } else {
+ // Compile-time check throwing when not all possible cases are handled
+ assertUnreachable(onKeyExpiration);
+ }
+ break;
+ }
+
+ badKeyStatuses.push(keyStatusObj);
break;
}
case KEY_STATUSES.INTERNAL_ERROR: {
- const error = new EncryptedMediaError(
- "KEY_STATUS_CHANGE_ERROR",
- `A "${keyStatus}" status has been encountered (${bytesToHex(keyId)})`);
if (fallbackOn.keyInternalError !== true) {
- throw error;
+ throw new EncryptedMediaError(
+ "KEY_STATUS_CHANGE_ERROR",
+ `A "${keyStatus}" status has been encountered (${bytesToHex(keyId)})`,
+ { keyStatuses: [keyStatusObj, ...badKeyStatuses] });
}
- warnings.push({ type: "warning", value: error });
+ badKeyStatuses.push(keyStatusObj);
blacklistedKeyIds.push(keyId);
break;
}
case KEY_STATUSES.OUTPUT_RESTRICTED: {
- const error = new EncryptedMediaError(
- "KEY_STATUS_CHANGE_ERROR",
- `A "${keyStatus}" status has been encountered (${bytesToHex(keyId)})`);
if (fallbackOn.keyOutputRestricted !== true) {
- throw error;
+ throw new EncryptedMediaError(
+ "KEY_STATUS_CHANGE_ERROR",
+ `A "${keyStatus}" status has been encountered (${bytesToHex(keyId)})`,
+ { keyStatuses: [keyStatusObj, ...badKeyStatuses] });
}
- warnings.push({ type: "warning", value: error });
+ badKeyStatuses.push(keyStatusObj);
blacklistedKeyIds.push(keyId);
break;
}
@@ -139,5 +173,15 @@ export default function checkKeyStatuses(
break;
}
});
- return { warnings, blacklistedKeyIds, whitelistedKeyIds };
+
+ let warning;
+ if (badKeyStatuses.length > 0) {
+ warning = new EncryptedMediaError(
+ "KEY_STATUS_CHANGE_ERROR",
+ "One or several problematic key statuses have been encountered",
+ { keyStatuses: badKeyStatuses });
+ }
+ return { warning,
+ blacklistedKeyIds,
+ whitelistedKeyIds };
}
diff --git a/src/core/decrypt/utils/persistent_sessions_store.ts b/src/core/decrypt/utils/persistent_sessions_store.ts
index 97a5d8b0db..53ec5f994e 100644
--- a/src/core/decrypt/utils/persistent_sessions_store.ts
+++ b/src/core/decrypt/utils/persistent_sessions_store.ts
@@ -177,8 +177,7 @@ export default class PersistentSessionsStore {
/**
* Delete stored MediaKeySession information based on its session id.
- * @param {Uint8Array} initData
- * @param {string|undefined} initDataType
+ * @param {string} sessionId
*/
public delete(sessionId : string) : void {
let index = -1;
diff --git a/src/core/stream/orchestrator/get_blacklisted_ranges.ts b/src/core/stream/orchestrator/get_time_ranges_for_content.ts
similarity index 98%
rename from src/core/stream/orchestrator/get_blacklisted_ranges.ts
rename to src/core/stream/orchestrator/get_time_ranges_for_content.ts
index 3b8190bd0a..118d616192 100644
--- a/src/core/stream/orchestrator/get_blacklisted_ranges.ts
+++ b/src/core/stream/orchestrator/get_time_ranges_for_content.ts
@@ -30,7 +30,7 @@ import { SegmentBuffer } from "../../segment_buffers";
* @param {Array.