UNPKG

rx-player

Version:
878 lines 64.3 kB
"use strict"; /** * 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. */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getMissingKeyIds = getMissingKeyIds; exports.getMissingKnownKeyIds = getMissingKnownKeyIds; exports.getMissingInitDataKeyIds = getMissingInitDataKeyIds; var eme_1 = require("../../compat/eme"); var config_1 = require("../../config"); var errors_1 = require("../../errors"); var log_1 = require("../../log"); var are_arrays_of_numbers_equal_1 = require("../../utils/are_arrays_of_numbers_equal"); var array_find_1 = require("../../utils/array_find"); var array_includes_1 = require("../../utils/array_includes"); var event_emitter_1 = require("../../utils/event_emitter"); var is_null_or_undefined_1 = require("../../utils/is_null_or_undefined"); var object_values_1 = require("../../utils/object_values"); var string_parsing_1 = require("../../utils/string_parsing"); var task_canceller_1 = require("../../utils/task_canceller"); var create_or_load_session_1 = require("./create_or_load_session"); var init_media_keys_1 = require("./init_media_keys"); var session_events_listener_1 = require("./session_events_listener"); var set_server_certificate_1 = require("./set_server_certificate"); var types_1 = require("./types"); var check_key_statuses_1 = require("./utils/check_key_statuses"); var clean_old_stored_persistent_info_1 = require("./utils/clean_old_stored_persistent_info"); var get_drm_system_id_1 = require("./utils/get_drm_system_id"); var init_data_values_container_1 = require("./utils/init_data_values_container"); var is_compatible_codec_supported_1 = require("./utils/is_compatible_codec_supported"); var key_id_comparison_1 = require("./utils/key_id_comparison"); var media_keys_attacher_1 = require("./utils/media_keys_attacher"); /** * Module communicating with the Content Decryption Module (or CDM) to be able * to decrypt contents. * * The `ContentDecryptor` starts communicating with the CDM, to initialize the * key system, as soon as it is created. * * You can be notified of various events, such as fatal errors, by registering * to one of its multiple events (@see IContentDecryptorEvent). * * @class ContentDecryptor */ var ContentDecryptor = /** @class */ (function (_super) { __extends(ContentDecryptor, _super); /** * Create a new `ContentDecryptor`, and initialize its decryption capabilities * right away. * Goes into the `WaitingForAttachment` state once that initialization is * done, after which you should call the `attach` method when you're ready for * those decryption capabilities to be attached to the HTMLMediaElement. * * @param {HTMLMediaElement} mediaElement - The MediaElement which will be * associated to a MediaKeys object * @param {Array.<Object>} ksOptions - key system configuration. * The `ContentDecryptor` can be given one or multiple key system * configurations. It will choose the appropriate one depending on user * settings and browser support. */ function ContentDecryptor(mediaElement, ksOptions) { var _this = _super.call(this) || this; log_1.default.debug("DRM: Starting ContentDecryptor logic."); var canceller = new task_canceller_1.default(); _this._currentSessions = []; _this._canceller = canceller; _this._initDataQueue = []; _this._stateData = { state: types_1.ContentDecryptorState.Initializing, isMediaKeysAttached: 0 /* MediaKeyAttachmentStatus.NotAttached */, isInitDataQueueLocked: true, data: null, }; _this._supportedCodecWhenEncrypted = []; _this.error = null; eme_1.default.onEncrypted(mediaElement, function (evt) { log_1.default.debug("DRM: Encrypted event received from media element."); var initData = (0, eme_1.getInitData)(evt); if (initData !== null) { _this.onInitializationData(initData); } }, canceller.signal); (0, init_media_keys_1.default)(mediaElement, ksOptions, canceller.signal) .then(function (mediaKeysInfo) { var options = mediaKeysInfo.options, mediaKeySystemAccess = mediaKeysInfo.mediaKeySystemAccess; _this._supportedCodecWhenEncrypted = mediaKeysInfo.codecSupport; /** * String identifying the key system, allowing the rest of the code to * only advertise the required initialization data for license requests. * * Note that we only set this value if retro-compatibility to older * persistent logic in the RxPlayer is not important, as the * optimizations this property unlocks can break the loading of * MediaKeySessions persisted in older RxPlayer's versions. */ var systemId; if ((0, is_null_or_undefined_1.default)(options.persistentLicenseConfig) || options.persistentLicenseConfig.disableRetroCompatibility === true) { systemId = (0, get_drm_system_id_1.default)(mediaKeySystemAccess.keySystem); } _this.systemId = systemId; if (_this._stateData.state === types_1.ContentDecryptorState.Initializing) { log_1.default.debug("DRM: Waiting for attachment."); _this._stateData = { state: types_1.ContentDecryptorState.WaitingForAttachment, isInitDataQueueLocked: true, isMediaKeysAttached: 0 /* MediaKeyAttachmentStatus.NotAttached */, data: { mediaKeysInfo: mediaKeysInfo, mediaElement: mediaElement }, }; _this.trigger("stateChange", _this._stateData.state); } }) .catch(function (err) { _this._onFatalError(err); }); return _this; } /** * `true` if the EME API are available on the current platform according to * the default EME implementation used. * `false` otherwise. * @returns {boolean} */ ContentDecryptor.hasEmeApis = function () { return !(0, is_null_or_undefined_1.default)(eme_1.default.requestMediaKeySystemAccess); }; /** * Returns the current state of the ContentDecryptor. * @see ContentDecryptorState * @returns {Object} */ ContentDecryptor.prototype.getState = function () { return this._stateData.state; }; /** * Attach the current decryption capabilities to the HTMLMediaElement. * This method should only be called once the `ContentDecryptor` is in the * `WaitingForAttachment` state. * * You might want to first set the HTMLMediaElement's `src` attribute before * calling this method, and only push data to it once the `ReadyForContent` * state is reached, for compatibility reasons. */ ContentDecryptor.prototype.attach = function () { var _this = this; if (this._stateData.state !== types_1.ContentDecryptorState.WaitingForAttachment) { throw new Error("`attach` should only be called when " + "in the WaitingForAttachment state"); } else if (this._stateData.isMediaKeysAttached !== 0 /* MediaKeyAttachmentStatus.NotAttached */) { log_1.default.warn("DRM: ContentDecryptor's `attach` method called more than once."); return; } var _a = this._stateData.data, mediaElement = _a.mediaElement, mediaKeysInfo = _a.mediaKeysInfo; var options = mediaKeysInfo.options, mediaKeys = mediaKeysInfo.mediaKeys, mediaKeySystemAccess = mediaKeysInfo.mediaKeySystemAccess, stores = mediaKeysInfo.stores, askedConfiguration = mediaKeysInfo.askedConfiguration; var shouldDisableLock = options.disableMediaKeysAttachmentLock === true; if (shouldDisableLock) { log_1.default.debug("DRM: disabling MediaKeys attachment lock. Ready for content"); this._stateData = { state: types_1.ContentDecryptorState.ReadyForContent, isInitDataQueueLocked: true, isMediaKeysAttached: 1 /* MediaKeyAttachmentStatus.Pending */, data: { mediaKeysInfo: mediaKeysInfo, mediaElement: mediaElement }, }; this.trigger("stateChange", this._stateData.state); // previous trigger might have lead to disposal if (this._isStopped()) { return; } } this._stateData.isMediaKeysAttached = 1 /* MediaKeyAttachmentStatus.Pending */; var stateToAttach = { emeImplementation: eme_1.default, loadedSessionsStore: stores.loadedSessionsStore, mediaKeySystemAccess: mediaKeySystemAccess, mediaKeys: mediaKeys, askedConfiguration: askedConfiguration, keySystemOptions: options, }; log_1.default.debug("DRM: Attaching current MediaKeys"); media_keys_attacher_1.default.attach(mediaElement, stateToAttach) .then(function () { return __awaiter(_this, void 0, void 0, function () { var serverCertificate, resSsc, prevState; return __generator(this, function (_a) { switch (_a.label) { case 0: if (this._isStopped()) { // We might be stopped since then return [2 /*return*/]; } this._stateData.isMediaKeysAttached = 2 /* MediaKeyAttachmentStatus.Attached */; serverCertificate = options.serverCertificate; if (!!(0, is_null_or_undefined_1.default)(serverCertificate)) return [3 /*break*/, 2]; return [4 /*yield*/, (0, set_server_certificate_1.default)(mediaKeys, serverCertificate)]; case 1: resSsc = _a.sent(); if (resSsc.type === "error") { this.trigger("warning", resSsc.value); } _a.label = 2; case 2: if (this._isStopped()) { // We might be stopped since then return [2 /*return*/]; } prevState = this._stateData.state; this._stateData = { state: types_1.ContentDecryptorState.ReadyForContent, isMediaKeysAttached: 2 /* MediaKeyAttachmentStatus.Attached */, isInitDataQueueLocked: false, data: { mediaKeysData: mediaKeysInfo }, }; if (prevState !== types_1.ContentDecryptorState.ReadyForContent) { this.trigger("stateChange", types_1.ContentDecryptorState.ReadyForContent); } if (!this._isStopped()) { this._processCurrentInitDataQueue(); } return [2 /*return*/]; } }); }); }) .catch(function (err) { _this._onFatalError(err); }); }; /** * Stop this `ContentDecryptor` instance: * - stop listening and reacting to the various event listeners * - abort all operations. * * Once disposed, a `ContentDecryptor` cannot be used anymore. */ ContentDecryptor.prototype.dispose = function () { this.removeEventListener(); this._stateData = { state: types_1.ContentDecryptorState.Disposed, isMediaKeysAttached: undefined, isInitDataQueueLocked: undefined, data: null, }; this._canceller.cancel(); this.trigger("stateChange", this._stateData.state); }; /** * Returns `true` if the given mimeType and codec couple should be supported * by the current key system. * Returns `false` if it isn't. * * Returns `undefined` if we cannot determine if it is supported. * * @param {string} mimeType * @param {string} codec * @returns {boolean} */ ContentDecryptor.prototype.isCodecSupported = function (mimeType, codec) { if (this._stateData.state === types_1.ContentDecryptorState.Initializing) { log_1.default.error("DRM: Asking for codec support while the ContentDecryptor is still initializing"); return undefined; } if (this._stateData.state === types_1.ContentDecryptorState.Error || this._stateData.state === types_1.ContentDecryptorState.Disposed) { log_1.default.error("DRM: Asking for codec support while the ContentDecryptor is disposed"); } return (0, is_compatible_codec_supported_1.default)(mimeType, codec, this._supportedCodecWhenEncrypted); }; /** * Method to call when new protection initialization data is encounted on the * content. * * When called, the `ContentDecryptor` will try to obtain the decryption key * if not already obtained. * * @param {Object} initializationData */ ContentDecryptor.prototype.onInitializationData = function (initializationData) { var _this = this; if (this._stateData.isInitDataQueueLocked !== false) { if (this._isStopped()) { throw new Error("ContentDecryptor either disposed or stopped."); } this._initDataQueue.push(initializationData); return; } var mediaKeysData = this._stateData.data.mediaKeysData; var processedInitializationData = __assign(__assign({}, initializationData), { values: new init_data_values_container_1.default(initializationData.values) }); this._processInitializationData(processedInitializationData, mediaKeysData).catch(function (err) { _this._onFatalError(err); }); }; /** * Async logic run each time new initialization data has to be processed. * The promise return may reject, in which case a fatal error should be linked * the current `ContentDecryptor`. * * The Promise's resolution however provides no semantic value. * @param {Object} initializationData * @returns {Promise.<void>} */ ContentDecryptor.prototype._processInitializationData = function (initializationData, mediaKeysData) { return __awaiter(this, void 0, void 0, function () { var mediaKeySystemAccess, stores, options, firstCreatedSession, keyIds, hexKids, period, createdSessions, periodKeys, createdSessions_1, createdSessions_1_1, createdSess, periodKeysArr, periodKeysArr_1, periodKeysArr_1_1, kid, _loop_1, periodKeysArr_2, periodKeysArr_2_1, innerKid, wantedSessionType, _a, EME_DEFAULT_MAX_SIMULTANEOUS_MEDIA_KEY_SESSIONS, EME_MAX_STORED_PERSISTENT_SESSION_INFORMATION, maxSessionCacheSize, sessionRes, sessionInfo, _b, mediaKeySession, sessionType, isSessionPersisted, requestData, error_1, entry, indexInCurrent; var e_1, _c, e_2, _d, e_3, _e; var _this = this; var _f, _g, _h; return __generator(this, function (_j) { switch (_j.label) { case 0: if (log_1.default.hasLevel("DEBUG")) { log_1.default.debug("DRM: processing init data", (_f = initializationData.content) === null || _f === void 0 ? void 0 : _f.adaptation.type, (_g = initializationData.content) === null || _g === void 0 ? void 0 : _g.representation.bitrate, ((_h = initializationData.keyIds) !== null && _h !== void 0 ? _h : []).map(function (k) { return (0, string_parsing_1.bytesToHex)(k); }).join(", ")); } mediaKeySystemAccess = mediaKeysData.mediaKeySystemAccess, stores = mediaKeysData.stores, options = mediaKeysData.options; if (this._tryToUseAlreadyCreatedSession(initializationData, mediaKeysData) || this._isStopped()) { // _isStopped is voluntarly checked after here return [2 /*return*/]; } if (options.singleLicensePer === "content") { firstCreatedSession = (0, array_find_1.default)(this._currentSessions, function (x) { return x.source === "created-session" /* MediaKeySessionLoadingType.Created */; }); if (firstCreatedSession !== undefined) { keyIds = initializationData.keyIds; if (keyIds === undefined) { if (initializationData.content === undefined) { log_1.default.warn("DRM: Unable to fallback from a non-decipherable quality."); } else { log_1.default.debug("DRM: Blacklisting new init data (due to singleLicensePer content policy)"); this.trigger("blackListProtectionData", initializationData); } return [2 /*return*/]; } firstCreatedSession.record.associateKeyIds(keyIds); if (initializationData.content === undefined) { log_1.default.warn("DRM: Unable to fallback from a non-decipherable quality."); } else { if (log_1.default.hasLevel("DEBUG")) { hexKids = keyIds.reduce(function (acc, kid) { return "".concat(acc, ", ").concat((0, string_parsing_1.bytesToHex)(kid)); }, ""); log_1.default.debug("DRM: Blacklisting new key ids", hexKids); } this.trigger("keyIdsCompatibilityUpdate", { whitelistedKeyIds: [], blacklistedKeyIds: keyIds, delistedKeyIds: [], }); } return [2 /*return*/]; } } else if (options.singleLicensePer === "periods" && initializationData.content !== undefined) { period = initializationData.content.period; createdSessions = this._currentSessions.filter(function (x) { return x.source === "created-session" /* MediaKeySessionLoadingType.Created */; }); periodKeys = new Set(); addKeyIdsFromPeriod(periodKeys, period); try { for (createdSessions_1 = __values(createdSessions), createdSessions_1_1 = createdSessions_1.next(); !createdSessions_1_1.done; createdSessions_1_1 = createdSessions_1.next()) { createdSess = createdSessions_1_1.value; periodKeysArr = Array.from(periodKeys); try { for (periodKeysArr_1 = (e_2 = void 0, __values(periodKeysArr)), periodKeysArr_1_1 = periodKeysArr_1.next(); !periodKeysArr_1_1.done; periodKeysArr_1_1 = periodKeysArr_1.next()) { kid = periodKeysArr_1_1.value; if (createdSess.record.isAssociatedWithKeyId(kid)) { createdSess.record.associateKeyIds(periodKeys.values()); _loop_1 = function (innerKid) { if (!createdSess.keyStatuses.whitelisted.some(function (k) { return (0, are_arrays_of_numbers_equal_1.default)(k, innerKid); }) && !createdSess.keyStatuses.blacklisted.some(function (k) { return (0, are_arrays_of_numbers_equal_1.default)(k, innerKid); })) { createdSess.keyStatuses.blacklisted.push(innerKid); } }; try { // Re-loop through the Period's key ids to blacklist ones that are missing // from `createdSess`'s `keyStatuses` and to update the content's // decipherability. for (periodKeysArr_2 = (e_3 = void 0, __values(periodKeysArr)), periodKeysArr_2_1 = periodKeysArr_2.next(); !periodKeysArr_2_1.done; periodKeysArr_2_1 = periodKeysArr_2.next()) { innerKid = periodKeysArr_2_1.value; _loop_1(innerKid); } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (periodKeysArr_2_1 && !periodKeysArr_2_1.done && (_e = periodKeysArr_2.return)) _e.call(periodKeysArr_2); } finally { if (e_3) throw e_3.error; } } if (log_1.default.hasLevel("DEBUG")) { log_1.default.debug("DRM: Session already created for", (0, string_parsing_1.bytesToHex)(kid), 'under singleLicensePer "periods" policy'); } this.trigger("keyIdsCompatibilityUpdate", { whitelistedKeyIds: createdSess.keyStatuses.whitelisted, blacklistedKeyIds: createdSess.keyStatuses.blacklisted, delistedKeyIds: [], }); return [2 /*return*/]; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (periodKeysArr_1_1 && !periodKeysArr_1_1.done && (_d = periodKeysArr_1.return)) _d.call(periodKeysArr_1); } finally { if (e_2) throw e_2.error; } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (createdSessions_1_1 && !createdSessions_1_1.done && (_c = createdSessions_1.return)) _c.call(createdSessions_1); } finally { if (e_1) throw e_1.error; } } } // /!\ Do not forget to unlock when done // TODO this is error-prone and can lead to performance issue when loading // persistent sessions. // Can we find a better strategy? this._lockInitDataQueue(); if (canCreatePersistentSession(mediaKeySystemAccess) && (!(0, is_null_or_undefined_1.default)(options.persistentLicenseConfig) || !canCreateTemporarySession(mediaKeySystemAccess))) { wantedSessionType = "persistent-license"; } else { wantedSessionType = "temporary"; } _a = config_1.default.getCurrent(), EME_DEFAULT_MAX_SIMULTANEOUS_MEDIA_KEY_SESSIONS = _a.EME_DEFAULT_MAX_SIMULTANEOUS_MEDIA_KEY_SESSIONS, EME_MAX_STORED_PERSISTENT_SESSION_INFORMATION = _a.EME_MAX_STORED_PERSISTENT_SESSION_INFORMATION; maxSessionCacheSize = typeof options.maxSessionCacheSize === "number" ? options.maxSessionCacheSize : EME_DEFAULT_MAX_SIMULTANEOUS_MEDIA_KEY_SESSIONS; return [4 /*yield*/, (0, create_or_load_session_1.default)(initializationData, stores, wantedSessionType, maxSessionCacheSize, this._canceller.signal)]; case 1: sessionRes = _j.sent(); if (this._isStopped()) { return [2 /*return*/]; } sessionInfo = { record: sessionRes.value.keySessionRecord, source: sessionRes.type, keyStatuses: { whitelisted: [], blacklisted: [] }, blacklistedSessionError: null, }; this._currentSessions.push(sessionInfo); _b = sessionRes.value, mediaKeySession = _b.mediaKeySession, sessionType = _b.sessionType; isSessionPersisted = false; (0, session_events_listener_1.default)(mediaKeySession, options, mediaKeySystemAccess.keySystem, { onKeyUpdate: function (value) { var linkedKeys = getKeyIdsLinkedToSession(initializationData, sessionInfo.record, options.singleLicensePer, sessionInfo.source === "created-session" /* MediaKeySessionLoadingType.Created */, value.whitelistedKeyIds, value.blacklistedKeyIds); sessionInfo.record.associateKeyIds(linkedKeys.whitelisted); sessionInfo.record.associateKeyIds(linkedKeys.blacklisted); sessionInfo.keyStatuses = { whitelisted: linkedKeys.whitelisted, blacklisted: linkedKeys.blacklisted, }; if (sessionInfo.record.getAssociatedKeyIds().length !== 0 && sessionType === "persistent-license" && stores.persistentSessionsStore !== null && !isSessionPersisted) { var persistentSessionsStore = stores.persistentSessionsStore; (0, clean_old_stored_persistent_info_1.default)(persistentSessionsStore, EME_MAX_STORED_PERSISTENT_SESSION_INFORMATION - 1); persistentSessionsStore.add(initializationData, sessionInfo.record.getAssociatedKeyIds(), mediaKeySession); isSessionPersisted = true; } if (initializationData.content !== undefined) { _this.trigger("keyIdsCompatibilityUpdate", { whitelistedKeyIds: linkedKeys.whitelisted, blacklistedKeyIds: linkedKeys.blacklisted, delistedKeyIds: [], }); } _this._unlockInitDataQueue(); }, onWarning: function (value) { _this.trigger("warning", value); }, onError: function (err) { var _a; if (err instanceof check_key_statuses_1.DecommissionedSessionError) { log_1.default.warn("DRM: A session's closing condition has been triggered"); _this._lockInitDataQueue(); var indexOf = _this._currentSessions.indexOf(sessionInfo); if (indexOf >= 0) { _this._currentSessions.splice(indexOf); } if (initializationData.content !== undefined) { _this.trigger("keyIdsCompatibilityUpdate", { whitelistedKeyIds: [], blacklistedKeyIds: [], delistedKeyIds: sessionInfo.record.getAssociatedKeyIds(), }); } (_a = stores.persistentSessionsStore) === null || _a === void 0 ? void 0 : _a.delete(mediaKeySession.sessionId); stores.loadedSessionsStore .closeSession(mediaKeySession) .catch(function (e) { var closeError = e instanceof Error ? e : "unknown error"; log_1.default.warn("DRM: failed to close expired session", closeError); }) .then(function () { return _this._unlockInitDataQueue(); }) .catch(function (retryError) { return _this._onFatalError(retryError); }); if (!_this._isStopped()) { _this.trigger("warning", err.reason); } return; } if (!(err instanceof session_events_listener_1.BlacklistedSessionError)) { _this._onFatalError(err); return; } sessionInfo.blacklistedSessionError = err; if (initializationData.content !== undefined) { log_1.default.info("DRM: blacklisting Representations based on " + "protection data."); _this.trigger("blackListProtectionData", initializationData); } _this._unlockInitDataQueue(); // TODO warning for blacklisted session? }, }, this._canceller.signal); if (options.singleLicensePer === undefined || options.singleLicensePer === "init-data") { this._unlockInitDataQueue(); } if (!(sessionRes.type === "created-session" /* MediaKeySessionLoadingType.Created */)) return [3 /*break*/, 5]; requestData = initializationData.values.constructRequestData(); _j.label = 2; case 2: _j.trys.push([2, 4, , 5]); return [4 /*yield*/, stores.loadedSessionsStore.generateLicenseRequest(mediaKeySession, initializationData.type, requestData)]; case 3: _j.sent(); return [3 /*break*/, 5]; case 4: error_1 = _j.sent(); entry = stores.loadedSessionsStore.getEntryForSession(mediaKeySession); if (entry === null || entry.closingStatus.type !== "none") { indexInCurrent = this._currentSessions.indexOf(sessionInfo); if (indexInCurrent >= 0) { this._currentSessions.splice(indexInCurrent, 1); } return [2 /*return*/, Promise.resolve()]; } throw new errors_1.EncryptedMediaError("KEY_GENERATE_REQUEST_ERROR", error_1 instanceof Error ? error_1.toString() : "Unknown error"); case 5: return [2 /*return*/, Promise.resolve()]; } }); }); }; ContentDecryptor.prototype._tryToUseAlreadyCreatedSession = function (initializationData, mediaKeysData) { var stores = mediaKeysData.stores, options = mediaKeysData.options; /** * If set, a currently-used key session is already compatible to this * initialization data. */ var compatibleSessionInfo = (0, array_find_1.default)(this._currentSessions, function (x) { return x.record.isCompatibleWith(initializationData); }); if (compatibleSessionInfo === undefined) { return false; } /** * On Safari using Directfile, the old EME implementation triggers * the "webkitneedkey" event instead of "encrypted". There's an issue in Safari * where "webkitneedkey" fires too early before all tracks are added from an HLS playlist. * Safari incorrectly assumes some keys are missing for these tracks, * leading to repeated "webkitneedkey" events. Because RxPlayer recognizes * it already has a session for these keys and ignores the events, * the content remains frozen. To resolve this, the session is re-created. */ var forceSessionRecreation = initializationData.forceSessionRecreation; if (forceSessionRecreation === true) { this.removeSessionForInitData(initializationData, mediaKeysData); return false; } // Check if the compatible session is blacklisted var blacklistedSessionError = compatibleSessionInfo.blacklistedSessionError; if (!(0, is_null_or_undefined_1.default)(blacklistedSessionError)) { if (initializationData.type === undefined || initializationData.content === undefined) { log_1.default.error("DRM: This initialization data has already been blacklisted " + "but the current content is not known."); return true; } else { log_1.default.info("DRM: This initialization data has already been blacklisted. " + "Blacklisting the related content."); this.trigger("blackListProtectionData", initializationData); return true; } } // Check if the current key id(s) has been blacklisted by this session if (initializationData.keyIds !== undefined) { /** * If set to `true`, the Representation(s) linked to this * initialization data's key id should be marked as "not decipherable". */ var isUndecipherable = void 0; if (options.singleLicensePer === undefined || options.singleLicensePer === "init-data") { // Note: In the default "init-data" mode, we only avoid a // Representation if the key id was originally explicitely // blacklisted (and not e.g. if its key was just not present in // the license). // // This is to enforce v3.x.x retro-compatibility: we cannot // fallback from a Representation unless some RxPlayer option // documentating this behavior has been set. var blacklisted = compatibleSessionInfo.keyStatuses.blacklisted; isUndecipherable = (0, key_id_comparison_1.areSomeKeyIdsContainedIn)(initializationData.keyIds, blacklisted); } else { // In any other mode, as soon as not all of this initialization // data's linked key ids are explicitely whitelisted, we can mark // the corresponding Representation as "not decipherable". // This is because we've no such retro-compatibility guarantee to // make there. var whitelisted = compatibleSessionInfo.keyStatuses.whitelisted; isUndecipherable = !(0, key_id_comparison_1.areAllKeyIdsContainedIn)(initializationData.keyIds, whitelisted); } if (isUndecipherable) { if (initializationData.content === undefined) { log_1.default.error("DRM: Cannot forbid key id, the content is unknown."); return true; } log_1.default.info("DRM: Current initialization data is linked to blacklisted keys. " + "Marking Representations as not decipherable"); this.trigger("keyIdsCompatibilityUpdate", { whitelistedKeyIds: [], blacklistedKeyIds: initializationData.keyIds, delistedKeyIds: [], }); return true; } } // If we reached here, it means that this initialization data is not // blacklisted in any way. // Search loaded session and put it on top of the cache if it exists. var entry = stores.loadedSessionsStore.reuse(initializationData); if (entry !== null) { // TODO update decipherability to `true` if not? log_1.default.debug("DRM: Init data already processed. Skipping it."); return true; } // Session not found in `loadedSessionsStore`, it might have been closed // since. // Remove from `this._currentSessions` and start again. var indexOf = this._currentSessions.indexOf(compatibleSessionInfo); if (indexOf === -1) { log_1.default.error("DRM: Unable to remove processed init data: not found."); } else { log_1.default.debug("DRM: A session from a processed init data is not available " + "anymore. Re-processing it."); this._currentSessions.splice(indexOf, 1); } return false; }; /** * Remove the session corresponding to the initData provided, and close it. * It does nothing if no session was found for this initData. * @param {Object} initData : The initialization data corresponding to the session * that need to be removed * @param {Object} mediaKeysData : The media keys data */ ContentDecryptor.prototype.removeSessionForInitData = function (initData, mediaKeysData) { var stores = mediaKeysData.stores; /** Remove the session and close it from the loadedSessionStore */ var entry = stores.loadedSessionsStore.reuse(initData); if (entry !== null) { stores.loadedSessionsStore .closeSession(entry.mediaKeySession) .catch(function () { return log_1.default.error("DRM: Cannot close the session from the loaded session store"); }); } /** * If set, a currently-used key session is already compatible to this * initialization data. */ var compatibleSessionInfo = (0, array_find_1.default)(this._currentSessions, function (x) { return x.record.isCompatibleWith(initData); }); if (compatibleSessionInfo === undefined) { return; } /** Remove the session from the currentSessions */ var indexOf = this._currentSessions.indexOf(compatibleSessionInfo); if (indexOf !== -1) { log_1.default.debug("DRM: A session from a processed init is removed due to forceSessionRecreation policy."); this._currentSessions.splice(indexOf, 1); } }; /** * Callback that should be called if an error that made the current * `ContentDecryptor` instance unusable arised. * This callbacks takes care of resetting state and sending the right events. * * Once called, no further actions should be taken. * * @param {*} err - The error object which describes the issue. Will be * formatted and sent in an "error" event. */ ContentDecryptor.prototype._onFatalError = function (err) { if (this._canceller.isUsed()) { return; } var formattedErr = err instanceof Error ? err : new errors_1.OtherError("NONE", "Unknown decryption error"); this.error = formattedErr; this._initDataQueue.length = 0; this._stateData = { state: types_1.ContentDecryptorState.Error, isMediaKeysAttached: undefined, isInitDataQueueLocked: undefined, data: null, }; this._canceller.cancel(); this.trigger("error", formattedErr); // The previous trigger might have lead to a disposal of the `ContentDecryptor`. if (this._stateData.state === types_1.ContentDecryptorState.Error) { this.trigger("stateChange", this._stateData.state); } }; /** * Return `true` if the `ContentDecryptor` has either been disposed or * encountered a fatal error which made it stop. * @returns {boolean} */ ContentDecryptor.prototype._isStopped = function () { return (this._stateData.state === types_1.ContentDecryptorState.Disposed || this._stateData.state === types_1.ContentDecryptorState.Error); }; /** * Start processing the next initialization data of the `_initDataQueue` if it * isn't lock. */ ContentDecryptor.prototype._processCurrentInitDataQueue = function () { while (this._stateData.isInitDataQueueLocked === false) { var initData = this._initDataQueue.shift(); if (initData === undefined) { return; } this.onInitializationData(initData); } }; /** * Lock new initialization data (from the `_initDataQueue`) from being * processed until `_unlockInitDataQueue` is called. * * You may want to call this method when performing operations which may have * an impact on the handling of other initialization data. */ ContentDecryptor.prototype._lockInitDataQueue = function () { if (this._stateData.isInitDataQueueLocked === false) { this._stateData.isInitDataQueueLocked = true; } }; /** * Unlock `_initDataQueue` and start processing the first element. * * Should have no effect if the `_initDataQueue` was not locked. */ ContentDecryptor.prototype._unlockInitDataQueue = function () { if (this._stateData.isMediaKeysAttached !== 2 /* MediaKeyAttachmentStatus.Attached */) { log_1.default.er