offline_controllers_OfflineController.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 OfflineConstants from '../constants/OfflineConstants';
import OfflineStoreController from './OfflineStoreController';
import OfflineDownload from '../OfflineDownload';
import IndexDBOfflineLoader from '../net/IndexDBOfflineLoader';
import OfflineUrlUtils from '../utils/OfflineUrlUtils';
import OfflineEvents from '../events/OfflineEvents';
import OfflineErrors from '../errors/OfflineErrors';
import OfflineRecord from '../vo/OfflineDownloadVo';

/**
 * @module OfflineController
 * @param {Object} config - dependencies
 * @description Provides access to offline stream recording and playback functionality. This module can be accessed using the MediaPlayer API getOfflineController()
 */
function OfflineController(config) {

    const context = this.context;
    const errHandler = config.errHandler;
    const events = config.events;
    const errors = config.errors;
    const settings = config.settings;
    const eventBus = config.eventBus;
    const debug = config.debug;
    const manifestLoader = config.manifestLoader;
    const manifestModel = config.manifestModel;
    const mediaPlayerModel = config.mediaPlayerModel;
    const abrController = config.abrController;
    const playbackController = config.playbackController;
    const dashMetrics = config.dashMetrics;
    const timelineConverter = config.timelineConverter;
    const segmentBaseController = config.segmentBaseController;
    const adapter = config.adapter;
    const manifestUpdater = config.manifestUpdater;
    const baseURLController = config.baseURLController;
    const schemeLoaderFactory = config.schemeLoaderFactory;
    const constants = config.constants;
    const dashConstants = config.dashConstants;
    const urlUtils = config.urlUtils;

    let instance,
        downloads,
        logger,
        offlineStoreController,
        offlineUrlUtils;

    function setup() {
        logger = debug.getLogger(instance);
        offlineStoreController = OfflineStoreController(context).create({
            eventBus: config.eventBus,
            errHandler: errHandler
        });
        offlineUrlUtils = OfflineUrlUtils(context).getInstance();
        urlUtils.registerUrlRegex(offlineUrlUtils.getRegex(), offlineUrlUtils);
        schemeLoaderFactory.registerLoader(OfflineConstants.OFFLINE_SCHEME, IndexDBOfflineLoader);

        downloads = [];
    }

    /*
    ---------------------------------------------------------------------------
        DOWNLOAD LIST FUNCTIONS
    ---------------------------------------------------------------------------
    */
    function getDownloadFromId(id) {
        let download = downloads.find((item) => {
            return item.getId() === id;
        });
        return download;
    }

    function createDownloadFromId(id) {
        let download;
        download = getDownloadFromId(id);

        if (!download) {
            // create download controller
            download = OfflineDownload(context).create({
                id: id,
                eventBus: eventBus,
                events: events,
                errors: errors,
                settings: settings,
                manifestLoader: manifestLoader,
                manifestModel: manifestModel,
                mediaPlayerModel: mediaPlayerModel,
                manifestUpdater: manifestUpdater,
                baseURLController: baseURLController,
                abrController: abrController,
                playbackController: playbackController,
                adapter: adapter,
                dashMetrics: dashMetrics,
                timelineConverter: timelineConverter,
                errHandler: errHandler,
                segmentBaseController: segmentBaseController,
                offlineStoreController: offlineStoreController,
                debug: debug,
                constants: constants,
                dashConstants: dashConstants,
                urlUtils: urlUtils
            });

            downloads.push(download);
        }

        return download;
    }

    function createDownloadFromStorage(offline) {
        let download = getDownloadFromId(offline.manifestId);

        if (!download) {
            download = createDownloadFromId(offline.manifestId);
            let status = offline.status;
            if (status === OfflineConstants.OFFLINE_STATUS_STARTED) {
                status = OfflineConstants.OFFLINE_STATUS_STOPPED;
            }

            download.setInitialState({
                url: offline.url,
                progress: offline.progress,
                originalUrl: offline.originalURL,
                status: status
            });
        }

        return download;
    }

    function removeDownloadFromId(id) {
        return new Promise(function (resolve, reject) {
            let download = getDownloadFromId(id);
            let waitForStatusChanged = false;
            if (download) {
                //is download running?
                if (download.isDownloading()) {
                    //register status changed event
                    waitForStatusChanged = true;
                    const downloadStopped = function () {
                        eventBus.off(events.OFFLINE_RECORD_STOPPED, downloadStopped, instance);
                        return offlineStoreController.deleteDownloadById(id).then(function () {
                            resolve();
                        }).catch(function (err) {
                            reject(err);
                        });
                    };
                    eventBus.on(events.OFFLINE_RECORD_STOPPED, downloadStopped, instance);
                }
                download.deleteDownload();
                let index = downloads.indexOf(download);
                downloads.splice(index, 1);
            }

            if (!waitForStatusChanged) {
                resolve();
            }
        });
    }

    function generateManifestId() {
        let timestamp = new Date().getTime();
        return timestamp;
    }

    /*
    ---------------------------------------------------------------------------

        OFFLINE CONTROLLER API

    ---------------------------------------------------------------------------
    */

    /**
     * Loads records from storage
     * This methods has to be called first, to be sure that all downloads have been loaded
     *
     * @return {Promise} asynchronously resolved
     * @memberof module:OfflineController
     */
    function loadRecordsFromStorage() {
        return new Promise(function (resolve, reject) {
            offlineStoreController.getAllManifests().then((items) => {
                items.manifests.forEach((offline) => {
                    createDownloadFromStorage(offline);
                });

                resolve();
            }).catch((e) => {
                logger.error('Failed to load downloads ' + e);
                reject(e);
            });
        });
    }

    /**
     * Get all records from storage
     *
     * @return {Promise} asynchronously resolved with records
     * @memberof module:OfflineController
     * @instance
     */
    function getAllRecords() {
        let records = [];
        downloads.forEach((download) => {
            const record = new OfflineRecord();
            record.id = download.getId();
            record.progress = download.getDownloadProgression();
            record.url = download.getOfflineUrl();
            record.originalUrl = download.getManifestUrl();
            record.status = download.getStatus();
            records.push(record);
        });
        return records;
    }

    /**
     * Create a new content record in storage and download manifest from url
     *
     * @param {string} manifestURL - the content manifest url
     * @return {Promise} asynchronously resolved with record identifier
     * @memberof module:OfflineController
     * @instance
     */
    function createRecord(manifestURL) {
        return new Promise(function (resolve, reject) {
            let id = generateManifestId();

            // create download controller
            let download = createDownloadFromId(id);

            download.downloadFromUrl(manifestURL).then(() => {
                download.initDownload();
                resolve(id);
            })
                .catch((e) => {
                    logger.error('Failed to download ' + e);
                    removeDownloadFromId(id).then(function () {
                        reject(e);
                    });
                });
        });
    }

    /**
     * Start downloading the record with selected tracks representations
     *
     * @param {string} id - record identifier
     * @param {MediaInfo[]} mediaInfos - the selected tracks representations
     * @memberof module:OfflineController
     * @instance
     */
    function startRecord(id, mediaInfos) {
        let download = getDownloadFromId(id);
        if (download) {
            download.startDownload(mediaInfos);
        }
    }

    /**
     * Stop downloading of the record
     *
     * @param {string} id - record identifier
     * @memberof module:OfflineController
     * @instance
     */
    function stopRecord(id) {
        let download = getDownloadFromId(id);
        if (download) {
            download.stopDownload();
        }
    }

    /**
     * Resume downloading of the record
     *
     * @param {string} id - record identifier
     * @memberof module:OfflineController
     * @instance
     */
    function resumeRecord(id) {
        let download = getDownloadFromId(id);
        if (download) {
            download.resumeDownload();
        }
    }

    /**
     * Deletes a record from storage
     *
     * @param {string} id - record identifier
     * @memberof module:OfflineController
     * @instance
     */
    function deleteRecord(id) {
        return removeDownloadFromId(id).then(function () {
            return offlineStoreController.deleteDownloadById(id);
        });
    }


    /**
     * Get download progression of a record
     *
     * @param {string} id - record identifier
     * @return {number} percentage progression
     * @memberof module:OfflineController
     * @instance
     */
    function getRecordProgression(id) {
        let download = getDownloadFromId(id);
        if (download) {
            return download.getDownloadProgression();
        }
        return 0;
    }

    /**
     * Reset all records
     * @memberof module:OfflineController
     * @instance
     */
    function resetRecords() {
        downloads.forEach((download) => {
            download.resetDownload();
        });
    }

    /**
     * Reset
     * @instance
     */
    function reset() {
        resetRecords();
        schemeLoaderFactory.unregisterLoader(OfflineConstants.OFFLINE_SCHEME);
    }

    instance = {
        loadRecordsFromStorage: loadRecordsFromStorage,
        createRecord: createRecord,
        startRecord: startRecord,
        stopRecord: stopRecord,
        resumeRecord: resumeRecord,
        deleteRecord: deleteRecord,
        getRecordProgression: getRecordProgression,
        getAllRecords: getAllRecords,
        resetRecords: resetRecords,
        reset: reset
    };

    setup();

    return instance;
}

OfflineController.__dashjs_factory_name = 'OfflineController';
const factory = dashjs.FactoryMaker.getClassFactory(OfflineController); /* jshint ignore:line */
factory.events = OfflineEvents;
factory.errors = OfflineErrors;
dashjs.FactoryMaker.updateClassFactory(OfflineController.__dashjs_factory_name, factory); /* jshint ignore:line */
export default factory;