dashjs
Version:
A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.
992 lines (872 loc) • 62.3 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>dash.js Source: streaming/protection/controllers/ProtectionController.js</title>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/sunlight.default.css">
<link type="text/css" rel="stylesheet" href="styles/site.spacelab.css">
</head>
<body>
<div class="navbar navbar-default navbar-fixed-top navbar-inverse">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="index.html">dash.js</a>
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#topNavigation">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="navbar-collapse collapse" id="topNavigation">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="modules.list.html" class="dropdown-toggle" data-toggle="dropdown">Modules<b class="caret"></b></a>
<ul class="dropdown-menu ">
<li><a href="module-DashAdapter.html">DashAdapter</a></li><li><a href="module-DashMetrics.html">DashMetrics</a></li><li><a href="module-MediaPlayer.html">MediaPlayer</a></li><li><a href="module-OfflineController.html">OfflineController</a></li><li><a href="module-ProtectionController.html">ProtectionController</a></li><li><a href="module-Settings.html">Settings</a></li>
</ul>
</li>
<li class="dropdown">
<a href="classes.list.html" class="dropdown-toggle" data-toggle="dropdown">Classes<b class="caret"></b></a>
<ul class="dropdown-menu ">
<li><a href="Errors.html">Errors</a></li><li><a href="MediaPlayerEvents.html">MediaPlayerEvents</a></li><li><a href="MediaPlayerModel.html">MediaPlayerModel</a></li><li><a href="MetricsReportingEvents.html">MetricsReportingEvents</a></li><li><a href="MssErrors.html">MssErrors</a></li><li><a href="OfflineErrors.html">OfflineErrors</a></li><li><a href="OfflineEvents.html">OfflineEvents</a></li><li><a href="ProtectionErrors.html">ProtectionErrors</a></li><li><a href="ProtectionEvents.html">ProtectionEvents</a></li>
</ul>
</li>
<li class="dropdown">
<a href="events.list.html" class="dropdown-toggle" data-toggle="dropdown">Events<b class="caret"></b></a>
<ul class="dropdown-menu ">
<li><a href="MediaPlayerEvents.html#event:ADAPTATION_SET_REMOVED_NO_CAPABILITIES">MediaPlayerEvents#event:ADAPTATION_SET_REMOVED_NO_CAPABILITIES</a></li><li><a href="MediaPlayerEvents.html#event:AST_IN_FUTURE">MediaPlayerEvents#event:AST_IN_FUTURE</a></li><li><a href="MediaPlayerEvents.html#event:BUFFER_EMPTY">MediaPlayerEvents#event:BUFFER_EMPTY</a></li><li><a href="MediaPlayerEvents.html#event:BUFFER_LEVEL_STATE_CHANGED">MediaPlayerEvents#event:BUFFER_LEVEL_STATE_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:BUFFER_LEVEL_UPDATED">MediaPlayerEvents#event:BUFFER_LEVEL_UPDATED</a></li><li><a href="MediaPlayerEvents.html#event:BUFFER_LOADED">MediaPlayerEvents#event:BUFFER_LOADED</a></li><li><a href="MediaPlayerEvents.html#event:CAN_PLAY">MediaPlayerEvents#event:CAN_PLAY</a></li><li><a href="MediaPlayerEvents.html#event:CAN_PLAY_THROUGH">MediaPlayerEvents#event:CAN_PLAY_THROUGH</a></li><li><a href="MediaPlayerEvents.html#event:CAPTION_CONTAINER_RESIZE">MediaPlayerEvents#event:CAPTION_CONTAINER_RESIZE</a></li><li><a href="MediaPlayerEvents.html#event:CAPTION_RENDERED">MediaPlayerEvents#event:CAPTION_RENDERED</a></li><li><a href="MediaPlayerEvents.html#event:CONFORMANCE_VIOLATION">MediaPlayerEvents#event:CONFORMANCE_VIOLATION</a></li><li><a href="MediaPlayerEvents.html#event:CONTENT_STEERING_REQUEST_COMPLETED">MediaPlayerEvents#event:CONTENT_STEERING_REQUEST_COMPLETED</a></li><li><a href="MediaPlayerEvents.html#event:DYNAMIC_TO_STATIC">MediaPlayerEvents#event:DYNAMIC_TO_STATIC</a></li><li><a href="MediaPlayerEvents.html#event:ERROR">MediaPlayerEvents#event:ERROR</a></li><li><a href="MediaPlayerEvents.html#event:EVENT_MODE_ON_RECEIVE">MediaPlayerEvents#event:EVENT_MODE_ON_RECEIVE</a></li><li><a href="MediaPlayerEvents.html#event:EVENT_MODE_ON_START">MediaPlayerEvents#event:EVENT_MODE_ON_START</a></li><li><a href="MediaPlayerEvents.html#event:FRAGMENT_LOADING_ABANDONED">MediaPlayerEvents#event:FRAGMENT_LOADING_ABANDONED</a></li><li><a href="MediaPlayerEvents.html#event:FRAGMENT_LOADING_COMPLETED">MediaPlayerEvents#event:FRAGMENT_LOADING_COMPLETED</a></li><li><a href="MediaPlayerEvents.html#event:FRAGMENT_LOADING_PROGRESS">MediaPlayerEvents#event:FRAGMENT_LOADING_PROGRESS</a></li><li><a href="MediaPlayerEvents.html#event:FRAGMENT_LOADING_STARTED">MediaPlayerEvents#event:FRAGMENT_LOADING_STARTED</a></li><li><a href="MediaPlayerEvents.html#event:LOG">MediaPlayerEvents#event:LOG</a></li><li><a href="MediaPlayerEvents.html#event:MANIFEST_LOADED">MediaPlayerEvents#event:MANIFEST_LOADED</a></li><li><a href="MediaPlayerEvents.html#event:MANIFEST_VALIDITY_CHANGED">MediaPlayerEvents#event:MANIFEST_VALIDITY_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:METRIC_ADDED">MediaPlayerEvents#event:METRIC_ADDED</a></li><li><a href="MediaPlayerEvents.html#event:METRIC_CHANGED">MediaPlayerEvents#event:METRIC_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:METRIC_UPDATED">MediaPlayerEvents#event:METRIC_UPDATED</a></li><li><a href="MediaPlayerEvents.html#event:METRICS_CHANGED">MediaPlayerEvents#event:METRICS_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:PERIOD_SWITCH_COMPLETED">MediaPlayerEvents#event:PERIOD_SWITCH_COMPLETED</a></li><li><a href="MediaPlayerEvents.html#event:PERIOD_SWITCH_STARTED">MediaPlayerEvents#event:PERIOD_SWITCH_STARTED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_ENDED">MediaPlayerEvents#event:PLAYBACK_ENDED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_ERROR">MediaPlayerEvents#event:PLAYBACK_ERROR</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_LOADED_DATA">MediaPlayerEvents#event:PLAYBACK_LOADED_DATA</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_METADATA_LOADED">MediaPlayerEvents#event:PLAYBACK_METADATA_LOADED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_NOT_ALLOWED">MediaPlayerEvents#event:PLAYBACK_NOT_ALLOWED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_PAUSED">MediaPlayerEvents#event:PLAYBACK_PAUSED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_PLAYING">MediaPlayerEvents#event:PLAYBACK_PLAYING</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_PROGRESS">MediaPlayerEvents#event:PLAYBACK_PROGRESS</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_RATE_CHANGED">MediaPlayerEvents#event:PLAYBACK_RATE_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_SEEKED">MediaPlayerEvents#event:PLAYBACK_SEEKED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_SEEKING">MediaPlayerEvents#event:PLAYBACK_SEEKING</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_STALLED">MediaPlayerEvents#event:PLAYBACK_STALLED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_STARTED">MediaPlayerEvents#event:PLAYBACK_STARTED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_TIME_UPDATED">MediaPlayerEvents#event:PLAYBACK_TIME_UPDATED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_VOLUME_CHANGED">MediaPlayerEvents#event:PLAYBACK_VOLUME_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_WAITING">MediaPlayerEvents#event:PLAYBACK_WAITING</a></li><li><a href="MediaPlayerEvents.html#event:QUALITY_CHANGE_RENDERED">MediaPlayerEvents#event:QUALITY_CHANGE_RENDERED</a></li><li><a href="MediaPlayerEvents.html#event:QUALITY_CHANGE_REQUESTED">MediaPlayerEvents#event:QUALITY_CHANGE_REQUESTED</a></li><li><a href="MediaPlayerEvents.html#event:REPRESENTATION_SWITCH">MediaPlayerEvents#event:REPRESENTATION_SWITCH</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_ACTIVATED">MediaPlayerEvents#event:STREAM_ACTIVATED</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_DEACTIVATED">MediaPlayerEvents#event:STREAM_DEACTIVATED</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_INITIALIZED">MediaPlayerEvents#event:STREAM_INITIALIZED</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_INITIALIZING">MediaPlayerEvents#event:STREAM_INITIALIZING</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_TEARDOWN_COMPLETE">MediaPlayerEvents#event:STREAM_TEARDOWN_COMPLETE</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_UPDATED">MediaPlayerEvents#event:STREAM_UPDATED</a></li><li><a href="MediaPlayerEvents.html#event:TEXT_TRACK_ADDED">MediaPlayerEvents#event:TEXT_TRACK_ADDED</a></li><li><a href="MediaPlayerEvents.html#event:TEXT_TRACKS_ADDED">MediaPlayerEvents#event:TEXT_TRACKS_ADDED</a></li><li><a href="MediaPlayerEvents.html#event:TRACK_CHANGE_RENDERED">MediaPlayerEvents#event:TRACK_CHANGE_RENDERED</a></li><li><a href="MediaPlayerEvents.html#event:TTML_PARSED">MediaPlayerEvents#event:TTML_PARSED</a></li><li><a href="MediaPlayerEvents.html#event:TTML_TO_PARSE">MediaPlayerEvents#event:TTML_TO_PARSE</a></li><li><a href="MetricsReportingEvents.html#event:CMCD_DATA_GENERATED">MetricsReportingEvents#event:CMCD_DATA_GENERATED</a></li><li><a href="OfflineEvents.html#event:OFFLINE_RECORD_FINISHED">OfflineEvents#event:OFFLINE_RECORD_FINISHED</a></li><li><a href="OfflineEvents.html#event:OFFLINE_RECORD_LOADEDMETADATA">OfflineEvents#event:OFFLINE_RECORD_LOADEDMETADATA</a></li><li><a href="OfflineEvents.html#event:OFFLINE_RECORD_STARTED">OfflineEvents#event:OFFLINE_RECORD_STARTED</a></li><li><a href="OfflineEvents.html#event:OFFLINE_RECORD_STOPPED">OfflineEvents#event:OFFLINE_RECORD_STOPPED</a></li><li><a href="ProtectionEvents.html#event:KEY_ADDED">ProtectionEvents#event:KEY_ADDED</a></li><li><a href="ProtectionEvents.html#event:KEY_ERROR">ProtectionEvents#event:KEY_ERROR</a></li><li><a href="ProtectionEvents.html#event:KEY_MESSAGE">ProtectionEvents#event:KEY_MESSAGE</a></li><li><a href="ProtectionEvents.html#event:KEY_SESSION_CLOSED">ProtectionEvents#event:KEY_SESSION_CLOSED</a></li><li><a href="ProtectionEvents.html#event:KEY_SESSION_CREATED">ProtectionEvents#event:KEY_SESSION_CREATED</a></li><li><a href="ProtectionEvents.html#event:KEY_SESSION_REMOVED">ProtectionEvents#event:KEY_SESSION_REMOVED</a></li><li><a href="ProtectionEvents.html#event:KEY_STATUSES_CHANGED">ProtectionEvents#event:KEY_STATUSES_CHANGED</a></li><li><a href="ProtectionEvents.html#event:KEY_SYSTEM_SELECTED">ProtectionEvents#event:KEY_SYSTEM_SELECTED</a></li><li><a href="ProtectionEvents.html#event:LICENSE_REQUEST_COMPLETE">ProtectionEvents#event:LICENSE_REQUEST_COMPLETE</a></li><li><a href="ProtectionEvents.html#event:LICENSE_REQUEST_SENDING">ProtectionEvents#event:LICENSE_REQUEST_SENDING</a></li><li><a href="ProtectionEvents.html#event:PROTECTION_CREATED">ProtectionEvents#event:PROTECTION_CREATED</a></li><li><a href="ProtectionEvents.html#event:PROTECTION_DESTROYED">ProtectionEvents#event:PROTECTION_DESTROYED</a></li>
</ul>
</li>
<li class="dropdown">
<a href="global.html" class="dropdown-toggle" data-toggle="dropdown">Global<b class="caret"></b></a>
<ul class="dropdown-menu ">
<li><a href="global.html#LICENSE_SERVER_MANIFEST_CONFIGURATIONS">LICENSE_SERVER_MANIFEST_CONFIGURATIONS</a></li>
</ul>
</li>
</ul>
<div class="col-sm-3 col-md-3">
<form class="navbar-form" role="search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search" name="q" id="search-input">
<div class="input-group-btn">
<button class="btn btn-default" id="search-submit"><i class="glyphicon glyphicon-search"></i></button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="container" id="toc-content">
<div class="row">
<div class="col-md-12">
<div id="main">
<h1 class="page-title">Source: streaming/protection/controllers/ProtectionController.js</h1>
<section>
<article>
<pre
class="sunlight-highlight-javascript linenums">/**
* The copyright in this software is being made available under the BSD License,
* included below. This software may be subject to other third party and contributor
* rights, including patent rights, and no such rights are granted under this license.
*
* Copyright (c) 2013, Dash Industry Forum.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* * Neither the name of Dash Industry Forum nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import CommonEncryption from '../CommonEncryption';
import MediaCapability from '../vo/MediaCapability';
import KeySystemConfiguration from '../vo/KeySystemConfiguration';
import ProtectionErrors from '../errors/ProtectionErrors';
import DashJSError from '../../vo/DashJSError';
import LicenseRequest from '../vo/LicenseRequest';
import LicenseResponse from '../vo/LicenseResponse';
import {HTTPRequest} from '../../vo/metrics/HTTPRequest';
import Utils from '../../../core/Utils';
import Constants from '../../constants/Constants';
import FactoryMaker from '../../../core/FactoryMaker';
const NEEDKEY_BEFORE_INITIALIZE_RETRIES = 5;
const NEEDKEY_BEFORE_INITIALIZE_TIMEOUT = 500;
const LICENSE_SERVER_REQUEST_RETRIES = 3;
const LICENSE_SERVER_REQUEST_RETRY_INTERVAL = 1000;
const LICENSE_SERVER_REQUEST_DEFAULT_TIMEOUT = 8000;
/**
* @module ProtectionController
* @description Provides access to media protection information and functionality. Each
* ProtectionController manages a single {@link MediaPlayer.models.ProtectionModel}
* which encapsulates a set of protection information (EME APIs, selected key system,
* key sessions). The APIs of ProtectionController mostly align with the latest EME
* APIs. Key system selection is mostly automated when combined with app-overrideable
* functionality provided in {@link ProtectionKeyController}.
* @todo ProtectionController does almost all of its tasks automatically after init() is
* called. Applications might want more control over this process and want to go through
* each step manually (key system selection, session creation, session maintenance).
* This module can be accessed using the MediaPlayer API getProtectionController()
* @param {Object} config
*/
function ProtectionController(config) {
config = config || {};
const protectionKeyController = config.protectionKeyController;
let protectionModel = config.protectionModel;
const eventBus = config.eventBus;
const events = config.events;
const debug = config.debug;
const BASE64 = config.BASE64;
const constants = config.constants;
let needkeyRetries = [];
const cmcdModel = config.cmcdModel;
const settings = config.settings;
const customParametersModel = config.customParametersModel;
let instance,
logger,
pendingKeySessionsToHandle,
mediaInfoArr,
protDataSet,
sessionType,
robustnessLevel,
selectedKeySystem,
keySystemSelectionInProgress,
licenseXhrRequest,
licenseRequestRetryTimeout;
function setup() {
logger = debug.getLogger(instance);
pendingKeySessionsToHandle = [];
mediaInfoArr = [];
sessionType = 'temporary';
robustnessLevel = '';
licenseXhrRequest = null;
licenseRequestRetryTimeout = null;
eventBus.on(events.INTERNAL_KEY_MESSAGE, _onKeyMessage, instance);
eventBus.on(events.INTERNAL_KEY_STATUS_CHANGED, _onKeyStatusChanged, instance);
}
function checkConfig() {
if (!eventBus || !eventBus.hasOwnProperty('on') || !protectionKeyController || !protectionKeyController.hasOwnProperty('getSupportedKeySystemsFromContentProtection')) {
throw new Error('Missing config parameter(s)');
}
}
/**
* Initialize this protection system for a given media type.
*
* @param {StreamInfo} [mediaInfo] Media information
* @memberof module:ProtectionController
* @instance
*/
function initializeForMedia(mediaInfo) {
// Not checking here if a session for similar KS/KID combination is already created
// because still don't know which keysystem will be selected.
// Once Keysystem is selected and before creating the session, we will do that check
// so we create the strictly necessary DRM sessions
if (!mediaInfo) {
throw new Error('mediaInfo can not be null or undefined');
}
checkConfig();
mediaInfoArr.push(mediaInfo);
}
/**
* Once all mediaInfo objects have been added to our mediaInfoArray we can select a key system or check if the kid has changed and we need to trigger a new license request
* @memberof module:ProtectionController
* @instance
*/
function handleKeySystemFromManifest() {
if (!mediaInfoArr || mediaInfoArr.length === 0) {
return;
}
let supportedKeySystems = [];
mediaInfoArr.forEach((mInfo) => {
const currentKs = protectionKeyController.getSupportedKeySystemsFromContentProtection(mInfo.contentProtection, protDataSet, sessionType);
// We assume that the same key systems are signaled for each AS. We can use the first entry we found
if (currentKs.length > 0) {
if (supportedKeySystems.length === 0) {
supportedKeySystems = currentKs;
}
// Save config for creating key session once we selected a key system
pendingKeySessionsToHandle.push(currentKs);
}
})
if (supportedKeySystems && supportedKeySystems.length > 0) {
_selectKeySystemOrUpdateKeySessions(supportedKeySystems, true);
}
}
/**
* Selects a key system if we dont have any one yet. Otherwise we use the existing key system and trigger a new license request if the initdata has changed
* @param {array} supportedKs
* @private
*/
function _handleKeySystemFromPssh(supportedKs) {
pendingKeySessionsToHandle.push(supportedKs);
_selectKeySystemOrUpdateKeySessions(supportedKs, false);
}
/**
* Select the key system or update one of our existing key sessions
* @param {array} supportedKs
* @param {boolean} fromManifest
* @private
*/
function _selectKeySystemOrUpdateKeySessions(supportedKs, fromManifest) {
// First time, so we need to select a key system
if (!selectedKeySystem && !keySystemSelectionInProgress) {
_selectInitialKeySystem(supportedKs, fromManifest);
}
// We already selected a key system. We only need to trigger a new license exchange if the init data has changed
else if (selectedKeySystem) {
_handleKeySessions();
}
}
/**
* We do not have a key system yet. Select one
* @param {array} supportedKs
* @param {boolean} fromManifest
* @private
*/
function _selectInitialKeySystem(supportedKs, fromManifest) {
if (!keySystemSelectionInProgress) {
keySystemSelectionInProgress = true;
const requestedKeySystems = [];
// Reorder key systems according to priority order provided in protectionData
supportedKs = supportedKs.sort((ksA, ksB) => {
let indexA = (protDataSet && protDataSet[ksA.ks.systemString] && protDataSet[ksA.ks.systemString].priority >= 0) ? protDataSet[ksA.ks.systemString].priority : supportedKs.length;
let indexB = (protDataSet && protDataSet[ksB.ks.systemString] && protDataSet[ksB.ks.systemString].priority >= 0) ? protDataSet[ksB.ks.systemString].priority : supportedKs.length;
return indexA - indexB;
});
// Add all key systems to our request list since we have yet to select a key system
for (let i = 0; i < supportedKs.length; i++) {
const keySystemConfiguration = _getKeySystemConfiguration(supportedKs[i]);
requestedKeySystems.push({
ks: supportedKs[i].ks,
configs: [keySystemConfiguration],
protData: supportedKs[i].protData
});
}
let keySystemAccess;
protectionModel.requestKeySystemAccess(requestedKeySystems)
.then((event) => {
keySystemAccess = event.data;
let selectedSystemString = keySystemAccess.mksa && keySystemAccess.mksa.selectedSystemString ? keySystemAccess.mksa.selectedSystemString : keySystemAccess.keySystem.systemString;
logger.info('DRM: KeySystem Access Granted for system string (' + selectedSystemString + ')! Selecting key system...');
return protectionModel.selectKeySystem(keySystemAccess);
})
.then((keySystem) => {
selectedKeySystem = keySystem;
keySystemSelectionInProgress = false;
if (!protectionModel) {
return;
}
eventBus.trigger(events.KEY_SYSTEM_SELECTED, { data: keySystemAccess });
// Set server certificate from protData
const protData = _getProtDataForKeySystem(selectedKeySystem);
if (protData && protData.serverCertificate && protData.serverCertificate.length > 0) {
protectionModel.setServerCertificate(BASE64.decodeArray(protData.serverCertificate).buffer);
}
_handleKeySessions();
})
.catch((event) => {
selectedKeySystem = null;
keySystemSelectionInProgress = false;
if (!fromManifest) {
eventBus.trigger(events.KEY_SYSTEM_SELECTED, {
data: null,
error: new DashJSError(ProtectionErrors.KEY_SYSTEM_ACCESS_DENIED_ERROR_CODE, ProtectionErrors.KEY_SYSTEM_ACCESS_DENIED_ERROR_MESSAGE + 'Error selecting key system! -- ' + event.error)
});
}
})
}
}
/**
* If we have already selected a key system we only need to create a new key session and issue a new license request if the init data has changed.
* @private
*/
function _handleKeySessions() {
// Create key sessions for the different AdaptationSets
let ksIdx;
for (let i = 0; i < pendingKeySessionsToHandle.length; i++) {
for (ksIdx = 0; ksIdx < pendingKeySessionsToHandle[i].length; ksIdx++) {
if (selectedKeySystem === pendingKeySessionsToHandle[i][ksIdx].ks) {
const current = pendingKeySessionsToHandle[i][ksIdx]
_loadOrCreateKeySession(current)
break;
}
}
}
pendingKeySessionsToHandle = [];
}
/**
* Loads an existing key session if we already have a session id. Otherwise we create a new key session
* @param {object} keySystemInfo
* @private
*/
function _loadOrCreateKeySession(keySystemInfo) {
// Clearkey
if (protectionKeyController.isClearKey(selectedKeySystem)) {
// For Clearkey: if parameters for generating init data was provided by the user, use them for generating
// initData and overwrite possible initData indicated in encrypted event (EME)
if (keySystemInfo.protData && keySystemInfo.protData.hasOwnProperty('clearkeys') && Object.keys(keySystemInfo.protData.clearkeys).length !== 0) {
const initData = { kids: Object.keys(keySystemInfo.protData.clearkeys) };
keySystemInfo.initData = new TextEncoder().encode(JSON.stringify(initData));
}
}
// Reuse existing KeySession
if (keySystemInfo.sessionId) {
// Load MediaKeySession with sessionId
loadKeySession(keySystemInfo);
}
// Create a new KeySession
else if (keySystemInfo.initData !== null) {
// Create new MediaKeySession with initData
createKeySession(keySystemInfo);
}
}
/**
* Loads a key session with the given session ID from persistent storage. This essentially creates a new key session
*
* @param {object} ksInfo
* @memberof module:ProtectionController
* @instance
* @fires ProtectionController#KeySessionCreated
* @ignore
*/
function loadKeySession(keySystemInfo) {
checkConfig();
protectionModel.loadKeySession(keySystemInfo);
}
/**
* Create a new key session associated with the given initialization data from the MPD or from the PSSH box in the media
* For the latest version of the EME a request is generated. Once this request is ready we get notified via the INTERNAL_KEY_MESSAGE event
* @param {ArrayBuffer} initData the initialization data
* @param {Uint8Array} cdmData the custom data to provide to licenser
* @memberof module:ProtectionController
* @instance
* @fires ProtectionController#KeySessionCreated
* @ignore
*/
function createKeySession(keySystemInfo) {
const initDataForKS = CommonEncryption.getPSSHForKeySystem(selectedKeySystem, keySystemInfo ? keySystemInfo.initData : null);
if (initDataForKS) {
// Check for duplicate key id
if (_isKeyIdDuplicate(keySystemInfo.keyId)) {
return;
}
// Check for duplicate initData
if (_isInitDataDuplicate(initDataForKS)) {
return;
}
try {
keySystemInfo.initData = initDataForKS;
protectionModel.createKeySession(keySystemInfo);
} catch (error) {
eventBus.trigger(events.KEY_SESSION_CREATED, {
data: null,
error: new DashJSError(ProtectionErrors.KEY_SESSION_CREATED_ERROR_CODE, ProtectionErrors.KEY_SESSION_CREATED_ERROR_MESSAGE + error.message)
});
}
} else if (keySystemInfo && keySystemInfo.initData) {
protectionModel.createKeySession(keySystemInfo);
} else {
eventBus.trigger(events.KEY_SESSION_CREATED, {
data: null,
error: new DashJSError(ProtectionErrors.KEY_SESSION_CREATED_ERROR_CODE, ProtectionErrors.KEY_SESSION_CREATED_ERROR_MESSAGE + 'Selected key system is ' + (selectedKeySystem ? selectedKeySystem.systemString : null) + '. needkey/encrypted event contains no initData corresponding to that key system!')
});
}
}
/**
* Returns the protectionData for a specific keysystem as specified by the application.
* @param {object} keySystem
* @return {object | null}
* @private
*/
function _getProtDataForKeySystem(keySystem) {
if (keySystem) {
const keySystemString = keySystem.systemString;
if (protDataSet) {
return (keySystemString in protDataSet) ? protDataSet[keySystemString] : null;
}
}
return null;
}
/**
* Removes all entries from the mediaInfoArr
*/
function clearMediaInfoArray() {
mediaInfoArr = [];
}
/**
* Returns a set of supported key systems and CENC initialization data
* from the given array of ContentProtection elements. Only
* key systems that are supported by this player will be returned.
* Key systems are returned in priority order (highest first).
*
* @param {Array.<Object>} cps - array of content protection elements parsed
* from the manifest
* @returns {Array.<Object>} array of objects indicating which supported key
* systems were found. Empty array is returned if no
* supported key systems were found
* @memberof module:ProtectionKeyController
* @instance
* @ignore
*/
function getSupportedKeySystemsFromContentProtection(cps) {
checkConfig();
return protectionKeyController.getSupportedKeySystemsFromContentProtection(cps, protDataSet, sessionType);
}
/**
* Checks if a session has already created for the provided key id
* @param {string} keyId
* @return {boolean}
* @private
*/
function _isKeyIdDuplicate(keyId) {
if (!keyId) {
return false;
}
try {
const sessions = protectionModel.getSessions();
for (let i = 0; i < sessions.length; i++) {
if (sessions[i].getKeyId() === keyId) {
return true;
}
}
return false;
} catch (e) {
return false;
}
}
/**
* Checks if the provided init data is equal to one of the existing init data values
* @param {any} initDataForKS
* @return {boolean}
* @private
*/
function _isInitDataDuplicate(initDataForKS) {
if (!initDataForKS) {
return false;
}
try {
const currentInitData = protectionModel.getAllInitData();
for (let i = 0; i < currentInitData.length; i++) {
if (protectionKeyController.initDataEquals(initDataForKS, currentInitData[i])) {
logger.debug('DRM: Ignoring initData because we have already seen it!');
return true;
}
}
return false;
} catch (e) {
return false;
}
}
/**
* Removes the given key session from persistent storage and closes the session
* as if {@link ProtectionController#closeKeySession}
* was called
*
* @param {SessionToken} sessionToken the session
* token
* @memberof module:ProtectionController
* @instance
* @fires ProtectionController#KeySessionRemoved
* @fires ProtectionController#KeySessionClosed
* @ignore
*/
function removeKeySession(sessionToken) {
checkConfig();
protectionModel.removeKeySession(sessionToken);
}
/**
* Closes the key session and releases all associated decryption keys. These
* keys will no longer be available for decrypting media
*
* @param {SessionToken} sessionToken the session
* token
* @memberof module:ProtectionController
* @instance
* @fires ProtectionController#KeySessionClosed
* @ignore
*/
function closeKeySession(sessionToken) {
checkConfig();
protectionModel.closeKeySession(sessionToken);
}
/**
* Sets a server certificate for use by the CDM when signing key messages
* intended for a particular license server. This will fire
* an error event if a key system has not yet been selected.
*
* @param {ArrayBuffer} serverCertificate a CDM-specific license server
* certificate
* @memberof module:ProtectionController
* @instance
* @fires ProtectionController#ServerCertificateUpdated
*/
function setServerCertificate(serverCertificate) {
checkConfig();
protectionModel.setServerCertificate(serverCertificate);
}
/**
* Associate this protection system with the given HTMLMediaElement. This
* causes the system to register for needkey/encrypted events from the given
* element and provides a destination for setting of MediaKeys
*
* @param {HTMLMediaElement} element the media element to which the protection
* system should be associated
* @memberof module:ProtectionController
* @instance
*/
function setMediaElement(element) {
checkConfig();
if (element) {
protectionModel.setMediaElement(element);
eventBus.on(events.NEED_KEY, _onNeedKey, instance);
} else if (element === null) {
protectionModel.setMediaElement(element);
eventBus.off(events.NEED_KEY, _onNeedKey, instance);
}
}
/**
* Sets the session type to use when creating key sessions. Either "temporary" or
* "persistent-license". Default is "temporary".
*
* @param {string} value the session type
* @memberof module:ProtectionController
* @instance
*/
function setSessionType(value) {
sessionType = value;
}
/**
* Sets the robustness level for video and audio capabilities. Optional to remove Chrome warnings.
* Possible values are SW_SECURE_CRYPTO, SW_SECURE_DECODE, HW_SECURE_CRYPTO, HW_SECURE_CRYPTO, HW_SECURE_DECODE, HW_SECURE_ALL.
*
* @param {string} level the robustness level
* @memberof module:ProtectionController
* @instance
*/
function setRobustnessLevel(level) {
robustnessLevel = level;
}
/**
* Attach KeySystem-specific data to use for license acquisition with EME
*
* @param {Object} data an object containing property names corresponding to
* key system name strings (e.g. "org.w3.clearkey") and associated values
* being instances of {@link ProtectionData}
* @memberof module:ProtectionController
* @instance
* @ignore
*/
function setProtectionData(data) {
protDataSet = data;
protectionKeyController.setProtectionData(data);
}
/**
* Stop method is called when current playback is stopped/resetted.
*
* @memberof module:ProtectionController
* @instance
*/
function stop() {
_abortLicenseRequest();
if (protectionModel) {
protectionModel.stop();
}
}
/**
* Destroys all protection data associated with this protection set. This includes
* deleting all key sessions. In the case of persistent key sessions, the sessions
* will simply be unloaded and not deleted. Additionally, if this protection set is
* associated with a HTMLMediaElement, it will be detached from that element.
*
* @memberof module:ProtectionController
* @instance
* @ignore
*/
function reset() {
eventBus.off(events.INTERNAL_KEY_MESSAGE, _onKeyMessage, instance);
eventBus.off(events.INTERNAL_KEY_STATUS_CHANGED, _onKeyStatusChanged, instance);
checkConfig();
_abortLicenseRequest();
setMediaElement(null);
selectedKeySystem = null;
keySystemSelectionInProgress = false;
if (protectionModel) {
protectionModel.reset();
protectionModel = null;
}
needkeyRetries.forEach(retryTimeout => clearTimeout(retryTimeout));
needkeyRetries = [];
mediaInfoArr = [];
pendingKeySessionsToHandle = [];
}
/**
* Returns an object corresponding to the EME MediaKeySystemConfiguration dictionary
* @param {object} keySystem
* @return {KeySystemConfiguration}
* @private
*/
function _getKeySystemConfiguration(keySystemData) {
const protData = keySystemData.protData;
const audioCapabilities = [];
const videoCapabilities = [];
const audioRobustness = (protData && protData.audioRobustness && protData.audioRobustness.length > 0) ? protData.audioRobustness : robustnessLevel;
const videoRobustness = (protData && protData.videoRobustness && protData.videoRobustness.length > 0) ? protData.videoRobustness : robustnessLevel;
const ksSessionType = keySystemData.sessionType;
const distinctiveIdentifier = (protData && protData.distinctiveIdentifier) ? protData.distinctiveIdentifier : 'optional';
const persistentState = (protData && protData.persistentState) ? protData.persistentState : (ksSessionType === 'temporary') ? 'optional' : 'required';
mediaInfoArr.forEach((media) => {
if (media.type === constants.AUDIO) {
audioCapabilities.push(new MediaCapability(media.codec, audioRobustness));
} else if (media.type === constants.VIDEO) {
videoCapabilities.push(new MediaCapability(media.codec, videoRobustness));
}
});
return new KeySystemConfiguration(
audioCapabilities, videoCapabilities, distinctiveIdentifier,
persistentState,
[ksSessionType]);
}
/**
* Event handler for when the status of the key has changed
* @param {object} e
* @private
*/
function _onKeyStatusChanged(e) {
if (e.error) {
eventBus.trigger(events.KEY_STATUSES_CHANGED, { data: null, error: e.error });
} else {
logger.debug('DRM: key status = ' + e.status);
}
}
/**
* Event handler for the key message event. Once we have a key message we can issue a license request
* @param {object} e
* @private
*/
function _onKeyMessage(e) {
logger.debug('DRM: onKeyMessage');
// Dispatch event to applications indicating we received a key message
const keyMessage = e.data;
eventBus.trigger(events.KEY_MESSAGE, { data: keyMessage });
const messageType = (keyMessage.messageType) ? keyMessage.messageType : 'license-request';
const message = keyMessage.message;
const sessionToken = keyMessage.sessionToken;
const protData = _getProtDataForKeySystem(selectedKeySystem);
const licenseServerModelInstance = protectionKeyController.getLicenseServerModelInstance(selectedKeySystem, protData, messageType);
const eventData = { sessionToken: sessionToken, messageType: messageType };
// Ensure message from CDM is not empty
if (!message || message.byteLength === 0) {
_sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_NO_CHALLENGE_ERROR_CODE, ProtectionErrors.MEDIA_KEY_MESSAGE_NO_CHALLENGE_ERROR_MESSAGE));
return;
}
// Message not destined for license server
if (!licenseServerModelInstance) {
logger.debug('DRM: License server request not required for this message (type = ' + e.data.messageType + '). Session ID = ' + sessionToken.getSessionId());
_sendLicenseRequestCompleteEvent(eventData);
return;
}
// Perform any special handling for ClearKey
if (protectionKeyController.isClearKey(selectedKeySystem)) {
const clearkeys = protectionKeyController.processClearKeyLicenseRequest(selectedKeySystem, protData, message);
if (clearkeys && clearkeys.keyPairs && clearkeys.keyPairs.length > 0) {
logger.debug('DRM: ClearKey license request handled by application!');
_sendLicenseRequestCompleteEvent(eventData);
protectionModel.updateKeySession(sessionToken, clearkeys);
return;
}
}
// In all other cases we have to make a license request
_issueLicenseRequest(keyMessage, licenseServerModelInstance, protData);
}
/**
* Notify other classes that the license request was completed
* @param {object} data
* @param {object} error
* @private
*/
function _sendLicenseRequestCompleteEvent(data, error) {
eventBus.trigger(events.LICENSE_REQUEST_COMPLETE, { data: data, error: error });
}
/**
* Start issuing a license request
* @param {object} keyMessage
* @param {object} licenseServerData
* @param {object} protData
* @private
*/
function _issueLicenseRequest(keyMessage, licenseServerData, protData) {
const sessionToken = keyMessage.sessionToken;
const messageType = (keyMessage.messageType) ? keyMessage.messageType : 'license-request';
const eventData = { sessionToken: sessionToken, messageType: messageType };
const keySystemString = selectedKeySystem ? selectedKeySystem.systemString : null;
// Determine license server URL
let url = _getLicenseServerUrl(protData, messageType, sessionToken, keyMessage, licenseServerData);
// Ensure valid license server URL
if (!url) {
_sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_NO_LICENSE_SERVER_URL_ERROR_CODE, ProtectionErrors.MEDIA_KEY_MESSAGE_NO_LICENSE_SERVER_URL_ERROR_MESSAGE));
return;
}
// Set optional XMLHttpRequest headers from protection data and message
const reqHeaders = {};
let withCredentials = false;
if (protData) {
_updateHeaders(reqHeaders, protData.httpRequestHeaders);
}
const message = keyMessage.message;
const headersFromMessage = selectedKeySystem.getRequestHeadersFromMessage(message);
_updateHeaders(reqHeaders, headersFromMessage);
Object.keys(reqHeaders).forEach((key) => {
if ('authorization' === key.toLowerCase()) {
withCredentials = true;
}
});
// Overwrite withCredentials property from protData if present
if (protData && typeof protData.withCredentials == 'boolean') {
withCredentials = protData.withCredentials;
}
const onLoad = function (xhr) {
if (!protectionModel) {
return;
}
if (xhr.status >= 200 && xhr.status <= 299) {
const responseHeaders = Utils.parseHttpHeaders(xhr.getAllResponseHeaders ? xhr.getAllResponseHeaders() : null);
let licenseResponse = new LicenseResponse(xhr.responseURL, responseHeaders, xhr.response);
const licenseResponseFilters = customParametersModel.getLicenseResponseFilters();
_applyFilters(licenseResponseFilters, licenseResponse)
.then(() => {
const licenseMessage = licenseServerData.getLicenseMessage(licenseResponse.data, keySystemString, messageType);
if (licenseMessage !== null) {
_sendLicenseRequestCompleteEvent(eventData);
protectionModel.updateKeySession(sessionToken, licenseMessage);
} else {
_reportError(xhr, eventData, keySystemString, messageType, licenseServerData);
}
});
} else {
_reportError(xhr, eventData, keySystemString, messageType, licenseServerData);
}
};
const onAbort = function (xhr) {
_sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_CODE,
ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_MESSAGE + keySystemString + ' update, XHR aborted. status is "' +
xhr.statusText + '" (' + xhr.status + '), readyState is ' + xhr.readyState));
};
const onError = function (xhr) {
_sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_CODE,
ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_MESSAGE + keySystemString + ' update, XHR error. status is "' +
xhr.statusText + '" (' + xhr.status + '), readyState is ' + xhr.readyState));
};
const reqPayload = selectedKeySystem.getLicenseRequestFromMessage(message);
const reqMethod = licenseServerData.getHTTPMethod(messageType);
const responseType = licenseServerData.getResponseType(keySystemString, messageType);
const timeout = protData && !isNaN(protData.httpTimeout) ? protData.httpTimeout : LICENSE_SERVER_REQUEST_DEFAULT_TIMEOUT;
const sessionId = sessionToken.getSessionId() || null;
let licenseRequest = new LicenseRequest(url, reqMethod, responseType, reqHeaders, withCredentials, messageType, sessionId, reqPayload);
const retryAttempts = !isNaN(settings.get().streaming.retryAttempts[HTTPRequest.LICENSE]) ? settings.get().streaming.retryAttempts[HTTPRequest.LICENSE] : LICENSE_SERVER_REQUEST_RETRIES;
const licenseRequestFilters = customParametersModel.getLicenseRequestFilters();
_applyFilters(licenseRequestFilters, licenseRequest)
.then(() => {
_doLicenseRequest(licenseRequest, retryAttempts, timeout, onLoad, onAbort, onError);
});
}
/**
* Implement license requests with a retry mechanism to avoid temporary network issues to affect playback experience
* @param {object} request
* @param {number} retriesCount
* @param {number} timeout
* @param {function} onLoad
* @param {function} onAbort
* @param {function} onError
* @private
*/
function _doLicenseRequest(request, retriesCount, timeout, onLoad, onAbort, onError) {
const xhr = new XMLHttpRequest();
if (settings.get().streaming.cmcd && settings.get().streaming.cmcd.enabled) {
const cmcdMode = settings.get().streaming.cmcd.mode;
if (cmcdMode === Constants.CMCD_MODE_QUERY) {
const cmcdParams = cmcdModel.getQueryParameter({
url: request.url,
type: HTTPRequest.LICENSE
});
if (cmcdParams) {
request.url = Utils.addAditionalQueryParameterToUrl(request.url, [cmcdParams]);
}
}
}
xhr.open(request.method, request.url, true);
xhr.responseType = request.responseType;
xhr.withCredentials = request.withCredentials;
if (timeout > 0) {
xhr.timeout = timeout;
}
for (const key in request.headers) {
xhr.setRequestHeader(key, request.headers[key]);
}
if (settings.get().streaming.cmcd && settings.get().streaming.cmcd.enabled) {
const cmcdMode = settings.get().streaming.cmcd.mode;
if (cmcdMode === Constants.CMCD_MODE_HEADER) {
const cmcdHeaders = cmcdModel.getHeaderParameters({
url: request.url,
type: HTTPRequest.LICENSE
});
if (cmcdHeaders) {
for (const header in cmcdHeaders) {
let value = cmcdHeaders[header];
if (value) {
xhr.setRequestHeader(header, value);
}
}
}
}
}
const _retryRequest = function () {
// fail silently and retry
retriesCount--;
const retryInterval = !isNaN(settings.get().streaming.retryIntervals[HTTPRequest.LICENSE]) ? settings.get().streaming.retryIntervals[HTTPRequest.LICENSE] : LICENSE_SERVER_REQUEST_RETRY_INTERVAL;
licenseRequestRetryTimeout = setTimeout(function () {
_doLicenseRequest(request, retriesCount, timeout, onLoad, onAbort, onError);
}, retryInterval);
};
xhr.onload = function () {
licenseXhrRequest = null;
if (this.status >= 200 && this.status <= 299 || retriesCount <= 0) {
onLoad(this);
} else {
logger.warn('License request failed (' + this.status + '). Retrying it... Pending retries: ' + retriesCount);
_retryRequest();
}
};
xhr.ontimeout = xhr.onerror = function () {
licenseXhrRequest = null;
if (retriesCount <= 0) {
onError(this);
} else {
logger.warn('License request network request failed . Retrying it... Pending retries: ' + retriesCount);
_retryRequest();
}
};
xhr.onabort = function () {
onAbort(this);
};
// deprecated, to be removed
eventBus.trigger(events.LICENSE_REQUEST_SENDING, {