streaming_controllers_AbrController.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 ABRRulesCollection from '../rules/abr/ABRRulesCollection';
import Constants from '../constants/Constants';
import MetricsConstants from '../constants/MetricsConstants';
import BitrateInfo from '../vo/BitrateInfo';
import FragmentModel from '../models/FragmentModel';
import EventBus from '../../core/EventBus';
import Events from '../../core/events/Events';
import FactoryMaker from '../../core/FactoryMaker';
import RulesContext from '../rules/RulesContext';
import SwitchRequest from '../rules/SwitchRequest';
import SwitchRequestHistory from '../rules/SwitchRequestHistory';
import DroppedFramesHistory from '../rules/DroppedFramesHistory';
import ThroughputHistory from '../rules/ThroughputHistory';
import Debug from '../../core/Debug';
import {HTTPRequest} from '../vo/metrics/HTTPRequest';
import {checkInteger} from '../utils/SupervisorTools';
import MediaPlayerEvents from '../MediaPlayerEvents';

const DEFAULT_VIDEO_BITRATE = 1000;
const DEFAULT_AUDIO_BITRATE = 100;
const QUALITY_DEFAULT = 0;

function AbrController() {

    const context = this.context;
    const debug = Debug(context).getInstance();
    const eventBus = EventBus(context).getInstance();

    let instance,
        logger,
        abrRulesCollection,
        streamController,
        topQualities,
        qualityDict,
        streamProcessorDict,
        abandonmentStateDict,
        abandonmentTimeout,
        windowResizeEventCalled,
        elementWidth,
        elementHeight,
        adapter,
        videoModel,
        mediaPlayerModel,
        customParametersModel,
        cmsdModel,
        domStorage,
        playbackIndex,
        switchHistoryDict,
        droppedFramesHistory,
        throughputHistory,
        isUsingBufferOccupancyAbrDict,
        isUsingL2AAbrDict,
        isUsingLoLPAbrDict,
        dashMetrics,
        settings;

    function setup() {
        logger = debug.getLogger(instance);
        resetInitialSettings();
    }

    /**
     * Initialize everything that is not Stream specific. We only have one instance of the ABR Controller for all periods.
     */
    function initialize() {
        droppedFramesHistory = DroppedFramesHistory(context).create();
        throughputHistory = ThroughputHistory(context).create({
            settings
        });

        abrRulesCollection = ABRRulesCollection(context).create({
            dashMetrics,
            customParametersModel,
            mediaPlayerModel,
            settings
        });

        abrRulesCollection.initialize();

        eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, _onQualityChangeRendered, instance);
        eventBus.on(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance);
        eventBus.on(Events.LOADING_PROGRESS, _onFragmentLoadProgress, instance);
    }

    /**
     * Whenever a StreamProcessor is created it is added to the list of streamProcessorDict
     * In addition, the corresponding objects for this object and its stream id are created
     * @param {object} type
     * @param {object} streamProcessor
     */
    function registerStreamType(type, streamProcessor) {
        const streamId = streamProcessor.getStreamInfo().id;

        if (!streamProcessorDict[streamId]) {
            streamProcessorDict[streamId] = {};
        }

        if (!switchHistoryDict[streamId]) {
            switchHistoryDict[streamId] = {};
        }

        if (!abandonmentStateDict[streamId]) {
            abandonmentStateDict[streamId] = {};
        }

        switchHistoryDict[streamId][type] = SwitchRequestHistory(context).create();
        streamProcessorDict[streamId][type] = streamProcessor;

        abandonmentStateDict[streamId][type] = {};
        abandonmentStateDict[streamId][type].state = MetricsConstants.ALLOW_LOAD;

        _initializeAbrStrategy(type);

        if (type === Constants.VIDEO) {
            setElementSize();
        }
    }

    function _initializeAbrStrategy(type) {
        const strategy = settings.get().streaming.abr.ABRStrategy;

        if (strategy === Constants.ABR_STRATEGY_L2A) {
            isUsingBufferOccupancyAbrDict[type] = false;
            isUsingLoLPAbrDict[type] = false;
            isUsingL2AAbrDict[type] = true;
        } else if (strategy === Constants.ABR_STRATEGY_LoLP) {
            isUsingBufferOccupancyAbrDict[type] = false;
            isUsingLoLPAbrDict[type] = true;
            isUsingL2AAbrDict[type] = false;
        } else if (strategy === Constants.ABR_STRATEGY_BOLA) {
            isUsingBufferOccupancyAbrDict[type] = true;
            isUsingLoLPAbrDict[type] = false;
            isUsingL2AAbrDict[type] = false;
        } else if (strategy === Constants.ABR_STRATEGY_THROUGHPUT) {
            isUsingBufferOccupancyAbrDict[type] = false;
            isUsingLoLPAbrDict[type] = false;
            isUsingL2AAbrDict[type] = false;
        } else if (strategy === Constants.ABR_STRATEGY_DYNAMIC) {
            isUsingBufferOccupancyAbrDict[type] = isUsingBufferOccupancyAbrDict && isUsingBufferOccupancyAbrDict[type] ? isUsingBufferOccupancyAbrDict[type] : false;
            isUsingLoLPAbrDict[type] = false;
            isUsingL2AAbrDict[type] = false;
        }
    }

    function unRegisterStreamType(streamId, type) {
        try {
            if (streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
                delete streamProcessorDict[streamId][type];
            }

            if (switchHistoryDict[streamId] && switchHistoryDict[streamId][type]) {
                delete switchHistoryDict[streamId][type];
            }

            if (abandonmentStateDict[streamId] && abandonmentStateDict[streamId][type]) {
                delete abandonmentStateDict[streamId][type];
            }

        } catch (e) {

        }
    }

    function resetInitialSettings() {
        topQualities = {};
        qualityDict = {};
        abandonmentStateDict = {};
        streamProcessorDict = {};
        switchHistoryDict = {};
        isUsingBufferOccupancyAbrDict = {};
        isUsingL2AAbrDict = {};
        isUsingLoLPAbrDict = {};

        if (windowResizeEventCalled === undefined) {
            windowResizeEventCalled = false;
        }
        if (droppedFramesHistory) {
            droppedFramesHistory.reset();
        }

        playbackIndex = undefined;
        droppedFramesHistory = undefined;
        throughputHistory = undefined;
        clearTimeout(abandonmentTimeout);
        abandonmentTimeout = null;
    }

    function reset() {

        resetInitialSettings();

        eventBus.off(Events.LOADING_PROGRESS, _onFragmentLoadProgress, instance);
        eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, _onQualityChangeRendered, instance);
        eventBus.off(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance);

        if (abrRulesCollection) {
            abrRulesCollection.reset();
        }
    }

    function setConfig(config) {
        if (!config) return;

        if (config.streamController) {
            streamController = config.streamController;
        }
        if (config.domStorage) {
            domStorage = config.domStorage;
        }
        if (config.mediaPlayerModel) {
            mediaPlayerModel = config.mediaPlayerModel;
        }
        if (config.customParametersModel) {
            customParametersModel = config.customParametersModel;
        }
        if (config.cmsdModel) {
            cmsdModel = config.cmsdModel
        }
        if (config.dashMetrics) {
            dashMetrics = config.dashMetrics;
        }
        if (config.adapter) {
            adapter = config.adapter;
        }
        if (config.videoModel) {
            videoModel = config.videoModel;
        }
        if (config.settings) {
            settings = config.settings;
        }
    }

    function checkConfig() {
        if (!domStorage || !domStorage.hasOwnProperty('getSavedBitrateSettings')) {
            throw new Error(Constants.MISSING_CONFIG_ERROR);
        }
    }

    /**
     * While fragment loading is in progress we check if we might need to abort the request
     * @param {object} e
     * @private
     */
    function _onFragmentLoadProgress(e) {
        const type = e.request.mediaType;
        const streamId = e.streamId;

        if (!type || !streamId || !streamProcessorDict[streamId] || !settings.get().streaming.abr.autoSwitchBitrate[type]) {
            return;
        }

        const streamProcessor = streamProcessorDict[streamId][type];
        if (!streamProcessor) {
            return;
        }

        const rulesContext = RulesContext(context).create({
            abrController: instance,
            streamProcessor: streamProcessor,
            currentRequest: e.request,
            useBufferOccupancyABR: isUsingBufferOccupancyAbrDict[type],
            useL2AABR: isUsingL2AAbrDict[type],
            useLoLPABR: isUsingLoLPAbrDict[type],
            videoModel
        });
        const switchRequest = abrRulesCollection.shouldAbandonFragment(rulesContext, streamId);

        if (switchRequest.quality > SwitchRequest.NO_CHANGE) {
            const fragmentModel = streamProcessor.getFragmentModel();
            const request = fragmentModel.getRequests({
                state: FragmentModel.FRAGMENT_MODEL_LOADING,
                index: e.request.index
            })[0];
            if (request) {
                abandonmentStateDict[streamId][type].state = MetricsConstants.ABANDON_LOAD;
                switchHistoryDict[streamId][type].reset();
                switchHistoryDict[streamId][type].push({
                    oldValue: getQualityFor(type, streamId),
                    newValue: switchRequest.quality,
                    confidence: 1,
                    reason: switchRequest.reason
                });
                setPlaybackQuality(type, streamController.getActiveStreamInfo(), switchRequest.quality, switchRequest.reason);

                clearTimeout(abandonmentTimeout);
                abandonmentTimeout = setTimeout(
                    () => {
                        abandonmentStateDict[streamId][type].state = MetricsConstants.ALLOW_LOAD;
                        abandonmentTimeout = null;
                    },
                    settings.get().streaming.abandonLoadTimeout
                );
            }
        }
    }

    /**
     * Update dropped frames history when the quality was changed
     * @param {object} e
     * @private
     */
    function _onQualityChangeRendered(e) {
        if (e.mediaType === Constants.VIDEO) {
            if (playbackIndex !== undefined) {
                droppedFramesHistory.push(e.streamId, playbackIndex, videoModel.getPlaybackQuality());
            }
            playbackIndex = e.newQuality;
        }
    }

    /**
     * When the buffer level is updated we check if we need to change the ABR strategy
     * @param e
     * @private
     */
    function _onMetricAdded(e) {
        if (e.metric === MetricsConstants.HTTP_REQUEST && e.value && e.value.type === HTTPRequest.MEDIA_SEGMENT_TYPE && (e.mediaType === Constants.AUDIO || e.mediaType === Constants.VIDEO)) {
            throughputHistory.push(e.mediaType, e.value, settings.get().streaming.abr.useDeadTimeLatency);
        }

        if (e.metric === MetricsConstants.BUFFER_LEVEL && (e.mediaType === Constants.AUDIO || e.mediaType === Constants.VIDEO)) {
            _updateAbrStrategy(e.mediaType, 0.001 * e.value.level);
        }
    }

    /**
     * Returns the highest possible index taking limitations like maxBitrate, representationRatio and portal size into account.
     * @param {string} type
     * @param {string} streamId
     * @return {undefined|number}
     */
    function getMaxAllowedIndexFor(type, streamId) {
        try {
            let idx;
            topQualities[streamId] = topQualities[streamId] || {};

            if (!topQualities[streamId].hasOwnProperty(type)) {
                topQualities[streamId][type] = 0;
            }

            idx = _checkMaxBitrate(type, streamId);
            idx = _checkMaxRepresentationRatio(idx, type, streamId);
            idx = _checkPortalSize(idx, type, streamId);
            // Apply maximum suggested bitrate from CMSD headers if enabled 
            if (settings.get().streaming.cmsd.enabled && settings.get().streaming.cmsd.abr.applyMb) {
                idx = _checkCmsdMaxBitrate(idx, type, streamId);
            }
            return idx;
        } catch (e) {
            return undefined
        }
    }

    /**
     * Returns the minimum allowed index. We consider thresholds defined in the settings, i.e. minBitrate for the corresponding media type.
     * @param {string} type
     * @param {string} streamId
     * @return {undefined|number}
     */
    function getMinAllowedIndexFor(type, streamId) {
        try {
            return _getMinIndexBasedOnBitrateFor(type, streamId);
        } catch (e) {
            return undefined
        }
    }

    /**
     * Returns the maximum allowed index.
     * @param {string} type
     * @param {string} streamId
     * @return {undefined|number}
     */
    function _getMaxIndexBasedOnBitrateFor(type, streamId) {
        try {
            const maxBitrate = mediaPlayerModel.getAbrBitrateParameter('maxBitrate', type);
            if (maxBitrate > -1) {
                return getQualityForBitrate(streamProcessorDict[streamId][type].getMediaInfo(), maxBitrate, streamId);
            } else {
                return undefined;
            }
        } catch (e) {
            return undefined
        }
    }

    /**
     * Returns the minimum allowed index.
     * @param {string} type
     * @param {string} streamId
     * @return {undefined|number}
     */
    function _getMinIndexBasedOnBitrateFor(type, streamId) {
        try {
            const minBitrate = mediaPlayerModel.getAbrBitrateParameter('minBitrate', type);

            if (minBitrate > -1) {
                const mediaInfo = streamProcessorDict[streamId][type].getMediaInfo();
                const bitrateList = getBitrateList(mediaInfo);
                // This returns the quality index <= for the given bitrate
                let minIdx = getQualityForBitrate(mediaInfo, minBitrate, streamId);
                if (bitrateList[minIdx] && minIdx < bitrateList.length - 1 && bitrateList[minIdx].bitrate < minBitrate * 1000) {
                    minIdx++; // Go to the next bitrate
                }
                return minIdx;
            } else {
                return undefined;
            }
        } catch (e) {
            return undefined;
        }
    }

    /**
     * Returns the maximum possible index
     * @param type
     * @param streamId
     * @return {number|*}
     */
    function _checkMaxBitrate(type, streamId) {
        let idx = topQualities[streamId][type];
        let newIdx = idx;

        if (!streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) {
            return newIdx;
        }

        const minIdx = getMinAllowedIndexFor(type, streamId);
        if (minIdx !== undefined) {
            newIdx = Math.max(idx, minIdx);
        }

        const maxIdx = _getMaxIndexBasedOnBitrateFor(type, streamId);
        if (maxIdx !== undefined) {
            newIdx = Math.min(newIdx, maxIdx);
        }

        return newIdx;
    }

    /**
     * Returns the maximum possible index from CMSD model
     * @param type
     * @param streamId
     * @return {number|*}
     */
    function _checkCmsdMaxBitrate(idx, type, streamId) {
        // Check CMSD max suggested bitrate only for video segments
        if (type !== 'video') {
            return idx;
        }
        // Get max suggested bitrate
        let maxCmsdBitrate = cmsdModel.getMaxBitrate(type);
        if (maxCmsdBitrate < 0) {
            return idx;
        }
        // Substract audio bitrate
        const audioBitrate = _getBitrateInfoForQuality(streamId, 'audio', getQualityFor('audio', streamId));
        maxCmsdBitrate -= audioBitrate ? (audioBitrate.bitrate / 1000) : 0;
        const maxIdx = getQualityForBitrate(streamProcessorDict[streamId][type].getMediaInfo(), maxCmsdBitrate, streamId);
        logger.debug('Stream ID: ' + streamId + ' [' + type + '] Apply max bit rate from CMSD: ' + maxCmsdBitrate);
        return Math.min(idx, maxIdx);
    }

    /**
     * Returns the maximum index according to maximum representation ratio
     * @param idx
     * @param type
     * @param streamId
     * @return {number|*}
     * @private
     */
    function _checkMaxRepresentationRatio(idx, type, streamId) {
        let maxIdx = topQualities[streamId][type]
        const maxRepresentationRatio = settings.get().streaming.abr.maxRepresentationRatio[type];

        if (isNaN(maxRepresentationRatio) || maxRepresentationRatio >= 1 || maxRepresentationRatio < 0) {
            return idx;
        }
        return Math.min(idx, Math.round(maxIdx * maxRepresentationRatio));
    }

    /**
     * Returns the maximum index according to the portal size
     * @param idx
     * @param type
     * @param streamId
     * @return {number|*}
     * @private
     */
    function _checkPortalSize(idx, type, streamId) {
        if (type !== Constants.VIDEO || !settings.get().streaming.abr.limitBitrateByPortal || !streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) {
            return idx;
        }

        if (!windowResizeEventCalled) {
            setElementSize();
        }
        const streamInfo = streamProcessorDict[streamId][type].getStreamInfo();
        const representation = adapter.getAdaptationForType(streamInfo.index, type, streamInfo).Representation_asArray;
        let newIdx = idx;

        if (elementWidth > 0 && elementHeight > 0) {
            while (
                newIdx > 0 &&
                representation[newIdx] &&
                elementWidth < representation[newIdx].width &&
                elementWidth - representation[newIdx - 1].width < representation[newIdx].width - elementWidth) {
                newIdx = newIdx - 1;
            }

            // Make sure that in case of multiple representation elements have same
            // resolution, every such element is included
            while (newIdx < representation.length - 1 && representation[newIdx].width === representation[newIdx + 1].width) {
                newIdx = newIdx + 1;
            }
        }

        return newIdx;
    }

    /**
     * Gets top BitrateInfo for the player
     * @param {string} type - 'video' or 'audio' are the type options.
     * @param {string} streamId - Id of the stream
     * @returns {BitrateInfo | null}
     */
    function getTopBitrateInfoFor(type, streamId = null) {
        if (!streamId) {
            streamId = streamController.getActiveStreamInfo().id;
        }
        if (type && streamProcessorDict && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
            const idx = getMaxAllowedIndexFor(type, streamId);
            const bitrates = getBitrateList(streamProcessorDict[streamId][type].getMediaInfo());
            return bitrates[idx] ? bitrates[idx] : null;
        }
        return null;
    }

    /**
     * Returns the initial bitrate for a specific media type and stream id
     * @param {string} type
     * @param {string} streamId
     * @returns {number} A value of the initial bitrate, kbps
     * @memberof AbrController#
     */
    function getInitialBitrateFor(type, streamId) {
        checkConfig();

        if (type === Constants.TEXT) {
            return NaN;
        }

        const savedBitrate = domStorage.getSavedBitrateSettings(type);
        let configBitrate = mediaPlayerModel.getAbrBitrateParameter('initialBitrate', type);
        let configRatio = settings.get().streaming.abr.initialRepresentationRatio[type];

        if (configBitrate === -1) {
            if (configRatio > -1) {
                const streamInfo = streamProcessorDict[streamId][type].getStreamInfo();
                const representation = adapter.getAdaptationForType(streamInfo.index, type, streamInfo).Representation_asArray;
                if (Array.isArray(representation)) {
                    const repIdx = Math.max(Math.round(representation.length * configRatio) - 1, 0);
                    configBitrate = representation[repIdx].bandwidth / 1000;
                } else {
                    configBitrate = 0;
                }
            } else if (!isNaN(savedBitrate)) {
                configBitrate = savedBitrate;
            } else {
                configBitrate = (type === Constants.VIDEO) ? DEFAULT_VIDEO_BITRATE : DEFAULT_AUDIO_BITRATE;
            }
        }

        return configBitrate;
    }

    /**
     * This function is called by the scheduleControllers to check if the quality should be changed.
     * Consider this the main entry point for the ABR decision logic
     * @param {string} type
     * @param {string} streamId
     */
    function checkPlaybackQuality(type, streamId) {
        try {
            if (!type || !streamProcessorDict || !streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) {
                return false;
            }

            if (droppedFramesHistory) {
                const playbackQuality = videoModel.getPlaybackQuality();
                if (playbackQuality) {
                    droppedFramesHistory.push(streamId, playbackIndex, playbackQuality);
                }
            }

            // ABR is turned off, do nothing
            if (!settings.get().streaming.abr.autoSwitchBitrate[type]) {
                return false;
            }

            const oldQuality = getQualityFor(type, streamId);
            const rulesContext = RulesContext(context).create({
                abrController: instance,
                switchHistory: switchHistoryDict[streamId][type],
                droppedFramesHistory: droppedFramesHistory,
                streamProcessor: streamProcessorDict[streamId][type],
                currentValue: oldQuality,
                useBufferOccupancyABR: isUsingBufferOccupancyAbrDict[type],
                useL2AABR: isUsingL2AAbrDict[type],
                useLoLPABR: isUsingLoLPAbrDict[type],
                videoModel
            });
            const minIdx = getMinAllowedIndexFor(type, streamId);
            const maxIdx = getMaxAllowedIndexFor(type, streamId);
            const switchRequest = abrRulesCollection.getMaxQuality(rulesContext);
            let newQuality = switchRequest.quality;

            if (minIdx !== undefined && ((newQuality > SwitchRequest.NO_CHANGE) ? newQuality : oldQuality) < minIdx) {
                newQuality = minIdx;
            }
            if (newQuality > maxIdx) {
                newQuality = maxIdx;
            }

            switchHistoryDict[streamId][type].push({ oldValue: oldQuality, newValue: newQuality });

            if (newQuality > SwitchRequest.NO_CHANGE && newQuality !== oldQuality && (abandonmentStateDict[streamId][type].state === MetricsConstants.ALLOW_LOAD || newQuality < oldQuality)) {
                _changeQuality(type, oldQuality, newQuality, maxIdx, switchRequest.reason, streamId);
                return true;
            }

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

    }

    /**
     * Returns the current quality for a specific media type and a specific streamId
     * @param {string} type
     * @param {string} streamId
     * @return {number|*}
     */
    function getQualityFor(type, streamId = null) {
        try {
            if (!streamId) {
                streamId = streamController.getActiveStreamInfo().id;
            }
            if (type && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
                let quality;

                if (streamId) {
                    qualityDict[streamId] = qualityDict[streamId] || {};

                    if (!qualityDict[streamId].hasOwnProperty(type)) {
                        qualityDict[streamId][type] = QUALITY_DEFAULT;
                    }

                    quality = qualityDict[streamId][type];
                    return quality;
                }
            }
            return QUALITY_DEFAULT;
        } catch (e) {
            return QUALITY_DEFAULT;
        }
    }

    /**
     * Sets the new playback quality. Starts from index 0.
     * If the index of the new quality is the same as the old one changeQuality will not be called.
     * @param {string} type
     * @param {object} streamInfo
     * @param {number} newQuality
     * @param {string} reason
     */
    function setPlaybackQuality(type, streamInfo, newQuality, reason = null) {
        if (!streamInfo || !streamInfo.id || !type) {
            return;
        }
        const streamId = streamInfo.id;
        const oldQuality = getQualityFor(type, streamId);

        checkInteger(newQuality);

        const topQualityIdx = getMaxAllowedIndexFor(type, streamId);

        if (newQuality !== oldQuality && newQuality >= 0 && newQuality <= topQualityIdx) {
            _changeQuality(type, oldQuality, newQuality, topQualityIdx, reason, streamId);
        }
    }

    /**
     *
     * @param {string} streamId
     * @param {type} type
     * @return {*|null}
     */
    function getAbandonmentStateFor(streamId, type) {
        return abandonmentStateDict[streamId] && abandonmentStateDict[streamId][type] ? abandonmentStateDict[streamId][type].state : null;
    }


    /**
     * Changes the internal qualityDict values according to the new quality
     * @param {string} type
     * @param {number} oldQuality
     * @param {number} newQuality
     * @param {number} maxIdx
     * @param {string} reason
     * @param {object} streamId
     * @private
     */
    function _changeQuality(type, oldQuality, newQuality, maxIdx, reason, streamId) {
        if (type && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
            const streamInfo = streamProcessorDict[streamId][type].getStreamInfo();
            const isDynamic = streamInfo && streamInfo.manifestInfo && streamInfo.manifestInfo.isDynamic;
            const bufferLevel = dashMetrics.getCurrentBufferLevel(type);
            logger.info('Stream ID: ' + streamId + ' [' + type + '] switch from ' + oldQuality + ' to ' + newQuality + '/' + maxIdx + ' (buffer: ' + bufferLevel + ') ' + (reason ? JSON.stringify(reason) : '.'));

            qualityDict[streamId] = qualityDict[streamId] || {};
            qualityDict[streamId][type] = newQuality;
            const bitrateInfo = _getBitrateInfoForQuality(streamId, type, newQuality);
            eventBus.trigger(Events.QUALITY_CHANGE_REQUESTED,
                {
                    oldQuality,
                    newQuality,
                    reason,
                    streamInfo,
                    bitrateInfo,
                    maxIdx,
                    mediaType: type
                },
                { streamId: streamInfo.id, mediaType: type }
            );
            const bitrate = throughputHistory.getAverageThroughput(type, isDynamic);
            if (!isNaN(bitrate)) {
                domStorage.setSavedBitrateSettings(type, bitrate);
            }
        }
    }

    function _getBitrateInfoForQuality(streamId, type, idx) {
        if (type && streamProcessorDict && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
            const bitrates = getBitrateList(streamProcessorDict[streamId][type].getMediaInfo());
            return bitrates[idx] ? bitrates[idx] : null;
        }
        return null;
    }

    /**
     * @param {MediaInfo} mediaInfo
     * @param {number} bitrate A bitrate value, kbps
     * @param {String} streamId Period ID
     * @param {number|null} latency Expected latency of connection, ms
     * @returns {number} A quality index <= for the given bitrate
     * @memberof AbrController#
     */
    function getQualityForBitrate(mediaInfo, bitrate, streamId, latency = null) {
        const voRepresentation = mediaInfo && mediaInfo.type ? streamProcessorDict[streamId][mediaInfo.type].getRepresentationInfo() : null;

        if (settings.get().streaming.abr.useDeadTimeLatency && latency && voRepresentation && voRepresentation.fragmentDuration) {
            latency = latency / 1000;
            const fragmentDuration = voRepresentation.fragmentDuration;
            if (latency > fragmentDuration) {
                return 0;
            } else {
                const deadTimeRatio = latency / fragmentDuration;
                bitrate = bitrate * (1 - deadTimeRatio);
            }
        }

        const bitrateList = getBitrateList(mediaInfo);

        for (let i = bitrateList.length - 1; i >= 0; i--) {
            const bitrateInfo = bitrateList[i];
            if (bitrate * 1000 >= bitrateInfo.bitrate) {
                return i;
            }
        }
        return QUALITY_DEFAULT;
    }

    /**
     * @param {MediaInfo} mediaInfo
     * @returns {Array|null} A list of {@link BitrateInfo} objects
     * @memberof AbrController#
     */
    function getBitrateList(mediaInfo) {
        const infoList = [];
        if (!mediaInfo || !mediaInfo.bitrateList) return infoList;

        const bitrateList = mediaInfo.bitrateList;
        const type = mediaInfo.type;

        let bitrateInfo;

        for (let i = 0, ln = bitrateList.length; i < ln; i++) {
            bitrateInfo = new BitrateInfo();
            bitrateInfo.mediaType = type;
            bitrateInfo.qualityIndex = i;
            bitrateInfo.bitrate = bitrateList[i].bandwidth;
            bitrateInfo.width = bitrateList[i].width;
            bitrateInfo.height = bitrateList[i].height;
            bitrateInfo.scanType = bitrateList[i].scanType;
            infoList.push(bitrateInfo);
        }

        return infoList;
    }

    function _updateAbrStrategy(mediaType, bufferLevel) {
        // else ABR_STRATEGY_DYNAMIC
        const strategy = settings.get().streaming.abr.ABRStrategy;

        if (strategy === Constants.ABR_STRATEGY_DYNAMIC) {
            _updateDynamicAbrStrategy(mediaType, bufferLevel);
        }
    }

    function _updateDynamicAbrStrategy(mediaType, bufferLevel) {
        try {
            const stableBufferTime = mediaPlayerModel.getStableBufferTime();
            const switchOnThreshold = stableBufferTime;
            const switchOffThreshold = 0.5 * stableBufferTime;

            const useBufferABR = isUsingBufferOccupancyAbrDict[mediaType];
            const newUseBufferABR = bufferLevel > (useBufferABR ? switchOffThreshold : switchOnThreshold); // use hysteresis to avoid oscillating rules
            isUsingBufferOccupancyAbrDict[mediaType] = newUseBufferABR;

            if (newUseBufferABR !== useBufferABR) {
                if (newUseBufferABR) {
                    logger.info('[' + mediaType + '] switching from throughput to buffer occupancy ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').');
                } else {
                    logger.info('[' + mediaType + '] switching from buffer occupancy to throughput ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').');
                }
            }
        } catch (e) {
            logger.error(e);
        }
    }

    function getThroughputHistory() {
        return throughputHistory;
    }

    function updateTopQualityIndex(mediaInfo) {
        const type = mediaInfo.type;
        const streamId = mediaInfo.streamInfo.id;
        const max = mediaInfo.representationCount - 1;

        topQualities[streamId] = topQualities[streamId] || {};
        topQualities[streamId][type] = max;

        return max;
    }

    function isPlayingAtTopQuality(streamInfo) {
        const streamId = streamInfo ? streamInfo.id : null;
        const audioQuality = getQualityFor(Constants.AUDIO, streamId);
        const videoQuality = getQualityFor(Constants.VIDEO, streamId);

        const isAtTop = (audioQuality === getMaxAllowedIndexFor(Constants.AUDIO, streamId)) &&
            (videoQuality === getMaxAllowedIndexFor(Constants.VIDEO, streamId));

        return isAtTop;
    }

    function setWindowResizeEventCalled(value) {
        windowResizeEventCalled = value;
    }

    function setElementSize() {
        if (videoModel) {
            const hasPixelRatio = settings.get().streaming.abr.usePixelRatioInLimitBitrateByPortal && window.hasOwnProperty('devicePixelRatio');
            const pixelRatio = hasPixelRatio ? window.devicePixelRatio : 1;
            elementWidth = videoModel.getClientWidth() * pixelRatio;
            elementHeight = videoModel.getClientHeight() * pixelRatio;
        }
    }

    function clearDataForStream(streamId) {
        if (droppedFramesHistory) {
            droppedFramesHistory.clearForStream(streamId);
        }
        if (streamProcessorDict[streamId]) {
            delete streamProcessorDict[streamId];
        }
        if (switchHistoryDict[streamId]) {
            delete switchHistoryDict[streamId];
        }

        if (abandonmentStateDict[streamId]) {
            delete abandonmentStateDict[streamId];
        }
    }

    instance = {
        initialize,
        isPlayingAtTopQuality,
        updateTopQualityIndex,
        clearDataForStream,
        getThroughputHistory,
        getBitrateList,
        getQualityForBitrate,
        getTopBitrateInfoFor,
        getMinAllowedIndexFor,
        getMaxAllowedIndexFor,
        getInitialBitrateFor,
        getQualityFor,
        getAbandonmentStateFor,
        setPlaybackQuality,
        checkPlaybackQuality,
        setElementSize,
        setWindowResizeEventCalled,
        registerStreamType,
        unRegisterStreamType,
        setConfig,
        reset
    };

    setup();

    return instance;
}

AbrController.__dashjs_factory_name = 'AbrController';
const factory = FactoryMaker.getSingletonFactory(AbrController);
factory.QUALITY_DEFAULT = QUALITY_DEFAULT;
FactoryMaker.updateSingletonFactory(AbrController.__dashjs_factory_name, factory);
export default factory;