streaming_protection_controllers_ProtectionController.js

/**
 * The copyright in this software is being made available under the BSD License,
 * included below. This software may be subject to other third party and contributor
 * rights, including patent rights, and no such rights are granted under this license.
 *
 * Copyright (c) 2013, Dash Industry Forum.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation and/or
 *  other materials provided with the distribution.
 *  * Neither the name of Dash Industry Forum nor the names of its
 *  contributors may be used to endorse or promote products derived from this software
 *  without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
 *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 *  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 *  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 *  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

import CommonEncryption from '../CommonEncryption';
import MediaCapability from '../vo/MediaCapability';
import KeySystemConfiguration from '../vo/KeySystemConfiguration';
import ProtectionErrors from '../errors/ProtectionErrors';
import DashJSError from '../../vo/DashJSError';
import LicenseRequest from '../vo/LicenseRequest';
import LicenseResponse from '../vo/LicenseResponse';
import {HTTPRequest} from '../../vo/metrics/HTTPRequest';
import Utils from '../../../core/Utils';
import Constants from '../../constants/Constants';
import FactoryMaker from '../../../core/FactoryMaker';

const NEEDKEY_BEFORE_INITIALIZE_RETRIES = 5;
const NEEDKEY_BEFORE_INITIALIZE_TIMEOUT = 500;

const LICENSE_SERVER_REQUEST_RETRIES = 3;
const LICENSE_SERVER_REQUEST_RETRY_INTERVAL = 1000;
const LICENSE_SERVER_REQUEST_DEFAULT_TIMEOUT = 8000;

/**
 * @module ProtectionController
 * @description Provides access to media protection information and functionality.  Each
 * ProtectionController manages a single {@link MediaPlayer.models.ProtectionModel}
 * which encapsulates a set of protection information (EME APIs, selected key system,
 * key sessions).  The APIs of ProtectionController mostly align with the latest EME
 * APIs.  Key system selection is mostly automated when combined with app-overrideable
 * functionality provided in {@link ProtectionKeyController}.
 * @todo ProtectionController does almost all of its tasks automatically after init() is
 * called.  Applications might want more control over this process and want to go through
 * each step manually (key system selection, session creation, session maintenance).
 * This module can be accessed using the MediaPlayer API getProtectionController()
 * @param {Object} config
 */

