diff --git a/index.d.ts b/index.d.ts index 1198e8a35e..6a66fe6948 100644 --- a/index.d.ts +++ b/index.d.ts @@ -43,7 +43,7 @@ declare namespace dashjs { interface ProtectionController { initializeForMedia(mediaInfo: ProtectionMediaInfo): void; - clearMediaInfoArrayByStreamId(streamId: string): void; + clearMediaInfoArray(): void; createKeySession(initData: ArrayBuffer, cdmData: Uint8Array): void; @@ -175,6 +175,7 @@ declare namespace dashjs { }, protection?: { keepProtectionMediaKeys?: boolean, + ignoreEmeEncryptedEvent?: boolean }, buffer?: { enableSeekDecorrelationFix?: boolean, diff --git a/samples/drm/dashif-laurl.html b/samples/drm/dashif-laurl.html new file mode 100644 index 0000000000..946172f947 --- /dev/null +++ b/samples/drm/dashif-laurl.html @@ -0,0 +1,95 @@ + + + + + License server via MPD example + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Widevine DRM instantiation example

+

This example shows how to specify the license server url as part of the MPD using + 'dashif:laurl'.

+

For a detailed explanation on this checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ + + + + + diff --git a/samples/samples.json b/samples/samples.json index 898d32f835..e781c28f14 100644 --- a/samples/samples.json +++ b/samples/samples.json @@ -389,7 +389,22 @@ "Video", "Audio" ] + }, + { + "title": "License server via MPD", + "description": "This example shows how to specify the license server url as part of the MPD using 'dashif:laurl'", + "href": "drm/dashif-laurl.html", + "image": "lib/img/tos-3.jpg", + "labels": [ + "VoD", + "DRM", + "Widevine", + "Playready", + "Video", + "Audio" + ] } + ] }, { diff --git a/src/core/Settings.js b/src/core/Settings.js index 3151e73fe5..835c8785d2 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -521,6 +521,8 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * Set the value for the ProtectionController and MediaKeys life cycle. * * If true, the ProtectionController and then created MediaKeys and MediaKeySessions will be preserved during the MediaPlayer lifetime. + * @property {boolean} ignoreEmeEncryptedEvent + * If set to true the player will ignore "encrypted" and "needkey" events thrown by the EME. */ /** @@ -773,7 +775,8 @@ function Settings() { applyServiceDescription: true }, protection: { - keepProtectionMediaKeys: false + keepProtectionMediaKeys: false, + ignoreEmeEncryptedEvent: false }, buffer: { enableSeekDecorrelationFix: false, diff --git a/src/streaming/Stream.js b/src/streaming/Stream.js index 7ccdaa7d20..70ca834892 100644 --- a/src/streaming/Stream.js +++ b/src/streaming/Stream.js @@ -715,7 +715,7 @@ function Stream(config) { if (protectionController) { // Need to check if streamProcessors exists because streamProcessors // could be cleared in case an error is detected while initializing DRM keysystem - protectionController.clearMediaInfoArrayByStreamId(getId()); + protectionController.clearMediaInfoArray(); for (let i = 0; i < ln && streamProcessors[i]; i++) { const type = streamProcessors[i].getType(); const mediaInfo = streamProcessors[i].getMediaInfo(); diff --git a/src/streaming/constants/ProtectionConstants.js b/src/streaming/constants/ProtectionConstants.js index b3dccfaad5..a47c1c0c6d 100644 --- a/src/streaming/constants/ProtectionConstants.js +++ b/src/streaming/constants/ProtectionConstants.js @@ -40,6 +40,9 @@ class ProtectionConstants { this.CLEARKEY_KEYSTEM_STRING = 'org.w3.clearkey'; this.WIDEVINE_KEYSTEM_STRING = 'com.widevine.alpha'; this.PLAYREADY_KEYSTEM_STRING = 'com.microsoft.playready'; + this.INITIALIZATION_DATA_TYPE_CENC = 'cenc'; + this.INITIALIZATION_DATA_TYPE_KEYIDS = 'keyids' + this.INITIALIZATION_DATA_TYPE_WEBM = 'webm' } constructor () { diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 9e2e91bb76..3b08bf3669 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -114,7 +114,7 @@ function StreamController() { } function initialize(autoPl, protData) { - checkConfig(); + _checkConfig(); autoPlay = autoPl; protectionData = protData; @@ -496,7 +496,7 @@ function StreamController() { _handleOuterPeriodSeek(e, seekToStream); } - createPlaylistMetrics(PlayList.SEEK_START_REASON); + _createPlaylistMetrics(PlayList.SEEK_START_REASON); } /** @@ -698,7 +698,7 @@ function StreamController() { if (isNaN(initialBufferLevel) || initialBufferLevel <= playbackController.getBufferLevel() || (adapter.getIsDynamic() && initialBufferLevel > playbackController.getLiveDelay())) { initialPlayback = false; - createPlaylistMetrics(PlayList.INITIAL_PLAYOUT_START_REASON); + _createPlaylistMetrics(PlayList.INITIAL_PLAYOUT_START_REASON); playbackController.play(); } } @@ -752,9 +752,9 @@ function StreamController() { * @private */ function _onPlaybackStarted( /*e*/) { - logger.debug('[onPlaybackStarted]'); - if (!initialPlayback && isPaused) { - createPlaylistMetrics(PlayList.RESUME_FROM_PAUSE_START_REASON); + logger.debug('[onPlaybackStarted]'); + if (!initialPlayback && isPaused) { + _createPlaylistMetrics(PlayList.RESUME_FROM_PAUSE_START_REASON); } if (initialPlayback) { initialPlayback = false; @@ -1233,7 +1233,7 @@ function StreamController() { dashMetrics.addPlayList(); } - function createPlaylistMetrics(startReason) { + function _createPlaylistMetrics(startReason) { dashMetrics.createPlaylistMetrics(playbackController.getTime() * 1000, startReason); } @@ -1324,7 +1324,7 @@ function StreamController() { return null; } - function checkConfig() { + function _checkConfig() { if (!manifestLoader || !manifestLoader.hasOwnProperty('load') || !timelineConverter || !timelineConverter.hasOwnProperty('initialize') || !timelineConverter.hasOwnProperty('reset') || !timelineConverter.hasOwnProperty('getClientTimeOffset') || !manifestModel || !errHandler || !dashMetrics || !playbackController) { @@ -1332,19 +1332,19 @@ function StreamController() { } } - function checkInitialize() { + function _checkInitialize() { if (!manifestUpdater || !manifestUpdater.hasOwnProperty('setManifest')) { throw new Error('initialize function has to be called previously'); } } function load(url) { - checkConfig(); + _checkConfig(); manifestLoader.load(url); } function loadWithManifest(manifest) { - checkInitialize(); + _checkInitialize(); manifestUpdater.setManifest(manifest); } @@ -1446,7 +1446,7 @@ function StreamController() { } function reset() { - checkConfig(); + _checkConfig(); timeSyncController.reset(); diff --git a/src/streaming/protection/CommonEncryption.js b/src/streaming/protection/CommonEncryption.js index fd3ff98f75..bc92cdd5ef 100644 --- a/src/streaming/protection/CommonEncryption.js +++ b/src/streaming/protection/CommonEncryption.js @@ -29,6 +29,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +const LICENSE_SERVER_MANIFEST_CONFIGURATIONS = { + attributes: ['Laurl','laurl'], + prefixes: ['clearkey', 'dashif'] +}; + /** * @class * @ignore @@ -211,6 +216,54 @@ class CommonEncryption { return pssh; } + + static getLicenseServerUrlFromMediaInfo(mediaInfo, schemeIdUri) { + try { + + if (!mediaInfo || mediaInfo.length === 0) { + return null; + } + + let i = 0; + let licenseServer = null; + + while (i < mediaInfo.length && !licenseServer) { + const info = mediaInfo[i]; + + if (info && info.contentProtection && info.contentProtection.length > 0) { + const targetProtectionData = info.contentProtection.filter((cp) => { + return cp.schemeIdUri && cp.schemeIdUri === schemeIdUri; + }); + + if (targetProtectionData && targetProtectionData.length > 0) { + let j = 0; + while (j < targetProtectionData.length && !licenseServer) { + const ckData = targetProtectionData[j]; + let k = 0; + while (k < LICENSE_SERVER_MANIFEST_CONFIGURATIONS.attributes.length && !licenseServer) { + let l = 0; + const attribute = LICENSE_SERVER_MANIFEST_CONFIGURATIONS.attributes[k]; + while (l < LICENSE_SERVER_MANIFEST_CONFIGURATIONS.prefixes.length && !licenseServer) { + const prefix = LICENSE_SERVER_MANIFEST_CONFIGURATIONS.prefixes[l]; + if (ckData[attribute] && ckData[attribute].__prefix && ckData[attribute].__prefix === prefix && ckData[attribute].__text) { + licenseServer = ckData[attribute].__text; + } + l += 1; + } + k += 1; + } + j += 1; + } + } + } + i += 1; + } + return licenseServer; + } catch + (e) { + return null; + } + } } export default CommonEncryption; diff --git a/src/streaming/protection/Protection.js b/src/streaming/protection/Protection.js index 81585fa65f..f7d0b9c7a0 100644 --- a/src/streaming/protection/Protection.js +++ b/src/streaming/protection/Protection.js @@ -119,7 +119,7 @@ function Protection() { protectionKeyController.setConfig({ debug: config.debug, BASE64: config.BASE64 }); protectionKeyController.initialize(); - let protectionModel = getProtectionModel(config); + let protectionModel = _getProtectionModel(config); if (!controller && protectionModel) {//TODO add ability to set external controller if still needed at all? controller = ProtectionController(context).create({ @@ -138,7 +138,7 @@ function Protection() { return controller; } - function getProtectionModel(config) { + function _getProtectionModel(config) { const debug = config.debug; const logger = debug.getLogger(instance); const eventBus = config.eventBus; @@ -149,19 +149,19 @@ function Protection() { (!videoElement || videoElement.mediaKeys !== undefined)) { logger.info('EME detected on this user agent! (ProtectionModel_21Jan2015)'); return ProtectionModel_21Jan2015(context).create({ debug: debug, eventBus: eventBus, events: config.events }); - } else if (getAPI(videoElement, APIS_ProtectionModel_3Feb2014)) { + } else if (_getAPI(videoElement, APIS_ProtectionModel_3Feb2014)) { logger.info('EME detected on this user agent! (ProtectionModel_3Feb2014)'); - return ProtectionModel_3Feb2014(context).create({ debug: debug, eventBus: eventBus, events: config.events, api: getAPI(videoElement, APIS_ProtectionModel_3Feb2014) }); - } else if (getAPI(videoElement, APIS_ProtectionModel_01b)) { + return ProtectionModel_3Feb2014(context).create({ debug: debug, eventBus: eventBus, events: config.events, api: _getAPI(videoElement, APIS_ProtectionModel_3Feb2014) }); + } else if (_getAPI(videoElement, APIS_ProtectionModel_01b)) { logger.info('EME detected on this user agent! (ProtectionModel_01b)'); - return ProtectionModel_01b(context).create({ debug: debug, eventBus: eventBus, errHandler: errHandler, events: config.events, api: getAPI(videoElement, APIS_ProtectionModel_01b) }); + return ProtectionModel_01b(context).create({ debug: debug, eventBus: eventBus, errHandler: errHandler, events: config.events, api: _getAPI(videoElement, APIS_ProtectionModel_01b) }); } else { logger.warn('No supported version of EME detected on this user agent! - Attempts to play encrypted content will fail!'); return null; } } - function getAPI(videoElement, apis) { + function _getAPI(videoElement, apis) { for (let i = 0; i < apis.length; i++) { const api = apis[i]; // detect if api is supported by browser @@ -177,7 +177,7 @@ function Protection() { } instance = { - createProtectionSystem: createProtectionSystem + createProtectionSystem }; return instance; diff --git a/src/streaming/protection/ProtectionEvents.js b/src/streaming/protection/ProtectionEvents.js index 60aad835e3..de5cf53f0d 100644 --- a/src/streaming/protection/ProtectionEvents.js +++ b/src/streaming/protection/ProtectionEvents.js @@ -49,13 +49,6 @@ class ProtectionEvents extends EventsBase { */ this.INTERNAL_KEY_MESSAGE = 'internalKeyMessage'; - /** - * Event ID for events delivered when a key system selection procedure - * completes - * @ignore - */ - this.INTERNAL_KEY_SYSTEM_SELECTED = 'internalKeySystemSelected'; - /** * Event ID for events delivered when the status of one decryption keys has changed * @ignore diff --git a/src/streaming/protection/controllers/ProtectionController.js b/src/streaming/protection/controllers/ProtectionController.js index 9477a8dcad..1afe93d93f 100644 --- a/src/streaming/protection/controllers/ProtectionController.js +++ b/src/streaming/protection/controllers/ProtectionController.js @@ -39,6 +39,7 @@ 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; @@ -78,23 +79,26 @@ function ProtectionController(config) { let instance, logger, - pendingNeedKeyData, + pendingKeySystemData, mediaInfoArr, protDataSet, sessionType, robustnessLevel, - keySystem, + selectedKeySystem, + keySystemSelectionInProgress, licenseRequestFilters, licenseResponseFilters; function setup() { logger = debug.getLogger(instance); - pendingNeedKeyData = []; + pendingKeySystemData = []; mediaInfoArr = []; sessionType = 'temporary'; robustnessLevel = ''; licenseRequestFilters = []; licenseResponseFilters = []; + eventBus.on(events.INTERNAL_KEY_MESSAGE, _onKeyMessage, instance); + eventBus.on(events.INTERNAL_KEY_STATUS_CHANGED, _onKeyStatusChanged, instance); } function checkConfig() { @@ -104,16 +108,11 @@ function ProtectionController(config) { } /** - * Initialize this protection system with a given audio - * or video stream information. + * Initialize this protection system for a given media type. * * @param {StreamInfo} [mediaInfo] Media information * @memberof module:ProtectionController * @instance - * @todo This API will change when we have better support for allowing applications - * to select different adaptation sets for playback. Right now it is clunky for - * applications to create {@link StreamInfo} with the right information, - * @ignore */ function initializeForMedia(mediaInfo) { // Not checking here if a session for similar KS/KID combination is already created @@ -126,66 +125,201 @@ function ProtectionController(config) { checkConfig(); - eventBus.on(events.INTERNAL_KEY_MESSAGE, onKeyMessage, this); - eventBus.on(events.INTERNAL_KEY_STATUS_CHANGED, onKeyStatusChanged, this); mediaInfoArr.push(mediaInfo); // ContentProtection elements are specified at the AdaptationSet level, so the CP for audio - // and video will be the same. Just use one valid MediaInfo object - const supportedKS = protectionKeyController.getSupportedKeySystemsFromContentProtection(mediaInfo.contentProtection); + // and video will be the same. Just use one valid MediaInfo object + let supportedKS = protectionKeyController.getSupportedKeySystemsFromContentProtection(mediaInfo.contentProtection); + + // 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; + }); + if (supportedKS && supportedKS.length > 0) { - selectKeySystem(supportedKS, true); + _selectKeySystem(supportedKS, 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 + * @param {boolean} fromManifest + * @private + */ + function _selectKeySystem(supportedKS, fromManifest) { + + // We are in the process of selecting a key system, so just save the data which might be coming from additional AdaptationSets. + if (keySystemSelectionInProgress) { + pendingKeySystemData.push(supportedKS); + } + + // First time, so we need to select a key system + else if (!selectedKeySystem) { + _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) { + _initiateWithExistingKeySystem(supportedKS); } } /** - * Removes all entries from the mediaInfoArr array for a specific stream id - * @param {String} streamId + * We do not have a key system yet. Select one + * @param {array} supportedKS + * @param {boolean} fromManifest + * @private */ - function clearMediaInfoArrayByStreamId(streamId) { - mediaInfoArr = mediaInfoArr.filter((mediaInfo) => { - return mediaInfo.streamInfo.id !== streamId; + function _selectInitialKeySystem(supportedKS, fromManifest) { + keySystemSelectionInProgress = true; + const requestedKeySystems = []; + + pendingKeySystemData.push(supportedKS); + + // 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].ks); + requestedKeySystems.push({ + ks: supportedKS[i].ks, + configs: [keySystemConfiguration] + }); + } + + let keySystemAccess; + + protectionModel.requestKeySystemAccess(requestedKeySystems) + .then((event) => { + keySystemAccess = event.data; + logger.info('DRM: KeySystem Access Granted (' + keySystemAccess.keySystem.systemString + ')! 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); + } + + // Create key sessions for the different AdaptationSets + let ksIdx; + for (let i = 0; i < pendingKeySystemData.length; i++) { + for (ksIdx = 0; ksIdx < pendingKeySystemData[i].length; ksIdx++) { + if (selectedKeySystem === pendingKeySystemData[i][ksIdx].ks) { + const current = pendingKeySystemData[i][ksIdx] + _loadOrCreateKeySession(protData, current) + break; + } + } + } + }) + .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 keysytem we only need to create a new key session and issue a new license request if the init data has changed. + * @param {array} supportedKS + * @private + */ + function _initiateWithExistingKeySystem(supportedKS,) { + const ksIdx = supportedKS.findIndex((entry) => { + return entry.ks === selectedKeySystem; }); + + const current = supportedKS[ksIdx]; + if (ksIdx === -1 || !current.initData) { + return; + } + + // we only need to create or load a new key session if the init data has changed + const initDataForKs = CommonEncryption.getPSSHForKeySystem(selectedKeySystem, current.initData); + if (_isInitDataDuplicate(initDataForKs)) { + return; + } + + const protData = _getProtDataForKeySystem(selectedKeySystem); + _loadOrCreateKeySession(protData, current); } /** - * 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). + * Loads an existing key session if we already have a session id. Otherwise we create a new key session + * @param {object} protData + * @param {object} keySystemInfo + * @private + */ + function _loadOrCreateKeySession(protData, 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 (protData && protData.hasOwnProperty('clearkeys')) { + const initData = { kids: Object.keys(protData.clearkeys) }; + keySystemInfo.initData = new TextEncoder().encode(JSON.stringify(initData)); + } + } + + // Reuse existing KeySession + if (keySystemInfo.sessionId) { + // Load MediaKeySession with sessionId + loadKeySession(keySystemInfo.sessionId, keySystemInfo.initData); + } + + // Create a new KeySession + else if (keySystemInfo.initData !== null) { + // Create new MediaKeySession with initData + createKeySession(keySystemInfo.initData, keySystemInfo.cdmData); + } + } + + /** + * Loads a key session with the given session ID from persistent storage. This essentially creates a new key session * - * @param {Array.} cps - array of content protection elements parsed - * from the manifest - * @returns {Array.} array of objects indicating which supported key - * systems were found. Empty array is returned if no - * supported key systems were found - * @memberof module:ProtectionKeyController + * @param {string} sessionID + * @param {string} initData + * @memberof module:ProtectionController * @instance + * @fires ProtectionController#KeySessionCreated * @ignore */ - function getSupportedKeySystemsFromContentProtection(cps) { + function loadKeySession(sessionID, initData) { checkConfig(); - return protectionKeyController.getSupportedKeySystemsFromContentProtection(cps); + protectionModel.loadKeySession(sessionID, initData, _getSessionType(selectedKeySystem)); } /** - * Create a new key session associated with the given initialization data from - * the MPD or from the PSSH box in the media - * + * 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 - * @todo In older versions of the EME spec, there was a one-to-one relationship between - * initialization data and key sessions. That is no longer true in the latest APIs. This - * API will need to modified (and a new "generateRequest(keySession, initData)" API created) - * to come up to speed with the latest EME standard * @ignore */ function createKeySession(initData, cdmData) { - const initDataForKS = CommonEncryption.getPSSHForKeySystem(keySystem, initData); - const protData = getProtData(keySystem); + const initDataForKS = CommonEncryption.getPSSHForKeySystem(selectedKeySystem, initData); + const protData = _getProtDataForKeySystem(selectedKeySystem); + if (initDataForKS) { // Check for duplicate initData @@ -194,7 +328,8 @@ function ProtectionController(config) { } try { - protectionModel.createKeySession(initDataForKS, protData, getSessionType(keySystem), cdmData); + const sessionType = _getSessionType(selectedKeySystem) + protectionModel.createKeySession(initDataForKS, protData, sessionType, cdmData); } catch (error) { eventBus.trigger(events.KEY_SESSION_CREATED, { data: null, @@ -202,15 +337,72 @@ function ProtectionController(config) { }); } } else if (initData) { - protectionModel.createKeySession(initData, protData, getSessionType(keySystem), cdmData); + const sessionType = _getSessionType(selectedKeySystem) + protectionModel.createKeySession(initData, protData, sessionType, cdmData); } 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 ' + (keySystem ? keySystem.systemString : null) + '. needkey/encrypted event contains no initData corresponding to that key system!') + 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; + } + + /** + * Returns the session type either from the protData or as defined via setSessionType() + * @param keySystem + * @return {*} + * @private + */ + function _getSessionType(keySystem) { + const protData = _getProtDataForKeySystem(keySystem); + + return (protData && protData.sessionType) ? protData.sessionType : sessionType; + } + + /** + * 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.} cps - array of content protection elements parsed + * from the manifest + * @returns {Array.} 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); + } + /** * Checks if the provided init data is equal to one of the existing init data values * @param {any} initDataForKS @@ -238,22 +430,6 @@ function ProtectionController(config) { } } - /** - * Loads a key session with the given session ID from persistent storage. This - * essentially creates a new key session - * - * @param {string} sessionID - * @param {string} initData - * @memberof module:ProtectionController - * @instance - * @fires ProtectionController#KeySessionCreated - * @ignore - */ - function loadKeySession(sessionID, initData) { - checkConfig(); - protectionModel.loadKeySession(sessionID, initData, getSessionType(keySystem)); - } - /** * Removes the given key session from persistent storage and closes the session * as if {@link ProtectionController#closeKeySession} @@ -318,10 +494,10 @@ function ProtectionController(config) { checkConfig(); if (element) { protectionModel.setMediaElement(element); - eventBus.on(events.NEED_KEY, onNeedKey, this); + eventBus.on(events.NEED_KEY, _onNeedKey, instance); } else if (element === null) { protectionModel.setMediaElement(element); - eventBus.off(events.NEED_KEY, onNeedKey, this); + eventBus.off(events.NEED_KEY, _onNeedKey, instance); } } @@ -388,17 +564,18 @@ function ProtectionController(config) { * @ignore */ function reset() { + eventBus.off(events.INTERNAL_KEY_MESSAGE, _onKeyMessage, instance); + eventBus.off(events.INTERNAL_KEY_STATUS_CHANGED, _onKeyStatusChanged, instance); + checkConfig(); licenseRequestFilters = []; licenseResponseFilters = []; - eventBus.off(events.INTERNAL_KEY_MESSAGE, onKeyMessage, this); - eventBus.off(events.INTERNAL_KEY_STATUS_CHANGED, onKeyStatusChanged, this); - setMediaElement(null); - keySystem = undefined; + selectedKeySystem = null; + keySystemSelectionInProgress = false; if (protectionModel) { protectionModel.reset(); @@ -409,31 +586,22 @@ function ProtectionController(config) { needkeyRetries = []; mediaInfoArr = []; + pendingKeySystemData = []; } - /////////////// - // Private - /////////////// - - function getProtData(keySystem) { - let protData = null; - if (keySystem) { - const keySystemString = keySystem.systemString; - - if (protDataSet) { - protData = (keySystemString in protDataSet) ? protDataSet[keySystemString] : null; - } - } - return protData; - } - - function getKeySystemConfiguration(keySystem) { - const protData = getProtData(keySystem); + /** + * Returns an object corresponding to the EME MediaKeySystemConfiguration dictionary + * @param {object} keySystem + * @return {KeySystemConfiguration} + * @private + */ + function _getKeySystemConfiguration(keySystem) { + const protData = _getProtDataForKeySystem(keySystem); 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 = getSessionType(keySystem); + const ksSessionType = _getSessionType(keySystem); const distinctiveIdentifier = (protData && protData.distinctiveIdentifier) ? protData.distinctiveIdentifier : 'optional'; const persistentState = (protData && protData.persistentState) ? protData.persistentState : (ksSessionType === 'temporary') ? 'optional' : 'required'; @@ -451,189 +619,12 @@ function ProtectionController(config) { [ksSessionType]); } - function getSessionType(keySystem) { - const protData = getProtData(keySystem); - const ksSessionType = (protData && protData.sessionType) ? protData.sessionType : sessionType; - return ksSessionType; - } - - function selectKeySystem(supportedKS, fromManifest) { - - // 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; - }); - - - // First time, so we need to select a key system - if (keySystem === undefined) { - _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 (keySystem) { - _selectWithExistingKeySystem(supportedKS, fromManifest); - } - - // We are in the process of selecting a key system, so just save the data which might be coming from additional AdaptationSets. - else { - pendingNeedKeyData.push(supportedKS); - } - } - - function _selectWithExistingKeySystem(supportedKS, fromManifest) { - const self = this; - const requestedKeySystems = []; - - const ksIdx = supportedKS.findIndex((entry) => { - return entry.ks === keySystem; - }); - - if (ksIdx === -1 || !supportedKS[ksIdx].initData) { - return; - } - - // we only need to call this if the init data has changed - const initDataForKs = CommonEncryption.getPSSHForKeySystem(keySystem, supportedKS[ksIdx].initData); - if (_isInitDataDuplicate(initDataForKs)) { - return; - } - - requestedKeySystems.push({ - ks: supportedKS[ksIdx].ks, - configs: [getKeySystemConfiguration(keySystem)] - }); - - // Ensure that we would be granted key system access using the key - // system and codec information - const onKeySystemAccessComplete = function (event) { - eventBus.off(events.KEY_SYSTEM_ACCESS_COMPLETE, onKeySystemAccessComplete, self); - if (event.error) { - if (!fromManifest) { - eventBus.trigger(events.KEY_SYSTEM_SELECTED, { error: new DashJSError(ProtectionErrors.KEY_SYSTEM_ACCESS_DENIED_ERROR_CODE, ProtectionErrors.KEY_SYSTEM_ACCESS_DENIED_ERROR_MESSAGE + event.error) }); - } - } else { - logger.info('DRM: KeySystem Access Granted'); - eventBus.trigger(events.KEY_SYSTEM_SELECTED, { data: event.data }); - const protData = getProtData(keySystem); - if (protectionKeyController.isClearKey(keySystem)) { - // 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 (protData && protData.hasOwnProperty('clearkeys')) { - const initData = { kids: Object.keys(protData.clearkeys) }; - supportedKS[ksIdx].initData = new TextEncoder().encode(JSON.stringify(initData)); - } - } - if (supportedKS[ksIdx].sessionId) { - // Load MediaKeySession with sessionId - loadKeySession(supportedKS[ksIdx].sessionId, supportedKS[ksIdx].initData); - } else if (supportedKS[ksIdx].initData) { - // Create new MediaKeySession with initData - createKeySession(supportedKS[ksIdx].initData, supportedKS[ksIdx].cdmData); - } - } - }; - - eventBus.on(events.KEY_SYSTEM_ACCESS_COMPLETE, onKeySystemAccessComplete, self); - protectionModel.requestKeySystemAccess(requestedKeySystems); - } - - function _selectInitialKeySystem(supportedKS, fromManifest) { - const self = this; - const requestedKeySystems = []; - let ksIdx; - - // First time through, so we need to select a key system - keySystem = null; - pendingNeedKeyData.push(supportedKS); - - // 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++) { - requestedKeySystems.push({ - ks: supportedKS[i].ks, - configs: [getKeySystemConfiguration(supportedKS[i].ks)] - }); - } - - let keySystemAccess; - const onKeySystemAccessComplete = function (event) { - eventBus.off(events.KEY_SYSTEM_ACCESS_COMPLETE, onKeySystemAccessComplete, self); - if (event.error) { - keySystem = undefined; - eventBus.off(events.INTERNAL_KEY_SYSTEM_SELECTED, onKeySystemSelected, self); - 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 + event.error) - }); - } - } else { - keySystemAccess = event.data; - logger.info('DRM: KeySystem Access Granted (' + keySystemAccess.keySystem.systemString + ')! Selecting key system...'); - protectionModel.selectKeySystem(keySystemAccess); - } - }; - var onKeySystemSelected = function (event) { - eventBus.off(events.INTERNAL_KEY_SYSTEM_SELECTED, onKeySystemSelected, self); - eventBus.off(events.KEY_SYSTEM_ACCESS_COMPLETE, onKeySystemAccessComplete, self); - if (!event.error) { - if (!protectionModel) { - return; - } - keySystem = protectionModel.getKeySystem(); - eventBus.trigger(events.KEY_SYSTEM_SELECTED, { data: keySystemAccess }); - // Set server certificate from protData - const protData = getProtData(keySystem); - if (protData && protData.serverCertificate && protData.serverCertificate.length > 0) { - protectionModel.setServerCertificate(BASE64.decodeArray(protData.serverCertificate).buffer); - } - - // Create key session for the remaining AdaptationSets which have been added to pendingNeedKeyData - for (let i = 0; i < pendingNeedKeyData.length; i++) { - for (ksIdx = 0; ksIdx < pendingNeedKeyData[i].length; ksIdx++) { - if (keySystem === pendingNeedKeyData[i][ksIdx].ks) { - if (protectionKeyController.isClearKey(keySystem)) { - // 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 (protData && protData.hasOwnProperty('clearkeys')) { - const initData = { kids: Object.keys(protData.clearkeys) }; - pendingNeedKeyData[i][ksIdx].initData = new TextEncoder().encode(JSON.stringify(initData)); - } - } - if (pendingNeedKeyData[i][ksIdx].sessionId) { - // Load MediaKeySession with sessionId - loadKeySession(pendingNeedKeyData[i][ksIdx].sessionId, pendingNeedKeyData[i][ksIdx].initData); - } else if (pendingNeedKeyData[i][ksIdx].initData !== null) { - // Create new MediaKeySession with initData - createKeySession(pendingNeedKeyData[i][ksIdx].initData, pendingNeedKeyData[i][ksIdx].cdmData); - } - break; - } - } - } - } else { - keySystem = undefined; - 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) - }); - } - } - }; - - eventBus.on(events.INTERNAL_KEY_SYSTEM_SELECTED, onKeySystemSelected, self); - eventBus.on(events.KEY_SYSTEM_ACCESS_COMPLETE, onKeySystemAccessComplete, self); - protectionModel.requestKeySystemAccess(requestedKeySystems); - } - - function sendLicenseRequestCompleteEvent(data, error) { - eventBus.trigger(events.LICENSE_REQUEST_COMPLETE, { data: data, error: error }); - } - - function onKeyStatusChanged(e) { + /** + * 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 { @@ -641,7 +632,12 @@ function ProtectionController(config) { } } - function onKeyMessage(e) { + /** + * 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 @@ -650,133 +646,127 @@ function ProtectionController(config) { const messageType = (keyMessage.messageType) ? keyMessage.messageType : 'license-request'; const message = keyMessage.message; const sessionToken = keyMessage.sessionToken; - const protData = getProtData(keySystem); - const keySystemString = keySystem ? keySystem.systemString : null; - const licenseServerData = protectionKeyController.getLicenseServer(keySystem, protData, messageType); + 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)); + _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 (!licenseServerData) { + if (!licenseServerModelInstance) { logger.debug('DRM: License server request not required for this message (type = ' + e.data.messageType + '). Session ID = ' + sessionToken.getSessionID()); - sendLicenseRequestCompleteEvent(eventData); + _sendLicenseRequestCompleteEvent(eventData); return; } // Perform any special handling for ClearKey - if (protectionKeyController.isClearKey(keySystem)) { - const clearkeys = protectionKeyController.processClearKeyLicenseRequest(keySystem, protData, message); + if (protectionKeyController.isClearKey(selectedKeySystem)) { + const clearkeys = protectionKeyController.processClearKeyLicenseRequest(selectedKeySystem, protData, message); if (clearkeys) { logger.debug('DRM: ClearKey license request handled by application!'); - sendLicenseRequestCompleteEvent(eventData); + _sendLicenseRequestCompleteEvent(eventData); protectionModel.updateKeySession(sessionToken, clearkeys); return; } } - // All remaining key system scenarios require a request to a remote license server + // 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 = null; - if (protData && protData.serverURL) { - const serverURL = protData.serverURL; - if (typeof serverURL === 'string' && serverURL !== '') { - url = serverURL; - } else if (typeof serverURL === 'object' && serverURL.hasOwnProperty(messageType)) { - url = serverURL[messageType]; - } - } else if (protData && protData.laURL && protData.laURL !== '') { - // TODO: Deprecated! - url = protData.laURL; - } else { - // For clearkey use the url defined in the manifest - if (protectionKeyController.isClearKey(keySystem)) { - url = keySystem.getLicenseServerUrlFromMediaInfo(mediaInfoArr); - } else { - const psshData = CommonEncryption.getPSSHData(sessionToken.initData); - url = keySystem.getLicenseServerURLFromInitData(psshData); - if (!url) { - url = e.data.laURL; - } - } - } - // Possibly update or override the URL based on the message - url = licenseServerData.getServerURLFromMessage(url, message, messageType); + 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)); + _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; - const updateHeaders = function (headers) { - if (headers) { - for (const key in headers) { - if ('authorization' === key.toLowerCase()) { - withCredentials = true; - } - reqHeaders[key] = headers[key]; - } - } - }; if (protData) { - updateHeaders(protData.httpRequestHeaders); + _updateHeaders(reqHeaders, protData.httpRequestHeaders); } - updateHeaders(keySystem.getRequestHeadersFromMessage(message)); + 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 reportError = function (xhr, eventData, keySystemString, messageType) { - const errorMsg = ((xhr.response) ? licenseServerData.getErrorResponse(xhr.response, keySystemString, messageType) : 'NONE'); - sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_CODE, - ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_MESSAGE + keySystemString + ' update, XHR complete. status is "' + - xhr.statusText + '" (' + xhr.status + '), readyState is ' + xhr.readyState + '. Response is ' + errorMsg)); - }; - const onLoad = function (xhr) { if (!protectionModel) { return; } if (xhr.status >= 200 && xhr.status <= 299) { - let licenseResponse = new LicenseResponse(xhr.responseURL, Utils.parseHttpHeaders(xhr.getAllResponseHeaders ? xhr.getAllResponseHeaders() : null), xhr.response); - 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); - } - }); + const responseHeaders = Utils.parseHttpHeaders(xhr.getAllResponseHeaders ? xhr.getAllResponseHeaders() : null); + let licenseResponse = new LicenseResponse(xhr.responseURL, responseHeaders, xhr.response); + _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); + _reportError(xhr, eventData, keySystemString, messageType, licenseServerData); } }; const onAbort = function (xhr) { - sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_CODE, + _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, + _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 = keySystem.getLicenseRequestFromMessage(message); + 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; @@ -784,13 +774,23 @@ function ProtectionController(config) { 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; - applyFilters(licenseRequestFilters, licenseRequest).then(() => { - doLicenseRequest(licenseRequest, retryAttempts, timeout, onLoad, onAbort, onError); + _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 - function doLicenseRequest(request, retriesCount, 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) { @@ -836,12 +836,12 @@ function ProtectionController(config) { } } - const retryRequest = function () { + 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; setTimeout(function () { - doLicenseRequest(request, retriesCount, timeout, onLoad, onAbort, onError); + _doLicenseRequest(request, retriesCount, timeout, onLoad, onAbort, onError); }, retryInterval); }; @@ -850,7 +850,7 @@ function ProtectionController(config) { onLoad(this); } else { logger.warn('License request failed (' + this.status + '). Retrying it... Pending retries: ' + retriesCount); - retryRequest(); + _retryRequest(); } }; @@ -859,7 +859,7 @@ function ProtectionController(config) { onError(this); } else { logger.warn('License request network request failed . Retrying it... Pending retries: ' + retriesCount); - retryRequest(); + _retryRequest(); } }; @@ -878,86 +878,196 @@ function ProtectionController(config) { xhr.send(request.data); } - function onNeedKey(event, retry) { - logger.debug('DRM: onNeedKey'); - // Ignore non-cenc initData - if (event.key.initDataType !== 'cenc') { - logger.warn('DRM: Only \'cenc\' initData is supported! Ignoring initData of type: ' + event.key.initDataType); - return; - } + /** + * Returns the url of the license server + * @param {object} protData + * @param {string} messageType + * @param {object} sessionToken + * @param {object} keyMessage + * @param {object} licenseServerData + * @return {*} + * @private + */ + function _getLicenseServerUrl(protData, messageType, sessionToken, keyMessage, licenseServerData) { + let url = null; + const message = keyMessage.message; - if (mediaInfoArr.length === 0) { - logger.warn('DRM: onNeedKey called before initializeForMedia, wait until initialized'); - retry = typeof retry === 'undefined' ? 1 : retry + 1; - if (retry < NEEDKEY_BEFORE_INITIALIZE_RETRIES) { - needkeyRetries.push(setTimeout(() => { - onNeedKey(event, retry); - }, NEEDKEY_BEFORE_INITIALIZE_TIMEOUT)); - return; + // Check if the url is defined by the application + if (protData && protData.serverURL) { + const serverURL = protData.serverURL; + if (typeof serverURL === 'string' && serverURL !== '') { + url = serverURL; + } else if (typeof serverURL === 'object' && serverURL.hasOwnProperty(messageType)) { + url = serverURL[messageType]; } - } - // Some browsers return initData as Uint8Array (IE), some as ArrayBuffer (Chrome). - // Convert to ArrayBuffer - let abInitData = event.key.initData; - if (ArrayBuffer.isView(abInitData)) { - abInitData = abInitData.buffer; + // This is the old way of providing the url + else if (protData && protData.laURL && protData.laURL !== '') { + url = protData.laURL; } - // If key system has already been selected and initData already seen, then do nothing - if (keySystem) { - const initDataForKS = CommonEncryption.getPSSHForKeySystem(keySystem, abInitData); - if (initDataForKS) { + // No url provided by the app. Check the manifest and the pssh + else { + // Check for url defined in the manifest + url = CommonEncryption.getLicenseServerUrlFromMediaInfo(mediaInfoArr, selectedKeySystem.schemeIdURI); - // Check for duplicate initData - if (_isInitDataDuplicate(initDataForKS)) { - return; + // In case we are not using Clearky we can still get a url from the pssh. + if (!url && !protectionKeyController.isClearKey(selectedKeySystem)) { + const psshData = CommonEncryption.getPSSHData(sessionToken.initData); + url = selectedKeySystem.getLicenseServerURLFromInitData(psshData); + + // Still no url, check the keymessage + if (!url) { + url = keyMessage.laURL; } } } + // Possibly update or override the URL based on the message + url = licenseServerData.getServerURLFromMessage(url, message, messageType); - logger.debug('DRM: initData:', String.fromCharCode.apply(null, new Uint8Array(abInitData))); + return url; + } - const supportedKS = protectionKeyController.getSupportedKeySystems(abInitData, protDataSet); - if (supportedKS.length === 0) { - logger.debug('DRM: Received needkey event with initData, but we don\'t support any of the key systems!'); - return; + /** + * Add new headers to the existing ones + * @param {array} reqHeaders + * @param {object} headers + * @private + */ + function _updateHeaders(reqHeaders, headers) { + if (headers) { + for (const key in headers) { + reqHeaders[key] = headers[key]; + } } + } + + /** + * Reports an error that might have occured during the license request + * @param {object} xhr + * @param {object} eventData + * @param {string} keySystemString + * @param {string} messageType + * @param {object} licenseServerData + * @private + */ + function _reportError(xhr, eventData, keySystemString, messageType, licenseServerData) { + const errorMsg = ((xhr.response) ? licenseServerData.getErrorResponse(xhr.response, keySystemString, messageType) : 'NONE'); + _sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_CODE, + ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_MESSAGE + keySystemString + ' update, XHR complete. status is "' + + xhr.statusText + '" (' + xhr.status + '), readyState is ' + xhr.readyState + '. Response is ' + errorMsg)); + } - selectKeySystem(supportedKS, false); + /** + * Applies custom filters defined by the application + * @param {array} filters + * @param {object} param + * @return {Promise|*} + * @private + */ + function _applyFilters(filters, param) { + if (!filters) return Promise.resolve(); + return filters.reduce((prev, next) => { + return prev.then(() => { + return next(param); + }); + }, Promise.resolve()); } + /** + * Event handler for "needkey" and "encrypted" events + * @param {object} event + * @param {number} retry + */ + function _onNeedKey(event, retry) { + if (!settings.get().streaming.protection.ignoreEmeEncryptedEvent) { + logger.debug('DRM: onNeedKey'); + + // Ignore non-cenc initData + if (event.key.initDataType !== 'cenc') { + logger.warn('DRM: Only \'cenc\' initData is supported! Ignoring initData of type: ' + event.key.initDataType); + return; + } + + if (mediaInfoArr.length === 0) { + logger.warn('DRM: onNeedKey called before initializeForMedia, wait until initialized'); + retry = typeof retry === 'undefined' ? 1 : retry + 1; + if (retry < NEEDKEY_BEFORE_INITIALIZE_RETRIES) { + needkeyRetries.push(setTimeout(() => { + _onNeedKey(event, retry); + }, NEEDKEY_BEFORE_INITIALIZE_TIMEOUT)); + return; + } + } + + // Some browsers return initData as Uint8Array (IE), some as ArrayBuffer (Chrome). + // Convert to ArrayBuffer + let abInitData = event.key.initData; + if (ArrayBuffer.isView(abInitData)) { + abInitData = abInitData.buffer; + } + + // If key system has already been selected and initData already seen, then do nothing + if (selectedKeySystem) { + const initDataForKS = CommonEncryption.getPSSHForKeySystem(selectedKeySystem, abInitData); + if (initDataForKS) { + // Check for duplicate initData + if (_isInitDataDuplicate(initDataForKS)) { + return; + } + } + } + + logger.debug('DRM: initData:', String.fromCharCode.apply(null, new Uint8Array(abInitData))); + + const supportedKS = protectionKeyController.getSupportedKeySystems(abInitData, protDataSet); + if (supportedKS.length === 0) { + logger.debug('DRM: Received needkey event with initData, but we don\'t support any of the key systems!'); + return; + } + + _selectKeySystem(supportedKS, false); + } + } + + /** + * Returns all available key systems + * @return {*|*[]} + */ function getKeySystems() { return protectionKeyController ? protectionKeyController.getKeySystems() : []; } + /** + * Sets all available key systems + * @param {array} keySystems + */ function setKeySystems(keySystems) { if (protectionKeyController) { protectionKeyController.setKeySystems(keySystems); } } + /** + * Sets the request filters to be applied before the license request is made + * @param {array} filters + */ function setLicenseRequestFilters(filters) { licenseRequestFilters = filters; } + /** + * Sets the response filters to be applied after the license response has been received. + * @param {array} filters + */ function setLicenseResponseFilters(filters) { licenseResponseFilters = filters; } - function applyFilters(filters, param) { - if (!filters) return Promise.resolve(); - return filters.reduce((prev, next) => { - return prev.then(() => { - return next(param); - }); - }, Promise.resolve()); - } - instance = { initializeForMedia, - clearMediaInfoArrayByStreamId, + clearMediaInfoArray, createKeySession, loadKeySession, removeKeySession, @@ -981,4 +1091,4 @@ function ProtectionController(config) { } ProtectionController.__dashjs_factory_name = 'ProtectionController'; -export default dashjs.FactoryMaker.getClassFactory(ProtectionController); /* jshint ignore:line */ +export default FactoryMaker.getClassFactory(ProtectionController); /* jshint ignore:line */ diff --git a/src/streaming/protection/controllers/ProtectionKeyController.js b/src/streaming/protection/controllers/ProtectionKeyController.js index e41151a852..bade83ee0a 100644 --- a/src/streaming/protection/controllers/ProtectionKeyController.js +++ b/src/streaming/protection/controllers/ProtectionKeyController.js @@ -279,7 +279,7 @@ function ProtectionKeyController() { * @instance * */ - function getLicenseServer(keySystem, protData, messageType) { + function getLicenseServerModelInstance(keySystem, protData, messageType) { // Our default server implementations do not do anything with "license-release" or // "individualization-request" messages, so we just send a success event @@ -340,18 +340,18 @@ function ProtectionKeyController() { } instance = { - initialize: initialize, - setProtectionData: setProtectionData, - isClearKey: isClearKey, - initDataEquals: initDataEquals, - getKeySystems: getKeySystems, - setKeySystems: setKeySystems, - getKeySystemBySystemString: getKeySystemBySystemString, - getSupportedKeySystemsFromContentProtection: getSupportedKeySystemsFromContentProtection, - getSupportedKeySystems: getSupportedKeySystems, - getLicenseServer: getLicenseServer, - processClearKeyLicenseRequest: processClearKeyLicenseRequest, - setConfig: setConfig + initialize, + setProtectionData, + isClearKey, + initDataEquals, + getKeySystems, + setKeySystems, + getKeySystemBySystemString, + getSupportedKeySystemsFromContentProtection, + getSupportedKeySystems, + getLicenseServerModelInstance, + processClearKeyLicenseRequest, + setConfig }; return instance; diff --git a/src/streaming/protection/drm/KeySystemClearKey.js b/src/streaming/protection/drm/KeySystemClearKey.js index 3111dfba27..5c1ad914da 100644 --- a/src/streaming/protection/drm/KeySystemClearKey.js +++ b/src/streaming/protection/drm/KeySystemClearKey.js @@ -43,10 +43,6 @@ function KeySystemClearKey(config) { config = config || {}; let instance; const BASE64 = config.BASE64; - const LICENSE_SERVER_MANIFEST_CONFIGURATIONS = { - attributes: ['Laurl', 'laurl'], - prefixes: ['clearkey', 'dashif'] - }; /** * Returns desired clearkeys (as specified in the CDM message) from protection data @@ -122,49 +118,6 @@ function KeySystemClearKey(config) { return null; } - function getLicenseServerUrlFromMediaInfo(mediaInfo) { - try { - if (!mediaInfo || mediaInfo.length === 0) { - return null; - } - let i = 0; - let licenseServer = null; - while (i < mediaInfo.length && !licenseServer) { - const info = mediaInfo[i]; - if (info && info.contentProtection && info.contentProtection.length > 0) { - const clearkeyProtData = info.contentProtection.filter((cp) => { - return cp.schemeIdUri && cp.schemeIdUri === schemeIdURI; - }); - if (clearkeyProtData && clearkeyProtData.length > 0) { - let j = 0; - while (j < clearkeyProtData.length && !licenseServer) { - const ckData = clearkeyProtData[j]; - let k = 0; - while (k < LICENSE_SERVER_MANIFEST_CONFIGURATIONS.attributes.length && !licenseServer) { - let l = 0; - const attribute = LICENSE_SERVER_MANIFEST_CONFIGURATIONS.attributes[k]; - while (l < LICENSE_SERVER_MANIFEST_CONFIGURATIONS.prefixes.length && !licenseServer) { - const prefix = LICENSE_SERVER_MANIFEST_CONFIGURATIONS.prefixes[l]; - if (ckData[attribute] && ckData[attribute].__prefix && ckData[attribute].__prefix === prefix && ckData[attribute].__text) { - licenseServer = ckData[attribute].__text; - } - l += 1; - } - k += 1; - } - j += 1; - } - } - } - i += 1; - } - return licenseServer; - } catch - (e) { - return null; - } - } - function getCDMData() { return null; } @@ -174,17 +127,16 @@ function KeySystemClearKey(config) { } instance = { - uuid: uuid, - schemeIdURI: schemeIdURI, - systemString: systemString, - getInitData: getInitData, - getRequestHeadersFromMessage: getRequestHeadersFromMessage, - getLicenseRequestFromMessage: getLicenseRequestFromMessage, - getLicenseServerURLFromInitData: getLicenseServerURLFromInitData, - getCDMData: getCDMData, - getSessionId: getSessionId, - getLicenseServerUrlFromMediaInfo, - getClearKeysFromProtectionData: getClearKeysFromProtectionData + uuid, + schemeIdURI, + systemString, + getInitData, + getRequestHeadersFromMessage, + getLicenseRequestFromMessage, + getLicenseServerURLFromInitData, + getCDMData, + getSessionId, + getClearKeysFromProtectionData }; return instance; diff --git a/src/streaming/protection/drm/KeySystemPlayReady.js b/src/streaming/protection/drm/KeySystemPlayReady.js index 4566ed4d3a..9d4c983960 100644 --- a/src/streaming/protection/drm/KeySystemPlayReady.js +++ b/src/streaming/protection/drm/KeySystemPlayReady.js @@ -296,17 +296,17 @@ function KeySystemPlayReady(config) { } instance = { - uuid: uuid, - schemeIdURI: schemeIdURI, - systemString: systemString, - getInitData: getInitData, - getRequestHeadersFromMessage: getRequestHeadersFromMessage, - getLicenseRequestFromMessage: getLicenseRequestFromMessage, - getLicenseServerURLFromInitData: getLicenseServerURLFromInitData, - getCDMData: getCDMData, - getSessionId: getSessionId, - setPlayReadyMessageFormat: setPlayReadyMessageFormat, - init: init + uuid, + schemeIdURI, + systemString, + getInitData, + getRequestHeadersFromMessage, + getLicenseRequestFromMessage, + getLicenseServerURLFromInitData, + getCDMData, + getSessionId, + setPlayReadyMessageFormat, + init }; return instance; diff --git a/src/streaming/protection/drm/KeySystemWidevine.js b/src/streaming/protection/drm/KeySystemWidevine.js index c4a421e680..7273d73eff 100644 --- a/src/streaming/protection/drm/KeySystemWidevine.js +++ b/src/streaming/protection/drm/KeySystemWidevine.js @@ -87,16 +87,16 @@ function KeySystemWidevine(config) { } instance = { - uuid: uuid, - schemeIdURI: schemeIdURI, - systemString: systemString, - init: init, - getInitData: getInitData, - getRequestHeadersFromMessage: getRequestHeadersFromMessage, - getLicenseRequestFromMessage: getLicenseRequestFromMessage, - getLicenseServerURLFromInitData: getLicenseServerURLFromInitData, - getCDMData: getCDMData, - getSessionId: getSessionId + uuid, + schemeIdURI, + systemString, + init, + getInitData, + getRequestHeadersFromMessage, + getLicenseRequestFromMessage, + getLicenseServerURLFromInitData, + getCDMData, + getSessionId }; return instance; diff --git a/src/streaming/protection/models/ProtectionModel_01b.js b/src/streaming/protection/models/ProtectionModel_01b.js index a814d7a706..9ba214b109 100644 --- a/src/streaming/protection/models/ProtectionModel_01b.js +++ b/src/streaming/protection/models/ProtectionModel_01b.js @@ -104,10 +104,6 @@ function ProtectionModel_01b(config) { eventBus.trigger(events.TEARDOWN_COMPLETE); } - function getKeySystem() { - return keySystem; - } - function getAllInitData() { const retVal = []; for (let i = 0; i < pendingSessions.length; i++) { @@ -120,59 +116,66 @@ function ProtectionModel_01b(config) { } function requestKeySystemAccess(ksConfigurations) { - let ve = videoElement; - if (!ve) { // Must have a video element to do this capability tests - ve = document.createElement('video'); - } - - // Try key systems in order, first one with supported key system configuration - // is used - let found = false; - for (let ksIdx = 0; ksIdx < ksConfigurations.length; ksIdx++) { - const systemString = ksConfigurations[ksIdx].ks.systemString; - const configs = ksConfigurations[ksIdx].configs; - let supportedAudio = null; - let supportedVideo = null; + return new Promise((resolve, reject) => { + let ve = videoElement; + if (!ve) { // Must have a video element to do this capability tests + ve = document.createElement('video'); + } - // Try key system configs in order, first one with supported audio/video + // Try key systems in order, first one with supported key system configuration // is used - for (let configIdx = 0; configIdx < configs.length; configIdx++) { - //let audios = configs[configIdx].audioCapabilities; - const videos = configs[configIdx].videoCapabilities; - // Look for supported video container/codecs - if (videos && videos.length !== 0) { - supportedVideo = []; // Indicates that we have a requested video config - for (let videoIdx = 0; videoIdx < videos.length; videoIdx++) { - if (ve.canPlayType(videos[videoIdx].contentType, systemString) !== '') { - supportedVideo.push(videos[videoIdx]); + let found = false; + for (let ksIdx = 0; ksIdx < ksConfigurations.length; ksIdx++) { + const systemString = ksConfigurations[ksIdx].ks.systemString; + const configs = ksConfigurations[ksIdx].configs; + let supportedAudio = null; + let supportedVideo = null; + + // Try key system configs in order, first one with supported audio/video + // is used + for (let configIdx = 0; configIdx < configs.length; configIdx++) { + //let audios = configs[configIdx].audioCapabilities; + const videos = configs[configIdx].videoCapabilities; + // Look for supported video container/codecs + if (videos && videos.length !== 0) { + supportedVideo = []; // Indicates that we have a requested video config + for (let videoIdx = 0; videoIdx < videos.length; videoIdx++) { + if (ve.canPlayType(videos[videoIdx].contentType, systemString) !== '') { + supportedVideo.push(videos[videoIdx]); + } } } - } - // No supported audio or video in this configuration OR we have - // requested audio or video configuration that is not supported - if ((!supportedAudio && !supportedVideo) || - (supportedAudio && supportedAudio.length === 0) || - (supportedVideo && supportedVideo.length === 0)) { - continue; - } + // No supported audio or video in this configuration OR we have + // requested audio or video configuration that is not supported + if ((!supportedAudio && !supportedVideo) || + (supportedAudio && supportedAudio.length === 0) || + (supportedVideo && supportedVideo.length === 0)) { + continue; + } - // This configuration is supported - found = true; - const ksConfig = new KeySystemConfiguration(supportedAudio, supportedVideo); - const ks = protectionKeyController.getKeySystemBySystemString(systemString); - eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { data: new KeySystemAccess(ks, ksConfig) }); - break; + // This configuration is supported + found = true; + const ksConfig = new KeySystemConfiguration(supportedAudio, supportedVideo); + const ks = protectionKeyController.getKeySystemBySystemString(systemString); + const keySystemAccess = new KeySystemAccess(ks, ksConfig) + eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { data: keySystemAccess }); + resolve({ data: keySystemAccess }); + break; + } } - } - if (!found) { - eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { error: 'Key system access denied! -- No valid audio/video content configurations detected!' }); - } + if (!found) { + const errorMessage = 'Key system access denied! -- No valid audio/video content configurations detected!'; + eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { error: errorMessage }); + reject({ error: errorMessage }); + } + }) + } function selectKeySystem(keySystemAccess) { keySystem = keySystemAccess.keySystem; - eventBus.trigger(events.INTERNAL_KEY_SYSTEM_SELECTED); + return Promise.resolve(keySystem); } function setMediaElement(mediaElement) { @@ -259,13 +262,21 @@ function ProtectionModel_01b(config) { try { videoElement[api.cancelKeyRequest](keySystem.systemString, sessionToken.sessionID); } catch (error) { - eventBus.trigger(events.KEY_SESSION_CLOSED, { data: null, error: 'Error closing session (' + sessionToken.sessionID + ') ' + error.message }); + eventBus.trigger(events.KEY_SESSION_CLOSED, { + data: null, + error: 'Error closing session (' + sessionToken.sessionID + ') ' + error.message + }); } } - function setServerCertificate(/*serverCertificate*/) { /* Not supported */ } - function loadKeySession(/*sessionID*/) { /* Not supported */ } - function removeKeySession(/*sessionToken*/) { /* Not supported */ } + function setServerCertificate(/*serverCertificate*/) { /* Not supported */ + } + + function loadKeySession(/*sessionID*/) { /* Not supported */ + } + + function removeKeySession(/*sessionToken*/) { /* Not supported */ + } function createEventHandler() { return { @@ -411,19 +422,18 @@ function ProtectionModel_01b(config) { } instance = { - getAllInitData: getAllInitData, - requestKeySystemAccess: requestKeySystemAccess, - getKeySystem: getKeySystem, - selectKeySystem: selectKeySystem, - setMediaElement: setMediaElement, - createKeySession: createKeySession, - updateKeySession: updateKeySession, - closeKeySession: closeKeySession, - setServerCertificate: setServerCertificate, - loadKeySession: loadKeySession, - removeKeySession: removeKeySession, + getAllInitData, + requestKeySystemAccess, + selectKeySystem, + setMediaElement, + createKeySession, + updateKeySession, + closeKeySession, + setServerCertificate, + loadKeySession, + removeKeySession, stop: reset, - reset: reset + reset }; setup(); diff --git a/src/streaming/protection/models/ProtectionModel_21Jan2015.js b/src/streaming/protection/models/ProtectionModel_21Jan2015.js index 0eb2cb6e8d..332d9327c0 100644 --- a/src/streaming/protection/models/ProtectionModel_21Jan2015.js +++ b/src/streaming/protection/models/ProtectionModel_21Jan2015.js @@ -100,7 +100,7 @@ function ProtectionModel_21Jan2015(config) { }); // Close the session and handle errors, otherwise promise // resolver above will be called - closeKeySessionInternal(session).catch(function () { + _closeKeySessionInternal(session).catch(function () { done(s); }); @@ -117,17 +117,13 @@ function ProtectionModel_21Jan2015(config) { for (let i = 0; i < sessions.length; i++) { session = sessions[i]; if (!session.getUsable()) { - closeKeySessionInternal(session).catch(function () { + _closeKeySessionInternal(session).catch(function () { removeSession(session); }); } } } - function getKeySystem() { - return keySystem; - } - function getAllInitData() { const retVal = []; for (let i = 0; i < sessions.length; i++) { @@ -139,24 +135,82 @@ function ProtectionModel_21Jan2015(config) { } function requestKeySystemAccess(ksConfigurations) { - requestKeySystemAccessInternal(ksConfigurations, 0); + return new Promise((resolve, reject) => { + _requestKeySystemAccessInternal(ksConfigurations, 0, resolve, reject); + }) + } + + /** + * Initializes access to a key system. Once we found a valid configuration we get a mediaKeySystemAccess object + * @param ksConfigurations + * @param idx + * @param resolve + * @param reject + * @private + */ + function _requestKeySystemAccessInternal(ksConfigurations, idx, resolve, reject) { + if (navigator.requestMediaKeySystemAccess === undefined || + typeof navigator.requestMediaKeySystemAccess !== 'function') { + const msg = 'Insecure origins are not allowed'; + eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { error: msg }); + reject({ error: msg }); + return; + } + + const keySystem = ksConfigurations[idx].ks; + const configs = ksConfigurations[idx].configs; + let systemString = keySystem.systemString; + + // Patch to support persistent licenses on Edge browser (see issue #2658) + if (systemString === ProtectionConstants.PLAYREADY_KEYSTEM_STRING && configs[0].persistentState === 'required') { + systemString += '.recommendation'; + } + + navigator.requestMediaKeySystemAccess(systemString, configs) + .then((mediaKeySystemAccess) => { + const configuration = (typeof mediaKeySystemAccess.getConfiguration === 'function') ? + mediaKeySystemAccess.getConfiguration() : null; + const keySystemAccess = new KeySystemAccess(keySystem, configuration); + + keySystemAccess.mksa = mediaKeySystemAccess; + eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { data: keySystemAccess }); + resolve({ data: keySystemAccess }); + }) + .catch((error) => { + if (idx + 1 < ksConfigurations.length) { + _requestKeySystemAccessInternal(ksConfigurations, idx + 1, resolve, reject); + } else { + const errorMessage = 'Key system access denied! '; + eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { error: errorMessage + error.message }); + reject({ error: errorMessage + error.message }); + } + }); } + /** + * Selects a key system by creating the mediaKeys and adding them to the video element + * @param keySystemAccess + * @return {Promise} + */ function selectKeySystem(keySystemAccess) { - keySystemAccess.mksa.createMediaKeys().then(function (mkeys) { - keySystem = keySystemAccess.keySystem; - mediaKeys = mkeys; - if (videoElement) { - videoElement.setMediaKeys(mediaKeys).then(function () { - eventBus.trigger(events.INTERNAL_KEY_SYSTEM_SELECTED); + return new Promise((resolve, reject) => { + keySystemAccess.mksa.createMediaKeys() + .then((mkeys) => { + keySystem = keySystemAccess.keySystem; + mediaKeys = mkeys; + if (videoElement) { + return videoElement.setMediaKeys(mediaKeys) + } else { + return Promise.resolve(); + } + }) + .then(() => { + resolve(keySystem); + }) + .catch(function () { + reject({ error: 'Error selecting keys system (' + keySystemAccess.keySystem.systemString + ')! Could not create MediaKeys -- TODO' }); }); - } else { - eventBus.trigger(events.INTERNAL_KEY_SYSTEM_SELECTED); - } - - }).catch(function () { - eventBus.trigger(events.INTERNAL_KEY_SYSTEM_SELECTED, { error: 'Error selecting keys system (' + keySystemAccess.keySystem.systemString + ')! Could not create MediaKeys -- TODO' }); - }); + }) } function setMediaElement(mediaElement) { @@ -194,6 +248,12 @@ function ProtectionModel_21Jan2015(config) { }); } + /** + * Create a key session, a session token and initialize a request by calling generateRequest + * @param initData + * @param protData + * @param sessionType + */ function createKeySession(initData, protData, sessionType) { if (!keySystem || !mediaKeys) { throw new Error('Can not create sessions until you have selected a key system'); @@ -201,16 +261,15 @@ function ProtectionModel_21Jan2015(config) { const session = mediaKeys.createSession(sessionType); const sessionToken = createSessionToken(session, initData, sessionType); - const ks = this.getKeySystem(); - // Generate initial key request. - // keyids type is used for clearkey when keys are provided directly in the protection data and then request to a license server is not needed - const dataType = ks.systemString === ProtectionConstants.CLEARKEY_KEYSTEM_STRING && (initData || (protData && protData.clearkeys)) ? 'keyids' : 'cenc'; + + // The "keyids" type is used for Clearkey when keys are provided directly in the protection data and a request to a license server is not needed + const dataType = keySystem.systemString === ProtectionConstants.CLEARKEY_KEYSTEM_STRING && (initData || (protData && protData.clearkeys)) ? ProtectionConstants.INITIALIZATION_DATA_TYPE_KEYIDS : ProtectionConstants.INITIALIZATION_DATA_TYPE_CENC; + session.generateRequest(dataType, initData).then(function () { logger.debug('DRM: Session created. SessionID = ' + sessionToken.getSessionID()); eventBus.trigger(events.KEY_SESSION_CREATED, { data: sessionToken }); }).catch(function (error) { - // TODO: Better error string removeSession(sessionToken); eventBus.trigger(events.KEY_SESSION_CREATED, { data: null, @@ -231,7 +290,7 @@ function ProtectionModel_21Jan2015(config) { eventBus.trigger(events.KEY_SESSION_UPDATED); }) .catch(function (error) { - eventBus.trigger(events.KEY_ERROR, {error: new DashJSError(ProtectionErrors.MEDIA_KEYERR_CODE, 'Error sending update() message! ' + error.name, sessionToken)}); + eventBus.trigger(events.KEY_ERROR, { error: new DashJSError(ProtectionErrors.MEDIA_KEYERR_CODE, 'Error sending update() message! ' + error.name, sessionToken) }); }); } @@ -289,7 +348,7 @@ function ProtectionModel_21Jan2015(config) { function closeKeySession(sessionToken) { // Send our request to the key session - closeKeySessionInternal(sessionToken).catch(function (error) { + _closeKeySessionInternal(sessionToken).catch(function (error) { removeSession(sessionToken); eventBus.trigger(events.KEY_SESSION_CLOSED, { data: null, @@ -298,43 +357,7 @@ function ProtectionModel_21Jan2015(config) { }); } - function requestKeySystemAccessInternal(ksConfigurations, idx) { - - if (navigator.requestMediaKeySystemAccess === undefined || - typeof navigator.requestMediaKeySystemAccess !== 'function') { - eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { error: 'Insecure origins are not allowed' }); - return; - } - - (function (i) { - const keySystem = ksConfigurations[i].ks; - const configs = ksConfigurations[i].configs; - let systemString = keySystem.systemString; - - // PATCH to support persistent licenses on Edge browser (see issue #2658) - if (systemString === ProtectionConstants.PLAYREADY_KEYSTEM_STRING && configs[0].persistentState === 'required') { - systemString += '.recommendation'; - } - - navigator.requestMediaKeySystemAccess(systemString, configs).then(function (mediaKeySystemAccess) { - // Chrome 40 does not currently implement MediaKeySystemAccess.getConfiguration() - const configuration = (typeof mediaKeySystemAccess.getConfiguration === 'function') ? - mediaKeySystemAccess.getConfiguration() : null; - const keySystemAccess = new KeySystemAccess(keySystem, configuration); - keySystemAccess.mksa = mediaKeySystemAccess; - eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { data: keySystemAccess }); - - }).catch(function (error) { - if (++i < ksConfigurations.length) { - requestKeySystemAccessInternal(ksConfigurations, i); - } else { - eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { error: 'Key system access denied! ' + error.message }); - } - }); - })(idx); - } - - function closeKeySessionInternal(sessionToken) { + function _closeKeySessionInternal(sessionToken) { const session = sessionToken.session; // Remove event listeners @@ -467,7 +490,7 @@ function ProtectionModel_21Jan2015(config) { session.addEventListener('message', token); // Register callback for session closed Promise - session.closed.then(function () { + session.closed.then(() => { removeSession(token); logger.debug('DRM: Session closed. SessionID = ' + token.getSessionID()); eventBus.trigger(events.KEY_SESSION_CLOSED, { data: token.getSessionID() }); @@ -480,19 +503,18 @@ function ProtectionModel_21Jan2015(config) { } instance = { - getAllInitData: getAllInitData, - requestKeySystemAccess: requestKeySystemAccess, - getKeySystem: getKeySystem, - selectKeySystem: selectKeySystem, - setMediaElement: setMediaElement, - setServerCertificate: setServerCertificate, - createKeySession: createKeySession, - updateKeySession: updateKeySession, - loadKeySession: loadKeySession, - removeKeySession: removeKeySession, - closeKeySession: closeKeySession, - stop: stop, - reset: reset + getAllInitData, + requestKeySystemAccess, + selectKeySystem, + setMediaElement, + setServerCertificate, + createKeySession, + updateKeySession, + loadKeySession, + removeKeySession, + closeKeySession, + stop, + reset }; setup(); diff --git a/src/streaming/protection/models/ProtectionModel_3Feb2014.js b/src/streaming/protection/models/ProtectionModel_3Feb2014.js index df69304193..87cbafe008 100644 --- a/src/streaming/protection/models/ProtectionModel_3Feb2014.js +++ b/src/streaming/protection/models/ProtectionModel_3Feb2014.js @@ -90,10 +90,6 @@ function ProtectionModel_3Feb2014(config) { } } - function getKeySystem() { - return keySystem; - } - function getAllInitData() { const retVal = []; for (let i = 0; i < sessions.length; i++) { @@ -103,75 +99,82 @@ function ProtectionModel_3Feb2014(config) { } function requestKeySystemAccess(ksConfigurations) { - - // Try key systems in order, first one with supported key system configuration - // is used - let found = false; - for (let ksIdx = 0; ksIdx < ksConfigurations.length; ksIdx++) { - const systemString = ksConfigurations[ksIdx].ks.systemString; - const configs = ksConfigurations[ksIdx].configs; - let supportedAudio = null; - let supportedVideo = null; - - // Try key system configs in order, first one with supported audio/video + return new Promise((resolve, reject) => { + // Try key systems in order, first one with supported key system configuration // is used - for (let configIdx = 0; configIdx < configs.length; configIdx++) { - const audios = configs[configIdx].audioCapabilities; - const videos = configs[configIdx].videoCapabilities; - - // Look for supported audio container/codecs - if (audios && audios.length !== 0) { - supportedAudio = []; // Indicates that we have a requested audio config - for (let audioIdx = 0; audioIdx < audios.length; audioIdx++) { - if (window[api.MediaKeys].isTypeSupported(systemString, audios[audioIdx].contentType)) { - supportedAudio.push(audios[audioIdx]); + let found = false; + for (let ksIdx = 0; ksIdx < ksConfigurations.length; ksIdx++) { + const systemString = ksConfigurations[ksIdx].ks.systemString; + const configs = ksConfigurations[ksIdx].configs; + let supportedAudio = null; + let supportedVideo = null; + + // Try key system configs in order, first one with supported audio/video + // is used + for (let configIdx = 0; configIdx < configs.length; configIdx++) { + const audios = configs[configIdx].audioCapabilities; + const videos = configs[configIdx].videoCapabilities; + + // Look for supported audio container/codecs + if (audios && audios.length !== 0) { + supportedAudio = []; // Indicates that we have a requested audio config + for (let audioIdx = 0; audioIdx < audios.length; audioIdx++) { + if (window[api.MediaKeys].isTypeSupported(systemString, audios[audioIdx].contentType)) { + supportedAudio.push(audios[audioIdx]); + } } } - } - // Look for supported video container/codecs - if (videos && videos.length !== 0) { - supportedVideo = []; // Indicates that we have a requested video config - for (let videoIdx = 0; videoIdx < videos.length; videoIdx++) { - if (window[api.MediaKeys].isTypeSupported(systemString, videos[videoIdx].contentType)) { - supportedVideo.push(videos[videoIdx]); + // Look for supported video container/codecs + if (videos && videos.length !== 0) { + supportedVideo = []; // Indicates that we have a requested video config + for (let videoIdx = 0; videoIdx < videos.length; videoIdx++) { + if (window[api.MediaKeys].isTypeSupported(systemString, videos[videoIdx].contentType)) { + supportedVideo.push(videos[videoIdx]); + } } } - } - // No supported audio or video in this configuration OR we have - // requested audio or video configuration that is not supported - if ((!supportedAudio && !supportedVideo) || - (supportedAudio && supportedAudio.length === 0) || - (supportedVideo && supportedVideo.length === 0)) { - continue; - } + // No supported audio or video in this configuration OR we have + // requested audio or video configuration that is not supported + if ((!supportedAudio && !supportedVideo) || + (supportedAudio && supportedAudio.length === 0) || + (supportedVideo && supportedVideo.length === 0)) { + continue; + } - // This configuration is supported - found = true; - const ksConfig = new KeySystemConfiguration(supportedAudio, supportedVideo); - const ks = protectionKeyController.getKeySystemBySystemString(systemString); - eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { data: new KeySystemAccess(ks, ksConfig) }); - break; + // This configuration is supported + found = true; + const ksConfig = new KeySystemConfiguration(supportedAudio, supportedVideo); + const ks = protectionKeyController.getKeySystemBySystemString(systemString); + const keySystemAccess = new KeySystemAccess(ks, ksConfig); + eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { data: keySystemAccess }); + resolve({ data: keySystemAccess }); + break; + } } - } - if (!found) { - eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { error: 'Key system access denied! -- No valid audio/video content configurations detected!' }); - } + if (!found) { + const errorMessage = 'Key system access denied! -- No valid audio/video content configurations detected!'; + eventBus.trigger(events.KEY_SYSTEM_ACCESS_COMPLETE, { error: errorMessage }); + reject({ error: errorMessage }); + } + }) } function selectKeySystem(ksAccess) { - try { - mediaKeys = ksAccess.mediaKeys = new window[api.MediaKeys](ksAccess.keySystem.systemString); - keySystem = ksAccess.keySystem; - keySystemAccess = ksAccess; - if (videoElement) { - setMediaKeys(); + return new Promise((resolve, reject) => { + try { + mediaKeys = ksAccess.mediaKeys = new window[api.MediaKeys](ksAccess.keySystem.systemString); + keySystem = ksAccess.keySystem; + keySystemAccess = ksAccess; + if (videoElement) { + setMediaKeys(); + } + resolve(keySystem); + } catch (error) { + reject({ error: 'Error selecting keys system (' + keySystem.systemString + ')! Could not create MediaKeys -- TODO' }); } - eventBus.trigger(events.INTERNAL_KEY_SYSTEM_SELECTED); - } catch (error) { - eventBus.trigger(events.INTERNAL_KEY_SYSTEM_SELECTED, { error: 'Error selecting keys system (' + keySystem.systemString + ')! Could not create MediaKeys -- TODO' }); - } + }) } function setMediaElement(mediaElement) { @@ -273,9 +276,14 @@ function ProtectionModel_3Feb2014(config) { session[api.release](); } - function setServerCertificate(/*serverCertificate*/) { /* Not supported */ } - function loadKeySession(/*sessionID*/) { /* Not supported */ } - function removeKeySession(/*sessionToken*/) { /* Not supported */ } + function setServerCertificate(/*serverCertificate*/) { /* Not supported */ + } + + function loadKeySession(/*sessionID*/) { /* Not supported */ + } + + function removeKeySession(/*sessionToken*/) { /* Not supported */ + } function createEventHandler() { @@ -361,19 +369,18 @@ function ProtectionModel_3Feb2014(config) { } instance = { - getAllInitData: getAllInitData, - requestKeySystemAccess: requestKeySystemAccess, - getKeySystem: getKeySystem, - selectKeySystem: selectKeySystem, - setMediaElement: setMediaElement, - createKeySession: createKeySession, - updateKeySession: updateKeySession, - closeKeySession: closeKeySession, - setServerCertificate: setServerCertificate, - loadKeySession: loadKeySession, - removeKeySession: removeKeySession, + getAllInitData, + requestKeySystemAccess, + selectKeySystem, + setMediaElement, + createKeySession, + updateKeySession, + closeKeySession, + setServerCertificate, + loadKeySession, + removeKeySession, stop: reset, - reset: reset + reset }; setup(); diff --git a/src/streaming/protection/servers/ClearKey.js b/src/streaming/protection/servers/ClearKey.js index 1fc1811fff..3087722098 100644 --- a/src/streaming/protection/servers/ClearKey.js +++ b/src/streaming/protection/servers/ClearKey.js @@ -77,11 +77,11 @@ function ClearKey() { } instance = { - getServerURLFromMessage: getServerURLFromMessage, - getHTTPMethod: getHTTPMethod, - getResponseType: getResponseType, - getLicenseMessage: getLicenseMessage, - getErrorResponse: getErrorResponse + getServerURLFromMessage, + getHTTPMethod, + getResponseType, + getLicenseMessage, + getErrorResponse }; return instance; diff --git a/src/streaming/protection/servers/DRMToday.js b/src/streaming/protection/servers/DRMToday.js index ed81d3f8b1..089f31f527 100644 --- a/src/streaming/protection/servers/DRMToday.js +++ b/src/streaming/protection/servers/DRMToday.js @@ -93,15 +93,15 @@ function DRMToday(config) { } instance = { - getServerURLFromMessage: getServerURLFromMessage, - getHTTPMethod: getHTTPMethod, - getResponseType: getResponseType, - getLicenseMessage: getLicenseMessage, - getErrorResponse: getErrorResponse + getServerURLFromMessage, + getHTTPMethod, + getResponseType, + getLicenseMessage, + getErrorResponse }; return instance; } DRMToday.__dashjs_factory_name = 'DRMToday'; -export default dashjs.FactoryMaker.getSingletonFactory(DRMToday); /* jshint ignore:line */ \ No newline at end of file +export default dashjs.FactoryMaker.getSingletonFactory(DRMToday); /* jshint ignore:line */ diff --git a/src/streaming/protection/servers/PlayReady.js b/src/streaming/protection/servers/PlayReady.js index 1ad76a4150..bea4b0e8ed 100644 --- a/src/streaming/protection/servers/PlayReady.js +++ b/src/streaming/protection/servers/PlayReady.js @@ -134,15 +134,15 @@ function PlayReady() { } instance = { - getServerURLFromMessage: getServerURLFromMessage, - getHTTPMethod: getHTTPMethod, - getResponseType: getResponseType, - getLicenseMessage: getLicenseMessage, - getErrorResponse: getErrorResponse + getServerURLFromMessage, + getHTTPMethod, + getResponseType, + getLicenseMessage, + getErrorResponse }; return instance; } PlayReady.__dashjs_factory_name = 'PlayReady'; -export default dashjs.FactoryMaker.getSingletonFactory(PlayReady); /* jshint ignore:line */ \ No newline at end of file +export default dashjs.FactoryMaker.getSingletonFactory(PlayReady); /* jshint ignore:line */ diff --git a/src/streaming/protection/servers/Widevine.js b/src/streaming/protection/servers/Widevine.js index 233d3e2c18..2609d5acb6 100644 --- a/src/streaming/protection/servers/Widevine.js +++ b/src/streaming/protection/servers/Widevine.js @@ -57,15 +57,15 @@ function Widevine() { } instance = { - getServerURLFromMessage: getServerURLFromMessage, - getHTTPMethod: getHTTPMethod, - getResponseType: getResponseType, - getLicenseMessage: getLicenseMessage, - getErrorResponse: getErrorResponse + getServerURLFromMessage, + getHTTPMethod, + getResponseType, + getLicenseMessage, + getErrorResponse }; return instance; } Widevine.__dashjs_factory_name = 'Widevine'; -export default dashjs.FactoryMaker.getSingletonFactory(Widevine); /* jshint ignore:line */ \ No newline at end of file +export default dashjs.FactoryMaker.getSingletonFactory(Widevine); /* jshint ignore:line */ diff --git a/test/unit/mocks/ProtectionKeyControllerMock.js b/test/unit/mocks/ProtectionKeyControllerMock.js index ab5bed397c..c719922b4c 100644 --- a/test/unit/mocks/ProtectionKeyControllerMock.js +++ b/test/unit/mocks/ProtectionKeyControllerMock.js @@ -11,6 +11,10 @@ function ProtectionKeyControllerMock () { this.getLicenseServer = function () { return null; }; + + this.getLicenseServerModelInstance = function () { + return {}; + } } -export default ProtectionKeyControllerMock; \ No newline at end of file +export default ProtectionKeyControllerMock; diff --git a/test/unit/mocks/ProtectionModelMock.js b/test/unit/mocks/ProtectionModelMock.js index 36f0a71179..ac5e3c8570 100644 --- a/test/unit/mocks/ProtectionModelMock.js +++ b/test/unit/mocks/ProtectionModelMock.js @@ -14,7 +14,8 @@ function ProtectionModelMock (config) { }; this.requestKeySystemAccess = function () { + return Promise.resolve(); }; } -export default ProtectionModelMock; \ No newline at end of file +export default ProtectionModelMock; diff --git a/test/unit/streaming.protection.CommonEncryption.js b/test/unit/streaming.protection.CommonEncryption.js index cf5c5871ce..d16abb4e30 100644 --- a/test/unit/streaming.protection.CommonEncryption.js +++ b/test/unit/streaming.protection.CommonEncryption.js @@ -4,7 +4,7 @@ import Base64 from '../../externals/base64'; const expect = require('chai').expect; let cpData; -describe('CommonEncryption', () => { +describe('CommonEncryption', () => { beforeEach(() => { cpData = { @@ -21,13 +21,13 @@ describe('CommonEncryption', () => { it('should return null if no init data is available in the ContentProtection element', () => { cpData = {}; - const result = CommonEncryption.parseInitDataFromContentProtection(cpData,Base64); + const result = CommonEncryption.parseInitDataFromContentProtection(cpData, Base64); expect(result).to.be.null; // jshint ignore:line }); it('should return base64 decoded string if init data is available in the ContentProtection element', () => { - const result = CommonEncryption.parseInitDataFromContentProtection(cpData,Base64); + const result = CommonEncryption.parseInitDataFromContentProtection(cpData, Base64); const expectedByteLength = Base64.decodeArray(cpData.pssh.__text).buffer.byteLength; expect(result.byteLength).to.equal(expectedByteLength); @@ -37,7 +37,7 @@ describe('CommonEncryption', () => { const expectedByteLength = Base64.decodeArray(cpData.pssh.__text).buffer.byteLength; cpData.pssh.__text = '\nAAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARABGgZlbHV2aW8iBmVsdXZpbw==\n'; const originalByteLength = Base64.decodeArray(cpData.pssh.__text).buffer.byteLength; - const result = CommonEncryption.parseInitDataFromContentProtection(cpData,Base64); + const result = CommonEncryption.parseInitDataFromContentProtection(cpData, Base64); expect(originalByteLength).to.not.equal(result.byteLength); expect(result.byteLength).to.equal(expectedByteLength); @@ -47,7 +47,7 @@ describe('CommonEncryption', () => { const expectedByteLength = Base64.decodeArray(cpData.pssh.__text).buffer.byteLength; cpData.pssh.__text = 'AAAANHBzc2gAAAAA7e+LqXnWSs6jy Cfc1R0h7QAAABQIARABGgZlbHV2aW8iBmVsdXZpbw=='; const originalByteLength = Base64.decodeArray(cpData.pssh.__text).buffer.byteLength; - const result = CommonEncryption.parseInitDataFromContentProtection(cpData,Base64); + const result = CommonEncryption.parseInitDataFromContentProtection(cpData, Base64); expect(originalByteLength).to.not.equal(result.byteLength); expect(result.byteLength).to.equal(expectedByteLength); @@ -57,7 +57,7 @@ describe('CommonEncryption', () => { const expectedByteLength = Base64.decodeArray(cpData.pssh.__text).buffer.byteLength; cpData.pssh.__text = '\n\n\nAAAANHBzc2gAAAAA7e+LqXnWSs6jy Cfc1R0h7QAAABQIARABGgZlbHV2aW8iBmVsdXZpbw==\n\n'; const originalByteLength = Base64.decodeArray(cpData.pssh.__text).buffer.byteLength; - const result = CommonEncryption.parseInitDataFromContentProtection(cpData,Base64); + const result = CommonEncryption.parseInitDataFromContentProtection(cpData, Base64); expect(originalByteLength).to.not.equal(result.byteLength); expect(result.byteLength).to.equal(expectedByteLength); @@ -65,4 +65,63 @@ describe('CommonEncryption', () => { }); -}); + describe('getLicenseServerUrlFromMediaInfo', () => { + let mediaInfo; + let schemeIdUri = 'abcd-efgh'; + + beforeEach(() => { + mediaInfo = [{ + contentProtection: [ + { + schemeIdUri: schemeIdUri, + laurl: { + __prefix: 'dashif', + __text: 'license-server-url' + } + } + ] + }] + }); + + it('should return null in case the schemeIdUri does not match', () => { + const result = CommonEncryption.getLicenseServerUrlFromMediaInfo(mediaInfo, 'nomatch'); + + expect(result).to.be.null; + }); + + it('should return null if license server url is empty', () => { + mediaInfo[0].contentProtection[0].laurl.__text = ''; + const result = CommonEncryption.getLicenseServerUrlFromMediaInfo(mediaInfo, schemeIdUri); + + expect(result).to.be.null; + }) + + it('should return null if wrong prefix', () => { + mediaInfo[0].contentProtection[0].laurl.__prefix = 'wrongprefix'; + const result = CommonEncryption.getLicenseServerUrlFromMediaInfo(mediaInfo, schemeIdUri); + + expect(result).to.be.null; + }) + + it('should return null if wrong attribute', () => { + delete mediaInfo[0].contentProtection[0].laurl; + const result = CommonEncryption.getLicenseServerUrlFromMediaInfo(mediaInfo, schemeIdUri); + + expect(result).to.be.null; + }) + + it('should return valid license server for dashif:laurl', () => { + const result = CommonEncryption.getLicenseServerUrlFromMediaInfo(mediaInfo, schemeIdUri); + + expect(result).to.be.equal('license-server-url'); + }) + + it('should return valid license server for dashif:Laurl', () => { + delete mediaInfo[0].contentProtection[0].laurl; + mediaInfo[0].contentProtection[0].Laurl = { __prefix: 'dashif', __text: 'license-server-url' }; + const result = CommonEncryption.getLicenseServerUrlFromMediaInfo(mediaInfo, schemeIdUri); + + expect(result).to.be.equal('license-server-url'); + }) + }); +}) diff --git a/test/unit/streaming.protection.controllers.ProtectionController.js b/test/unit/streaming.protection.controllers.ProtectionController.js index 176f540b01..6291df8736 100644 --- a/test/unit/streaming.protection.controllers.ProtectionController.js +++ b/test/unit/streaming.protection.controllers.ProtectionController.js @@ -13,12 +13,15 @@ const expect = require('chai').expect; const context = {}; const eventBus = EventBus(context).getInstance(); let protectionController; -const protectionKeyControllerMock = new ProtectionKeyControllerMock(); describe('ProtectionController', function () { describe('Not well initialized', function () { beforeEach(function () { - protectionController = ProtectionController(context).create({debug: new DebugMock()}); + protectionController = ProtectionController(context).create({ + debug: new DebugMock(), + events: ProtectionEvents, + eventBus + }); }); afterEach(function () { @@ -66,33 +69,21 @@ describe('ProtectionController', function () { describe('Well initialized', function () { beforeEach(function () { - protectionController = ProtectionController(context).create({protectionKeyController: protectionKeyControllerMock, + const protectionKeyControllerMock = new ProtectionKeyControllerMock(); + protectionController = ProtectionController(context).create({ + protectionKeyController: protectionKeyControllerMock, events: ProtectionEvents, debug: new DebugMock(), - protectionModel: new ProtectionModelMock({events: ProtectionEvents, eventBus: eventBus}), + protectionModel: new ProtectionModelMock({ events: ProtectionEvents, eventBus: eventBus }), eventBus: eventBus, - constants: Constants}); + constants: Constants + }); }); afterEach(function () { protectionController.reset(); }); - it('onKeyMessage behavior', function (done) { - let onDRMError = function (data) { - eventBus.off(ProtectionEvents.LICENSE_REQUEST_COMPLETE, onDRMError); - expect(data.error.code).to.be.equal(ProtectionErrors.MEDIA_KEY_MESSAGE_NO_CHALLENGE_ERROR_CODE); // jshint ignore:line - expect(data.error.message).to.be.equal(ProtectionErrors.MEDIA_KEY_MESSAGE_NO_CHALLENGE_ERROR_MESSAGE); // jshint ignore:line - done(); - }; - - eventBus.on(ProtectionEvents.LICENSE_REQUEST_COMPLETE, onDRMError, this); - - protectionController.initializeForMedia({type: 'VIDEO'}); - - eventBus.trigger(ProtectionEvents.INTERNAL_KEY_MESSAGE, {data: {}}); - }); - it('setServerCertificate behavior', function (done) { let onDRMError = function (data) { @@ -107,6 +98,21 @@ describe('ProtectionController', function () { protectionController.setServerCertificate(); }); + it('onKeyMessage behavior', function (done) { + let onDRMError = function (data) { + eventBus.off(ProtectionEvents.LICENSE_REQUEST_COMPLETE, onDRMError); + expect(data.error.code).to.be.equal(ProtectionErrors.MEDIA_KEY_MESSAGE_NO_CHALLENGE_ERROR_CODE); // jshint ignore:line + expect(data.error.message).to.be.equal(ProtectionErrors.MEDIA_KEY_MESSAGE_NO_CHALLENGE_ERROR_MESSAGE); // jshint ignore:line + done(); + }; + + eventBus.on(ProtectionEvents.LICENSE_REQUEST_COMPLETE, onDRMError, this); + + protectionController.initializeForMedia({ type: 'VIDEO' }); + + eventBus.trigger(ProtectionEvents.INTERNAL_KEY_MESSAGE, { data: {} }); + }); + it('should trigger KEY_SESSION_CREATED event with an error when createKeySession is called without parameter', function (done) { let onSessionCreated = function (data) { eventBus.off(ProtectionEvents.KEY_SESSION_CREATED, onSessionCreated); @@ -125,12 +131,5 @@ describe('ProtectionController', function () { expect(keySystems).not.to.be.empty; // jshint ignore:line }); - it('should ????? when setMediaElement is called', function () { - protectionController.initializeForMedia({type: 'VIDEO'}); - - protectionController.setMediaElement({}); - - expect(eventBus.trigger.bind(eventBus, ProtectionEvents.NEED_KEY, {key: {initDataType: 'cenc'}})).not.to.throw(); - }); }); -}); \ No newline at end of file +});