UNPKG

@l5i/dashjs

Version:

A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.

763 lines (661 loc) 29 kB
/** * 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'; import BufferController from './BufferController'; import URIFragmentModel from '../models/URIFragmentModel'; import EventBus from '../../core/EventBus'; import Events from '../../core/events/Events'; import FactoryMaker from '../../core/FactoryMaker'; import Debug from '../../core/Debug'; const LIVE_UPDATE_PLAYBACK_TIME_INTERVAL_MS = 500; const DEFAULT_CATCHUP_PLAYBACK_RATE = 0.05; // Start catching up mechanism for low latency live streaming // when latency goes beyong targetDelay * (1 + LIVE_CATCHUP_START_THRESHOLD) const LIVE_CATCHUP_START_THRESHOLD = 0.35; function PlaybackController() { const context = this.context; const eventBus = EventBus(context).getInstance(); let instance, logger, streamController, metricsModel, dashMetrics, manifestModel, dashManifestModel, adapter, videoModel, currentTime, liveStartTime, wallclockTimeIntervalId, commonEarliestTime, liveDelay, bufferedRange, streamInfo, isDynamic, mediaPlayerModel, playOnceInitialized, lastLivePlaybackTime, originalPlaybackRate, availabilityStartTime, compatibleWithPreviousStream; let catchUpPlaybackRate = DEFAULT_CATCHUP_PLAYBACK_RATE; function setup() { logger = Debug(context).getInstance().getLogger(instance); reset(); } function initialize(StreamInfo, compatible) { streamInfo = StreamInfo; addAllListeners(); isDynamic = streamInfo.manifestInfo.isDynamic; liveStartTime = streamInfo.start; compatibleWithPreviousStream = compatible; eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); eventBus.on(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, this); eventBus.on(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); eventBus.on(Events.PERIOD_SWITCH_STARTED, onPeriodSwitchStarted, this); eventBus.on(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this); eventBus.on(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); eventBus.on(Events.PLAYBACK_ENDED, onPlaybackEnded, this); if (playOnceInitialized) { playOnceInitialized = false; play(); } } function onPeriodSwitchStarted(e) { if (!isDynamic && e.fromStreamInfo && commonEarliestTime[e.fromStreamInfo.id] !== undefined) { delete bufferedRange[e.fromStreamInfo.id]; delete commonEarliestTime[e.fromStreamInfo.id]; } } function getTimeToStreamEnd() { return parseFloat(( getStreamEndTime() - getTime()).toFixed(5)); } function getStreamEndTime() { const startTime = getStreamStartTime(true); const offset = isDynamic ? startTime - streamInfo.start : 0; return startTime + (streamInfo.duration - offset); } function play() { if (streamInfo && videoModel && videoModel.getElement()) { videoModel.play(); } else { playOnceInitialized = true; } } function isPaused() { return streamInfo && videoModel ? videoModel.isPaused() : null; } function pause() { if (streamInfo && videoModel) { videoModel.pause(); } } function isSeeking() { return streamInfo && videoModel ? videoModel.isSeeking() : null; } function seek(time, stickToBuffered, internalSeek) { if (streamInfo && videoModel) { if (internalSeek === true) { if (time !== videoModel.getTime()) { // Internal seek = seek video model only (disable 'seeking' listener), // buffer(s) are already appended at given time (see onBytesAppended()) videoModel.removeEventListener('seeking', onPlaybackSeeking); logger.info('Requesting seek to time: ' + time); videoModel.setCurrentTime(time, stickToBuffered); } } else { eventBus.trigger(Events.PLAYBACK_SEEK_ASKED); logger.info('Requesting seek to time: ' + time); videoModel.setCurrentTime(time, stickToBuffered); } } } function getTime() { return streamInfo && videoModel ? videoModel.getTime() : null; } function getPlaybackRate() { return streamInfo && videoModel ? videoModel.getPlaybackRate() : null; } function getPlayedRanges() { return streamInfo && videoModel ? videoModel.getPlayedRanges() : null; } function getEnded() { return streamInfo && videoModel ? videoModel.getEnded() : null; } function getIsDynamic() { return isDynamic; } function getStreamController() { return streamController; } function setLiveStartTime(value) { liveStartTime = value; } function getLiveStartTime() { return liveStartTime; } function setCatchUpPlaybackRate(value) { catchUpPlaybackRate = value; // If value == 0.0, deactivate catchup mechanism if (value === 0.0 && getPlaybackRate() > 1.0) { stopPlaybackCatchUp(); } } function getCatchUpPlaybackRate() { return catchUpPlaybackRate; } /** * Computes the desirable delay for the live edge to avoid a risk of getting 404 when playing at the bleeding edge * @param {number} fragmentDuration - seconds? * @param {number} dvrWindowSize - seconds? * @returns {number} object * @memberof PlaybackController# */ function computeLiveDelay(fragmentDuration, dvrWindowSize) { const mpd = dashManifestModel.getMpd(manifestModel.getValue()); let delay; let ret; const END_OF_PLAYLIST_PADDING = 10; if (mediaPlayerModel.getUseSuggestedPresentationDelay() && mpd.hasOwnProperty(Constants.SUGGESTED_PRESENTATION_DELAY)) { delay = mpd.suggestedPresentationDelay; } else if (mediaPlayerModel.getLowLatencyEnabled()) { delay = 0; } else if (mediaPlayerModel.getLiveDelay()) { delay = mediaPlayerModel.getLiveDelay(); // If set by user, this value takes precedence } else if (!isNaN(fragmentDuration)) { delay = fragmentDuration * mediaPlayerModel.getLiveDelayFragmentCount(); } else { delay = streamInfo.manifestInfo.minBufferTime * 2; } if (mpd.availabilityStartTime) { availabilityStartTime = mpd.availabilityStartTime.getTime(); } if (dvrWindowSize > 0) { // cap target latency to: // - dvrWindowSize / 2 for short playlists // - dvrWindowSize - END_OF_PLAYLIST_PADDING for longer playlists const targetDelayCapping = Math.max(dvrWindowSize - END_OF_PLAYLIST_PADDING, dvrWindowSize / 2); ret = Math.min(delay, targetDelayCapping); } else { ret = delay; } liveDelay = ret; return ret; } function getLiveDelay() { return liveDelay; } function getCurrentLiveLatency() { if (!isDynamic || isNaN(availabilityStartTime)) { return NaN; } const currentTime = getTime(); if (isNaN(currentTime) || currentTime === 0) { return 0; } return ((Math.round(new Date().getTime() - (currentTime * 1000 + availabilityStartTime))) / 1000).toFixed(3); } function startPlaybackCatchUp() { if (videoModel) { const playbackRate = 1 + getCatchUpPlaybackRate(); const currentRate = getPlaybackRate(); if (playbackRate !== currentRate) { logger.info('Starting live catchup mechanism. Setting playback rate to', playbackRate); originalPlaybackRate = currentRate; videoModel.setPlaybackRate(playbackRate); eventBus.trigger(Events.PLAYBACK_CATCHUP_START, { sender: instance }); } } } function stopPlaybackCatchUp() { if (videoModel) { const playbackRate = originalPlaybackRate || 1; if (playbackRate !== getPlaybackRate()) { logger.info('Stopping live catchup mechanism. Setting playback rate to', playbackRate); videoModel.setPlaybackRate(playbackRate); eventBus.trigger(Events.PLAYBACK_CATCHUP_END, { sender: instance }); } } } function reset() { currentTime = 0; liveStartTime = NaN; playOnceInitialized = false; commonEarliestTime = {}; liveDelay = 0; availabilityStartTime = 0; catchUpPlaybackRate = DEFAULT_CATCHUP_PLAYBACK_RATE; bufferedRange = {}; if (videoModel) { eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); eventBus.off(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); eventBus.off(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, this); eventBus.off(Events.PERIOD_SWITCH_STARTED, onPeriodSwitchStarted, this); eventBus.off(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this); eventBus.off(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); eventBus.off(Events.PLAYBACK_ENDED, onPlaybackEnded, this); stopUpdatingWallclockTime(); removeAllListeners(); } wallclockTimeIntervalId = null; videoModel = null; streamInfo = null; isDynamic = null; } function setConfig(config) { if (!config) return; if (config.streamController) { streamController = config.streamController; } if (config.metricsModel) { metricsModel = config.metricsModel; } if (config.dashMetrics) { dashMetrics = config.dashMetrics; } if (config.manifestModel) { manifestModel = config.manifestModel; } if (config.dashManifestModel) { dashManifestModel = config.dashManifestModel; } if (config.mediaPlayerModel) { mediaPlayerModel = config.mediaPlayerModel; } if (config.adapter) { adapter = config.adapter; } if (config.videoModel) { videoModel = config.videoModel; } } function getStartTimeFromUriParameters() { const fragData = URIFragmentModel(context).getInstance().getURIFragmentData(); let uriParameters; if (fragData) { uriParameters = {}; const r = parseInt(fragData.r, 10); if (r >= 0 && streamInfo && r < streamInfo.manifestInfo.DVRWindowSize && fragData.t === null) { fragData.t = Math.floor(Date.now() / 1000) - streamInfo.manifestInfo.DVRWindowSize + r; } uriParameters.fragS = parseFloat(fragData.s); uriParameters.fragT = parseFloat(fragData.t); } return uriParameters; } /** * @param {boolean} ignoreStartOffset - ignore URL fragment start offset if true * @param {number} liveEdge - liveEdge value * @returns {number} object * @memberof PlaybackController# */ function getStreamStartTime(ignoreStartOffset, liveEdge) { let presentationStartTime; let startTimeOffset = NaN; const uriParameters = getStartTimeFromUriParameters(); if (uriParameters) { if (!ignoreStartOffset) { startTimeOffset = !isNaN(uriParameters.fragS) ? uriParameters.fragS : uriParameters.fragT; } else { startTimeOffset = streamInfo.start; } } else { // handle case where no media fragments are parsed from the manifest URL startTimeOffset = 0; } if (isDynamic) { if (!isNaN(startTimeOffset)) { presentationStartTime = startTimeOffset - (streamInfo.manifestInfo.availableFrom.getTime() / 1000); if (presentationStartTime > liveStartTime || presentationStartTime < (!isNaN(liveEdge) ? (liveEdge - streamInfo.manifestInfo.DVRWindowSize) : NaN)) { presentationStartTime = null; } } presentationStartTime = presentationStartTime || liveStartTime; } else { if (!isNaN(startTimeOffset) && startTimeOffset < Math.max(streamInfo.manifestInfo.duration, streamInfo.duration) && startTimeOffset >= 0) { presentationStartTime = startTimeOffset; } else { let earliestTime = commonEarliestTime[streamInfo.id]; //set by ready bufferStart after first onBytesAppended presentationStartTime = earliestTime !== undefined ? Math.max(earliestTime.audio !== undefined ? earliestTime.audio : 0, earliestTime.video !== undefined ? earliestTime.video : 0, streamInfo.start) : streamInfo.start; } } return presentationStartTime; } function getActualPresentationTime(currentTime) { const metrics = metricsModel.getReadOnlyMetricsFor(Constants.VIDEO) || metricsModel.getReadOnlyMetricsFor(Constants.AUDIO); const DVRMetrics = dashMetrics.getCurrentDVRInfo(metrics); const DVRWindow = DVRMetrics ? DVRMetrics.range : null; let actualTime; if (!DVRWindow) return NaN; if (currentTime > DVRWindow.end) { actualTime = Math.max(DVRWindow.end - streamInfo.manifestInfo.minBufferTime * 2, DVRWindow.start); } else if (currentTime + 0.250 < DVRWindow.start) { // Checking currentTime plus 250ms as the 'timeupdate' is fired with a frequency between 4Hz and 66Hz // https://developer.mozilla.org/en-US/docs/Web/Events/timeupdate // http://w3c.github.io/html/single-page.html#offsets-into-the-media-resource actualTime = DVRWindow.start; } else { return currentTime; } return actualTime; } function startUpdatingWallclockTime() { if (wallclockTimeIntervalId !== null) return; const tick = function () { onWallclockTime(); }; wallclockTimeIntervalId = setInterval(tick, mediaPlayerModel.getWallclockTimeUpdateInterval()); } function stopUpdatingWallclockTime() { clearInterval(wallclockTimeIntervalId); wallclockTimeIntervalId = null; } function updateCurrentTime() { if (isPaused() || !isDynamic || videoModel.getReadyState() === 0) return; const currentTime = getTime(); const actualTime = getActualPresentationTime(currentTime); const timeChanged = (!isNaN(actualTime) && actualTime !== currentTime); if (timeChanged) { seek(actualTime); } } function onDataUpdateCompleted(e) { if (e.error) return; const representationInfo = adapter.convertDataToRepresentationInfo(e.currentRepresentation); const info = representationInfo.mediaInfo.streamInfo; if (streamInfo.id !== info.id) return; streamInfo = info; updateCurrentTime(); } function onCanPlay() { eventBus.trigger(Events.CAN_PLAY); } function onPlaybackStart() { logger.info('Native video element event: play'); updateCurrentTime(); startUpdatingWallclockTime(); eventBus.trigger(Events.PLAYBACK_STARTED, { startTime: getTime() }); } function onPlaybackWaiting() { logger.info('Native video element event: waiting'); eventBus.trigger(Events.PLAYBACK_WAITING, { playingTime: getTime() }); } function onPlaybackPlaying() { logger.info('Native video element event: playing'); eventBus.trigger(Events.PLAYBACK_PLAYING, { playingTime: getTime() }); } function onPlaybackPaused() { logger.info('Native video element event: pause'); eventBus.trigger(Events.PLAYBACK_PAUSED, { ended: getEnded() }); } function onPlaybackSeeking() { const seekTime = getTime(); logger.info('Seeking to: ' + seekTime); startUpdatingWallclockTime(); eventBus.trigger(Events.PLAYBACK_SEEKING, { seekTime: seekTime }); } function onPlaybackSeeked() { logger.info('Native video element event: seeked'); eventBus.trigger(Events.PLAYBACK_SEEKED); // Reactivate 'seeking' event listener (see seek()) videoModel.addEventListener('seeking', onPlaybackSeeking); } function onPlaybackTimeUpdated() { const time = getTime(); currentTime = time; eventBus.trigger(Events.PLAYBACK_TIME_UPDATED, { timeToEnd: getTimeToStreamEnd(), time: time }); } function updateLivePlaybackTime() { const now = Date.now(); if (!lastLivePlaybackTime || now > lastLivePlaybackTime + LIVE_UPDATE_PLAYBACK_TIME_INTERVAL_MS) { lastLivePlaybackTime = now; onPlaybackTimeUpdated(); } } function onPlaybackProgress() { eventBus.trigger(Events.PLAYBACK_PROGRESS); } function onPlaybackRateChanged() { const rate = getPlaybackRate(); logger.info('Native video element event: ratechange: ', rate); eventBus.trigger(Events.PLAYBACK_RATE_CHANGED, { playbackRate: rate }); } function onPlaybackMetaDataLoaded() { logger.info('Native video element event: loadedmetadata'); eventBus.trigger(Events.PLAYBACK_METADATA_LOADED); startUpdatingWallclockTime(); } // Event to handle the native video element ended event function onNativePlaybackEnded() { logger.info('Native video element event: ended'); pause(); stopUpdatingWallclockTime(); eventBus.trigger(Events.PLAYBACK_ENDED, {'isLast': streamController.getActiveStreamInfo().isLast}); } // Handle DASH PLAYBACK_ENDED event function onPlaybackEnded(e) { if (wallclockTimeIntervalId && e.isLast) { // PLAYBACK_ENDED was triggered elsewhere, react. logger.info('[PlaybackController] onPlaybackEnded -- PLAYBACK_ENDED but native video element didn\'t fire ended'); videoModel.setCurrentTime(getStreamEndTime()); pause(); stopUpdatingWallclockTime(); } } function onPlaybackError(event) { const target = event.target || event.srcElement; eventBus.trigger(Events.PLAYBACK_ERROR, { error: target.error }); } function onWallclockTime() { eventBus.trigger(Events.WALLCLOCK_TIME_UPDATED, { isDynamic: isDynamic, time: new Date() }); // Updates playback time for paused dynamic streams // (video element doesn't call timeupdate when the playback is paused) if (getIsDynamic() && isPaused()) { updateLivePlaybackTime(); } } function checkTimeInRanges(time, ranges) { if (ranges && ranges.length > 0) { for (let i = 0, len = ranges.length; i < len; i++) { if (time >= ranges.start(i) && time < ranges.end(i)) { return true; } } } return false; } function onPlaybackProgression() { if (isDynamic && mediaPlayerModel.getLowLatencyEnabled() && getCatchUpPlaybackRate() > 0.0) { if (!isCatchingUp() && needToCatchUp()) { startPlaybackCatchUp(); } else if (stopCatchingUp()) { stopPlaybackCatchUp(); } } } function needToCatchUp() { return getCurrentLiveLatency() > (mediaPlayerModel.getLiveDelay() * (1 + LIVE_CATCHUP_START_THRESHOLD)); } function stopCatchingUp() { return getCurrentLiveLatency() <= (mediaPlayerModel.getLiveDelay() ); } function isCatchingUp() { return getCatchUpPlaybackRate() + 1 === getPlaybackRate(); } function onBytesAppended(e) { let earliestTime, initialStartTime; let ranges = e.bufferedRanges; if (!ranges || !ranges.length) return; if (commonEarliestTime[streamInfo.id] && commonEarliestTime[streamInfo.id].started === true) { //stream has already been started. return; } const type = e.sender.getType(); if (bufferedRange[streamInfo.id] === undefined) { bufferedRange[streamInfo.id] = []; } bufferedRange[streamInfo.id][type] = ranges; if (commonEarliestTime[streamInfo.id] === undefined) { commonEarliestTime[streamInfo.id] = []; commonEarliestTime[streamInfo.id].started = false; } if (commonEarliestTime[streamInfo.id][type] === undefined) { commonEarliestTime[streamInfo.id][type] = Math.max(ranges.start(0), streamInfo.start); } const hasVideoTrack = streamController.isTrackTypePresent(Constants.VIDEO); const hasAudioTrack = streamController.isTrackTypePresent(Constants.AUDIO); initialStartTime = getStreamStartTime(false); if (hasAudioTrack && hasVideoTrack) { //current stream has audio and video contents if (!isNaN(commonEarliestTime[streamInfo.id].audio) && !isNaN(commonEarliestTime[streamInfo.id].video)) { if (commonEarliestTime[streamInfo.id].audio < commonEarliestTime[streamInfo.id].video) { // common earliest is video time // check buffered audio range has video time, if ok, we seek, otherwise, we wait some other data earliestTime = commonEarliestTime[streamInfo.id].video > initialStartTime ? commonEarliestTime[streamInfo.id].video : initialStartTime; ranges = bufferedRange[streamInfo.id].audio; } else { // common earliest is audio time // check buffered video range has audio time, if ok, we seek, otherwise, we wait some other data earliestTime = commonEarliestTime[streamInfo.id].audio > initialStartTime ? commonEarliestTime[streamInfo.id].audio : initialStartTime; ranges = bufferedRange[streamInfo.id].video; } if (checkTimeInRanges(earliestTime, ranges)) { if (!isSeeking() && !compatibleWithPreviousStream && earliestTime !== 0) { seek(earliestTime, true, true); } commonEarliestTime[streamInfo.id].started = true; } } } else { //current stream has only audio or only video content if (commonEarliestTime[streamInfo.id][type]) { earliestTime = commonEarliestTime[streamInfo.id][type] > initialStartTime ? commonEarliestTime[streamInfo.id][type] : initialStartTime; if (!isSeeking() && !compatibleWithPreviousStream) { seek(earliestTime, false, true); } commonEarliestTime[streamInfo.id].started = true; } } } function onBufferLevelStateChanged(e) { // do not stall playback when get an event from Stream that is not active if (e.streamInfo.id !== streamInfo.id) return; videoModel.setStallState(e.mediaType, e.state === BufferController.BUFFER_EMPTY); } function onPlaybackStalled(e) { eventBus.trigger(Events.PLAYBACK_STALLED, { e: e }); } function addAllListeners() { videoModel.addEventListener('canplay', onCanPlay); videoModel.addEventListener('play', onPlaybackStart); videoModel.addEventListener('waiting', onPlaybackWaiting); videoModel.addEventListener('playing', onPlaybackPlaying); videoModel.addEventListener('pause', onPlaybackPaused); videoModel.addEventListener('error', onPlaybackError); videoModel.addEventListener('seeking', onPlaybackSeeking); videoModel.addEventListener('seeked', onPlaybackSeeked); videoModel.addEventListener('timeupdate', onPlaybackTimeUpdated); videoModel.addEventListener('progress', onPlaybackProgress); videoModel.addEventListener('ratechange', onPlaybackRateChanged); videoModel.addEventListener('loadedmetadata', onPlaybackMetaDataLoaded); videoModel.addEventListener('stalled', onPlaybackStalled); videoModel.addEventListener('ended', onNativePlaybackEnded); } function removeAllListeners() { videoModel.removeEventListener('canplay', onCanPlay); videoModel.removeEventListener('play', onPlaybackStart); videoModel.removeEventListener('waiting', onPlaybackWaiting); videoModel.removeEventListener('playing', onPlaybackPlaying); videoModel.removeEventListener('pause', onPlaybackPaused); videoModel.removeEventListener('error', onPlaybackError); videoModel.removeEventListener('seeking', onPlaybackSeeking); videoModel.removeEventListener('seeked', onPlaybackSeeked); videoModel.removeEventListener('timeupdate', onPlaybackTimeUpdated); videoModel.removeEventListener('progress', onPlaybackProgress); videoModel.removeEventListener('ratechange', onPlaybackRateChanged); videoModel.removeEventListener('loadedmetadata', onPlaybackMetaDataLoaded); videoModel.removeEventListener('stalled', onPlaybackStalled); videoModel.removeEventListener('ended', onNativePlaybackEnded); } instance = { initialize: initialize, setConfig: setConfig, getStartTimeFromUriParameters: getStartTimeFromUriParameters, getStreamStartTime: getStreamStartTime, getTimeToStreamEnd: getTimeToStreamEnd, getTime: getTime, getPlaybackRate: getPlaybackRate, getPlayedRanges: getPlayedRanges, getEnded: getEnded, getIsDynamic: getIsDynamic, getStreamController: getStreamController, setCatchUpPlaybackRate: setCatchUpPlaybackRate, setLiveStartTime: setLiveStartTime, getLiveStartTime: getLiveStartTime, computeLiveDelay: computeLiveDelay, getLiveDelay: getLiveDelay, getCurrentLiveLatency: getCurrentLiveLatency, play: play, isPaused: isPaused, pause: pause, isSeeking: isSeeking, seek: seek, reset: reset }; setup(); return instance; } PlaybackController.__dashjs_factory_name = 'PlaybackController'; export default FactoryMaker.getSingletonFactory(PlaybackController);