function ProtectionController(config) {

    config = config || {};
    const protectionKeyController = config.protectionKeyController;
    let protectionModel = config.protectionModel;
    const eventBus = config.eventBus;
    const events = config.events;
    const debug = config.debug;
    const BASE64 = config.BASE64;
    const constants = config.constants;
    let needkeyRetries = [];
    const cmcdModel = config.cmcdModel;
    const settings = config.settings;
    const customParametersModel = config.customParametersModel;

    let instance,
        logger,
        pendingKeySessionsToHandle,
        mediaInfoArr,
        protDataSet,
        sessionType,
        robustnessLevel,
        selectedKeySystem,
        keySystemSelectionInProgress,
        licenseXhrRequest,
        licenseRequestRetryTimeout;

    function setup() {
        logger = debug.getLogger(instance);
        pendingKeySessionsToHandle = [];
        mediaInfoArr = [];
        sessionType = 'temporary';
        robustnessLevel = '';
        licenseXhrRequest = null;
        licenseRequestRetryTimeout = null;
        eventBus.on(events.INTERNAL_KEY_MESSAGE, _onKeyMessage, instance);
        eventBus.on(events.INTERNAL_KEY_STATUS_CHANGED, _onKeyStatusChanged, instance);
    }

    function checkConfig() {
        if (!eventBus || !eventBus.hasOwnProperty('on') || !protectionKeyController || !protectionKeyController.hasOwnProperty('getSupportedKeySystemsFromContentProtection')) {
            throw new Error('Missing config parameter(s)');
        }
    }

    /**
     * Initialize this protection system for a given media type.
     *
     * @param {StreamInfo} [mediaInfo] Media information
     * @memberof module:ProtectionController
     * @instance
     */
    function initializeForMedia(mediaInfo) {
        // Not checking here if a session for similar KS/KID combination is already created
        // because still don't know which keysystem will be selected.
        // Once Keysystem is selected and before creating the session, we will do that check
        // so we create the strictly necessary DRM sessions
        if (!mediaInfo) {
            throw new Error('mediaInfo can not be null or undefined');
        }

        checkConfig();

        mediaInfoArr.push(mediaInfo);
    }

    /**
     * Once all mediaInfo objects have been added to our mediaInfoArray we can select a key system or check if the kid has changed and we need to trigger a new license request
     * @memberof module:ProtectionController
     * @instance
     */
    function handleKeySystemFromManifest() {
        if (!mediaInfoArr || mediaInfoArr.length === 0) {
            return;
        }

        let supportedKeySystems = [];
        mediaInfoArr.forEach((mInfo) => {
            const currentKs = protectionKeyController.getSupportedKeySystemsFromContentProtection(mInfo.contentProtection, protDataSet, sessionType);
            // We assume that the same key systems are signaled for each AS. We can use the first entry we found
            if (currentKs.length > 0) {
                if (supportedKeySystems.length === 0) {
                    supportedKeySystems = currentKs;
                }
                // Save config for creating key session once we selected a key system
                pendingKeySessionsToHandle.push(currentKs);
            }
        })

        if (supportedKeySystems && supportedKeySystems.length > 0) {
            _selectKeySystemOrUpdateKeySessions(supportedKeySystems, true);
        }
    }

    /**
     * Selects a key system if we dont have any one yet. Otherwise we use the existing key system and trigger a new license request if the initdata has changed
     * @param {array} supportedKs
     * @private
     */
    function _handleKeySystemFromPssh(supportedKs) {
        pendingKeySessionsToHandle.push(supportedKs);
        _selectKeySystemOrUpdateKeySessions(supportedKs, false);
    }

    /**
     * Select the key system or update one of our existing key sessions
     * @param {array} supportedKs
     * @param {boolean} fromManifest
     * @private
     */
    function _selectKeySystemOrUpdateKeySessions(supportedKs, fromManifest) {
        // First time, so we need to select a key system
        if (!selectedKeySystem && !keySystemSelectionInProgress) {
            _selectInitialKeySystem(supportedKs, fromManifest);
        }

        // We already selected a key system. We only need to trigger a new license exchange if the init data has changed
        else if (selectedKeySystem) {
            _handleKeySessions();
        }
    }

    /**
     * We do not have a key system yet. Select one
     * @param {array} supportedKs
     * @param {boolean} fromManifest
     * @private
     */
    function _selectInitialKeySystem(supportedKs, fromManifest) {
        if (!keySystemSelectionInProgress) {
            keySystemSelectionInProgress = true;
            const requestedKeySystems = [];

            // Reorder key systems according to priority order provided in protectionData
            supportedKs = supportedKs.sort((ksA, ksB) => {
                let indexA = (protDataSet && protDataSet[ksA.ks.systemString] && protDataSet[ksA.ks.systemString].priority >= 0) ? protDataSet[ksA.ks.systemString].priority : supportedKs.length;
                let indexB = (protDataSet && protDataSet[ksB.ks.systemString] && protDataSet[ksB.ks.systemString].priority >= 0) ? protDataSet[ksB.ks.systemString].priority : supportedKs.length;
                return indexA - indexB;
            });

            // Add all key systems to our request list since we have yet to select a key system
            for (let i = 0; i < supportedKs.length; i++) {
                const keySystemConfiguration = _getKeySystemConfiguration(supportedKs[i]);
                requestedKeySystems.push({
                    ks: supportedKs[i].ks,
                    configs: [keySystemConfiguration],
                    protData: supportedKs[i].protData
                });
            }

            let keySystemAccess;

            protectionModel.requestKeySystemAccess(requestedKeySystems)
                .then((event) => {
                    keySystemAccess = event.data;
                    let selectedSystemString = keySystemAccess.mksa && keySystemAccess.mksa.selectedSystemString ? keySystemAccess.mksa.selectedSystemString : keySystemAccess.keySystem.systemString;
                    logger.info('DRM: KeySystem Access Granted for system string (' + selectedSystemString + ')!  Selecting key system...');
                    return protectionModel.selectKeySystem(keySystemAccess);
                })
                .then((keySystem) => {
                    selectedKeySystem = keySystem;
                    keySystemSelectionInProgress = false;

                    if (!protectionModel) {
                        return;
                    }

                    eventBus.trigger(events.KEY_SYSTEM_SELECTED, { data: keySystemAccess });

                    // Set server certificate from protData
                    const protData = _getProtDataForKeySystem(selectedKeySystem);
                    if (protData && protData.serverCertificate && protData.serverCertificate.length > 0) {
                        protectionModel.setServerCertificate(BASE64.decodeArray(protData.serverCertificate).buffer);
                    }

                    _handleKeySessions();
                })
                .catch((event) => {
                    selectedKeySystem = null;
                    keySystemSelectionInProgress = false;
                    if (!fromManifest) {
                        eventBus.trigger(events.KEY_SYSTEM_SELECTED, {
                            data: null,
                            error: new DashJSError(ProtectionErrors.KEY_SYSTEM_ACCESS_DENIED_ERROR_CODE, ProtectionErrors.KEY_SYSTEM_ACCESS_DENIED_ERROR_MESSAGE + 'Error selecting key system! -- ' + event.error)
                        });
                    }
                })
        }
    }

    /**
     * If we have already selected a key system we only need to create a new key session and issue a new license request if the init data has changed.
     * @private
     */
    function _handleKeySessions() {
        // Create key sessions for the different AdaptationSets
        let ksIdx;
        for (let i = 0; i < pendingKeySessionsToHandle.length; i++) {
            for (ksIdx = 0; ksIdx < pendingKeySessionsToHandle[i].length; ksIdx++) {
                if (selectedKeySystem === pendingKeySessionsToHandle[i][ksIdx].ks) {
                    const current = pendingKeySessionsToHandle[i][ksIdx]
                    _loadOrCreateKeySession(current)
                    break;
                }
            }
        }
        pendingKeySessionsToHandle = [];
    }

    /**
     * Loads an existing key session if we already have a session id. Otherwise we create a new key session
     * @param {object} keySystemInfo
     * @private
     */
    function _loadOrCreateKeySession(keySystemInfo) {
        // Clearkey
        if (protectionKeyController.isClearKey(selectedKeySystem)) {
            // For Clearkey: if parameters for generating init data was provided by the user, use them for generating
            // initData and overwrite possible initData indicated in encrypted event (EME)
            if (keySystemInfo.protData && keySystemInfo.protData.hasOwnProperty('clearkeys') && Object.keys(keySystemInfo.protData.clearkeys).length !== 0) {
                const initData = { kids: Object.keys(keySystemInfo.protData.clearkeys) };
                keySystemInfo.initData = new TextEncoder().encode(JSON.stringify(initData));
            }
        }

        // Reuse existing KeySession
        if (keySystemInfo.sessionId) {
            // Load MediaKeySession with sessionId
            loadKeySession(keySystemInfo);
        }

        // Create a new KeySession
        else if (keySystemInfo.initData !== null) {
            // Create new MediaKeySession with initData
            createKeySession(keySystemInfo);
        }
    }

    /**
     * Loads a key session with the given session ID from persistent storage.  This essentially creates a new key session
     *
     * @param {object} ksInfo
     * @memberof module:ProtectionController
     * @instance
     * @fires ProtectionController#KeySessionCreated
     * @ignore
     */
    function loadKeySession(keySystemInfo) {
        checkConfig();
        protectionModel.loadKeySession(keySystemInfo);
    }

    /**
     * Create a new key session associated with the given initialization data from the MPD or from the PSSH box in the media
     * For the latest version of the EME a request is generated. Once this request is ready we get notified via the INTERNAL_KEY_MESSAGE event
     * @param {ArrayBuffer} initData the initialization data
     * @param {Uint8Array} cdmData the custom data to provide to licenser
     * @memberof module:ProtectionController
     * @instance
     * @fires ProtectionController#KeySessionCreated
     * @ignore
     */
    function createKeySession(keySystemInfo) {
        const initDataForKS = CommonEncryption.getPSSHForKeySystem(selectedKeySystem, keySystemInfo ? keySystemInfo.initData : null);

        if (initDataForKS) {

            // Check for duplicate key id
            if (_isKeyIdDuplicate(keySystemInfo.keyId)) {
                return;
            }

            // Check for duplicate initData
            if (_isInitDataDuplicate(initDataForKS)) {
                return;
            }

            try {
                keySystemInfo.initData = initDataForKS;
                protectionModel.createKeySession(keySystemInfo);
            } catch (error) {
                eventBus.trigger(events.KEY_SESSION_CREATED, {
                    data: null,
                    error: new DashJSError(ProtectionErrors.KEY_SESSION_CREATED_ERROR_CODE, ProtectionErrors.KEY_SESSION_CREATED_ERROR_MESSAGE + error.message)
                });
            }
        } else if (keySystemInfo && keySystemInfo.initData) {
            protectionModel.createKeySession(keySystemInfo);
        } else {
            eventBus.trigger(events.KEY_SESSION_CREATED, {
                data: null,
                error: new DashJSError(ProtectionErrors.KEY_SESSION_CREATED_ERROR_CODE, ProtectionErrors.KEY_SESSION_CREATED_ERROR_MESSAGE + 'Selected key system is ' + (selectedKeySystem ? selectedKeySystem.systemString : null) + '.  needkey/encrypted event contains no initData corresponding to that key system!')
            });
        }
    }

    /**
     * Returns the protectionData for a specific keysystem as specified by the application.
     * @param {object} keySystem
     * @return {object | null}
     * @private
     */
    function _getProtDataForKeySystem(keySystem) {
        if (keySystem) {
            const keySystemString = keySystem.systemString;

            if (protDataSet) {
                return (keySystemString in protDataSet) ? protDataSet[keySystemString] : null;
            }
        }
        return null;
    }

    /**
     * Removes all entries from the mediaInfoArr
     */
    function clearMediaInfoArray() {
        mediaInfoArr = [];
    }

    /**
     * Returns a set of supported key systems and CENC initialization data
     * from the given array of ContentProtection elements.  Only
     * key systems that are supported by this player will be returned.
     * Key systems are returned in priority order (highest first).
     *
     * @param {Array.<Object>} cps - array of content protection elements parsed
     * from the manifest
     * @returns {Array.<Object>} array of objects indicating which supported key
     * systems were found.  Empty array is returned if no
     * supported key systems were found
     * @memberof module:ProtectionKeyController
     * @instance
     * @ignore
     */
    function getSupportedKeySystemsFromContentProtection(cps) {
        checkConfig();
        return protectionKeyController.getSupportedKeySystemsFromContentProtection(cps, protDataSet, sessionType);
    }

    /**
     * Checks if a session has already created for the provided key id
     * @param {string} keyId
     * @return {boolean}
     * @private
     */
    function _isKeyIdDuplicate(keyId) {

        if (!keyId) {
            return false;
        }

        try {
            const sessions = protectionModel.getSessions();
            for (let i = 0; i < sessions.length; i++) {
                if (sessions[i].getKeyId() === keyId) {
                    return true;
                }
            }
            return false;
        } catch (e) {
            return false;
        }
    }

    /**
     * Checks if the provided init data is equal to one of the existing init data values
     * @param {any} initDataForKS
     * @return {boolean}
     * @private
     */
    function _isInitDataDuplicate(initDataForKS) {

        if (!initDataForKS) {
            return false;
        }

        try {
            const currentInitData = protectionModel.getAllInitData();
            for (let i = 0; i < currentInitData.length; i++) {
                if (protectionKeyController.initDataEquals(initDataForKS, currentInitData[i])) {
                    logger.debug('DRM: Ignoring initData because we have already seen it!');
                    return true;
                }
            }

            return false;
        } catch (e) {
            return false;
        }
    }

    /**
     * Removes the given key session from persistent storage and closes the session
     * as if {@link ProtectionController#closeKeySession}
     * was called
     *
     * @param {SessionToken} sessionToken the session
     * token
     * @memberof module:ProtectionController
     * @instance
     * @fires ProtectionController#KeySessionRemoved
     * @fires ProtectionController#KeySessionClosed
     * @ignore
     */
    function removeKeySession(sessionToken) {
        checkConfig();
        protectionModel.removeKeySession(sessionToken);
    }

    /**
     * Closes the key session and releases all associated decryption keys.  These
     * keys will no longer be available for decrypting media
     *
     * @param {SessionToken} sessionToken the session
     * token
     * @memberof module:ProtectionController
     * @instance
     * @fires ProtectionController#KeySessionClosed
     * @ignore
     */
    function closeKeySession(sessionToken) {
        checkConfig();
        protectionModel.closeKeySession(sessionToken);
    }

    /**
     * Sets a server certificate for use by the CDM when signing key messages
     * intended for a particular license server.  This will fire
     * an error event if a key system has not yet been selected.
     *
     * @param {ArrayBuffer} serverCertificate a CDM-specific license server
     * certificate
     * @memberof module:ProtectionController
     * @instance
     * @fires ProtectionController#ServerCertificateUpdated
     */
    function setServerCertificate(serverCertificate) {
        checkConfig();
        protectionModel.setServerCertificate(serverCertificate);
    }

    /**
     * Associate this protection system with the given HTMLMediaElement.  This
     * causes the system to register for needkey/encrypted events from the given
     * element and provides a destination for setting of MediaKeys
     *
     * @param {HTMLMediaElement} element the media element to which the protection
     * system should be associated
     * @memberof module:ProtectionController
     * @instance
     */
    function setMediaElement(element) {
        checkConfig();
        if (element) {
            protectionModel.setMediaElement(element);
            eventBus.on(events.NEED_KEY, _onNeedKey, instance);
        } else if (element === null) {
            protectionModel.setMediaElement(element);
            eventBus.off(events.NEED_KEY, _onNeedKey, instance);
        }
    }

    /**
     * Sets the session type to use when creating key sessions.  Either "temporary" or
     * "persistent-license".  Default is "temporary".
     *
     * @param {string} value the session type
     * @memberof module:ProtectionController
     * @instance
     */
    function setSessionType(value) {
        sessionType = value;
    }

    /**
     * Sets the robustness level for video and audio capabilities. Optional to remove Chrome warnings.
     * Possible values are SW_SECURE_CRYPTO, SW_SECURE_DECODE, HW_SECURE_CRYPTO, HW_SECURE_CRYPTO, HW_SECURE_DECODE, HW_SECURE_ALL.
     *
     * @param {string} level the robustness level
     * @memberof module:ProtectionController
     * @instance
     */
    function setRobustnessLevel(level) {
        robustnessLevel = level;
    }

    /**
     * Attach KeySystem-specific data to use for license acquisition with EME
     *
     * @param {Object} data an object containing property names corresponding to
     * key system name strings (e.g. "org.w3.clearkey") and associated values
     * being instances of {@link ProtectionData}
     * @memberof module:ProtectionController
     * @instance
     * @ignore
     */
    function setProtectionData(data) {
        protDataSet = data;
        protectionKeyController.setProtectionData(data);
    }

    /**
     * Stop method is called when current playback is stopped/resetted.
     *
     * @memberof module:ProtectionController
     * @instance
     */
    function stop() {
        _abortLicenseRequest();
        if (protectionModel) {
            protectionModel.stop();
        }
    }

    /**
     * Destroys all protection data associated with this protection set.  This includes
     * deleting all key sessions. In the case of persistent key sessions, the sessions
     * will simply be unloaded and not deleted.  Additionally, if this protection set is
     * associated with a HTMLMediaElement, it will be detached from that element.
     *
     * @memberof module:ProtectionController
     * @instance
     * @ignore
     */
    function reset() {
        eventBus.off(events.INTERNAL_KEY_MESSAGE, _onKeyMessage, instance);
        eventBus.off(events.INTERNAL_KEY_STATUS_CHANGED, _onKeyStatusChanged, instance);

        checkConfig();

        _abortLicenseRequest();

        setMediaElement(null);

        selectedKeySystem = null;
        keySystemSelectionInProgress = false;

        if (protectionModel) {
            protectionModel.reset();
            protectionModel = null;
        }

        needkeyRetries.forEach(retryTimeout => clearTimeout(retryTimeout));
        needkeyRetries = [];

        mediaInfoArr = [];
        pendingKeySessionsToHandle = [];
    }

    /**
     * Returns an object corresponding to the EME MediaKeySystemConfiguration dictionary
     * @param {object} keySystem
     * @return {KeySystemConfiguration}
     * @private
     */
    function _getKeySystemConfiguration(keySystemData) {
        const protData = keySystemData.protData;
        const audioCapabilities = [];
        const videoCapabilities = [];
        const audioRobustness = (protData && protData.audioRobustness && protData.audioRobustness.length > 0) ? protData.audioRobustness : robustnessLevel;
        const videoRobustness = (protData && protData.videoRobustness && protData.videoRobustness.length > 0) ? protData.videoRobustness : robustnessLevel;
        const ksSessionType = keySystemData.sessionType;
        const distinctiveIdentifier = (protData && protData.distinctiveIdentifier) ? protData.distinctiveIdentifier : 'optional';
        const persistentState = (protData && protData.persistentState) ? protData.persistentState : (ksSessionType === 'temporary') ? 'optional' : 'required';

        mediaInfoArr.forEach((media) => {
            if (media.type === constants.AUDIO) {
                audioCapabilities.push(new MediaCapability(media.codec, audioRobustness));
            } else if (media.type === constants.VIDEO) {
                videoCapabilities.push(new MediaCapability(media.codec, videoRobustness));
            }
        });

        return new KeySystemConfiguration(
            audioCapabilities, videoCapabilities, distinctiveIdentifier,
            persistentState,
            [ksSessionType]);
    }

    /**
     * Event handler for when the status of the key has changed
     * @param {object} e
     * @private
     */
    function _onKeyStatusChanged(e) {
        if (e.error) {
            eventBus.trigger(events.KEY_STATUSES_CHANGED, { data: null, error: e.error });
        } else {
            logger.debug('DRM: key status = ' + e.status);
        }
    }

    /**
     * Event handler for the key message event. Once we have a key message we can issue a license request
     * @param {object} e
     * @private
     */
    function _onKeyMessage(e) {
        logger.debug('DRM: onKeyMessage');

        // Dispatch event to applications indicating we received a key message
        const keyMessage = e.data;
        eventBus.trigger(events.KEY_MESSAGE, { data: keyMessage });
        const messageType = (keyMessage.messageType) ? keyMessage.messageType : 'license-request';
        const message = keyMessage.message;
        const sessionToken = keyMessage.sessionToken;
        const protData = _getProtDataForKeySystem(selectedKeySystem);
        const licenseServerModelInstance = protectionKeyController.getLicenseServerModelInstance(selectedKeySystem, protData, messageType);
        const eventData = { sessionToken: sessionToken, messageType: messageType };

        // Ensure message from CDM is not empty
        if (!message || message.byteLength === 0) {
            _sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_NO_CHALLENGE_ERROR_CODE, ProtectionErrors.MEDIA_KEY_MESSAGE_NO_CHALLENGE_ERROR_MESSAGE));
            return;
        }

        // Message not destined for license server
        if (!licenseServerModelInstance) {
            logger.debug('DRM: License server request not required for this message (type = ' + e.data.messageType + ').  Session ID = ' + sessionToken.getSessionId());
            _sendLicenseRequestCompleteEvent(eventData);
            return;
        }

        // Perform any special handling for ClearKey
        if (protectionKeyController.isClearKey(selectedKeySystem)) {
            const clearkeys = protectionKeyController.processClearKeyLicenseRequest(selectedKeySystem, protData, message);
            if (clearkeys && clearkeys.keyPairs && clearkeys.keyPairs.length > 0) {
                logger.debug('DRM: ClearKey license request handled by application!');
                _sendLicenseRequestCompleteEvent(eventData);
                protectionModel.updateKeySession(sessionToken, clearkeys);
                return;
            }
        }

        // In all other cases we have to make a license request
        _issueLicenseRequest(keyMessage, licenseServerModelInstance, protData);
    }

    /**
     * Notify other classes that the license request was completed
     * @param {object} data
     * @param {object} error
     * @private
     */
    function _sendLicenseRequestCompleteEvent(data, error) {
        eventBus.trigger(events.LICENSE_REQUEST_COMPLETE, { data: data, error: error });
    }

    /**
     * Start issuing a license request
     * @param {object} keyMessage
     * @param {object} licenseServerData
     * @param {object} protData
     * @private
     */
    function _issueLicenseRequest(keyMessage, licenseServerData, protData) {
        const sessionToken = keyMessage.sessionToken;
        const messageType = (keyMessage.messageType) ? keyMessage.messageType : 'license-request';
        const eventData = { sessionToken: sessionToken, messageType: messageType };
        const keySystemString = selectedKeySystem ? selectedKeySystem.systemString : null;

        // Determine license server URL
        let url = _getLicenseServerUrl(protData, messageType, sessionToken, keyMessage, licenseServerData);

        // Ensure valid license server URL
        if (!url) {
            _sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_NO_LICENSE_SERVER_URL_ERROR_CODE, ProtectionErrors.MEDIA_KEY_MESSAGE_NO_LICENSE_SERVER_URL_ERROR_MESSAGE));
            return;
        }

        // Set optional XMLHttpRequest headers from protection data and message
        const reqHeaders = {};
        let withCredentials = false;
        if (protData) {
            _updateHeaders(reqHeaders, protData.httpRequestHeaders);
        }
        const message = keyMessage.message;
        const headersFromMessage = selectedKeySystem.getRequestHeadersFromMessage(message);
        _updateHeaders(reqHeaders, headersFromMessage);

        Object.keys(reqHeaders).forEach((key) => {
            if ('authorization' === key.toLowerCase()) {
                withCredentials = true;
            }
        });

        // Overwrite withCredentials property from protData if present
        if (protData && typeof protData.withCredentials == 'boolean') {
            withCredentials = protData.withCredentials;
        }

        const onLoad = function (xhr) {
            if (!protectionModel) {
                return;
            }

            if (xhr.status >= 200 && xhr.status <= 299) {
                const responseHeaders = Utils.parseHttpHeaders(xhr.getAllResponseHeaders ? xhr.getAllResponseHeaders() : null);
                let licenseResponse = new LicenseResponse(xhr.responseURL, responseHeaders, xhr.response);
                const licenseResponseFilters = customParametersModel.getLicenseResponseFilters();
                _applyFilters(licenseResponseFilters, licenseResponse)
                    .then(() => {
                        const licenseMessage = licenseServerData.getLicenseMessage(licenseResponse.data, keySystemString, messageType);
                        if (licenseMessage !== null) {
                            _sendLicenseRequestCompleteEvent(eventData);
                            protectionModel.updateKeySession(sessionToken, licenseMessage);
                        } else {
                            _reportError(xhr, eventData, keySystemString, messageType, licenseServerData);
                        }
                    });
            } else {
                _reportError(xhr, eventData, keySystemString, messageType, licenseServerData);
            }
        };

        const onAbort = function (xhr) {
            _sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_CODE,
                ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_MESSAGE + keySystemString + ' update, XHR aborted. status is "' +
                xhr.statusText + '" (' + xhr.status + '), readyState is ' + xhr.readyState));
        };

        const onError = function (xhr) {
            _sendLicenseRequestCompleteEvent(eventData, new DashJSError(ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_CODE,
                ProtectionErrors.MEDIA_KEY_MESSAGE_LICENSER_ERROR_MESSAGE + keySystemString + ' update, XHR error. status is "' +
                xhr.statusText + '" (' + xhr.status + '), readyState is ' + xhr.readyState));
        };

        const reqPayload = selectedKeySystem.getLicenseRequestFromMessage(message);
        const reqMethod = licenseServerData.getHTTPMethod(messageType);
        const responseType = licenseServerData.getResponseType(keySystemString, messageType);
        const timeout = protData && !isNaN(protData.httpTimeout) ? protData.httpTimeout : LICENSE_SERVER_REQUEST_DEFAULT_TIMEOUT;
        const sessionId = sessionToken.getSessionId() || null;

        let licenseRequest = new LicenseRequest(url, reqMethod, responseType, reqHeaders, withCredentials, messageType, sessionId, reqPayload);
        const retryAttempts = !isNaN(settings.get().streaming.retryAttempts[HTTPRequest.LICENSE]) ? settings.get().streaming.retryAttempts[HTTPRequest.LICENSE] : LICENSE_SERVER_REQUEST_RETRIES;
        const licenseRequestFilters = customParametersModel.getLicenseRequestFilters();
        _applyFilters(licenseRequestFilters, licenseRequest)
            .then(() => {
                _doLicenseRequest(licenseRequest, retryAttempts, timeout, onLoad, onAbort, onError);
            });
    }

    /**
     * Implement license requests with a retry mechanism to avoid temporary network issues to affect playback experience
     * @param {object} request
     * @param {number} retriesCount
     * @param {number} timeout
     * @param {function} onLoad
     * @param {function} onAbort
     * @param {function} onError
     * @private
     */
    function _doLicenseRequest(request, retriesCount, timeout, onLoad, onAbort, onError) {
        const xhr = new XMLHttpRequest();

        if (settings.get().streaming.cmcd && settings.get().streaming.cmcd.enabled) {
            const cmcdMode = settings.get().streaming.cmcd.mode;
            if (cmcdMode === Constants.CMCD_MODE_QUERY) {
                const cmcdParams = cmcdModel.getQueryParameter({
                    url: request.url,
                    type: HTTPRequest.LICENSE
                });

                if (cmcdParams) {
                    request.url = Utils.addAditionalQueryParameterToUrl(request.url, [cmcdParams]);
                }
            }
        }

        xhr.open(request.method, request.url, true);
        xhr.responseType = request.responseType;
        xhr.withCredentials = request.withCredentials;
        if (timeout > 0) {
            xhr.timeout = timeout;
        }
        for (const key in request.headers) {
            xhr.setRequestHeader(key, request.headers[key]);
        }

        if (settings.get().streaming.cmcd && settings.get().streaming.cmcd.enabled) {
            const cmcdMode = settings.get().streaming.cmcd.mode;
            if (cmcdMode === Constants.CMCD_MODE_HEADER) {
                const cmcdHeaders = cmcdModel.getHeaderParameters({
                    url: request.url,
                    type: HTTPRequest.LICENSE
                });

                if (cmcdHeaders) {
                    for (const header in cmcdHeaders) {
                        let value = cmcdHeaders[header];
                        if (value) {
                            xhr.setRequestHeader(header, value);
                        }
                    }
                }
            }
        }

        const _retryRequest = function () {
            // fail silently and retry
            retriesCount--;
            const retryInterval = !isNaN(settings.get().streaming.retryIntervals[HTTPRequest.LICENSE]) ? settings.get().streaming.retryIntervals[HTTPRequest.LICENSE] : LICENSE_SERVER_REQUEST_RETRY_INTERVAL;
            licenseRequestRetryTimeout = setTimeout(function () {
                _doLicenseRequest(request, retriesCount, timeout, onLoad, onAbort, onError);
            }, retryInterval);
        };

        xhr.onload = function () {
            licenseXhrRequest = null;
            if (this.status >= 200 && this.status <= 299 || retriesCount <= 0) {
                onLoad(this);
            } else {
                logger.warn('License request failed (' + this.status + '). Retrying it... Pending retries: ' + retriesCount);
                _retryRequest();
            }
        };

        xhr.ontimeout = xhr.onerror = function () {
            licenseXhrRequest = null;
            if (retriesCount <= 0) {
                onError(this);
            } else {
                logger.warn('License request network request failed . Retrying it... Pending retries: ' + retriesCount);
                _retryRequest();
            }
        };

        xhr.onabort = function () {
            onAbort(this);
        };

        // deprecated, to be removed
        eventBus.trigger(events.LICENSE_REQUEST_SENDING, {
            url: request.url,
            headers: request.headers,
            payload: request.data,
            sessionId: request.sessionId
        });

        licenseXhrRequest = xhr;
        xhr.send(request.data);
    }

    /**
     * Aborts license request
     * @private
     */
    function _abortLicenseRequest() {
        if (licenseXhrRequest) {
            licenseXhrRequest.onloadend = licenseXhrRequest.onerror = licenseXhrRequest.onprogress = undefined; //Ignore events from aborted requests.
            licenseXhrRequest.abort();
            licenseXhrRequest = null;
        }

        if (licenseRequestRetryTimeout) {
            clearTimeout(licenseRequestRetryTimeout);
            licenseRequestRetryTimeout = null;
        }
    }

    /**
     * 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;

        // 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];
            }
        }

        // This is the old way of providing the url
        else if (protData && protData.laURL && protData.laURL !== '') {
            url = protData.laURL;
        }

        // 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);

            // 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);

        return url;
    }

    /**
     * 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) {
        let errorMsg = 'NONE';
        let data = null;

        if (xhr.response) {
            errorMsg = licenseServerData.getErrorResponse(xhr.response, keySystemString, messageType);
            data = {
                serverResponse: xhr.response || null,
                responseCode: xhr.status || null,
                responseText: xhr.statusText || null
            }
        }

        _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,
            data
        ));
    }

    /**
     * Applies custom filters defined by the application
     * @param {array} filters
     * @param {object} param
     * @return {Promise<void>|*}
     * @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
     * @private
     */
    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.getSupportedKeySystemsFromSegmentPssh(abInitData, protDataSet, sessionType);
            if (supportedKs.length === 0) {
                logger.debug('DRM: Received needkey event with initData, but we don\'t support any of the key systems!');
                return;
            }

            _handleKeySystemFromPssh(supportedKs);
        }
    }

    /**
     * Returns all available key systems
     * @return {array}
     */
    function getKeySystems() {
        return protectionKeyController ? protectionKeyController.getKeySystems() : [];
    }

    /**
     * Sets all available key systems
     * @param {array} keySystems
     */
    function setKeySystems(keySystems) {
        if (protectionKeyController) {
            protectionKeyController.setKeySystems(keySystems);
        }
    }

    instance = {
        initializeForMedia,
        clearMediaInfoArray,
        handleKeySystemFromManifest,
        createKeySession,
        loadKeySession,
        removeKeySession,
        closeKeySession,
        setServerCertificate,
        setMediaElement,
        setSessionType,
        setRobustnessLevel,
        setProtectionData,
        getSupportedKeySystemsFromContentProtection,
        getKeySystems,
        setKeySystems,
        stop,
        reset
    };

    setup();
    return instance;
}

ProtectionController.__dashjs_factory_name = 'ProtectionController';
export default FactoryMaker.getClassFactory(ProtectionController); /* jshint ignore:line */