# Source: 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) {
            return !settings.lang ||
            (settings.lang instanceof RegExp) ?
                (track.lang.match(settings.lang)) : track.lang !== '' ?
                    (extendedFilter(track.lang, bcp47Normalize(settings.lang)).length > 0) : 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;

#### Search results