rx-player
Version:
Canal+ HTML5 Video Player
878 lines • 64.3 kB
JavaScript
"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