rx-player
Version:
Canal+ HTML5 Video Player
431 lines (430 loc) • 21.3 kB
JavaScript
/**
* 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 { canRelyOnRequestMediaKeySystemAccess } from "../../compat/can_rely_on_request_media_key_system_access";
import eme from "../../compat/eme";
import { generatePlayReadyInitData, DUMMY_PLAY_READY_HEADER, } from "../../compat/generate_init_data";
import shouldRenewMediaKeySystemAccess from "../../compat/should_renew_media_key_system_access";
import config from "../../config";
import { EncryptedMediaError } from "../../errors";
import log from "../../log";
import { parseCodec } from "../../utils/are_codecs_compatible";
import arrayIncludes from "../../utils/array_includes";
import flatMap from "../../utils/flat_map";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import MediaKeysAttacher from "./utils/media_keys_attacher";
/**
* Takes a `newConfiguration` `MediaKeySystemConfiguration`, that is intended
* for the creation of a `MediaKeySystemAccess`, and a `prevConfiguration`
* `MediaKeySystemConfiguration`, that was the one used at creation of the
* current `MediaKeySystemAccess`.
*
* This function will then return `true` if it determined that the new
* configuration is conceptually compatible with the one used before, and
* `false` otherwise.
* @param {Object} newConfiguration - New wanted `MediaKeySystemConfiguration`
* @param {Object} prevConfiguration - The `MediaKeySystemConfiguration` that is
* relied on util now.
* @returns {boolean} - `true` if `newConfiguration` is compatible with
* `prevConfiguration`.
*/
function isNewMediaKeySystemConfigurationCompatibleWithPreviousOne(newConfiguration, prevConfiguration) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
if (newConfiguration.label !== prevConfiguration.label) {
return false;
}
const prevDistinctiveIdentifier = (_a = prevConfiguration.distinctiveIdentifier) !== null && _a !== void 0 ? _a : "optional";
const newDistinctiveIdentifier = (_b = newConfiguration.distinctiveIdentifier) !== null && _b !== void 0 ? _b : "optional";
if (prevDistinctiveIdentifier !== newDistinctiveIdentifier) {
return false;
}
const prevPersistentState = (_c = prevConfiguration.persistentState) !== null && _c !== void 0 ? _c : "optional";
const newPersistentState = (_d = newConfiguration.persistentState) !== null && _d !== void 0 ? _d : "optional";
if (prevPersistentState !== newPersistentState) {
return false;
}
const prevInitDataTypes = (_e = prevConfiguration.initDataTypes) !== null && _e !== void 0 ? _e : [];
const newInitDataTypes = (_f = newConfiguration.initDataTypes) !== null && _f !== void 0 ? _f : [];
if (!isArraySubsetOf(newInitDataTypes, prevInitDataTypes)) {
return false;
}
const prevSessionTypes = (_g = prevConfiguration.sessionTypes) !== null && _g !== void 0 ? _g : [];
const newSessionTypes = (_h = newConfiguration.sessionTypes) !== null && _h !== void 0 ? _h : [];
if (!isArraySubsetOf(newSessionTypes, prevSessionTypes)) {
return false;
}
for (const prop of ["audioCapabilities", "videoCapabilities"]) {
const newCapabilities = (_j = newConfiguration[prop]) !== null && _j !== void 0 ? _j : [];
const prevCapabilities = (_k = prevConfiguration[prop]) !== null && _k !== void 0 ? _k : [];
const wasFound = newCapabilities.every((n) => {
var _a, _b, _c, _d, _e, _f;
for (let i = 0; i < prevCapabilities.length; i++) {
const prevCap = prevCapabilities[i];
if (((_a = prevCap.robustness) !== null && _a !== void 0 ? _a : "") === ((_b = n.robustness) !== null && _b !== void 0 ? _b : "") ||
((_c = prevCap.encryptionScheme) !== null && _c !== void 0 ? _c : null) === ((_d = n.encryptionScheme) !== null && _d !== void 0 ? _d : null) ||
((_e = prevCap.robustness) !== null && _e !== void 0 ? _e : "") === ((_f = n.robustness) !== null && _f !== void 0 ? _f : "")) {
return true;
}
}
return false;
});
if (!wasFound) {
return false;
}
}
return true;
}
/**
* Find key system canonical name from key system type.
* @param {string} ksType - Obtained via inversion
* @returns {string|undefined} - Either the canonical name, or undefined.
*/
function findKeySystemCanonicalName(ksType) {
const { EME_KEY_SYSTEMS } = config.getCurrent();
for (const ksName of Object.keys(EME_KEY_SYSTEMS)) {
if (arrayIncludes(EME_KEY_SYSTEMS[ksName], ksType)) {
return ksName;
}
}
return undefined;
}
/**
* Build configuration for the requestMediaKeySystemAccess EME API, based
* on the current keySystem object.
* @param {Object} keySystemTypeInfo
* @returns {Array.<Object>} - Configuration to give to the
* requestMediaKeySystemAccess API.
*/
function buildKeySystemConfigurations(keySystemTypeInfo) {
const { keyName, keyType, keySystemOptions: keySystem } = keySystemTypeInfo;
let sessionTypes;
let persistentState = "optional";
let distinctiveIdentifier = "optional";
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 = ["persistent-license"];
}
else {
sessionTypes = ["temporary"];
}
if (!isNullOrUndefined(keySystem.persistentState)) {
persistentState = keySystem.persistentState;
}
if (!isNullOrUndefined(keySystem.distinctiveIdentifier)) {
distinctiveIdentifier = keySystem.distinctiveIdentifier;
}
const { EME_DEFAULT_AUDIO_CODECS, EME_DEFAULT_VIDEO_CODECS, EME_DEFAULT_WIDEVINE_ROBUSTNESSES, EME_DEFAULT_PLAYREADY_RECOMMENDATION_ROBUSTNESSES, } = config.getCurrent();
// From the W3 EME spec, we have to provide videoCapabilities and
// audioCapabilities.
// These capabilities must specify a codec (even though you can use a
// completely different codec afterward).
// It is also strongly recommended to specify the required security
// robustness. As we do not want to forbide any security level, we specify
// every existing security level from highest to lowest so that the best
// security level is selected.
// More details here:
// https://storage.googleapis.com/wvdocs/Chrome_EME_Changes_and_Best_Practices.pdf
// https://www.w3.org/TR/encrypted-media/#get-supported-configuration-and-consent
let audioCapabilities;
let videoCapabilities;
const { audioCapabilitiesConfig, videoCapabilitiesConfig } = keySystem;
if ((audioCapabilitiesConfig === null || audioCapabilitiesConfig === void 0 ? void 0 : audioCapabilitiesConfig.type) === "full") {
audioCapabilities = audioCapabilitiesConfig.value;
}
else {
let audioRobustnesses;
if ((audioCapabilitiesConfig === null || audioCapabilitiesConfig === void 0 ? void 0 : audioCapabilitiesConfig.type) === "robustness") {
audioRobustnesses = audioCapabilitiesConfig.value;
}
else if (keyName === "widevine") {
audioRobustnesses = EME_DEFAULT_WIDEVINE_ROBUSTNESSES;
}
else if (keyType === "com.microsoft.playready.recommendation") {
audioRobustnesses = EME_DEFAULT_PLAYREADY_RECOMMENDATION_ROBUSTNESSES;
}
else {
audioRobustnesses = [];
}
if (audioRobustnesses.length === 0) {
audioRobustnesses.push(undefined);
}
const audioCodecs = (audioCapabilitiesConfig === null || audioCapabilitiesConfig === void 0 ? void 0 : audioCapabilitiesConfig.type) === "contentType"
? audioCapabilitiesConfig.value
: EME_DEFAULT_AUDIO_CODECS;
audioCapabilities = flatMap(audioRobustnesses, (robustness) => audioCodecs.map((contentType) => {
return robustness !== undefined ? { contentType, robustness } : { contentType };
}));
}
if ((videoCapabilitiesConfig === null || videoCapabilitiesConfig === void 0 ? void 0 : videoCapabilitiesConfig.type) === "full") {
videoCapabilities = videoCapabilitiesConfig.value;
}
else {
let videoRobustnesses;
if ((videoCapabilitiesConfig === null || videoCapabilitiesConfig === void 0 ? void 0 : videoCapabilitiesConfig.type) === "robustness") {
videoRobustnesses = videoCapabilitiesConfig.value;
}
else if (keyName === "widevine") {
videoRobustnesses = EME_DEFAULT_WIDEVINE_ROBUSTNESSES;
}
else if (keyType === "com.microsoft.playready.recommendation") {
videoRobustnesses = EME_DEFAULT_PLAYREADY_RECOMMENDATION_ROBUSTNESSES;
}
else {
videoRobustnesses = [];
}
if (videoRobustnesses.length === 0) {
videoRobustnesses.push(undefined);
}
const videoCodecs = (videoCapabilitiesConfig === null || videoCapabilitiesConfig === void 0 ? void 0 : videoCapabilitiesConfig.type) === "contentType"
? videoCapabilitiesConfig.value
: EME_DEFAULT_VIDEO_CODECS;
videoCapabilities = flatMap(videoRobustnesses, (robustness) => videoCodecs.map((contentType) => {
return robustness !== undefined ? { contentType, robustness } : { contentType };
}));
}
const wantedMediaKeySystemConfiguration = {
initDataTypes: ["cenc"],
videoCapabilities,
audioCapabilities,
distinctiveIdentifier,
persistentState,
sessionTypes,
};
if (audioCapabilitiesConfig !== undefined) {
if (videoCapabilitiesConfig !== undefined) {
return [wantedMediaKeySystemConfiguration];
}
return [
wantedMediaKeySystemConfiguration,
Object.assign(Object.assign({}, wantedMediaKeySystemConfiguration), {
// Re-try without `videoCapabilities` in case the EME implementation is
// buggy
videoCapabilities: undefined }),
];
}
else if (videoCapabilitiesConfig !== undefined) {
return [
wantedMediaKeySystemConfiguration,
Object.assign(Object.assign({}, wantedMediaKeySystemConfiguration), {
// Re-try without `audioCapabilities` in case the EME implementation is
// buggy
audioCapabilities: undefined }),
];
}
return [
wantedMediaKeySystemConfiguration,
// Some legacy implementations have issues with `audioCapabilities` and
// `videoCapabilities`, so we're including a supplementary
// `MediaKeySystemConfiguration` without those properties.
Object.assign(Object.assign({}, wantedMediaKeySystemConfiguration), { audioCapabilities: undefined, videoCapabilities: undefined }),
];
}
/**
* Extract from the current mediaKeys the supported Codecs.
* @param {Object} initialConfiguration - The MediaKeySystemConfiguration given
* to the `navigator.requestMediaKeySystemAccess` API.
* @param {Object | undefined} mksConfiguration - The result of
* getConfiguration() of the media keys.
* @return {Array} The list of supported codec by the CDM.
*/
export function extractCodecSupportListFromConfiguration(initialConfiguration, mksConfiguration) {
var _a, _b, _c, _d, _e, _f;
const testedAudioCodecs = (_b = (_a = initialConfiguration.audioCapabilities) === null || _a === void 0 ? void 0 : _a.map((v) => v.contentType)) !== null && _b !== void 0 ? _b : [];
const testedVideoCodecs = (_d = (_c = initialConfiguration.videoCapabilities) === null || _c === void 0 ? void 0 : _c.map((v) => v.contentType)) !== null && _d !== void 0 ? _d : [];
const testedCodecs = testedAudioCodecs
.concat(testedVideoCodecs)
.filter((c) => c !== undefined);
const supportedVideoCodecs = (_e = mksConfiguration.videoCapabilities) === null || _e === void 0 ? void 0 : _e.map((entry) => entry.contentType);
const supportedAudioCodecs = (_f = mksConfiguration.audioCapabilities) === null || _f === void 0 ? void 0 : _f.map((entry) => entry.contentType);
const supportedCodecs = [
...(supportedVideoCodecs !== null && supportedVideoCodecs !== void 0 ? supportedVideoCodecs : []),
...(supportedAudioCodecs !== null && supportedAudioCodecs !== void 0 ? supportedAudioCodecs : []),
].filter((contentType) => contentType !== undefined);
if (supportedCodecs.length === 0) {
// Some legacy implementations have issues with `audioCapabilities` and
// `videoCapabilities` in requestMediaKeySystemAccess so the codecs are not provided.
// In this case, we can't tell which codec is supported or not.
// Let's instead provide an empty list.
// Note: on a correct EME implementation, if a list of codec is provided
// with `audioCapabilities` or `videoCapabilities`, but none of them is supported,
// requestMediaKeySystemAccess should yield an error "NotSupported" and we should
// never reach this code.
return [];
}
const codecSupportList = testedCodecs.map((codec) => {
const { codecs, mimeType } = parseCodec(codec);
const isSupported = arrayIncludes(supportedCodecs, codec);
return {
codec: codecs,
mimeType,
result: isSupported,
};
});
return codecSupportList;
}
/**
* Try to find a compatible key system from the keySystems array given.
*
* This function will request a MediaKeySystemAccess based on the various
* keySystems provided.
*
* This Promise might either:
* - resolves the MediaKeySystemAccess and the keySystems as an object, when
* found.
* - reject if no compatible key system has been found.
*
* @param {HTMLMediaElement} mediaElement
* @param {Array.<Object>} keySystemsConfigs - The keySystems you want to test.
* @param {Object} cancelSignal
* @returns {Promise.<Object>}
*/
export default function getMediaKeySystemAccess(mediaElement, keySystemsConfigs, cancelSignal) {
log.info("DRM: Searching for compatible MediaKeySystemAccess");
/** Array of set keySystems for this content. */
const keySystemsType = keySystemsConfigs.reduce((arr, keySystemOptions) => {
const { EME_KEY_SYSTEMS } = config.getCurrent();
const managedRDNs = EME_KEY_SYSTEMS[keySystemOptions.type];
let ksType;
if (!isNullOrUndefined(managedRDNs)) {
ksType = managedRDNs.map((keyType) => {
const keyName = keySystemOptions.type;
return { keyName, keyType, keySystemOptions };
});
}
else {
const keyName = findKeySystemCanonicalName(keySystemOptions.type);
const keyType = keySystemOptions.type;
ksType = [{ keyName, keyType, keySystemOptions }];
}
return arr.concat(ksType);
}, []);
return recursivelyTestKeySystems(0);
/**
* Test all key system configuration stored in `keySystemsType` one by one
* recursively.
* Returns a Promise which will emit the MediaKeySystemAccess if one was
* found compatible with one of the configurations or just reject if none
* were found to be compatible.
* @param {Number} index - The index in `keySystemsType` to start from.
* Should be set to `0` when calling directly.
* @returns {Promise.<Object>}
*/
async function recursivelyTestKeySystems(index) {
// if we iterated over the whole keySystemsType Array, quit on error
if (index >= keySystemsType.length) {
throw new EncryptedMediaError("INCOMPATIBLE_KEYSYSTEMS", "No key system compatible with your wanted " +
"configuration has been found in the current " +
"browser.");
}
if (isNullOrUndefined(eme.requestMediaKeySystemAccess)) {
throw new Error("requestMediaKeySystemAccess is not implemented in your browser.");
}
const chosenType = keySystemsType[index];
const { keyType, keySystemOptions } = chosenType;
const keySystemConfigurations = buildKeySystemConfigurations(chosenType);
log.debug(`DRM: Request keysystem access ${keyType},` +
`${index + 1} of ${keySystemsType.length}`);
let keySystemAccess;
const currentState = await MediaKeysAttacher.getAttachedMediaKeysState(mediaElement);
for (let configIdx = 0; configIdx < keySystemConfigurations.length; configIdx++) {
const keySystemConfiguration = keySystemConfigurations[configIdx];
// Check if the current `MediaKeySystemAccess` created cannot be reused here
if (currentState !== null &&
!shouldRenewMediaKeySystemAccess() &&
// TODO: Do it with MediaKeySystemAccess.prototype.keySystem instead?
keyType === currentState.mediaKeySystemAccess.keySystem &&
eme.implementation === currentState.emeImplementation.implementation &&
isNewMediaKeySystemConfigurationCompatibleWithPreviousOne(keySystemConfiguration, currentState.askedConfiguration)) {
log.info("DRM: Found cached compatible keySystem");
return Promise.resolve({
type: "reuse-media-key-system-access",
value: {
mediaKeySystemAccess: currentState.mediaKeySystemAccess,
askedConfiguration: currentState.askedConfiguration,
options: keySystemOptions,
codecSupport: extractCodecSupportListFromConfiguration(currentState.askedConfiguration, currentState.mediaKeySystemAccess.getConfiguration()),
},
});
}
try {
keySystemAccess = await testKeySystem(keyType, [keySystemConfiguration]);
log.info("DRM: Found compatible keysystem", keyType, index + 1);
return {
type: "create-media-key-system-access",
value: {
options: keySystemOptions,
mediaKeySystemAccess: keySystemAccess,
askedConfiguration: keySystemConfiguration,
codecSupport: extractCodecSupportListFromConfiguration(keySystemConfiguration, keySystemAccess.getConfiguration()),
},
};
}
catch (_) {
log.debug("DRM: Rejected access to keysystem", keyType, index + 1, configIdx);
if (cancelSignal.cancellationError !== null) {
throw cancelSignal.cancellationError;
}
}
}
return recursivelyTestKeySystems(index + 1);
}
}
/**
* Test a key system configuration, resolves with the MediaKeySystemAccess
* or reject if the key system is unsupported.
* @param {string} keyType - The KeySystem string to test (ex: com.microsoft.playready.recommendation)
* @param {Array.<MediaKeySystemMediaCapability>} keySystemConfigurations - Configurations for this keySystem
* @returns Promise resolving with the MediaKeySystemAccess. Rejects if unsupported.
*/
export async function testKeySystem(keyType, keySystemConfigurations) {
const keySystemAccess = await eme.requestMediaKeySystemAccess(keyType, keySystemConfigurations);
if (!canRelyOnRequestMediaKeySystemAccess(keyType)) {
try {
const mediaKeys = await keySystemAccess.createMediaKeys();
const session = mediaKeys.createSession();
const initData = generatePlayReadyInitData(DUMMY_PLAY_READY_HEADER);
await session.generateRequest("cenc", initData);
session.close().catch(() => {
log.warn("DRM: Failed to close the dummy session");
});
}
catch (err) {
log.debug("DRM: KeySystemAccess was granted but it is not usable");
throw err;
}
}
return keySystemAccess;
}
/**
* Returns `true` if `arr1`'s values are entirely contained in `arr2`.
* @param {string} arr1
* @param {string} arr2
* @return {boolean}
*/
function isArraySubsetOf(arr1, arr2) {
for (let i = 0; i < arr1.length; i++) {
if (!arrayIncludes(arr2, arr1[i])) {
return false;
}
}
return true;
}