streaming_controllers_MediaController.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 Constants from '../constants/Constants';
import Events from '../../core/events/Events';
import EventBus from '../../core/EventBus';
import FactoryMaker from '../../core/FactoryMaker';
import Debug from '../../core/Debug';
import bcp47Normalize from 'bcp-47-normalize';
import {extendedFilter} from 'bcp-47-match';

function MediaController() {

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

    let instance,
        logger,
        tracks,
        settings,
        initialSettings,
        lastSelectedTracks,
        customParametersModel,
        domStorage;

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

    /**
     * @param {string} type
     * @param {StreamInfo} streamInfo
     * @memberof MediaController#
     */
    function setInitialMediaSettingsForType(type, streamInfo) {
        let settings = lastSelectedTracks[type] || getInitialSettings(type);
        const tracksForType = getTracksFor(type, streamInfo.id);
        let tracks = [];

        if (!settings) {
            settings = domStorage.getSavedMediaSettings(type);
            if (settings) {
                // If the settings are defined locally, do not take codec into account or it'll be too strict.
                // eg: An audio track should not be selected by codec but merely by lang.
                delete settings.codec;
            }
            setInitialSettings(type, settings);
        }

        if (!tracksForType || (tracksForType.length === 0)) return;

        if (settings) {
            tracks = Array.from(tracksForType);

            tracks = filterTracksBySettings(tracks, matchSettingsLang, settings);
            tracks = filterTracksBySettings(tracks, matchSettingsIndex, settings);
            tracks = filterTracksBySettings(tracks, matchSettingsViewPoint, settings);
            if (!(type === Constants.AUDIO && !!lastSelectedTracks[type])) {
                tracks = filterTracksBySettings(tracks, matchSettingsRole, settings);
            }
            tracks = filterTracksBySettings(tracks, matchSettingsAccessibility, settings);
            tracks = filterTracksBySettings(tracks, matchSettingsAudioChannelConfig, settings);
            tracks = filterTracksBySettings(tracks, matchSettingsCodec, settings);
        }

        if (tracks.length === 0) {
            setTrack(selectInitialTrack(type, tracksForType), true);
        } else {
            if (tracks.length > 1) {
                setTrack(selectInitialTrack(type, tracks));
            } else {
                setTrack(tracks[0]);
            }
        }
    }

    /**
     * @param {MediaInfo} track
     * @memberof MediaController#
     */
    function addTrack(track) {
        if (!track) return;

        const mediaType = track.type;
        if (!_isMultiTrackSupportedByType(mediaType)) return;

        let streamId = track.streamInfo.id;
        if (!tracks[streamId]) {
            tracks[streamId] = createTrackInfo();
        }

        const mediaTracks = tracks[streamId][mediaType].list;
        for (let i = 0, len = mediaTracks.length; i < len; ++i) {
            //track is already set.
            if (isTracksEqual(mediaTracks[i], track)) {
                return;
            }
        }

        mediaTracks.push(track);
    }

    /**
     * @param {string} type
     * @param {string} streamId
     * @returns {Array}
     * @memberof MediaController#
     */
    function getTracksFor(type, streamId) {
        if (!type) return [];

        if (!tracks[streamId] || !tracks[streamId][type]) return [];

        return tracks[streamId][type].list;
    }

    /**
     * @param {string} type
     * @param {string} streamId
     * @returns {Object|null}
     * @memberof MediaController#
     */
    function getCurrentTrackFor(type, streamId) {
        if (!type || !tracks[streamId] || !tracks[streamId][type]) return null;
        return tracks[streamId][type].current;
    }

    /**
     * @param {MediaInfo} track
     * @returns {boolean}
     * @memberof MediaController#
     */
    function isCurrentTrack(track) {
        if (!track) {
            return false;
        }
        const type = track.type;
        const id = track.streamInfo.id;

        return (tracks[id] && tracks[id][type] && isTracksEqual(tracks[id][type].current, track));
    }

    /**
     * @param {MediaInfo} track
     * @param {boolean} noSettingsSave specify if settings must be not be saved
     * @memberof MediaController#
     */
    function setTrack(track, noSettingsSave = false) {
        if (!track || !track.streamInfo) return;

        const type = track.type;
        const streamInfo = track.streamInfo;
        const id = streamInfo.id;
        const current = getCurrentTrackFor(type, id);

        if (!tracks[id] || !tracks[id][type]) return;

        tracks[id][type].current = track;

        if (tracks[id][type].current && ((type !== Constants.TEXT && !isTracksEqual(track, current)) || (type === Constants.TEXT && track.isFragmented))) {
            eventBus.trigger(Events.CURRENT_TRACK_CHANGED, {
                oldMediaInfo: current,
                newMediaInfo: track,
                switchMode: settings.get().streaming.trackSwitchMode[type]
            }, { streamId: id });
        }

        if (!noSettingsSave) {

            let settings = extractSettings(track);

            if (!settings || !tracks[id][type].storeLastSettings) return;

            if (settings.roles) {
                settings.role = settings.roles[0];
                delete settings.roles;
            }

            if (settings.accessibility) {
                settings.accessibility = settings.accessibility[0];
            }

            if (settings.audioChannelConfiguration) {
                settings.audioChannelConfiguration = settings.audioChannelConfiguration[0];
            }

            lastSelectedTracks[type] = settings;
            domStorage.setSavedMediaSettings(type, settings);
        }
    }

    /**
     * @param {string} type
     * @param {Object} value
     * @memberof MediaController#
     */
    function setInitialSettings(type, value) {
        if (!type || !value) return;

        initialSettings[type] = value;
    }

    /**
     * @param {string} type
     * @returns {Object|null}
     * @memberof MediaController#
     */
    function getInitialSettings(type) {
        if (!type) return null;

        return initialSettings[type];
    }

    /**
     * @memberof MediaController#
     */
    function saveTextSettingsDisabled() {
        domStorage.setSavedMediaSettings(Constants.TEXT, null);
    }

    /**
     * @param {string} type
     * @returns {boolean}
     * @memberof MediaController#
     */
    function _isMultiTrackSupportedByType(type) {
        return (type === Constants.AUDIO || type === Constants.VIDEO || type === Constants.TEXT || type === Constants.IMAGE);
    }

    /**
     * @param {MediaInfo} t1 - first track to compare
     * @param {MediaInfo} t2 - second track to compare
     * @returns {boolean}
     * @memberof MediaController#
     */
    function isTracksEqual(t1, t2) {
        if (!t1 && !t2) {
            return true;
        }

        if (!t1 || !t2) {
            return false;
        }

        const sameId = t1.id === t2.id;
        const sameViewpoint = t1.viewpoint === t2.viewpoint;
        const sameViewpointDescriptors = JSON.stringify(t1.viewpointsWithSchemeIdUri) === JSON.stringify(t2.viewpointsWithSchemeIdUri);
        const sameLang = t1.lang === t2.lang;
        const sameCodec = t1.codec === t2.codec;
        const sameRoles = t1.roles.toString() === t2.roles.toString();
        const sameRoleDescriptors = JSON.stringify(t1.rolesWithSchemeIdUri) === JSON.stringify(t2.rolesWithSchemeIdUri);
        const sameAccessibility = t1.accessibility.toString() === t2.accessibility.toString();
        const sameAccessibilityDescriptors = JSON.stringify(t1.accessibilitiesWithSchemeIdUri) === JSON.stringify(t2.accessibilitiesWithSchemeIdUri);
        const sameAudioChannelConfiguration = t1.audioChannelConfiguration.toString() === t2.audioChannelConfiguration.toString();
        const sameAudioChannelConfigurationDescriptors = JSON.stringify(t1.audioChannelConfigurationsWithSchemeIdUri) === JSON.stringify(t2.audioChannelConfigurationsWithSchemeIdUri);

        return (sameId && sameCodec && sameViewpoint && sameViewpointDescriptors && sameLang && sameRoles && sameRoleDescriptors && sameAccessibility && sameAccessibilityDescriptors && sameAudioChannelConfiguration && sameAudioChannelConfigurationDescriptors);
    }

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

        if (config.domStorage) {
            domStorage = config.domStorage;
        }

        if (config.settings) {
            settings = config.settings;
        }

        if (config.customParametersModel) {
            customParametersModel = config.customParametersModel;
        }
    }


    /**
     * @memberof MediaController#
     */
    function reset() {
        tracks = {};
        lastSelectedTracks = {};
        resetInitialSettings();
    }

    function extractSettings(mediaInfo) {
        const settings = {
            lang: mediaInfo.lang,
            viewpoint: mediaInfo.viewpoint,
            roles: mediaInfo.roles,
            accessibility: mediaInfo.accessibility,
            audioChannelConfiguration: mediaInfo.audioChannelConfiguration,
            codec: mediaInfo.codec
        };
        let notEmpty = settings.lang || settings.viewpoint || (settings.role && settings.role.length > 0) ||
            (settings.accessibility && settings.accessibility.length > 0) || (settings.audioChannelConfiguration && settings.audioChannelConfiguration.length > 0);

        return notEmpty ? settings : null;
    }

    function filterTracksBySettings(tracks, filterFn, settings) {
        let tracksAfterMatcher = [];
        tracks.forEach(function (track) {
            if (filterFn(settings, track)) {
                tracksAfterMatcher.push(track);
            }
        });
        if (tracksAfterMatcher.length !== 0) {
            return tracksAfterMatcher;
        }
        return tracks;
    }

    function matchSettingsLang(settings, track) {
        try {
            return !settings.lang ||
            (settings.lang instanceof RegExp) ?
                (track.lang.match(settings.lang)) : track.lang !== '' ?
                    (extendedFilter(track.lang, bcp47Normalize(settings.lang)).length > 0) : false;
        } catch (e) {
            return false
        }
    }

    function matchSettingsIndex(settings, track) {
        return (settings.index === undefined) || (settings.index === null) || (track.index === settings.index);
    }

    function matchSettingsViewPoint(settings, track) {
        return !settings.viewpoint || (settings.viewpoint === track.viewpoint);
    }

    function matchSettingsRole(settings, track, isTrackActive = false) {
        const matchRole = !settings.role || !!track.roles.filter(function (item) {
            return item === settings.role;
        })[0];
        return (matchRole || (track.type === Constants.AUDIO && isTrackActive));
    }

    function matchSettingsAccessibility(settings, track) {
        let matchAccessibility;

        if (!settings.accessibility) {
            // if no accessibility is requested (or request is empty string),
            // match only those tracks having no accessibility element present
            matchAccessibility = !track.accessibility.length;
        } else {
            matchAccessibility = !!track.accessibility.filter(function (item) {
                return item === settings.accessibility;
            })[0];
        }

        return matchAccessibility;
    }

    function matchSettingsAudioChannelConfig(settings, track) {
        let matchAudioChannelConfiguration = !settings.audioChannelConfiguration || !!track.audioChannelConfiguration.filter(function (item) {
            return item === settings.audioChannelConfiguration;
        })[0];

        return matchAudioChannelConfiguration;
    }

    function matchSettingsCodec(settings, track) {
        return !settings.codec || (settings.codec === track.codec);
    }

    function matchSettings(settings, track, isTrackActive = false) {
        try {
            let matchLang = false;

            // If there is no language defined in the target settings we got a match
            if (!settings.lang) {
                matchLang = true;
            }

            // If the target language is provided as a RegExp apply match function
            else if (settings.lang instanceof RegExp) {
                matchLang = track.lang.match(settings.lang);
            }

            // If the track has a language and we can normalize the target language check if we got a match
            else if (track.lang !== '') {
                const normalizedSettingsLang = bcp47Normalize(settings.lang);
                if (normalizedSettingsLang) {
                    matchLang = extendedFilter(track.lang, normalizedSettingsLang).length > 0
                }
            }

            const matchIndex = (settings.index === undefined) || (settings.index === null) || (track.index === settings.index);
            const matchViewPoint = !settings.viewpoint || (settings.viewpoint === track.viewpoint);
            const matchRole = !settings.role || !!track.roles.filter(function (item) {
                return item === settings.role;
            })[0];
            let matchAccessibility = !settings.accessibility || !!track.accessibility.filter(function (item) {
                return item === settings.accessibility;
            })[0];
            let matchAudioChannelConfiguration = !settings.audioChannelConfiguration || !!track.audioChannelConfiguration.filter(function (item) {
                return item === settings.audioChannelConfiguration;
            })[0];


            return (matchLang && matchIndex && matchViewPoint && (matchRole || (track.type === Constants.AUDIO && isTrackActive)) && matchAccessibility && matchAudioChannelConfiguration);
        } catch (e) {
            return false;
            logger.error(e);
        }
    }

    function resetInitialSettings() {
        initialSettings = {
            audio: null,
            video: null,
            text: null
        };
    }

    function getTracksWithHighestSelectionPriority(trackArr) {
        let max = 0;
        let result = [];

        trackArr.forEach((track) => {
            if (!isNaN(track.selectionPriority)) {
                // Higher max value. Reset list and add new entry
                if (track.selectionPriority > max) {
                    max = track.selectionPriority;
                    result = [track];
                }
                // Same max value add to list
                else if (track.selectionPriority === max) {
                    result.push(track);
                }

            }
        })

        return result;
    }

    function getTracksWithHighestBitrate(trackArr) {
        let max = 0;
        let result = [];
        let tmp;

        trackArr.forEach(function (track) {
            tmp = Math.max.apply(Math, track.bitrateList.map(function (obj) {
                return obj.bandwidth;
            }));

            if (tmp > max) {
                max = tmp;
                result = [track];
            } else if (tmp === max) {
                result.push(track);
            }
        });

        return result;
    }

    function getTracksWithHighestEfficiency(trackArr) {
        let min = Infinity;
        let result = [];
        let tmp;

        trackArr.forEach(function (track) {
            const sum = track.bitrateList.reduce(function (acc, obj) {
                const resolution = Math.max(1, obj.width * obj.height);
                const efficiency = obj.bandwidth / resolution;
                return acc + efficiency;
            }, 0);
            tmp = sum / track.bitrateList.length;

            if (tmp < min) {
                min = tmp;
                result = [track];
            } else if (tmp === min) {
                result.push(track);
            }
        });

        return result;
    }

    function getTracksWithWidestRange(trackArr) {
        let max = 0;
        let result = [];
        let tmp;

        trackArr.forEach(function (track) {
            tmp = track.representationCount;

            if (tmp > max) {
                max = tmp;
                result = [track];
            } else if (tmp === max) {
                result.push(track);
            }
        });

        return result;
    }

    function selectInitialTrack(type, tracks) {
        if (type === Constants.TEXT) return tracks[0];

        let mode = settings.get().streaming.selectionModeForInitialTrack;
        let tmpArr;
        const customInitialTrackSelectionFunction = customParametersModel.getCustomInitialTrackSelectionFunction();

        if (customInitialTrackSelectionFunction && typeof customInitialTrackSelectionFunction === 'function') {
            tmpArr = customInitialTrackSelectionFunction(tracks);
        } else {
            switch (mode) {
                case Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY:
                    tmpArr = _trackSelectionModeHighestSelectionPriority(tracks);
                    break;
                case Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE:
                    tmpArr = _trackSelectionModeHighestBitrate(tracks);
                    break;
                case Constants.TRACK_SELECTION_MODE_FIRST_TRACK:
                    tmpArr = _trackSelectionModeFirstTrack(tracks);
                    break;
                case Constants.TRACK_SELECTION_MODE_HIGHEST_EFFICIENCY:
                    tmpArr = _trackSelectionModeHighestEfficiency(tracks);
                    break;
                case Constants.TRACK_SELECTION_MODE_WIDEST_RANGE:
                    tmpArr = _trackSelectionModeWidestRange(tracks);
                    break;
                default:
                    logger.warn(`Track selection mode ${mode} is not supported. Falling back to TRACK_SELECTION_MODE_FIRST_TRACK`);
                    tmpArr = _trackSelectionModeFirstTrack(tracks);
                    break;
            }
        }

        return tmpArr.length > 0 ? tmpArr[0] : tracks[0];
    }


    function _trackSelectionModeHighestSelectionPriority(tracks) {
        let tmpArr = getTracksWithHighestSelectionPriority(tracks);

        if (tmpArr.length > 1) {
            tmpArr = getTracksWithHighestBitrate(tmpArr);
        }

        if (tmpArr.length > 1) {
            tmpArr = getTracksWithWidestRange(tmpArr);
        }

        return tmpArr;
    }

    function _trackSelectionModeHighestBitrate(tracks) {
        let tmpArr = getTracksWithHighestBitrate(tracks);

        if (tmpArr.length > 1) {
            tmpArr = getTracksWithWidestRange(tmpArr);
        }

        return tmpArr;
    }

    function _trackSelectionModeFirstTrack(tracks) {
        return tracks[0];
    }

    function _trackSelectionModeHighestEfficiency(tracks) {
        let tmpArr = getTracksWithHighestEfficiency(tracks);

        if (tmpArr.length > 1) {
            tmpArr = getTracksWithHighestBitrate(tmpArr);
        }

        return tmpArr;
    }

    function _trackSelectionModeWidestRange(tracks) {
        let tmpArr = getTracksWithWidestRange(tracks);

        if (tmpArr.length > 1) {
            tmpArr = getTracksWithHighestBitrate(tracks);
        }

        return tmpArr;
    }


    function createTrackInfo() {
        const storeLastSettings = settings.get().streaming.saveLastMediaSettingsForCurrentStreamingSession;

        return {
            audio: {
                list: [],
                storeLastSettings,
                current: null
            },
            video: {
                list: [],
                storeLastSettings,
                current: null
            },
            text: {
                list: [],
                storeLastSettings,
                current: null
            },
            image: {
                list: [],
                storeLastSettings,
                current: null
            }
        };
    }

    instance = {
        setInitialMediaSettingsForType,
        addTrack,
        getTracksFor,
        getCurrentTrackFor,
        isCurrentTrack,
        setTrack,
        selectInitialTrack,
        setInitialSettings,
        getInitialSettings,
        getTracksWithHighestBitrate,
        getTracksWithHighestEfficiency,
        getTracksWithWidestRange,
        isTracksEqual,
        matchSettings,
        matchSettingsLang,
        matchSettingsIndex,
        matchSettingsViewPoint,
        matchSettingsRole,
        matchSettingsAccessibility,
        matchSettingsAudioChannelConfig,
        saveTextSettingsDisabled,
        setConfig,
        reset
    };

    setup();

    return instance;
}

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