streaming_rules_abr_ABRRulesCollection.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 ThroughputRule from './ThroughputRule';
import InsufficientBufferRule from './InsufficientBufferRule';
import AbandonRequestsRule from './AbandonRequestsRule';
import DroppedFramesRule from './DroppedFramesRule';
import SwitchHistoryRule from './SwitchHistoryRule';
import BolaRule from './BolaRule';
import L2ARule from './L2ARule.js';
import LoLPRule from './lolp/LoLpRule.js';
import FactoryMaker from '../../../core/FactoryMaker';
import SwitchRequest from '../SwitchRequest';
import Constants from '../../constants/Constants';

const QUALITY_SWITCH_RULES = 'qualitySwitchRules';
const ABANDON_FRAGMENT_RULES = 'abandonFragmentRules';


function ABRRulesCollection(config) {

    config = config || {};
    const context = this.context;

    const mediaPlayerModel = config.mediaPlayerModel;
    const customParametersModel = config.customParametersModel;
    const dashMetrics = config.dashMetrics;
    const settings = config.settings;

    let instance,
        qualitySwitchRules,
        abandonFragmentRules;

    function initialize() {
        qualitySwitchRules = [];
        abandonFragmentRules = [];

        if (settings.get().streaming.abr.useDefaultABRRules) {

            // If L2A is used we only need this one rule
            if (settings.get().streaming.abr.ABRStrategy === Constants.ABR_STRATEGY_L2A) {
                qualitySwitchRules.push(
                    L2ARule(context).create({
                        dashMetrics: dashMetrics,
                        settings: settings
                    })
                );
            }
            // If LoLP is used we only need this one rule
            else if (settings.get().streaming.abr.ABRStrategy === Constants.ABR_STRATEGY_LoLP) {
                qualitySwitchRules.push(
                    LoLPRule(context).create({
                        dashMetrics: dashMetrics
                    })
                );
            } else {
                // Only one of BolaRule and ThroughputRule will give a switchRequest.quality !== SwitchRequest.NO_CHANGE.
                // This is controlled by useBufferOccupancyABR mechanism in AbrController.
                qualitySwitchRules.push(
                    BolaRule(context).create({
                        dashMetrics: dashMetrics,
                        mediaPlayerModel: mediaPlayerModel,
                        settings: settings
                    })
                );

                qualitySwitchRules.push(
                    ThroughputRule(context).create({
                        dashMetrics: dashMetrics
                    })
                );

                if (settings.get().streaming.abr.additionalAbrRules.insufficientBufferRule) {
                    qualitySwitchRules.push(
                        InsufficientBufferRule(context).create({
                            dashMetrics: dashMetrics,
                            settings
                        })
                    );
                }

                if (settings.get().streaming.abr.additionalAbrRules.switchHistoryRule) {
                    qualitySwitchRules.push(
                        SwitchHistoryRule(context).create()
                    );
                }

                if (settings.get().streaming.abr.additionalAbrRules.droppedFramesRule) {
                    qualitySwitchRules.push(
                        DroppedFramesRule(context).create()
                    );
                }

                if (settings.get().streaming.abr.additionalAbrRules.abandonRequestsRule) {
                    abandonFragmentRules.push(
                        AbandonRequestsRule(context).create({
                            dashMetrics: dashMetrics,
                            mediaPlayerModel: mediaPlayerModel,
                            settings: settings
                        })
                    );
                }
            }
        }

        // add custom ABR rules if any
        const customRules = customParametersModel.getAbrCustomRules();
        customRules.forEach(function (rule) {
            if (rule.type === QUALITY_SWITCH_RULES) {
                qualitySwitchRules.push(rule.rule(context).create());
            }

            if (rule.type === ABANDON_FRAGMENT_RULES) {
                abandonFragmentRules.push(rule.rule(context).create());
            }
        });
    }

    function _getRulesWithChange(srArray) {
        return srArray.filter(sr => sr.quality > SwitchRequest.NO_CHANGE);
    }

    /**
     *
     * @param {array} srArray
     * @return {object} SwitchRequest
     */
    function getMinSwitchRequest(srArray) {
        const values = {};
        let newSwitchReq = null;
        let i,
            len,
            req,
            quality,
            reason;

        if (srArray.length === 0) {
            return;
        }

        values[SwitchRequest.PRIORITY.STRONG] = { quality: SwitchRequest.NO_CHANGE, reason: null };
        values[SwitchRequest.PRIORITY.WEAK] = { quality: SwitchRequest.NO_CHANGE, reason: null };
        values[SwitchRequest.PRIORITY.DEFAULT] = { quality: SwitchRequest.NO_CHANGE, reason: null };

        for (i = 0, len = srArray.length; i < len; i += 1) {
            req = srArray[i];
            if (req.quality !== SwitchRequest.NO_CHANGE) {
                // We only use the new quality in case it is lower than the already saved one or if no new quality has been selected for the respective priority
                if (values[req.priority].quality === SwitchRequest.NO_CHANGE || values[req.priority].quality > req.quality) {
                    values[req.priority].quality = req.quality;
                    values[req.priority].reason = req.reason || null;
                }
            }
        }

        if (values[SwitchRequest.PRIORITY.WEAK].quality !== SwitchRequest.NO_CHANGE) {
            newSwitchReq = values[SwitchRequest.PRIORITY.WEAK];
        }

        if (values[SwitchRequest.PRIORITY.DEFAULT].quality !== SwitchRequest.NO_CHANGE) {
            newSwitchReq = values[SwitchRequest.PRIORITY.DEFAULT];
        }

        if (values[SwitchRequest.PRIORITY.STRONG].quality !== SwitchRequest.NO_CHANGE) {
            newSwitchReq = values[SwitchRequest.PRIORITY.STRONG];
        }

        if (newSwitchReq) {
            quality = newSwitchReq.quality;
            reason = newSwitchReq.reason;
        }

        return SwitchRequest(context).create(quality, reason);
    }

    function getMaxQuality(rulesContext) {
        const switchRequestArray = qualitySwitchRules.map(rule => rule.getMaxIndex(rulesContext));
        const activeRules = _getRulesWithChange(switchRequestArray);
        const maxQuality = getMinSwitchRequest(activeRules);

        return maxQuality || SwitchRequest(context).create();
    }

    function shouldAbandonFragment(rulesContext, streamId) {
        const abandonRequestArray = abandonFragmentRules.map(rule => rule.shouldAbandon(rulesContext, streamId));
        const activeRules = _getRulesWithChange(abandonRequestArray);
        const shouldAbandon = getMinSwitchRequest(activeRules);

        if (shouldAbandon) {
            shouldAbandon.reason.forceAbandon = true
        }

        return shouldAbandon || SwitchRequest(context).create();
    }

    function reset() {
        [qualitySwitchRules, abandonFragmentRules].forEach(rules => {
            if (rules && rules.length) {
                rules.forEach(rule => rule.reset && rule.reset());
            }
        });
        qualitySwitchRules = [];
        abandonFragmentRules = [];
    }

    function getQualitySwitchRules() {
        return qualitySwitchRules;
    }

    instance = {
        initialize,
        reset,
        getMaxQuality,
        getMinSwitchRequest,
        shouldAbandonFragment,
        getQualitySwitchRules
    };

    return instance;
}

ABRRulesCollection.__dashjs_factory_name = 'ABRRulesCollection';
const factory = FactoryMaker.getClassFactory(ABRRulesCollection);
factory.QUALITY_SWITCH_RULES = QUALITY_SWITCH_RULES;
factory.ABANDON_FRAGMENT_RULES = ABANDON_FRAGMENT_RULES;
FactoryMaker.updateSingletonFactory(ABRRulesCollection.__dashjs_factory_name, factory);

export default factory;