streaming_mediaplayer_LifecycleApi.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 Events from '../../core/events/Events.js';
import EventBus from '../../core/EventBus.js';
import FactoryMaker from '../../core/FactoryMaker.js';
import MediaPlayerEvents from '../MediaPlayerEvents.js';
import {
    ELEMENT_NOT_ATTACHED_ERROR,
    MEDIA_PLAYER_NOT_INITIALIZED_ERROR,
    SOURCE_NOT_ATTACHED_ERROR
} from './MediaPlayerApiErrors.js';

function LifecycleApi() {
    const context = this.context;
    const eventBus = EventBus(context).getInstance();
    let instance,
        state,
        mediaPlayer,
        wiring;

    function setConfig(config) {
        if (!config) {
            return;
        }
        if (config.state) {
            state = config.state;
        }
        if (config.mediaPlayer) {
            mediaPlayer = config.mediaPlayer;
        }
        if (config.wiring) {
            wiring = config.wiring;
        }
    }

    /**
     * Upon creating the MediaPlayer you must call initialize before you call anything else.
     * There is one exception to this rule. It is crucial to call {@link module:MediaPlayer#extend extend()}
     * with all your extensions prior to calling initialize.
     *
     * ALL arguments are optional and there are individual methods to set each argument later on.
     * The args in this method are just for convenience and should only be used for a simple player setup.
     *
     * @param {HTML5MediaElement=} view - Optional arg to set the video element. {@link module:MediaPlayer#attachView attachView()}
     * @param {string=} source - Optional arg to set the media source. {@link module:MediaPlayer#attachSource attachSource()}
     * @param {boolean=} autoPlay - Optional arg to set auto play. {@link module:MediaPlayer#setAutoPlay setAutoPlay()}
     * @param {number|string} startTime - For VoD content the start time is relative to the start time of the first period.
     * For live content
     * If the parameter starts from prefix posix: it signifies the absolute time range defined in seconds of Coordinated Universal Time (ITU-R TF.460-6). This is the number of seconds since 01-01-1970 00:00:00 UTC. Fractions of seconds may be optionally specified down to the millisecond level.
     * If no posix prefix is used the starttime is relative to MPD@availabilityStartTime
     * @see {@link module:MediaPlayer#attachSource attachSource()}
     * @see {@link module:MediaPlayer#setAutoPlay setAutoPlay()}
     * @memberof module:MediaPlayer
     * @instance
     */
    function initialize(view, source, autoPlay, startTime = NaN) {
        if (!wiring.supportsMediaSource()) {
            return;
        }
        if (!state.mediaPlayerInitialized) {
            state.mediaPlayerInitialized = true;

            wiring.createCoreControllers();
            state.customParametersModel.restoreDefaultUTCTimingSources();
            mediaPlayer.setAutoPlay(autoPlay !== undefined ? autoPlay : true);

            // Detect and initialize offline module to support offline contents playback
            wiring.detectOffline();
        }

        if (view) {
            attachView(view);
        }

        if (source) {
            attachSource(source, startTime);
        }

        state.logger.info('[dash.js ' + mediaPlayer.getVersion() + '] ' + 'MediaPlayer has been initialized');
    }

    /**
     * Sets the MPD source and the video element to null. You can also reset the MediaPlayer by
     * calling attachSource with a new source file.
     *
     * This call does not destroy the MediaPlayer. To destroy the MediaPlayer and free all of its
     * memory, call destroy().
     *
     * @memberof module:MediaPlayer
     * @instance
     */
    function reset() {
        attachSource(null);
        attachView(null);
        state.protectionData = null;
        if (state.protectionController) {
            state.protectionController.reset();
            state.protectionController = null;
        }
        if (state.customParametersModel) {
            state.customParametersModel.reset();
        }

        state.settings.reset();

        wiring.reset();

        if (state.retrieveManifestRequest) {
            state.retrieveManifestRequest.resetLoader();
            state.retrieveManifestRequest = null;
        }
    }

    /**
     * Completely destroys the media player and frees all memory.
     *
     * @memberof module:MediaPlayer
     * @instance
     */
    function destroy() {
        reset();

        if (state.videoModel) {
            state.videoModel.destroy();
            state.videoModel = null;
        }

        if (state.adapter) {
            state.adapter.destroy();
        }

        FactoryMaker.deleteSingletonInstances(context);
    }

    /**
     * The ready state of the MediaPlayer based on both the video element and MPD source being defined.
     *
     * @returns {boolean} The current ready state of the MediaPlayer
     * @see {@link module:MediaPlayer#attachView attachView()}
     * @see {@link module:MediaPlayer#attachSource attachSource()}
     * @memberof module:MediaPlayer
     * @instance
     */
    function isReady() {
        return (!!state.source && !!state.videoModel.getElement());
    }

    /**
     * Causes the player to begin streaming the media as set by the {@link module:MediaPlayer#attachSource attachSource()}
     * method in preparation for playing. It specifically does not require a view to be attached with {@link module:MediaPlayer#attachSource attachView()} to begin preloading.
     * When a view is attached after preloading, the buffered data is transferred to the attached mediaSource buffers.
     *
     * @see {@link module:MediaPlayer#attachSource attachSource()}
     * @see {@link module:MediaPlayer#attachView attachView()}
     * @memberof module:MediaPlayer
     * @throws {@link module:MediaPlayer~SOURCE_NOT_ATTACHED_ERROR SOURCE_NOT_ATTACHED_ERROR} if called before attachSource function
     * @instance
     */
    function preload() {
        if (state.videoModel.getElement() || state.streamingInitialized) {
            return;
        }
        if (state.source) {
            _initializePlayback(state.providedStartTime);
        } else {
            throw SOURCE_NOT_ATTACHED_ERROR;
        }
    }

    /**
     * Returns instance of Video Element that was attached by calling attachView()
     * @returns {Object}
     * @memberof module:MediaPlayer
     * @throws {@link module:MediaPlayer~ELEMENT_NOT_ATTACHED_ERROR ELEMENT_NOT_ATTACHED_ERROR} if called before attachView function
     * @instance
     */
    function getVideoElement() {
        if (!state.videoModel.getElement()) {
            throw ELEMENT_NOT_ATTACHED_ERROR;
        }
        return state.videoModel.getElement();
    }

    /**
     * Use this method to attach an HTML5 VideoElement for dash.js to operate upon.
     *
     * @param {Object} element - An HTMLMediaElement that has already been defined in the DOM (or equivalent stub).
     * @memberof module:MediaPlayer
     * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function
     * @instance
     */
    function attachView(element) {
        if (!state.mediaPlayerInitialized) {
            throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR;
        }

        state.videoModel.setElement(element);

        if (element) {
            wiring.detectProtection();
            wiring.detectMetricsReporting();
            wiring.detectMss();

            if (state.streamController) {
                state.streamController.switchToVideoElement(state.providedStartTime);
            }
        }

        if (state.playbackInitialized) { //Reset if we have been playing before, so this is a new element.
            wiring.resetPlaybackControllers();
            state.customParametersModel.resetPlaybackSessionSpecificSettings();
        }

        _initializePlayback(state.providedStartTime);
    }

    /**
     * Detects if Protection is included and returns an instance of ProtectionController.js
     * @memberof module:MediaPlayer
     * @instance
     */
    function getProtectionController() {
        return wiring.detectProtection();
    }

    /**
     * Will override dash.js protection controller.
     * @param {ProtectionController} value - valid protection controller instance.
     * @memberof module:MediaPlayer
     * @instance
     */
    function attachProtectionController(value) {
        state.protectionController = value;
    }

    /**
     * Sets Protection Data required to setup the Protection Module (DRM). Protection Data must
     * be set before initializing MediaPlayer or, once initialized, before PROTECTION_CREATED event is fired.
     * @see {@link module:MediaPlayer#initialize initialize()}
     * @see {@link ProtectionEvents#event:PROTECTION_CREATED dashjs.Protection.events.PROTECTION_CREATED}
     * @param {ProtectionDataSet} value - object containing
     * property names corresponding to key system name strings and associated
     * values being instances of.
     * @memberof module:MediaPlayer
     * @instance
     */
    function setProtectionData(value) {
        state.protectionData = value;

        // Propagate changes in case StreamController is already created
        if (state.streamController) {
            state.streamController.setProtectionData(state.protectionData);
        }
    }

    function getProtectionData() {
        return state.streamController ? state.streamController.getProtectionData() : null;
    }

    /**
     * Allows application to retrieve a manifest.  Manifest loading is asynchronous and requires the app-provided callback function
     *
     * @param {string} url - url the manifest url
     * @param {function} callback - A Callback function provided when retrieving manifests
     * @memberof module:MediaPlayer
     * @instance
     */
    function retrieveManifest(url, callback) {
        if (state.retrieveManifestRequest) {
            state.retrieveManifestRequest.resetLoader();
        }

        const manifestLoader = wiring.createManifestLoader();
        const resetLoader = () => {
            eventBus.off(Events.INTERNAL_MANIFEST_LOADED, handler, this);
            manifestLoader.reset();
            state.retrieveManifestRequest = null;
        };

        state.retrieveManifestRequest = { manifestLoader, resetLoader };

        const handler = (e) => {
            if (typeof callback == 'function') {
                if (!e.error) {
                    callback(e.manifest);
                } else {
                    callback(null, e.error);
                }
            }

            resetLoader();
        };

        eventBus.on(Events.INTERNAL_MANIFEST_LOADED, handler, this);

        state.uriFragmentModel.initialize(url);
        manifestLoader.load(url);
    }

    /**
     * Use this method to set a source URL to a valid MPD manifest file OR
     * a previously downloaded and parsed manifest object.  Optionally, can
     * also provide protection information
     *
     * @param {string|Object} urlOrManifest - A URL to a valid MPD manifest file, or a
     * parsed manifest object.
     * @param {number|string} startTime - For VoD content the start time is relative to the start time of the first period.
     * For live content
     * If the parameter starts from prefix posix: it signifies the absolute time range defined in seconds of Coordinated Universal Time (ITU-R TF.460-6). This is the number of seconds since 01-01-1970 00:00:00 UTC. Fractions of seconds may be optionally specified down to the millisecond level.
     * If no posix prefix is used the starttime is relative to MPD@availabilityStartTime
     *
     * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function
     *
     * @memberof module:MediaPlayer
     * @instance
     */
    function attachSource(urlOrManifest, startTime = NaN) {
        if (!state.mediaPlayerInitialized) {
            throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR;
        }

        if (typeof urlOrManifest === 'string') {
            state.uriFragmentModel.initialize(urlOrManifest);
        }

        if (startTime == null) {
            startTime = NaN;
        }

        if (!isNaN(startTime)) {
            startTime = Math.max(0, startTime);
        }

        state.providedStartTime = startTime;
        state.source = urlOrManifest;

        if (state.streamingInitialized || state.playbackInitialized) {
            wiring.resetPlaybackControllers();
            state.customParametersModel.resetPlaybackSessionSpecificSettings()
        }

        if (isReady()) {
            _initializePlayback(state.providedStartTime);
        }
    }

    function _initializePlayback(startTime = NaN) {

        if (state.offlineController) {
            state.offlineController.resetRecords();
        }

        if (!state.streamingInitialized && state.source) {
            state.streamingInitialized = true;
            state.logger.info('Streaming Initialized');
            wiring.createPlaybackControllers();

            if (typeof state.source === 'string') {
                state.streamController.load(state.source, startTime);
            } else {
                state.streamController.loadWithManifest(state.source, startTime);
            }
        }

        if (!state.playbackInitialized && isReady()) {
            state.playbackInitialized = true;
            eventBus.trigger(MediaPlayerEvents.PLAYBACK_INITIALIZED)
            state.logger.info('Playback Initialized');
        }
    }

    instance = {
        attachProtectionController,
        attachSource,
        attachView,
        destroy,
        getProtectionController,
        getProtectionData,
        getVideoElement,
        initialize,
        isReady,
        preload,
        reset,
        retrieveManifest,
        setConfig,
        setProtectionData,
    };

    return instance;
}

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