streaming_models_MediaPlayerModel.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 Debug from '../../core/Debug';
import FactoryMaker from '../../core/FactoryMaker';
import Settings from '../../core/Settings';

const DEFAULT_MIN_BUFFER_TIME = 12;
const DEFAULT_MIN_BUFFER_TIME_FAST_SWITCH = 20;
const LOW_LATENCY_REDUCTION_FACTOR = 10;
const LOW_LATENCY_MULTIPLY_FACTOR = 5;
const DEFAULT_CATCHUP_MAX_DRIFT = 12;
const DEFAULT_CATCHUP_PLAYBACK_RATE_MIN = -0.5;
const DEFAULT_CATCHUP_PLAYBACK_RATE_MAX = 0.5;
const CATCHUP_PLAYBACK_RATE_MIN_LIMIT = -0.5;
const CATCHUP_PLAYBACK_RATE_MAX_LIMIT = 1;

/**
 * We use this model as a wrapper/proxy between Settings.js and classes that are using parameters from Settings.js.
 * In some cases we require additional logic to be applied and the settings might need to be adjusted before being used.
 * @class
 * @constructor
 */
function MediaPlayerModel() {

    let instance,
        logger,
        playbackController,
        serviceDescriptionController;

    const context = this.context;
    const settings = Settings(context).getInstance();

    function setup() {
        logger = Debug(context).getInstance().getLogger(instance);
    }

    function setConfig(config) {
        if (config.playbackController) {
            playbackController = config.playbackController;
        }
        if (config.serviceDescriptionController) {
            serviceDescriptionController = config.serviceDescriptionController;
        }
    }

    /**
     * Checks the supplied min playback rate is a valid vlaue and within supported limits
     * @param {number} rate - Supplied min playback rate 
     * @param {boolean} log - wether to shown warning or not 
     * @returns {number} corrected min playback rate
     */
    function _checkMinPlaybackRate (rate, log) {
        if (isNaN(rate)) return 0;
        if (rate > 0) {
            if (log) {
                logger.warn(`Supplied minimum playback rate is a positive value when it should be negative or 0. The supplied rate will not be applied and set to 0: 100% playback speed.`)
            }
            return 0;
        } 
        if (rate < CATCHUP_PLAYBACK_RATE_MIN_LIMIT) {
            if (log) {
                logger.warn(`Supplied minimum playback rate is out of range and will be limited to ${CATCHUP_PLAYBACK_RATE_MIN_LIMIT}: ${CATCHUP_PLAYBACK_RATE_MIN_LIMIT * 100}% playback speed.`);
            }
            return CATCHUP_PLAYBACK_RATE_MIN_LIMIT;
        }
        return rate;
    };

    /**
     * Checks the supplied max playback rate is a valid vlaue and within supported limits
     * @param {number} rate - Supplied max playback rate 
     * @param {boolean} log - wether to shown warning or not 
     * @returns {number} corrected max playback rate
     */
    function _checkMaxPlaybackRate (rate, log) {
        if (isNaN(rate)) return 0;
        if (rate < 0) {
            if (log) {
                logger.warn(`Supplied maximum playback rate is a negative value when it should be negative or 0. The supplied rate will not be applied and set to 0: 100% playback speed.`)
            }
            return 0;
        } 
        if (rate > CATCHUP_PLAYBACK_RATE_MAX_LIMIT) {
            if (log) {
                logger.warn(`Supplied maximum playback rate is out of range and will be limited to ${CATCHUP_PLAYBACK_RATE_MAX_LIMIT}: ${(1 + CATCHUP_PLAYBACK_RATE_MAX_LIMIT) * 100}% playback speed.`);
            }
            return CATCHUP_PLAYBACK_RATE_MAX_LIMIT;
        }
        return rate;
    };

    /**
     * Returns the maximum drift allowed before applying a seek back to the live edge when the catchup mode is enabled
     * @return {number}
     */
    function getCatchupMaxDrift() {
        if (!isNaN(settings.get().streaming.liveCatchup.maxDrift) && settings.get().streaming.liveCatchup.maxDrift > 0) {
            return settings.get().streaming.liveCatchup.maxDrift;
        }

        const serviceDescriptionSettings = serviceDescriptionController.getServiceDescriptionSettings();
        if (serviceDescriptionSettings && serviceDescriptionSettings.liveCatchup && !isNaN(serviceDescriptionSettings.liveCatchup.maxDrift) && serviceDescriptionSettings.liveCatchup.maxDrift > 0) {
            return serviceDescriptionSettings.liveCatchup.maxDrift;
        }

        return DEFAULT_CATCHUP_MAX_DRIFT;
    }

    /**
     * Returns the minimum and maximum playback rates to be used when applying the catchup mechanism
     * If only one of the min/max values has been set then the other will default to 0 (no playback rate change).
     * @return {number}
     */
    function getCatchupPlaybackRates(log) {
        const settingsPlaybackRate = settings.get().streaming.liveCatchup.playbackRate;
        
        if(!isNaN(settingsPlaybackRate.min) || !isNaN(settingsPlaybackRate.max)) {
            return {
                min: _checkMinPlaybackRate(settingsPlaybackRate.min, log),
                max: _checkMaxPlaybackRate(settingsPlaybackRate.max, log),
            }
        }

        const serviceDescriptionSettings = serviceDescriptionController.getServiceDescriptionSettings();
        if (serviceDescriptionSettings && serviceDescriptionSettings.liveCatchup && (!isNaN(serviceDescriptionSettings.liveCatchup.playbackRate.min) || !isNaN(serviceDescriptionSettings.liveCatchup.playbackRate.max))) {
            const sdPlaybackRate = serviceDescriptionSettings.liveCatchup.playbackRate;
            return {
                min: _checkMinPlaybackRate(sdPlaybackRate.min, log),
                max: _checkMaxPlaybackRate(sdPlaybackRate.max, log),
            }
        }

        return {
            min: DEFAULT_CATCHUP_PLAYBACK_RATE_MIN,
            max: DEFAULT_CATCHUP_PLAYBACK_RATE_MAX
        }
    }

    /**
     * Returns whether the catchup mode is activated via the settings or internally in the PlaybackController
     * @return {boolean}
     */
    function getCatchupModeEnabled() {
        if (settings.get().streaming.liveCatchup.enabled !== null) {
            return settings.get().streaming.liveCatchup.enabled;
        }

        return playbackController.getInitialCatchupModeActivated();
    }

    /**
     * Returns the min,max or initial bitrate for a specific media type.
     * @param {string} field
     * @param {string} mediaType
     */
    function getAbrBitrateParameter(field, mediaType) {
        try {
            const setting = settings.get().streaming.abr[field][mediaType];
            if(!isNaN(setting) && setting !== -1) {
                return setting;
            }

            const serviceDescriptionSettings = serviceDescriptionController.getServiceDescriptionSettings();
            if(serviceDescriptionSettings && serviceDescriptionSettings[field] && !isNaN(serviceDescriptionSettings[field][mediaType])) {
                return serviceDescriptionSettings[field][mediaType];
            }

            return -1;
        }
        catch(e) {
            return -1;
        }
    }

    /**
     * Returns the initial buffer level taking the stable buffer time into account
     * @return {number}
     */
    function getInitialBufferLevel() {
        const initialBufferLevel = settings.get().streaming.buffer.initialBufferLevel;

        if (isNaN(initialBufferLevel) || initialBufferLevel < 0) {
            return 0;
        }

        return Math.min(getStableBufferTime(), initialBufferLevel);
    }

    /**
     * Returns the stable buffer time taking the live delay into account
     * @return {number}
     */
    function getStableBufferTime() {
        let stableBufferTime = settings.get().streaming.buffer.stableBufferTime > 0 ? settings.get().streaming.buffer.stableBufferTime : settings.get().streaming.buffer.fastSwitchEnabled ? DEFAULT_MIN_BUFFER_TIME_FAST_SWITCH : DEFAULT_MIN_BUFFER_TIME;
        const liveDelay = playbackController.getLiveDelay();

        return !isNaN(liveDelay) && liveDelay > 0 ? Math.min(stableBufferTime, liveDelay) : stableBufferTime;
    }

    /**
     * Returns the number of retry attempts for a specific media type
     * @param type
     * @return {number}
     */
    function getRetryAttemptsForType(type) {
        const lowLatencyMultiplyFactor = !isNaN(settings.get().streaming.retryAttempts.lowLatencyMultiplyFactor) ? settings.get().streaming.retryAttempts.lowLatencyMultiplyFactor : LOW_LATENCY_MULTIPLY_FACTOR;

        return playbackController.getLowLatencyModeEnabled() ? settings.get().streaming.retryAttempts[type] * lowLatencyMultiplyFactor : settings.get().streaming.retryAttempts[type];
    }

    /**
     * Returns the retry interval for a specific media type
     * @param type
     * @return {number}
     */
    function getRetryIntervalsForType(type) {
        const lowLatencyReductionFactor = !isNaN(settings.get().streaming.retryIntervals.lowLatencyReductionFactor) ? settings.get().streaming.retryIntervals.lowLatencyReductionFactor : LOW_LATENCY_REDUCTION_FACTOR;

        return playbackController.getLowLatencyModeEnabled() ? settings.get().streaming.retryIntervals[type] / lowLatencyReductionFactor : settings.get().streaming.retryIntervals[type];
    }

    function reset() {
    }

    instance = {
        getCatchupMaxDrift,
        getCatchupModeEnabled,
        getStableBufferTime,
        getInitialBufferLevel,
        getRetryAttemptsForType,
        getRetryIntervalsForType,
        getCatchupPlaybackRates,
        getAbrBitrateParameter,
        setConfig,
        reset
    };

    setup();

    return instance;
}

MediaPlayerModel.__dashjs_factory_name = 'MediaPlayerModel';
export default FactoryMaker.getSingletonFactory(MediaPlayerModel);