Source: dash/controllers/RepresentationController.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 '../../streaming/constants/Constants.js';
import FactoryMaker from '../../core/FactoryMaker.js';
import MediaPlayerEvents from '../../streaming/MediaPlayerEvents.js';
import {getTimeBasedSegment} from '../utils/SegmentsUtils.js';

function RepresentationController(config) {

    config = config || {};
    const eventBus = config.eventBus;
    const events = config.events;
    const abrController = config.abrController;
    const dashMetrics = config.dashMetrics;
    const playbackController = config.playbackController;
    const timelineConverter = config.timelineConverter;
    const type = config.type;
    const streamInfo = config.streamInfo;
    const segmentsController = config.segmentsController;
    const isDynamic = config.isDynamic;

    let instance,
        voAvailableRepresentations,
        currentVoRepresentation;

    function setup() {
        resetInitialSettings();

        eventBus.on(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, onManifestValidityChanged, instance);
    }

    function getStreamId() {
        return streamInfo.id;
    }

    function getType() {
        return type;
    }

    function checkConfig() {
        if (!abrController || !dashMetrics || !playbackController || !timelineConverter) {
            throw new Error(Constants.MISSING_CONFIG_ERROR);
        }
    }

    function getCurrentRepresentation() {
        return currentVoRepresentation;
    }

    function resetInitialSettings() {
        voAvailableRepresentations = [];
    }

    function reset() {
        eventBus.off(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, onManifestValidityChanged, instance);

        resetInitialSettings();
    }

    function updateData(availableRepresentations, isFragmented, selectedRepresentationId) {
        return new Promise((resolve, reject) => {
            voAvailableRepresentations = availableRepresentations;
            const selectedRepresentation = getRepresentationById(selectedRepresentationId);
            _setCurrentVoRepresentation(selectedRepresentation);


            if (type !== Constants.VIDEO && type !== Constants.AUDIO && (type !== Constants.TEXT || !isFragmented)) {
                endDataUpdate();
                resolve();
                return;
            }

            const promises = [];
            for (let i = 0, ln = voAvailableRepresentations.length; i < ln; i++) {
                const currentRep = voAvailableRepresentations[i];
                promises.push(_updateRepresentation(currentRep));
            }

            Promise.all(promises)
                .then(() => {
                    _onAllRepresentationsUpdated();
                    resolve();
                })
                .catch((e) => {
                    reject(e);
                })
        })
    }

    function _onAllRepresentationsUpdated() {
        abrController.setPlaybackQuality(type, streamInfo, currentVoRepresentation);
        const dvrInfo = dashMetrics.getCurrentDVRInfo(type);
        if (dvrInfo) {
            dashMetrics.updateManifestUpdateInfo({ latency: dvrInfo.range.end - playbackController.getTime() });
        }

        endDataUpdate();
    }

    function _updateRepresentation(currentRep) {
        return new Promise((resolve, reject) => {
            const hasInitialization = currentRep.hasInitialization();
            const hasSegments = currentRep.hasSegments();

            // If representation has initialization and segments information we are done
            // otherwise, it means that a request has to be made to get initialization and/or segments information
            const promises = [];

            promises.push(segmentsController.updateInitData(currentRep, hasInitialization));
            promises.push(segmentsController.updateSegmentData(currentRep, hasSegments));

            Promise.all(promises)
                .then((data) => {
                    if (data[0] && !data[0].error) {
                        currentRep = _onInitLoaded(currentRep, data[0]);
                    }
                    if (data[1] && !data[1].error) {
                        currentRep = _onSegmentsLoaded(currentRep, data[1]);
                    }
                    currentRep.fragmentDuration = currentRep.segmentDuration ? currentRep.segmentDuration : currentRep.segments && currentRep.segments.length > 0 ? currentRep.segments[0].duration : NaN;
                    _setMediaFinishedInformation(currentRep);
                    _onRepresentationUpdated(currentRep);
                    resolve();
                })
                .catch((e) => {
                    reject(e);
                });
        });
    }

    function _setMediaFinishedInformation(representation) {
        representation.mediaFinishedInformation = segmentsController.getMediaFinishedInformation(representation);
    }

    function _onInitLoaded(representation, e) {
        if (!e || e.error || !e.representation) {
            return representation;
        }
        return e.representation;
    }

    function _onSegmentsLoaded(representation, e) {
        if (!e || e.error) return;

        const fragments = e.segments;
        const segments = [];
        let count = 0;

        let i,
            len,
            s,
            seg;

        for (i = 0, len = fragments ? fragments.length : 0; i < len; i++) {
            s = fragments[i];

            seg = getTimeBasedSegment(
                timelineConverter,
                isDynamic,
                representation,
                s.startTime,
                s.duration,
                s.timescale,
                s.media,
                s.mediaRange,
                count);

            if (seg) {
                segments.push(seg);
                seg = null;
                count++;
            }
        }

        if (segments.length > 0) {
            representation.segments = segments;
        }

        return representation;
    }

    function _addRepresentationSwitch(currentRepresentation) {
        checkConfig();
        const now = new Date();
        const currentVideoTimeMs = playbackController.getTime() * 1000;
        if (currentRepresentation) {
            dashMetrics.addRepresentationSwitch(currentRepresentation.adaptation.type, now, currentVideoTimeMs, currentRepresentation.id);
        }

        eventBus.trigger(MediaPlayerEvents.REPRESENTATION_SWITCH, {
            mediaType: type,
            streamId: streamInfo.id,
            currentRepresentation,
        }, { streamId: streamInfo.id, mediaType: type })
    }

    function getRepresentationById(id) {
        if (!voAvailableRepresentations || voAvailableRepresentations.length === 0) {
            return null;
        }

        const reps = voAvailableRepresentations.filter((rep) => {
            return rep.id === id;
        })

        if (reps.length > 0) {
            return reps[0]
        }

        return null;
    }

    function endDataUpdate(error) {
        eventBus.trigger(events.DATA_UPDATE_COMPLETED,
            {
                currentRepresentation: currentVoRepresentation,
                error: error
            },
            { streamId: streamInfo.id, mediaType: type }
        );
    }

    function _onRepresentationUpdated(r) {
        let manifestUpdateInfo = dashMetrics.getCurrentManifestUpdate();
        let alreadyAdded = false;
        let repInfo;


        if (manifestUpdateInfo) {
            for (let i = 0; i < manifestUpdateInfo.representationInfo.length; i++) {
                repInfo = manifestUpdateInfo.representationInfo[i];
                if (repInfo.index === r.index && repInfo.mediaType === getType()) {
                    alreadyAdded = true;
                    break;
                }
            }

            if (!alreadyAdded) {
                dashMetrics.addManifestUpdateRepresentationInfo(r, getType());
            }
        }
    }

    /**
     * We get the new selected Representation which will not hold the ranges and the segment references in case of SegmentBase.
     * In any case use the id to find the right Representation instance in our array of Representations.
     * @param newRep
     */
    function prepareQualityChange(newRep) {
        const voRepresentations = voAvailableRepresentations.filter((rep) => {
            return rep.id === newRep.id;
        })

        if (voRepresentations.length > 0) {
            _setCurrentVoRepresentation(voRepresentations[0]);
        }
    }

    function _setCurrentVoRepresentation(value) {
        if (!currentVoRepresentation || currentVoRepresentation.id !== value.id) {
            _addRepresentationSwitch(value);
        }
        currentVoRepresentation = value;
    }

    function onManifestValidityChanged(e) {
        if (e.newDuration) {
            const representation = getCurrentRepresentation();
            if (representation && representation.adaptation.period) {
                const period = representation.adaptation.period;
                period.duration = e.newDuration;
            }
        }
    }

    instance = {
        getCurrentRepresentation,
        getRepresentationById,
        getStreamId,
        getType,
        prepareQualityChange,
        reset,
        updateData,
    };

    setup();
    return instance;
}

RepresentationController.__dashjs_factory_name = 'RepresentationController';
export default FactoryMaker.getClassFactory(RepresentationController);