offline_OfflineStream.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 OfflineStreamProcessor from './OfflineStreamProcessor';

/**
 * Initialize and Manage Offline Stream for each type
 */
/**
 * @class OfflineStream
 * @description Initialize and Manage Offline Stream for each type
 * @param {Object} config - dependences
 * @ignore
 */
function OfflineStream(config) {

    config = config || {};
    const context = this.context;
    const eventBus = config.eventBus;
    const events = config.events;
    const errors = config.errors;
    const constants = config.constants;
    const dashConstants = config.dashConstants;
    const settings = config.settings;
    const debug = config.debug;
    const errHandler = config.errHandler;
    const mediaPlayerModel = config.mediaPlayerModel;
    const abrController = config.abrController;
    const playbackController = config.playbackController;
    const adapter = config.adapter;
    const dashMetrics = config.dashMetrics;
    const baseURLController = config.baseURLController;
    const timelineConverter = config.timelineConverter;
    const segmentBaseController = config.segmentBaseController;
    const offlineStoreController = config.offlineStoreController;
    const manifestId = config.id;
    const startedCb = config.callbacks && config.callbacks.started;
    const progressionCb = config.callbacks && config.callbacks.progression;
    const finishedCb = config.callbacks && config.callbacks.finished;
    const updateManifest = config.callbacks && config.callbacks.updateManifestNeeded;

    let instance,
        offlineStreamProcessors,
        startedOfflineStreamProcessors,
        finishedOfflineStreamProcessors,
        streamInfo,
        representationsToUpdate,
        allMediasInfosList,
        progressionById;

    function setup() {
        resetInitialSettings();
    }

    /**
     * Reset
     */
    function resetInitialSettings() {
        streamInfo = null;
        offlineStreamProcessors = [];
        startedOfflineStreamProcessors = 0;
        finishedOfflineStreamProcessors = 0;
        allMediasInfosList = [];
        representationsToUpdate = [];
        progressionById = {};
    }

    /**
     * Initialize offlinestream
     * @param {Object} initStreamInfo
     */
    function initialize(initStreamInfo) {
        streamInfo = initStreamInfo;
        eventBus.on(events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance);
    }

    function getStreamId() {
        return streamInfo.id;
    }

    /**
     * Creates media infos list, so that user will be able to choose the representation he wants to download
     */
    function getMediaInfos() {
        let mediaInfos = adapter.getAllMediaInfoForType(streamInfo, constants.VIDEO);
        mediaInfos = mediaInfos.concat(adapter.getAllMediaInfoForType(streamInfo, constants.AUDIO));
        mediaInfos = mediaInfos.concat(adapter.getAllMediaInfoForType(streamInfo, constants.TEXT));

        // mediaInfos = mediaInfos.concat(adapter.getAllMediaInfoForType(streamInfo, constants.MUXED));
        // mediaInfos = mediaInfos.concat(adapter.getAllMediaInfoForType(streamInfo, constants.IMAGE));

        eventBus.trigger(events.OFFLINE_RECORD_LOADEDMETADATA, {
            id: manifestId,
            mediaInfos: mediaInfos
        });
    }

    /**
     * Initialize with choosen representations by user
     * @param {Object} mediasInfoList
     */
    function initializeAllMediasInfoList(mediasInfoList) {
        allMediasInfosList = mediasInfoList;
        initializeMedia(streamInfo);
    }

    /**
     * Initialize media for each type
     * @param {Object} streamInfo
     */
    function initializeMedia(streamInfo) {
        createOfflineStreamProcessorFor(constants.VIDEO,streamInfo);
        createOfflineStreamProcessorFor(constants.AUDIO,streamInfo);
        createOfflineStreamProcessorFor(constants.TEXT,streamInfo);
        createOfflineStreamProcessorFor(constants.MUXED,streamInfo);
        createOfflineStreamProcessorFor(constants.IMAGE,streamInfo);
    }

    function createOfflineStreamProcessorFor(type, streamInfo) {
        // filter mediaInfo according to choosen representation id
        let allMediaInfoForType = adapter.getAllMediaInfoForType(streamInfo, type);
        allMediaInfoForType.forEach((media) => {
            media.bitrateList = media.bitrateList.filter((bitrate) => {
                if (allMediasInfosList[type] && allMediasInfosList[type].indexOf(bitrate.id) !== -1) {
                    return true;
                }
                return false;
            });
        });

        allMediaInfoForType = allMediaInfoForType.filter((media) => {
            return (media.bitrateList && media.bitrateList.length > 0);
        });

        // cration of an offline stream processor for each choosen representation
        allMediaInfoForType.forEach((mediaInfo) => {
            if (mediaInfo.bitrateList) {
                mediaInfo.bitrateList.forEach((bitrate) => {
                    createStreamProcessor(mediaInfo, bitrate);
                });
            }
        });
        return allMediaInfoForType;
    }

    function createStreamProcessor (mediaInfo, bitrate) {

        let streamProcessor = OfflineStreamProcessor(context).create({
            id: manifestId,
            streamInfo: streamInfo,
            debug: debug,
            events: events,
            errors: errors,
            eventBus: eventBus,
            constants: constants,
            dashConstants: dashConstants,
            settings: settings,
            type: mediaInfo.type,
            mimeType: mediaInfo.mimeType,
            bitrate: bitrate,
            errHandler: errHandler,
            mediaPlayerModel: mediaPlayerModel,
            abrController: abrController,
            playbackController: playbackController,
            adapter: adapter,
            dashMetrics: dashMetrics,
            baseURLController: baseURLController,
            timelineConverter: timelineConverter,
            offlineStoreController: offlineStoreController,
            segmentBaseController: segmentBaseController,
            callbacks: {
                completed: onStreamCompleted,
                progression: onStreamProgression
            }
        });
        offlineStreamProcessors.push(streamProcessor);
        streamProcessor.initialize(mediaInfo);

        progressionById[bitrate.id] = null;
    }

    function onStreamCompleted() {
        finishedOfflineStreamProcessors++;
        if (finishedOfflineStreamProcessors === offlineStreamProcessors.length) {
            finishedCb({sender: this, id: manifestId, message: 'Downloading has been successfully completed for this stream !'});
        }
    }

    function onStreamProgression(streamProcessor, downloadedSegments, availableSegments ) {
        progressionById[streamProcessor.getRepresentationId()] = {
            downloadedSegments,
            availableSegments
        };

        let segments = 0;
        let allSegments = 0;
        let waitForAllProgress;
        for (var property in progressionById) {
            if (progressionById.hasOwnProperty(property)) {
                if (progressionById[property] === null) {
                    waitForAllProgress = true;
                } else {
                    segments += progressionById[property].downloadedSegments;
                    allSegments += progressionById[property].availableSegments;
                }
            }
        }

        if (!waitForAllProgress && progressionCb) {
            // all progression have been started, we can compute global progression
            if (allSegments > 0) {
                progressionCb(instance, segments, allSegments);
            }
        }
    }

    function onDataUpdateCompleted(e) {
        if (e.currentRepresentation.segments && e.currentRepresentation.segments.length > 0) {
            representationsToUpdate.push(e.currentRepresentation);
        }

        let sp;
        // data are ready fr stream processor, let's start download
        for (let i = 0; i < offlineStreamProcessors.length; i++ ) {
            if (offlineStreamProcessors[i].getRepresentationController().getType() === e.mediaType) {
                sp = offlineStreamProcessors[i];
                break;
            }
        }

        if (sp) {
            checkIfAllOfflineStreamProcessorsStarted();
        }
    }

    function checkIfAllOfflineStreamProcessorsStarted() {
        startedOfflineStreamProcessors++;
        if (startedOfflineStreamProcessors === offlineStreamProcessors.length) {
            startedCb({sender: this, id: manifestId, message: 'Downloading started for this stream !'});

            if (representationsToUpdate.length > 0) {
                updateManifest({sender: this, id: manifestId, representations: representationsToUpdate });
            } else {
                startOfflineStreamProcessors();
            }
        }
    }

    function getStreamInfo() {
        return streamInfo;
    }

    function getStartTime() {
        return streamInfo ? streamInfo.start : NaN;
    }

    function getDuration() {
        return streamInfo ? streamInfo.duration : NaN;
    }

    /**
     * Stop offline stream processors
     */
    function stopOfflineStreamProcessors() {
        for (let i = 0; i < offlineStreamProcessors.length; i++) {
            offlineStreamProcessors[i].stop();
        }
    }

    /**
     * Start offline stream processors
     */
    function startOfflineStreamProcessors() {
        for (let i = 0; i < offlineStreamProcessors.length; i++) {
            offlineStreamProcessors[i].start();
        }
    }

    function deactivate() {
        let ln = offlineStreamProcessors ? offlineStreamProcessors.length : 0;
        for (let i = 0; i < ln; i++) {
            offlineStreamProcessors[i].removeExecutedRequestsBeforeTime(getStartTime() + getDuration());
            offlineStreamProcessors[i].reset();
        }
    }

    /**
     * Reset
     */
    function reset() {
        stopOfflineStreamProcessors();
        deactivate();
        resetInitialSettings();

        eventBus.off(events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance);
    }

    instance = {
        initialize: initialize,
        getStreamId: getStreamId,
        getMediaInfos: getMediaInfos,
        initializeAllMediasInfoList: initializeAllMediasInfoList,
        getStreamInfo: getStreamInfo,
        stopOfflineStreamProcessors: stopOfflineStreamProcessors,
        startOfflineStreamProcessors: startOfflineStreamProcessors,
        reset: reset
    };

    setup();
    return instance;
}

OfflineStream.__dashjs_factory_name = 'OfflineStream';
export default dashjs.FactoryMaker.getClassFactory(OfflineStream); /* jshint ignore:line */