streaming_mediaplayer_PlaybackApi.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.js';
import FactoryMaker from '../../core/FactoryMaker.js';
import { checkParameterType } from '../utils/SupervisorTools.js';
import { MEDIA_PLAYER_NOT_INITIALIZED_ERROR, PLAYBACK_NOT_INITIALIZED_ERROR } from './MediaPlayerApiErrors.js';

function PlaybackApi() {
    let instance,
        state,
        mediaPlayer;

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

    /**
     * The play method initiates playback of the media defined by the {@link module:MediaPlayer#attachSource attachSource()} method.
     * This method will call play on the native Video Element.
     *
     * @see {@link module:MediaPlayer#attachSource attachSource()}
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @memberof module:MediaPlayer
     * @instance
     */
    function play() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }
        if (!state.autoPlay || (isPaused() && state.playbackInitialized)) {
            state.playbackController.play(true);
        }
    }

    /**
     * This method will call pause on the native Video Element.
     *
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @memberof module:MediaPlayer
     * @instance
     */
    function pause() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }
        state.playbackController.pause();
    }

    /**
     * Returns a Boolean that indicates whether the Video Element is paused.
     * @return {boolean}
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @memberof module:MediaPlayer
     * @instance
     */
    function isPaused() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }
        return state.playbackController.isPaused();
    }

    /**
     * Sets the currentTime property of the attached video element.  If it is a live stream with a
     * timeShiftBufferLength, then the DVR window offset will be automatically calculated.
     *
     * @param {number} value - A relative time, in seconds, based on the return value of the {@link module:MediaPlayer#duration duration()} method is expected.
     * For dynamic streams duration() returns DVRWindow.end - DVRWindow.start. Consequently, the value provided to this function should be relative to DVRWindow.start.
     * @see {@link module:MediaPlayer#getDvrSeekOffset getDvrSeekOffset()}
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @throws {@link Constants#BAD_ARGUMENT_ERROR BAD_ARGUMENT_ERROR} if called with an invalid argument, not number type or is NaN.
     * @memberof module:MediaPlayer
     * @instance
     */
    function seek(value) {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }

        checkParameterType(value, 'number');

        if (isNaN(value)) {
            throw Constants.BAD_ARGUMENT_ERROR;
        }

        if (value < 0) {
            value = 0;
        }

        let s = state.playbackController.getIsDynamic() ? getDvrSeekOffset(value) : value;

        // For VoD limit the seek to the duration of the content
        const videoElement = mediaPlayer.getVideoElement();
        if (!state.playbackController.getIsDynamic() && videoElement.duration) {
            s = Math.min(videoElement.duration, s);
        }

        state.playbackController.seek(s, false, false, true);
    }

    /**
     * Sets the currentTime property of the attached video element. Compared to the seek() function this function does not add the DVR window offset. Instead, it takes a presentation time relative to the availability start time.
     * For VoD this function behaves similar to the seek() function.

     * @param {number} value - A presentation time in seconds
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @throws {@link Constants#BAD_ARGUMENT_ERROR BAD_ARGUMENT_ERROR} if called with an invalid argument, not number type or is NaN.
     * @memberof module:MediaPlayer
     * @instance
     */
    function seekToPresentationTime(seektime) {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }

        checkParameterType(seektime, 'number');

        if (isNaN(seektime)) {
            throw Constants.BAD_ARGUMENT_ERROR;
        }

        if (seektime < 0) {
            seektime = 0;
        }

        // For VoD limit the seek to the duration of the content
        const videoElement = mediaPlayer.getVideoElement();
        if (!state.playbackController.getIsDynamic() && videoElement.duration) {
            seektime = Math.min(videoElement.duration, seektime);
        }

        // For live, take live delay into account
        if (state.playbackController.getIsDynamic()) {
            const type = state.streamController && state.streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO;
            let metric = state.dashMetrics.getCurrentDVRInfo(type);
            if (!metric) {
                return;
            }
            seektime = _adjustSeekTimeBasedOnLiveDelay(seektime, metric)
            if (seektime < metric.range.start) {
                seektime = metric.range.start
            }
        }

        state.playbackController.seek(seektime, false, false, true);
    }

    /**
     * Seeks back to the original live edge (live edge as calculated at playback start). Only applies to live streams, for VoD streams this call will be ignored.
     */
    function seekToOriginalLive() {
        if (!state.playbackInitialized || !isDynamic()) {
            return;
        }

        state.playbackController.seekToOriginalLive();
    }

    /**
     * Returns a Boolean that indicates whether the media is in the process of seeking to a new position.
     * @return {boolean}
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @memberof module:MediaPlayer
     * @instance
     */
    function isSeeking() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }
        return state.playbackController.isSeeking();
    }

    /**
     * Returns a Boolean that indicates whether the media is in the process of dynamic.
     * @return {boolean}
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @memberof module:MediaPlayer
     * @instance
     */
    function isDynamic() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }
        return state.playbackController.getIsDynamic();
    }

    /**
     * Returns a boolean that indicates whether the player is operating in low latency mode.
     * @return {boolean}
     * @memberof module:MediaPlayer
     * @instance
     */
    function getLowLatencyModeEnabled() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }
        return state.playbackController.getLowLatencyModeEnabled();
    }

    /**
     * Use this method to set the native Video Element's playback rate.
     * @param {number} value
     * @memberof module:MediaPlayer
     * @instance
     */
    function setPlaybackRate(value) {
        mediaPlayer.getVideoElement().playbackRate = value;
    }

    /**
     * Returns the current playback rate.
     * @returns {number}
     * @memberof module:MediaPlayer
     * @instance
     */
    function getPlaybackRate() {
        return mediaPlayer.getVideoElement().playbackRate;
    }

    /**
     * Use this method to set the native Video Element's muted state. Takes a Boolean that determines whether audio is muted. true if the audio is muted and false otherwise.
     * @param {boolean} value
     * @memberof module:MediaPlayer
     * @throws {@link Constants#BAD_ARGUMENT_ERROR BAD_ARGUMENT_ERROR} if called with an invalid argument, not boolean type.
     * @instance
     */
    function setMute(value) {
        checkParameterType(value, 'boolean');
        mediaPlayer.getVideoElement().muted = value;
    }

    /**
     * A Boolean that determines whether audio is muted.
     * @returns {boolean}
     * @memberof module:MediaPlayer
     * @instance
     */
    function isMuted() {
        return mediaPlayer.getVideoElement().muted;
    }

    /**
     * A double indicating the audio volume, from 0.0 (silent) to 1.0 (loudest).
     * @param {number} value
     * @memberof module:MediaPlayer
     * @throws {@link Constants#BAD_ARGUMENT_ERROR BAD_ARGUMENT_ERROR} if called with an invalid argument, not number type, or is NaN or not between 0 and 1.
     * @instance
     */
    function setVolume(value) {
        if (typeof value !== 'number' || isNaN(value) || value < 0.0 || value > 1.0) {
            throw Constants.BAD_ARGUMENT_ERROR;
        }
        mediaPlayer.getVideoElement().volume = value;
    }

    /**
     * Returns the current audio volume, from 0.0 (silent) to 1.0 (loudest).
     * @returns {number}
     * @memberof module:MediaPlayer
     * @instance
     */
    function getVolume() {
        return mediaPlayer.getVideoElement().volume;
    }

    /**
     * The length of the buffer for a given media type, in seconds. Valid media
     * types are "video", "audio" and "text". If no type is passed
     * in, then the minimum of video, audio and text buffer length is
     * returned. NaN is returned if an invalid type is requested, the
     * presentation does not contain that type, or if no arguments are passed
     * and the presentation does not include any adaption sets of valid media
     * type.
     *
     * @param {MediaType} type - 'video', 'audio' or 'text'
     * @returns {number} The length of the buffer for the given media type, in
     *  seconds, or NaN
     * @memberof module:MediaPlayer
     * @instance
     */
    function getBufferLength(type) {
        const types = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT];
        if (!type) {
            const buffer = types.map(
                t => mediaPlayer.getTracksFor(t).length > 0 ? state.dashMetrics.getCurrentBufferLevel(t) : Number.MAX_VALUE
            ).reduce(
                (p, c) => Math.min(p, c)
            );
            return buffer === Number.MAX_VALUE ? NaN : buffer;
        } else {
            if (types.indexOf(type) !== -1) {
                const buffer = state.dashMetrics.getCurrentBufferLevel(type);
                return buffer ? buffer : NaN;
            } else {
                state.logger.warn('getBufferLength requested for invalid type');
                return NaN;
            }
        }
    }

    /**
     * This method should only be used with a live stream that has a valid timeShiftBufferLength (DVR Window).
     * NOTE - If you do not need the raw offset value (i.e. media analytics, tracking, etc) consider using the {@link module:MediaPlayer#seek seek()} method
     * which will calculate this value for you and set the video element's currentTime property all in one simple call.
     *
     * @param {number} value - A relative time, in seconds, based on the return value of the {@link module:MediaPlayer#duration duration()} method is expected.
     * @returns {number} A value that is relative the available range within the timeShiftBufferLength (DVR Window).
     * @see {@link module:MediaPlayer#seek seek()}
     * @memberof module:MediaPlayer
     * @instance
     */
    function getDvrSeekOffset(value) {
        const type = state.streamController && state.streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO;
        let metric = state.dashMetrics.getCurrentDVRInfo(type);
        if (!metric) {
            return 0;
        }

        let val = metric.range.start + value;

        return _adjustSeekTimeBasedOnLiveDelay(val, metric);
    }

    function _adjustSeekTimeBasedOnLiveDelay(seektime, metric) {
        let liveDelay = state.playbackController.getOriginalLiveDelay();
        if (seektime > (metric.range.end - liveDelay)) {
            seektime = metric.range.end - liveDelay;
        }

        return seektime;
    }

    /**
     * Returns the target live delay
     * @returns {number} The target live delay
     * @memberof module:MediaPlayer
     * @instance
     */
    function getTargetLiveDelay() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }

        return state.playbackController.getOriginalLiveDelay();
    }

    /**
     * Current playhead time in seconds.
     *
     * If called with no arguments then the returned value is the current time of the video element.
     * However, if a period ID is supplied then time is relative to the start of that period, or is null if there is no such period id in the manifest.
     *
     * @param {string} periodId - The ID of a period that the returned playhead time must be relative to the start of. If undefined, then playhead time is relative to the first period or the AST.
     * @returns {number} The current playhead time of the media, or null.
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @memberof module:MediaPlayer
     * @instance
     */
    function time(periodId = '') {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }
        let t = mediaPlayer.getVideoElement().currentTime;

        if (periodId !== '') {
            t = state.streamController.getTimeRelativeToStreamId(t, periodId);
        }

        return t;
    }

    /**
     * Returns the current playhead time relative to the start of the DVR window.
     * For VoD this method returns the same value as time()
     * @returns {number} The current playhead time of the media relative to the start of the DVR window
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @memberof module:MediaPlayer
     * @instance
     */
    function timeInDvrWindow() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }

        if (!state.playbackController.getIsDynamic()) {
            return time()
        }

        let t = mediaPlayer.getVideoElement().currentTime;
        const type = state.streamController && state.streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO;
        let metric = state.dashMetrics.getCurrentDVRInfo(type);
        t = (metric === null || t === 0) ? 0 : Math.max(0, (t - metric.range.start));

        return t
    }

    /**
     * Returns information about the current DVR window including the start time, the end time, the window size.
     * @returns {{startAsUtc: (*|number), size: number, endAsUtc: (*|number), start, end}|{}}
     */
    function getDvrWindow() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }

        const type = state.streamController && state.streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO;
        let metric = state.dashMetrics.getCurrentDVRInfo(type);

        if (!metric) {
            return {}
        }

        let offset = 0;
        const isDynamic = state.playbackController.getIsDynamic();
        if (isDynamic) {
            offset = metric.manifestInfo.availableFrom.getTime() / 1000;
        }
        return {
            start: metric.range.start,
            end: metric.range.end,
            startAsUtc: isDynamic ? offset + metric.range.start : NaN,
            endAsUtc: isDynamic ? offset + metric.range.end : NaN,
            size: metric.range.end - metric.range.start
        }
    }

    /**
     * Total duration of the media in seconds.
     *
     * @returns {number} The total duration of the media. For a dynamic stream this will return DVRWindow.end - DVRWindow.start
     * @memberof module:MediaPlayer
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @instance
     */
    function duration() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }
        let d = mediaPlayer.getVideoElement().duration;

        if (state.playbackController.getIsDynamic()) {
            const type = state.streamController && state.streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO;
            let metric = state.dashMetrics.getCurrentDVRInfo(type);
            d = metric ? (metric.range.end - metric.range.start) : 0;
        }
        return d;
    }

    /**
     * Use this method to get the current playhead time as an absolute value in seconds since midnight UTC, Jan 1 1970.
     * Note - this property only has meaning for live streams and is NaN for VoD content. If called before play() has begun, it will return a value of NaN.
     *
     * @returns {number} The current playhead time as UTC timestamp.
     * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
     * @memberof module:MediaPlayer
     * @instance
     */
    function timeAsUTC() {
        if (!state.playbackInitialized) {
            throw PLAYBACK_NOT_INITIALIZED_ERROR;
        }

        if (!state.playbackController.getIsDynamic() || time() < 0) {
            return NaN
        }

        const type = state.streamController && state.streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO;
        let metric = state.dashMetrics.getCurrentDVRInfo(type);
        let availabilityStartTime,
            utcValue;

        if (!metric) {
            return 0;
        }
        availabilityStartTime = metric.manifestInfo.availableFrom.getTime() / 1000;
        utcValue = availabilityStartTime + time()
        return utcValue;
    }

    /**
     * <p>Set to false to prevent stream from auto-playing when the view is attached.</p>
     *
     * @param {boolean} value
     * @default true
     * @memberof module:MediaPlayer
     * @see {@link module:MediaPlayer#attachView attachView()}
     * @throws {@link Constants#BAD_ARGUMENT_ERROR BAD_ARGUMENT_ERROR} if called with an invalid argument, not boolean type.
     * @instance
     *
     */
    function setAutoPlay(value) {
        checkParameterType(value, 'boolean');
        state.autoPlay = value;
    }

    /**
     * @returns {boolean} The current autoPlay state.
     * @memberof module:MediaPlayer
     * @instance
     */
    function getAutoPlay() {
        return state.autoPlay;
    }

    /**
     * @memberof module:MediaPlayer
     * @instance
     * @returns {number|NaN} Current live stream latency in seconds. It is the difference between now time and time position at the playback head.
     * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function
     */
    function getCurrentLiveLatency() {
        if (!state.mediaPlayerInitialized) {
            throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR;
        }

        if (!state.playbackInitialized) {
            return NaN;
        }

        return state.playbackController.getCurrentLiveLatency();
    }

    instance = {
        duration,
        getAutoPlay,
        getBufferLength,
        getCurrentLiveLatency,
        getDvrSeekOffset,
        getDvrWindow,
        getLowLatencyModeEnabled,
        getPlaybackRate,
        getTargetLiveDelay,
        getVolume,
        isDynamic,
        isMuted,
        isPaused,
        isSeeking,
        pause,
        play,
        seek,
        seekToOriginalLive,
        seekToPresentationTime,
        setAutoPlay,
        setConfig,
        setMute,
        setPlaybackRate,
        setVolume,
        time,
        timeAsUTC,
        timeInDvrWindow,
    };

    return instance;
}

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