rx-player
Version:
Canal+ HTML5 Video Player
254 lines (232 loc) • 8.69 kB
text/typescript
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
IMediaKeySession,
IMediaKeySystemAccess,
} from "../../../compat/browser_compatibility_types";
import getUUIDKidFromKeyStatusKID from "../../../compat/eme/get_uuid_kid_from_keystatus_kid";
import { EncryptedMediaError } from "../../../errors";
import log from "../../../log";
import type {
IEncryptedMediaErrorKeyStatusObject,
IKeySystemOption,
IPlayerError,
} from "../../../public_types";
import { assertUnreachable } from "../../../utils/assert";
import { bytesToHex } from "../../../utils/string_parsing";
/**
* 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;
/**
* Creates a new `DecommissionedSessionError`.
* @param {Error} reason - Error that led to the decision to close the
* current MediaKeySession. Should be used for reporting purposes.
*/
constructor(reason: IPlayerError) {
super(reason.message);
// @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<
IKeySystemOption,
"onKeyOutputRestricted" | "onKeyInternalError" | "onKeyExpiration"
>;
/**
* MediaKeyStatusMap's iterator seems to be quite peculiar and wrongly defined
* by TypeScript.
*/
type IKeyStatusesForEach = (
callback:
| ((arg1: MediaKeyStatus, arg2: ArrayBuffer) => void)
| ((arg1: ArrayBuffer, arg2: MediaKeyStatus) => void),
) => void;
/**
* Look at the current key statuses in the sessions and construct the
* appropriate warnings, whitelisted and blacklisted key ids.
*
* Throws if one of the keyID is on an error.
* @param {MediaKeySession} session - The MediaKeySession from which the keys
* will be checked.
* @param {Object} options
* @param {MediaKeySystemAccess} mediaKeySystemAccess - The
* `MediaKeySystemAccess` that produced the linked `MediaKeys` instance.
* @returns {Object} - Warnings to send, whitelisted and blacklisted key ids.
*/
export default function checkKeyStatuses(
session: IMediaKeySession,
options: IKeyStatusesCheckingOptions,
mediaKeySystemAccess: IMediaKeySystemAccess,
): {
warning: EncryptedMediaError | undefined;
blacklistedKeyIds: Uint8Array[];
whitelistedKeyIds: Uint8Array[];
} {
const { onKeyInternalError, onKeyOutputRestricted, onKeyExpiration } = options;
const blacklistedKeyIds: Uint8Array[] = [];
const whitelistedKeyIds: Uint8Array[] = [];
const badKeyStatuses: IEncryptedMediaErrorKeyStatusObject[] = [];
(session.keyStatuses.forEach as IKeyStatusesForEach)(
(_arg1: unknown, _arg2: unknown) => {
// Hack present because the order of the arguments has changed in spec
// and is not the same between some versions of Edge and Chrome.
const [keyStatus, keyStatusKeyId] = (() => {
return (typeof _arg1 === "string" ? [_arg1, _arg2] : [_arg2, _arg1]) as [
MediaKeyStatus,
ArrayBuffer,
];
})();
const keyId = getUUIDKidFromKeyStatusKID(
mediaKeySystemAccess.keySystem,
new Uint8Array(keyStatusKeyId),
);
const keyStatusObj = { keyId: keyId.buffer, keyStatus };
if (log.hasLevel("DEBUG")) {
log.debug(`DRM`, `key status update`, { keyId: bytesToHex(keyId), keyStatus });
}
switch (keyStatus) {
case KEY_STATUSES.EXPIRED: {
const error = new EncryptedMediaError(
"KEY_STATUS_CHANGE_ERROR",
`A decryption key expired (${bytesToHex(keyId)})`,
{
keyStatuses: [keyStatusObj, ...badKeyStatuses],
keySystem: mediaKeySystemAccess.keySystem,
keySystemConfiguration: mediaKeySystemAccess.getConfiguration(),
},
);
if (onKeyExpiration === "error" || onKeyExpiration === undefined) {
throw error;
}
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)})`,
{
keyStatuses: [keyStatusObj, ...badKeyStatuses],
keySystem: mediaKeySystemAccess.keySystem,
keySystemConfiguration: mediaKeySystemAccess.getConfiguration(),
},
);
switch (onKeyInternalError) {
case undefined:
case "error":
throw error;
case "close-session":
throw new DecommissionedSessionError(error);
case "fallback":
blacklistedKeyIds.push(keyId);
break;
case "continue":
whitelistedKeyIds.push(keyId);
break;
default:
// Weirdly enough, TypeScript is not checking properly
// `case undefined` (bug?)
if (onKeyInternalError !== undefined) {
assertUnreachable(onKeyInternalError);
} else {
throw error;
}
}
badKeyStatuses.push(keyStatusObj);
break;
}
case KEY_STATUSES.OUTPUT_RESTRICTED: {
const error = new EncryptedMediaError(
"KEY_STATUS_CHANGE_ERROR",
`A "${keyStatus}" status has been encountered (${bytesToHex(keyId)})`,
{
keyStatuses: [keyStatusObj, ...badKeyStatuses],
keySystem: mediaKeySystemAccess.keySystem,
keySystemConfiguration: mediaKeySystemAccess.getConfiguration(),
},
);
switch (onKeyOutputRestricted) {
case undefined:
case "error":
throw error;
case "fallback":
blacklistedKeyIds.push(keyId);
break;
case "continue":
whitelistedKeyIds.push(keyId);
break;
default:
// Weirdly enough, TypeScript is not checking properly
// `case undefined` (bug?)
if (onKeyOutputRestricted !== undefined) {
assertUnreachable(onKeyOutputRestricted);
} else {
throw error;
}
}
badKeyStatuses.push(keyStatusObj);
break;
}
default:
whitelistedKeyIds.push(keyId);
break;
}
},
);
let warning;
if (badKeyStatuses.length > 0) {
warning = new EncryptedMediaError(
"KEY_STATUS_CHANGE_ERROR",
"One or several problematic key statuses have been encountered",
{
keyStatuses: badKeyStatuses,
keySystem: mediaKeySystemAccess.keySystem,
keySystemConfiguration: mediaKeySystemAccess.getConfiguration(),
},
);
}
return { warning, blacklistedKeyIds, whitelistedKeyIds };
}