From 03bc302e4a9dcd95ee67ff8a766c6c6ecbedd9c3 Mon Sep 17 00:00:00 2001 From: Sebastian Piquerez Date: Tue, 5 Nov 2024 14:56:11 -0300 Subject: [PATCH 001/175] Alternative palyer POC --- samples/develop.html | 79 ++++++++++++++++++++++++++++++++++++ src/streaming/MediaPlayer.js | 57 +++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 samples/develop.html diff --git a/samples/develop.html b/samples/develop.html new file mode 100644 index 0000000000..c4a2ce9e75 --- /dev/null +++ b/samples/develop.html @@ -0,0 +1,79 @@ + + + + + Buffer target + + + + + + + + + + +
+
+
+ +
+
+
+ © DASH-IF +
+ +
+ + + + \ No newline at end of file diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index e212ce0cc6..d9df13b065 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -170,6 +170,7 @@ function MediaPlayer() { uriFragmentModel, domStorage, segmentBaseController, + alternativePlayer, clientDataReportingController; /* @@ -286,7 +287,7 @@ function MediaPlayer() { * @memberof module:MediaPlayer * @instance */ - function initialize(view, source, autoPlay, startTime = NaN) { + function initialize(view, source, autoPlay,startTime = NaN) { if (!capabilities) { capabilities = Capabilities(context).getInstance(); capabilities.setConfig({ @@ -457,8 +458,12 @@ function MediaPlayer() { * @memberof module:MediaPlayer * @instance */ - function reset() { - attachSource(null); + function reset(onlyControllers) { + + if (!onlyControllers) { + attachSource(null); + } + attachView(null); protectionData = null; if (protectionController) { @@ -585,12 +590,14 @@ function MediaPlayer() { * @throws {@link module:MediaPlayer~SOURCE_NOT_ATTACHED_ERROR SOURCE_NOT_ATTACHED_ERROR} if called before attachSource function * @instance */ - function preload() { - if (videoModel.getElement() || streamingInitialized) { + function preload(time) { + if (videoModel.getElement() || (streamingInitialized && !time)) { return; } if (source) { - _initializePlayback(providedStartTime); + const playbackTime = time ? time : providedStartTime; + console.log(playbackTime) + _initializePlayback(playbackTime); } else { throw SOURCE_NOT_ATTACHED_ERROR; } @@ -2732,6 +2739,42 @@ function MediaPlayer() { } } + let videoNew + function setAlternativePlayer() { + videoNew = videoModel.getElement().cloneNode(true); + const mediaPlayerFactory = FactoryMaker.getClassFactory(MediaPlayer); + alternativePlayer = mediaPlayerFactory().create() + // const alternativeUrl = 'https://livesim2.dashif.org/livesim2/scte35_2/testpic_2s/Manifest.mpd'; + // const alternativeUrl = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' + const alternativeUrl = 'http://localhost:3000/stream.mpd' + const parent = videoModel.getElement().parentNode + parent.append(videoNew) + alternativePlayer.initialize(null, alternativeUrl, false); + alternativePlayer.updateSettings({ + // debug: {logLevel: 5}, + streaming: {cacheInitSegments: true} + }); + alternativePlayer.preload(10) + } + + async function switchView() { + const video = videoModel.getElement(); + // const alternativeVideo = alternativePlayer.getVideoElement() + pause() + const currentTime = time() + preload(currentTime) + alternativePlayer.attachView(video) + alternativePlayer.play() + setTimeout(async () => { + await alternativePlayer.attachView(null) + attachView(video); + alternativePlayer.destroy(); + alternativePlayer = null; + play(); + }, 7000) + } + + instance = { addABRCustomRule, addRequestInterceptor, @@ -2815,6 +2858,7 @@ function MediaPlayer() { seek, seekToOriginalLive, seekToPresentationTime, + setAlternativePlayer, setAutoPlay, setConfig, setCurrentTrack, @@ -2828,6 +2872,7 @@ function MediaPlayer() { setTextTrack, setVolume, setXHRWithCredentialsForType, + switchView, time, timeAsUtc, timeInDvrWindow, From 0c9aed2816e290a5ac7f3bf404647610c1d4f385 Mon Sep 17 00:00:00 2001 From: Joaquin Bartaburu Date: Wed, 6 Nov 2024 16:54:58 -0300 Subject: [PATCH 002/175] try to keep buffers on alternative change --- src/streaming/MediaPlayer.js | 56 +++++++++++++++---- src/streaming/Stream.js | 30 ++++++++++ .../controllers/PlaybackController.js | 4 +- src/streaming/controllers/StreamController.js | 52 +++++++++++++++++ 4 files changed, 131 insertions(+), 11 deletions(-) diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index d9df13b065..e408622bdb 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -1461,7 +1461,35 @@ function MediaPlayer() { if (playbackInitialized) { //Reset if we have been playing before, so this is a new element. _resetPlaybackControllers(); } + console.log(providedStartTime); + + _initializePlayback(providedStartTime); + } + + function _attachViewAlt(element, config) { + if (!mediaPlayerInitialized) { + throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; + } + + videoModel.setElement(element); + + if (element) { + _detectProtection(); + _detectMetricsReporting(); + _detectMss(); + if (streamController) { + streamController.switchToVideoElement(10); + } + } + + streamController.setConfig(config); + + if (playbackInitialized) { //Reset if we have been playing before, so this is a new element. + _resetPlaybackControllers(); + } + console.log(providedStartTime); + _initializePlayback(providedStartTime); } @@ -2337,6 +2365,11 @@ function MediaPlayer() { // PRIVATE METHODS //*********************************** + function _resetControllers() { + const streams = streamController.resetAlt(); + return streams; + } + function _resetPlaybackControllers() { playbackInitialized = false; streamingInitialized = false; @@ -2739,35 +2772,38 @@ function MediaPlayer() { } } - let videoNew function setAlternativePlayer() { - videoNew = videoModel.getElement().cloneNode(true); const mediaPlayerFactory = FactoryMaker.getClassFactory(MediaPlayer); alternativePlayer = mediaPlayerFactory().create() // const alternativeUrl = 'https://livesim2.dashif.org/livesim2/scte35_2/testpic_2s/Manifest.mpd'; - // const alternativeUrl = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' - const alternativeUrl = 'http://localhost:3000/stream.mpd' - const parent = videoModel.getElement().parentNode - parent.append(videoNew) + const alternativeUrl = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' + // const alternativeUrl = 'http://localhost:3000/stream.mpd' alternativePlayer.initialize(null, alternativeUrl, false); alternativePlayer.updateSettings({ // debug: {logLevel: 5}, streaming: {cacheInitSegments: true} }); - alternativePlayer.preload(10) + alternativePlayer.preload() } async function switchView() { const video = videoModel.getElement(); // const alternativeVideo = alternativePlayer.getVideoElement() pause() - const currentTime = time() - preload(currentTime) + // attachView(null); + updateSettings({ + // debug: {logLevel: 5}, + streaming: {cacheInitSegments: true} + }); + // const currentTime = time() + // providedStartTime = 5; + // preload() + const streams = _resetControllers(); alternativePlayer.attachView(video) alternativePlayer.play() setTimeout(async () => { await alternativePlayer.attachView(null) - attachView(video); + _attachViewAlt(video, streams); alternativePlayer.destroy(); alternativePlayer = null; play(); diff --git a/src/streaming/Stream.js b/src/streaming/Stream.js index dad44879c5..8a529106b0 100644 --- a/src/streaming/Stream.js +++ b/src/streaming/Stream.js @@ -608,6 +608,35 @@ function Stream(config) { trackChangedEvents = []; } + function resetAlt(keepBuffers) { + + if (fragmentController) { + fragmentController.reset(); + fragmentController = null; + } + + if (abrController && streamInfo) { + abrController.clearDataForStream(streamInfo.id); + } + + if (segmentBlacklistController) { + segmentBlacklistController.reset(); + segmentBlacklistController = null; + } + + console.log('resetAlt, keepBuffers', keepBuffers); + + + resetInitialSettings(keepBuffers); + + streamInfo = null; + + unRegisterEvents(); + + unRegisterProtectionEvents(); + + } + function reset(keepBuffers) { if (fragmentController) { @@ -1083,6 +1112,7 @@ function Stream(config) { prepareQualityChange, prepareTrackChange, reset, + resetAlt, setIsEndedEventSignaled, setMediaSource, startPreloading, diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index cc5503aa77..6e1bf5ce7f 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -703,7 +703,9 @@ function PlaybackController() { } function _onPlaybackProgress() { - eventBus.trigger(Events.PLAYBACK_PROGRESS, { streamId: streamInfo.id }); + if (streamInfo){ + eventBus.trigger(Events.PLAYBACK_PROGRESS, { streamId: streamInfo.id }); + } } function _onPlaybackRateChanged() { diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index c2c6cd5303..cbcfe6ddae 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -650,6 +650,8 @@ function StreamController() { // If the track was changed in the active stream we need to stop preloading and remove the already prebuffered stuff. Since we do not support preloading specific handling of specific AdaptationSets yet. _deactivateAllPreloadingStreams(); + console.log('_onCurrentTrackChanged'); + if (settings.get().streaming.buffer.resetSourceBuffersForTrackSwitch && e.oldMediaInfo && e.oldMediaInfo.codec !== e.newMediaInfo.codec) { const seekTime = playbackController.getTime(); activeStream.deactivate(false); @@ -1309,6 +1311,8 @@ function StreamController() { function switchToVideoElement(seekTime) { + console.log('switchToVideoElement'); + if (activeStream) { playbackController.initialize(getActiveStreamInfo()); _openMediaSource({ seekTime, keepBuffers: false, streamActivated: true }); @@ -1502,6 +1506,9 @@ function StreamController() { return; } + if (config.streams) { + streams = config.streams; + } if (config.capabilities) { capabilities = config.capabilities; } @@ -1606,6 +1613,50 @@ function StreamController() { } } + function resetAlt() { + // _checkConfig(); + + // timeSyncController.reset(); + + // _flushPlaylistMetrics(hasMediaError || hasInitialisationError ? PlayListTrace.FAILURE_STOP_REASON : PlayListTrace.USER_REQUEST_STOP_REASON); + const config = {}; + config.streamsCpy = [...streams]; + + for (let i = 0, ln = streams ? streams.length : 0; i < ln; i++) { + const stream = streams[i]; + stream.resetAlt(true); + } + + unRegisterEvents(); + + // baseURLController.reset(); + // manifestUpdater.reset(); + // eventController.reset(); + // dashMetrics.clearAllCurrentMetrics(); + // manifestModel.setValue(null); + // manifestLoader.reset(); + // timelineConverter.reset(); + // initCache.reset(); + + // if (mediaSource) { + // mediaSourceController.detachMediaSource(videoModel); + // mediaSource = null; + // } + // videoModel = null; + // if (protectionController) { + // protectionController = null; + // protectionData = null; + // if (manifestModel.getValue()) { + // eventBus.trigger(Events.PROTECTION_DESTROYED, { data: manifestModel.getValue().url }); + // } + // } + + _stopPlaybackEndedTimerInterval(); + eventBus.trigger(Events.STREAM_TEARDOWN_COMPLETE); + resetInitialSettings(); + return config; + } + function reset() { _checkConfig(); @@ -1688,6 +1739,7 @@ function StreamController() { loadWithManifest, refreshManifest, reset, + resetAlt, setConfig, setProtectionData, switchToVideoElement, From 5f340c17e14b225b55132e5d21c95a50c55110a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Caballero?= Date: Tue, 12 Nov 2024 19:11:20 -0300 Subject: [PATCH 003/175] restore bytes to original mediasource --- samples/develop.html | 5 +- src/streaming/MediaPlayer.js | 38 ++++++------ src/streaming/SourceBufferSink.js | 21 +++++++ src/streaming/StreamProcessor.js | 15 +++++ src/streaming/controllers/BufferController.js | 10 +++ .../controllers/MediaSourceController.js | 10 +++ src/streaming/controllers/StreamController.js | 61 +++++++++++++++++++ 7 files changed, 136 insertions(+), 24 deletions(-) diff --git a/samples/develop.html b/samples/develop.html index c4a2ce9e75..5f8a6b7201 100644 --- a/samples/develop.html +++ b/samples/develop.html @@ -21,7 +21,7 @@
- +
@@ -48,7 +48,7 @@ player, // url = "https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/ListMPDs/case1.mpd"; url = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' - // url = "http://localhost:3000/stream.mpd" + // url = "https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/TestAssets/MediaOfflineErrorAsset/stream.mpd" video = document.querySelector("video"); player = dashjs.MediaPlayer().create(); @@ -67,7 +67,6 @@ // }, // }); player.initialize(video, url, true); - player.setAlternativePlayer() setTimeout(()=> { // player.attachView(video); diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index e408622bdb..29b4b2e271 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -596,7 +596,7 @@ function MediaPlayer() { } if (source) { const playbackTime = time ? time : providedStartTime; - console.log(playbackTime) + console.log('Preload initialized!') _initializePlayback(playbackTime); } else { throw SOURCE_NOT_ATTACHED_ERROR; @@ -1461,8 +1461,6 @@ function MediaPlayer() { if (playbackInitialized) { //Reset if we have been playing before, so this is a new element. _resetPlaybackControllers(); } - console.log(providedStartTime); - _initializePlayback(providedStartTime); } @@ -1488,8 +1486,6 @@ function MediaPlayer() { if (playbackInitialized) { //Reset if we have been playing before, so this is a new element. _resetPlaybackControllers(); } - console.log(providedStartTime); - _initializePlayback(providedStartTime); } @@ -2365,11 +2361,6 @@ function MediaPlayer() { // PRIVATE METHODS //*********************************** - function _resetControllers() { - const streams = streamController.resetAlt(); - return streams; - } - function _resetPlaybackControllers() { playbackInitialized = false; streamingInitialized = false; @@ -2776,8 +2767,8 @@ function MediaPlayer() { const mediaPlayerFactory = FactoryMaker.getClassFactory(MediaPlayer); alternativePlayer = mediaPlayerFactory().create() // const alternativeUrl = 'https://livesim2.dashif.org/livesim2/scte35_2/testpic_2s/Manifest.mpd'; - const alternativeUrl = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' - // const alternativeUrl = 'http://localhost:3000/stream.mpd' + // const alternativeUrl = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' + const alternativeUrl = 'https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/TestAssets/MediaOfflineErrorAsset/stream.mpd' alternativePlayer.initialize(null, alternativeUrl, false); alternativePlayer.updateSettings({ // debug: {logLevel: 5}, @@ -2788,26 +2779,31 @@ function MediaPlayer() { async function switchView() { const video = videoModel.getElement(); - // const alternativeVideo = alternativePlayer.getVideoElement() pause() - // attachView(null); updateSettings({ // debug: {logLevel: 5}, streaming: {cacheInitSegments: true} }); - // const currentTime = time() - // providedStartTime = 5; - // preload() - const streams = _resetControllers(); + const streamConfig = streamController.getConfig() + + let time = getVideoElement().currentTime; + const savedbuffers = streamController.getBufferBackup() + console.log('the data to restore is', savedbuffers) + console.log('current time is', time) alternativePlayer.attachView(video) alternativePlayer.play() setTimeout(async () => { await alternativePlayer.attachView(null) - _attachViewAlt(video, streams); + _attachViewAlt(video, streamConfig); + + setTimeout(() => { + streamController.restoreBuffer(savedbuffers) + play() + // seek(time); + }, 100) alternativePlayer.destroy(); alternativePlayer = null; - play(); - }, 7000) + }, 5000) } diff --git a/src/streaming/SourceBufferSink.js b/src/streaming/SourceBufferSink.js index 2fb5a5123a..e306b17d47 100644 --- a/src/streaming/SourceBufferSink.js +++ b/src/streaming/SourceBufferSink.js @@ -271,6 +271,24 @@ function SourceBufferSink(config) { } } + let savedSegments = []; + + function restoreSavedBuffer(segments) { + if (segments.length > 0) { + segments.forEach(chunk => { + appendQueue.push({ data: chunk, promise: {resolve: ()=>{}, reject: ()=>{}}}); + _waitForUpdateEnd(_appendNextInQueue.bind(this)); + }); + } else { + console.error('No saved buffer to restore'); + } + savedSegments = []; + } + + function getSavedBuffer(){ + return savedSegments; + } + function append(chunk, request = null) { return new Promise((resolve, reject) => { if (!chunk) { @@ -280,6 +298,7 @@ function SourceBufferSink(config) { }); return; } + savedSegments.push({ ...chunk }); appendQueue.push({ data: chunk, promise: { resolve, reject }, request }); _waitForUpdateEnd(_appendNextInQueue.bind(this)); }); @@ -481,6 +500,8 @@ function SourceBufferSink(config) { reset, updateAppendWindow, updateTimestampOffset, + restoreSavedBuffer, + getSavedBuffer, }; setup(); diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index d046d0cf22..a09d7d39e9 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -206,6 +206,10 @@ function StreamProcessor(config) { return type; } + function getMimeType() { + return mimeType; + } + function resetInitialSettings() { mediaInfoArr = []; currentMediaInfo = null; @@ -1068,6 +1072,14 @@ function StreamProcessor(config) { return bufferController ? bufferController.getBuffer() : null; } + function getSavedBuffer() { + return bufferController.getSavedBuffer(); + } + + function restoreBuffer(buffer){ + bufferController.restoreBuffer(buffer); + } + function getBufferController() { return bufferController; } @@ -1521,6 +1533,9 @@ function StreamProcessor(config) { setMediaSource, setTrackSwitchInProgress, updateStreamInfo, + getMimeType, + getSavedBuffer, + restoreBuffer, }; setup(); diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index e76ce62a76..1b0ee78b47 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -1169,6 +1169,14 @@ function BufferController(config) { return isPruningInProgress; } + function restoreBuffer(buffer) { + sourceBufferSink.restoreSavedBuffer(buffer); + } + + function getSavedBuffer(){ + return sourceBufferSink.getSavedBuffer() + } + function getTotalBufferedTime() { try { const ranges = sourceBufferSink.getAllBufferRanges(); @@ -1321,6 +1329,8 @@ function BufferController(config) { setSeekTarget, updateAppendWindow, updateBufferTimestampOffset, + restoreBuffer, + getSavedBuffer, }; setup(); diff --git a/src/streaming/controllers/MediaSourceController.js b/src/streaming/controllers/MediaSourceController.js index fbf6a5fa62..1cad0ac709 100644 --- a/src/streaming/controllers/MediaSourceController.js +++ b/src/streaming/controllers/MediaSourceController.js @@ -167,10 +167,20 @@ function MediaSourceController() { } } + function getDuration(){ + return mediaSource.duration; + } + + function getSourceBuffers(){ + return mediaSource.sourceBuffers; + } + instance = { attachMediaSource, createMediaSource, detachMediaSource, + getDuration, + getSourceBuffers, setConfig, setDuration, setSeekable, diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index cbcfe6ddae..3338e644e1 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -190,6 +190,7 @@ function StreamController() { * @param {number} startTime */ function load(url, startTime = NaN) { + console.log('Streamcontroller - load') _checkConfig(); providedStartTime = startTime; manifestLoader.load(url); @@ -1501,6 +1502,63 @@ function StreamController() { } } + function restoreBuffer(data){ + activeStream.getStreamProcessors()[0].restoreBuffer(data.videoBuffer) + activeStream.getStreamProcessors()[1].restoreBuffer(data.audioBuffer) + } + + function getBufferBackup(){ + + console.log('video buffer is', activeStream.getStreamProcessors()[0].getBuffer().getType()) + console.log('audio buffer is', activeStream.getStreamProcessors()[1].getBuffer().getType()) + + const videoBuffer = activeStream.getStreamProcessors()[0].getSavedBuffer() + const audioBuffer = activeStream.getStreamProcessors()[1].getSavedBuffer() + + return { + videoBuffer, + audioBuffer, + }; + + // console.log("MediaSource - duration", mediaSourceController.getDuration()) + // console.log("MediaSoruce - buffers", mediaSourceController.getSourceBuffers()) + // console.log("StreamProcessors", activeStream.getStreamProcessors()) + // console.log("StreamProcessor 0 - type:", activeStream.getStreamProcessors()[0].getType()) + // console.log("StreamProcessor 1 - type:", activeStream.getStreamProcessors()[1].getType()) + // console.log("SourceBufferSink 0 - buffer:", activeStream.getStreamProcessors()[0].getBuffer().getBuffer()) + // console.log("SourceBufferSink 1 - buffer:", activeStream.getStreamProcessors()[1].getBuffer().getBuffer()) + } + + function getConfig(){ + return { + streams, + capabilities, + capabilitiesFilter, + manifestLoader, + manifestModel, + mediaPlayerModel, + customParametersModel, + protectionController, + adapter, + dashMetrics, + errHandler, + timelineConverter, + videoModel, + playbackController, + throughputController, + serviceDescriptionController, + contentSteeringController, + textController, + abrController, + mediaController, + settings, + baseURLController, + uriFragmentModel, + segmentBaseController, + manifestUpdater, + } + } + function setConfig(config) { if (!config) { return; @@ -1740,9 +1798,12 @@ function StreamController() { refreshManifest, reset, resetAlt, + getConfig, setConfig, setProtectionData, switchToVideoElement, + restoreBuffer, + getBufferBackup, }; setup(); From b7b48277b32a34b2063bfd33fe167e5c48d75388 Mon Sep 17 00:00:00 2001 From: mhargain Date: Thu, 5 Dec 2024 09:24:49 -0300 Subject: [PATCH 004/175] AlternativeMpd: Two video tag approach --- samples/develop.html | 11 +- src/core/events/CoreEvents.js | 1 + src/dash/constants/DashConstants.js | 2 + src/dash/models/DashManifestModel.js | 18 + src/dash/vo/AlternativeMpd.js | 48 +++ src/dash/vo/Event.js | 3 +- src/streaming/ManifestLoader.js | 11 +- src/streaming/MediaPlayer.js | 25 +- src/streaming/MediaPlayerEvents.js | 5 + .../controllers/AlternativeMpdController.js | 366 ++++++++++++++++++ 10 files changed, 479 insertions(+), 11 deletions(-) create mode 100644 src/dash/vo/AlternativeMpd.js create mode 100644 src/streaming/controllers/AlternativeMpdController.js diff --git a/samples/develop.html b/samples/develop.html index 5f8a6b7201..d7dc55ad34 100644 --- a/samples/develop.html +++ b/samples/develop.html @@ -47,8 +47,9 @@ var video, player, // url = "https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/ListMPDs/case1.mpd"; - url = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' + // url = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' // url = "https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/TestAssets/MediaOfflineErrorAsset/stream.mpd" + url = 'http://localhost:3030/manifest.mpd' video = document.querySelector("video"); player = dashjs.MediaPlayer().create(); @@ -67,11 +68,11 @@ // }, // }); player.initialize(video, url, true); - player.setAlternativePlayer() - setTimeout(()=> { + // player.setAlternativePlayer() + //setTimeout(()=> { // player.attachView(video); - player.switchView() - }, '5000') + // player.switchView() + //}, '5000') }); diff --git a/src/core/events/CoreEvents.js b/src/core/events/CoreEvents.js index 8bffb0aa56..dc8ff69ac3 100644 --- a/src/core/events/CoreEvents.js +++ b/src/core/events/CoreEvents.js @@ -54,6 +54,7 @@ class CoreEvents extends EventsBase { this.INIT_FRAGMENT_LOADED = 'initFragmentLoaded'; this.INIT_FRAGMENT_NEEDED = 'initFragmentNeeded'; this.INTERNAL_MANIFEST_LOADED = 'internalManifestLoaded'; + this.ORIGINAL_ALTERNATIVE_MANIFEST_LOADED = 'internalAlternativeManifestLoaded' this.ORIGINAL_MANIFEST_LOADED = 'originalManifestLoaded'; this.LOADING_COMPLETED = 'loadingCompleted'; this.LOADING_PROGRESS = 'loadingProgress'; diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index 347e33f2c3..77bfa880cb 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -39,6 +39,8 @@ export default { ADAPTATION_SETS: 'adaptationSets', ADAPTATION_SET_SWITCHING_SCHEME_ID_URI: 'urn:mpeg:dash:adaptation-set-switching:2016', ADD: 'add', + ALTERNATIVE_MPD: 'AlternativeMPD', + ALTERNATIVE_MPD_SCHEME_ID: 'urn:mpeg:dash:event:alternativeMPD:2022', ASSET_IDENTIFIER: 'AssetIdentifier', AUDIO_CHANNEL_CONFIGURATION: 'AudioChannelConfiguration', AUDIO_SAMPLING_RATE: 'audioSamplingRate', diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index e67615f5ef..72f06fc507 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -50,6 +50,7 @@ import PatchLocation from '../vo/PatchLocation.js'; import Period from '../vo/Period.js'; import ProducerReferenceTime from '../vo/ProducerReferenceTime.js'; import Representation from '../vo/Representation.js'; +import AlternativeMpd from '../vo/AlternativeMpd.js'; import URLUtils from '../../streaming/utils/URLUtils.js'; import UTCTiming from '../vo/UTCTiming.js'; import Utils from '../../core/Utils.js'; @@ -1062,6 +1063,13 @@ function DashManifestModel() { event.id = null; } + if (currentMpdEvent.hasOwnProperty(DashConstants.ALTERNATIVE_MPD)) { + event.alternativeMpd = getAlternativeMpd(currentMpdEvent.AlternativeMPD); + event.calculatedPresentationTime = event.calculatedPresentationTime - event.alternativeMpd.earliestResolutionTimeOffset; + } else { + event.alternativeMpd = null; + } + if (currentMpdEvent.Signal && currentMpdEvent.Signal.Binary) { // toString is used to manage both regular and namespaced tags event.messageData = BASE64.decodeArray(currentMpdEvent.Signal.Binary.toString()); @@ -1084,6 +1092,16 @@ function DashManifestModel() { return events; } + function getAlternativeMpd(event) { + const alternativeMpd = new AlternativeMpd(); + alternativeMpd.uri = event.uri ?? null; + alternativeMpd.earliestResolutionTimeOffset = event.earliestResolutionTimeOffset ?? null; + alternativeMpd.mode = event.mode ?? null; + alternativeMpd.disableJumpTimeOffest = event.disableJumpTimeOffest ?? null; + alternativeMpd.playTimes = event.playTimes ?? null; + return alternativeMpd; + } + function getEventStreams(inbandStreams, representation, period) { const eventStreams = []; let i; diff --git a/src/dash/vo/AlternativeMpd.js b/src/dash/vo/AlternativeMpd.js new file mode 100644 index 0000000000..10f5933dc6 --- /dev/null +++ b/src/dash/vo/AlternativeMpd.js @@ -0,0 +1,48 @@ +/** + * 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. + */ +/** + * @class + * @ignore + */ + +class AlternativeMpd { + constructor() { + this.uri = ''; + this.earliestResolutionTimeOffset = NaN; + this.mode = ''; + this.disableJumpTimeOffest = NaN; + this.playTimes = ''; + this.presentationTime = NaN; + this.duration = NaN + } +} + +export default AlternativeMpd; diff --git a/src/dash/vo/Event.js b/src/dash/vo/Event.js index 28e40c7eba..234007091d 100644 --- a/src/dash/vo/Event.js +++ b/src/dash/vo/Event.js @@ -42,7 +42,8 @@ class Event { this.eventStream = null; this.presentationTimeDelta = NaN; // Specific EMSG Box parameter this.parsedMessageData = null; // Parsed value of the event message + this.alternativeMpd = null; } } -export default Event; \ No newline at end of file +export default Event; diff --git a/src/streaming/ManifestLoader.js b/src/streaming/ManifestLoader.js index 94b69b6876..86cf001a11 100644 --- a/src/streaming/ManifestLoader.js +++ b/src/streaming/ManifestLoader.js @@ -107,7 +107,7 @@ function ManifestLoader(config) { } } - function load(url, serviceLocation = null, queryParams = null) { + function load(url, serviceLocation = null, queryParams = null, alternative = false) { const requestStartDate = new Date(); const request = new TextRequest(url, HTTPRequest.MPD_TYPE); @@ -222,11 +222,18 @@ function ManifestLoader(config) { } } + // This should be done recursively until every alternative is parsed + manifest.baseUri = baseUri; manifest.loadedTime = new Date(); xlinkController.resolveManifestOnLoad(manifest); - eventBus.trigger(Events.ORIGINAL_MANIFEST_LOADED, { originalManifest: data }); + if (alternative) { + eventBus.trigger(Events.ORIGINAL_ALTERNATIVE_MANIFEST_LOADED, { manifest: data }); + } else { + eventBus.trigger(Events.ORIGINAL_MANIFEST_LOADED, { originalManifest: data }); + } + } else { eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, { manifest: null, diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 29b4b2e271..67ac71c5d4 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -31,6 +31,7 @@ import {Cta608Parser} from '@svta/common-media-library/cta/608/Cta608Parser'; import Constants from './constants/Constants.js'; import DashConstants from '../dash/constants/DashConstants.js'; +import AlternativeMpdController from './controllers/AlternativeMpdController.js'; import MetricsConstants from './constants/MetricsConstants.js'; import PlaybackController from './controllers/PlaybackController.js'; import StreamController from './controllers/StreamController.js'; @@ -143,6 +144,7 @@ function MediaPlayer() { throughputController, schemeLoaderFactory, timelineConverter, + alternativeMpdController, mediaController, protectionController, metricsReportingController, @@ -225,6 +227,9 @@ function MediaPlayer() { if (config.gapController) { gapController = config.gapController; } + if (config.alternativeMpdController) { + alternativeMpdController = config.alternativeMpdController; + } if (config.throughputController) { throughputController = config.throughputController } @@ -287,7 +292,7 @@ function MediaPlayer() { * @memberof module:MediaPlayer * @instance */ - function initialize(view, source, autoPlay,startTime = NaN) { + function initialize(view, source, autoPlay, startTime = NaN, alternativeContext = null) { if (!capabilities) { capabilities = Capabilities(context).getInstance(); capabilities.setConfig({ @@ -321,6 +326,10 @@ function MediaPlayer() { schemeLoaderFactory = SchemeLoaderFactory(context).getInstance(); } + if (!alternativeMpdController) { + alternativeMpdController = AlternativeMpdController(alternativeContext ? alternativeContext : context).getInstance(); + } + if (!playbackController) { playbackController = PlaybackController(context).getInstance(); } @@ -391,6 +400,15 @@ function MediaPlayer() { adapter }); + alternativeMpdController.setConfig({ + videoModel, + manifestModel, + DashConstants, + mediaPlayerFactory: FactoryMaker.getClassFactory(MediaPlayer)(), + playbackController, + alternativeContext: context + }); + if (!segmentBaseController) { segmentBaseController = SegmentBaseController(context).getInstance({ dashMetrics: dashMetrics, @@ -2513,6 +2531,7 @@ function MediaPlayer() { textController.initialize(); gapController.initialize(); catchupController.initialize(); + alternativeMpdController.initialize(); cmcdModel.initialize(); cmsdModel.initialize(); contentSteeringController.initialize(); @@ -2785,7 +2804,7 @@ function MediaPlayer() { streaming: {cacheInitSegments: true} }); const streamConfig = streamController.getConfig() - + let time = getVideoElement().currentTime; const savedbuffers = streamController.getBufferBackup() console.log('the data to restore is', savedbuffers) @@ -2795,7 +2814,7 @@ function MediaPlayer() { setTimeout(async () => { await alternativePlayer.attachView(null) _attachViewAlt(video, streamConfig); - + setTimeout(() => { streamController.restoreBuffer(savedbuffers) play() diff --git a/src/streaming/MediaPlayerEvents.js b/src/streaming/MediaPlayerEvents.js index c6b39b6ee3..1a02b6c65a 100644 --- a/src/streaming/MediaPlayerEvents.js +++ b/src/streaming/MediaPlayerEvents.js @@ -156,6 +156,11 @@ class MediaPlayerEvents extends EventsBase { * @event MediaPlayerEvents#MANIFEST_LOADED */ this.MANIFEST_LOADED = 'manifestLoaded'; + /** + * Triggered when TBD + * @event MediaPlayerEvents#MANIFEST_LOADED + */ + this.ALTERNATIVE_MANIFEST_LOADED = 'alternativeManifestLoaded' /** * Triggered anytime there is a change to the overall metrics. diff --git a/src/streaming/controllers/AlternativeMpdController.js b/src/streaming/controllers/AlternativeMpdController.js new file mode 100644 index 0000000000..f3021ed7d5 --- /dev/null +++ b/src/streaming/controllers/AlternativeMpdController.js @@ -0,0 +1,366 @@ +/** + * 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 MediaPlayerEvents from '../MediaPlayerEvents.js'; +import MediaPlayer from '../MediaPlayer.js'; +import EventBus from './../../core/EventBus.js'; +import FactoryMaker from '../../core/FactoryMaker.js'; + +/* +TODOS: +-> Prebuffering could be done in a queue style, where at initialization you preload the first one, when that is done you preload the following, etc + -> This is specially good for the timeupdate scheduling approach +-> I should save the last mainPlayer time reference in order to search current event while in alternative and to easily calculate time back +-> When going to the alternative you could seek into the content taking into account the duration of the alternative or go back to the time before the alternative +*/ + +function AlternativeMpdController() { + + const context = this.context; + const eventBus = EventBus(context).getInstance(); + + let instance, + dashConstants, + // manifestModel, + scheduledEvents = [], + eventTimeouts = [], + videoModel, + currentEvent = null, + isSwitching = false, + hideAlternativePlayerControls = true, + altPlayer, + useDashEventsForScheduling = true, + playbackController, + altVideoElement, + alternativeContext, + lastTimestamp = 0; + + function setConfig(config) { + if (!config) { + return; + } + + if (!videoModel) { + videoModel = config.videoModel; + } + // manifestModel = config.manifestModel; + dashConstants = config.DashConstants; + + // if (!altPlayer) { + // let mediaPlayerFactory = config.mediaPlayerFactory; + // altPlayer = mediaPlayerFactory.create(); + // } + + if (!!config.playbackController && !playbackController) { + playbackController = config.playbackController; + } + + if (!!config.hideAlternativePlayerControls && !hideAlternativePlayerControls) { + hideAlternativePlayerControls = config.hideAlternativePlayerControls; + } + + if (!!config.useDashEventsForScheduling && !useDashEventsForScheduling) { + useDashEventsForScheduling = config.useDashEventsForScheduling; + } + + if (!!config.alternativeContext && !alternativeContext) { + alternativeContext = config.alternativeContext + } + + // if (!!config.lastTimestamp) { + // lastTimestamp = config.lastTimestamp || 0; + // } + + if (!!config.currentEvent && !currentEvent) { + currentEvent = config.currentEvent; + } + } + + function initialize() { + eventBus.on(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); + if (altPlayer) { + altPlayer.on(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); + } + + if (useDashEventsForScheduling) { + _startDashEventPlaybackTimeMonitoring(); + } + } + + function _onManifestLoaded(e) { + console.log('I\'m about to experience brain damange') + console.log(e) + const manifest = e.data; + const events = _parseAlternativeMPDEvents(manifest) + if (scheduledEvents && scheduledEvents.length > 0) { + scheduledEvents.push(events) + } else { + scheduledEvents = events + } + if (!useDashEventsForScheduling) { + _scheduleAlternativeMPDEvents(); + } + } + + function _startDashEventPlaybackTimeMonitoring() { + eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onDashPlaybackTimeUpdated, this); + if (altPlayer) { + altPlayer.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onDashPlaybackTimeUpdated, this); + } + } + + function _onDashPlaybackTimeUpdated(e) { + try { + if (e.streamId == 'defaultId_0') { + const currentTime = e.time; + lastTimestamp = e.time; + console.log(`Time elapsed for main player ${e.time}`); + + const event = _getCurrentEvent(currentTime); + + if (event && !isSwitching && !currentEvent) { + _switchToAlternativeContent(event); + } else if (!event && !isSwitching && currentEvent) { + _switchBackToMainContent(currentEvent); + } + } else { + console.log('I\'m not being played during main content'); + console.log(lastTimestamp); + console.log(`Time elapsed for alternative player ${e.time}`); + // Check of end of event and dispatch switch to mainPlayer + if (currentEvent && currentEvent.duration <= e.time) { + _switchBackToMainContent(currentEvent); + } + } + } catch (err) { + console.error('Error in onDashPlaybackTimeUpdated:', err); + } + } + + function _getCurrentEvent(currentTime) { + return scheduledEvents.find(event => { + return currentTime >= event.presentationTime && + currentTime < event.presentationTime + event.duration; + }); + } + + function _initializeAlternativePlayer(event) { + if (altVideoElement && altPlayer) { return }; + + // Create a new video element for the alternative content + altVideoElement = document.createElement('video'); + altVideoElement.style.display = 'none'; // Hide the alternative video element initially + altVideoElement.autoplay = false; + altVideoElement.controls = !hideAlternativePlayerControls; + + // Insert the alternative video element into the DOM + videoModel.getElement().parentNode.insertBefore(altVideoElement, videoModel.getElement().nextSibling); + + // Initialize alternative player + altPlayer = MediaPlayer().create(); + altPlayer.initialize(altVideoElement, event.alternativeMPD.uri, false, NaN, alternativeContext); + altPlayer.setAutoPlay(false); + + altPlayer.on(Events.ERROR, (e) => { + console.error('Alternative player error:', e); + }, this); + } + + function _parseAlternativeMPDEvents(manifest) { + // This should not be done instead using DashManifestModel and DashAdaptor + const events = []; + const periods = manifest.Period || []; + + periods.forEach(period => { + const eventStreams = period.EventStream || []; + eventStreams.forEach(eventStream => { + if (eventStream.schemeIdUri === dashConstants.ALTERNATIVE_MPD_SCHEME_ID) { + const timescale = eventStream.timescale || 1; + const eventsArray = eventStream.Event || []; + eventsArray.forEach(event => { + if (event && event.AlternativeMPD) { + const alternativeMPDNode = event.AlternativeMPD; + events.push({ + presentationTime: event.presentationTime / timescale, + duration: event.duration / timescale, + alternativeMPD: { + uri: alternativeMPDNode.uri, + earliestResolutionTimeOffset: parseInt(alternativeMPDNode.earliestResolutionTimeOffset || '0', 10) / 1000, // Convert to seconds + }, + triggered: false, + }); + } + }); + } + }); + }); + + return events; + } + + function _scheduleAlternativeMPDEvents() { + scheduledEvents.forEach(event => { + const currentTime = videoModel.getElement().currentTime; + const timeToEvent = event.presentationTime - currentTime; + const timeToPrebuffer = (event.presentationTime - event.alternativeMPD.earliestResolutionTimeOffset) - currentTime; + + if (timeToPrebuffer >= 0) { + const prebufferTimeoutId = setTimeout(() => { + _prebufferAlternativeContent(event); + }, timeToPrebuffer * 1000); + + eventTimeouts.push(prebufferTimeoutId); + } else { + _prebufferAlternativeContent(event); + } + + if (timeToEvent >= 0) { + const switchToAltTimeoutId = setTimeout(() => { + _switchToAlternativeContent(event); + }, timeToEvent * 1000); + + eventTimeouts.push(switchToAltTimeoutId); + + const switchToMainTimeoutId = setTimeout(() => { + _switchBackToMainContent(event); + }, (timeToEvent + event.duration) * 1000); + + eventTimeouts.push(switchToMainTimeoutId); + } else if (currentTime < event.presentationTime + event.duration) { + _switchToAlternativeContent(event); + + const timeLeft = (event.presentationTime + event.duration - currentTime) * 1000; + const switchToMainTimeoutId = setTimeout(() => { + _switchBackToMainContent(event); + }, timeLeft); + + eventTimeouts.push(switchToMainTimeoutId); + } + }); + } + + function _prebufferAlternativeContent(event) { + if (event.triggered) { return }; + event.triggered = true; + + _initializeAlternativePlayer(event); + + altPlayer.on(Events.STREAM_INITIALIZED, () => { + console.log('I\'m buffering') + // altPlayer.seek(event.earliestResolutionTimeOffset); + // Do not play yet, just buffer + }, this); + } + + function _switchToAlternativeContent(event) { + if (isSwitching) { return }; + isSwitching = true; + currentEvent = {...event, triggered: true}; + + if (!altVideoElement || !altPlayer) { + _initializeAlternativePlayer(event); + } + + videoModel.pause(); + + videoModel.getElement().style.display = 'none'; + altVideoElement.style.display = 'block'; + + altPlayer.play(); + + isSwitching = false; + } + + function _switchBackToMainContent(event) { + if (isSwitching) { return }; + isSwitching = true; + + altPlayer.pause(); + + altVideoElement.style.display = 'none'; + videoModel.getElement().style.display = 'block'; + + // If we implement more depth juggling this is necessary + // @TODO: This seek should be changed according to the mode + const returnTime = event.presentationTime + event.duration; + playbackController.seek(returnTime, false, false); + console.log(event); + videoModel.play(); + + altPlayer.reset(); + altPlayer = null; + + altVideoElement.parentNode.removeChild(altVideoElement); + altVideoElement = null; + + isSwitching = false; + currentEvent = null; + } + + + function reset() { + scheduledEvents = []; + eventTimeouts.forEach(timeoutId => clearTimeout(timeoutId)); + eventTimeouts = []; + + if (altPlayer) { + altPlayer.reset(); + altPlayer = null; + } + + if (altVideoElement) { + altVideoElement.parentNode.removeChild(altVideoElement); + altVideoElement = null; + } + + if (useDashEventsForScheduling && videoModel) { + videoModel.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onDashPlaybackTimeUpdated, this); + } + + isSwitching = false; + currentEvent = null; + + eventBus.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); + } + + instance = { + setConfig, + initialize, + reset + }; + + return instance; +} + +AlternativeMpdController.__dashjs_factory_name = 'AlternativeMpdController'; +const factory = FactoryMaker.getSingletonFactory(AlternativeMpdController); +FactoryMaker.updateSingletonFactory(AlternativeMpdController.__dashjs_factory_name, factory); +export default factory; From 75c6f94692be416deb94bdcfafcad5520f9ff5b8 Mon Sep 17 00:00:00 2001 From: mhargain Date: Wed, 18 Dec 2024 12:38:26 -0300 Subject: [PATCH 005/175] @AlternativeMpd: modes, returnOffset and mixin added --- samples/demo.html | 268 ++++++++++++++++++ samples/demo_bu.html | 228 +++++++++++++++ samples/develop.html | 26 +- src/core/events/CoreEvents.js | 2 +- src/dash/models/DashManifestModel.js | 6 +- src/dash/vo/AlternativeMpd.js | 3 +- src/streaming/MediaPlayer.js | 15 - src/streaming/constants/Constants.js | 7 + .../controllers/AlternativeMpdController.js | 217 ++++++++++---- src/streaming/controllers/EventController.js | 1 + 10 files changed, 675 insertions(+), 98 deletions(-) create mode 100644 samples/demo.html create mode 100644 samples/demo_bu.html diff --git a/samples/demo.html b/samples/demo.html new file mode 100644 index 0000000000..5ae2a761ab --- /dev/null +++ b/samples/demo.html @@ -0,0 +1,268 @@ + + + + + Alternative Config + + + + + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
URI
+
presentationTime(ms)
+
duration(ms)
+
eRTO(ms)
+
mode(insert/replace)
+
returnOffset(ms)
+
+
+
+
+ +
+
+
+
+ +
+
+
+ © DASH-IF +
+
+
+ + + + diff --git a/samples/demo_bu.html b/samples/demo_bu.html new file mode 100644 index 0000000000..8b3bdceb9d --- /dev/null +++ b/samples/demo_bu.html @@ -0,0 +1,228 @@ + + + + + Alternative Config + + + + + + + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
URI
+
presentationTime(ms)
+
duration(ms)
+
eRTIO(ms)
+
mode(insert/replace)
+
returnOffset(ms)
+
+
+
+
+ +
+
+
+
+ +
+
+
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/develop.html b/samples/develop.html index d7dc55ad34..6348d60527 100644 --- a/samples/develop.html +++ b/samples/develop.html @@ -46,33 +46,13 @@ document.addEventListener("DOMContentLoaded", function () { var video, player, - // url = "https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/ListMPDs/case1.mpd"; - // url = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' - // url = "https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/TestAssets/MediaOfflineErrorAsset/stream.mpd" - url = 'http://localhost:3030/manifest.mpd' + url = 'http://localhost:3030/manifest.mpd?alt=https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/TestAssets/MediaOfflineErrorAsset/stream.mpd|10000|10000|2000|insert|0&alt=https://media.axprod.net/TestVectors/v7-Clear/Manifest_1080p.mpd|40000|13000|5000|replace|5000&alt=https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/TestAssets/MediaOfflineErrorAsset/stream.mpd|80000|10000|2000|replace|0' + video = document.querySelector("video"); player = dashjs.MediaPlayer().create(); - // player.updateSettings({ - // streaming: { - // cmcd: { - // enabled: false, - // mode: "header", - // enabledKeys: ['br', 'd', 'ot', 'tb' , 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su' , 'bs', 'rtp' , 'cid', 'pr', 'sf', 'sid', 'st', 'v', 'msd', 'ltc'], - // reporting: { - // requestMode: { - // version: 2 - // } - // } - // }, - // }, - // }); + player.initialize(video, url, true); - // player.setAlternativePlayer() - //setTimeout(()=> { - // player.attachView(video); - // player.switchView() - //}, '5000') }); diff --git a/src/core/events/CoreEvents.js b/src/core/events/CoreEvents.js index dc8ff69ac3..5148e263dc 100644 --- a/src/core/events/CoreEvents.js +++ b/src/core/events/CoreEvents.js @@ -54,7 +54,7 @@ class CoreEvents extends EventsBase { this.INIT_FRAGMENT_LOADED = 'initFragmentLoaded'; this.INIT_FRAGMENT_NEEDED = 'initFragmentNeeded'; this.INTERNAL_MANIFEST_LOADED = 'internalManifestLoaded'; - this.ORIGINAL_ALTERNATIVE_MANIFEST_LOADED = 'internalAlternativeManifestLoaded' + this.ORIGINAL_ALTERNATIVE_MANIFEST_LOADED = 'originalAlternativeManifestLoaded' this.ORIGINAL_MANIFEST_LOADED = 'originalManifestLoaded'; this.LOADING_COMPLETED = 'loadingCompleted'; this.LOADING_PROGRESS = 'loadingProgress'; diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 72f06fc507..c70fac7fd1 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -1065,7 +1065,7 @@ function DashManifestModel() { if (currentMpdEvent.hasOwnProperty(DashConstants.ALTERNATIVE_MPD)) { event.alternativeMpd = getAlternativeMpd(currentMpdEvent.AlternativeMPD); - event.calculatedPresentationTime = event.calculatedPresentationTime - event.alternativeMpd.earliestResolutionTimeOffset; + event.calculatedPresentationTime = 0; } else { event.alternativeMpd = null; } @@ -1095,10 +1095,12 @@ function DashManifestModel() { function getAlternativeMpd(event) { const alternativeMpd = new AlternativeMpd(); alternativeMpd.uri = event.uri ?? null; - alternativeMpd.earliestResolutionTimeOffset = event.earliestResolutionTimeOffset ?? null; + alternativeMpd.duration = event.duration ?? null; + alternativeMpd.earliestResolutionTimeOffset = event.earliestResolutionTimeOffset / 1000 ?? null; alternativeMpd.mode = event.mode ?? null; alternativeMpd.disableJumpTimeOffest = event.disableJumpTimeOffest ?? null; alternativeMpd.playTimes = event.playTimes ?? null; + alternativeMpd.returnOffset = event.returnOffset ?? null; return alternativeMpd; } diff --git a/src/dash/vo/AlternativeMpd.js b/src/dash/vo/AlternativeMpd.js index 10f5933dc6..515f56df14 100644 --- a/src/dash/vo/AlternativeMpd.js +++ b/src/dash/vo/AlternativeMpd.js @@ -41,7 +41,8 @@ class AlternativeMpd { this.disableJumpTimeOffest = NaN; this.playTimes = ''; this.presentationTime = NaN; - this.duration = NaN + this.duration = NaN; + this.returnOffset = NaN; } } diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 67ac71c5d4..dc2bb92ed1 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -2782,20 +2782,6 @@ function MediaPlayer() { } } - function setAlternativePlayer() { - const mediaPlayerFactory = FactoryMaker.getClassFactory(MediaPlayer); - alternativePlayer = mediaPlayerFactory().create() - // const alternativeUrl = 'https://livesim2.dashif.org/livesim2/scte35_2/testpic_2s/Manifest.mpd'; - // const alternativeUrl = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' - const alternativeUrl = 'https://comcast-dash-6-assets.s3.us-east-2.amazonaws.com/TestAssets/MediaOfflineErrorAsset/stream.mpd' - alternativePlayer.initialize(null, alternativeUrl, false); - alternativePlayer.updateSettings({ - // debug: {logLevel: 5}, - streaming: {cacheInitSegments: true} - }); - alternativePlayer.preload() - } - async function switchView() { const video = videoModel.getElement(); pause() @@ -2909,7 +2895,6 @@ function MediaPlayer() { seek, seekToOriginalLive, seekToPresentationTime, - setAlternativePlayer, setAutoPlay, setConfig, setCurrentTrack, diff --git a/src/streaming/constants/Constants.js b/src/streaming/constants/Constants.js index 5ba4ba19e9..d938857d17 100644 --- a/src/streaming/constants/Constants.js +++ b/src/streaming/constants/Constants.js @@ -323,6 +323,13 @@ export default { ABANDON_FRAGMENT_RULES: { ABANDON_REQUEST_RULE: 'AbandonRequestsRule' }, + ALTERNATIVE_MPD: { + MODES: { + REPLACE: 'replace', + INSERT: 'insert' + }, + URI: 'urn:mpeg:dash:event:alternativeMPD:2022' + }, /** * @constant {string} ID3_SCHEME_ID_URI specifies scheme ID URI for ID3 timed metadata diff --git a/src/streaming/controllers/AlternativeMpdController.js b/src/streaming/controllers/AlternativeMpdController.js index f3021ed7d5..81b456f306 100644 --- a/src/streaming/controllers/AlternativeMpdController.js +++ b/src/streaming/controllers/AlternativeMpdController.js @@ -33,6 +33,7 @@ import MediaPlayerEvents from '../MediaPlayerEvents.js'; import MediaPlayer from '../MediaPlayer.js'; import EventBus from './../../core/EventBus.js'; import FactoryMaker from '../../core/FactoryMaker.js'; +import Constants from '../constants/Constants.js'; /* TODOS: @@ -49,18 +50,20 @@ function AlternativeMpdController() { let instance, dashConstants, - // manifestModel, scheduledEvents = [], eventTimeouts = [], videoModel, + bufferedEvent = null, currentEvent = null, isSwitching = false, - hideAlternativePlayerControls = true, + hideAlternativePlayerControls = false, altPlayer, - useDashEventsForScheduling = true, + fullscreenDiv, + useDashEventsForScheduling = false, playbackController, altVideoElement, alternativeContext, + isMainDynamic = false, lastTimestamp = 0; function setConfig(config) { @@ -88,6 +91,7 @@ function AlternativeMpdController() { } if (!!config.useDashEventsForScheduling && !useDashEventsForScheduling) { + console.log(`Event strategy: ${'Timestamps based' ? config.useDashEventsForScheduling : 'Interval based'}`) useDashEventsForScheduling = config.useDashEventsForScheduling; } @@ -95,9 +99,9 @@ function AlternativeMpdController() { alternativeContext = config.alternativeContext } - // if (!!config.lastTimestamp) { - // lastTimestamp = config.lastTimestamp || 0; - // } + if (!!config.lastTimestamp) { + lastTimestamp = config.lastTimestamp || 0; + } if (!!config.currentEvent && !currentEvent) { currentEvent = config.currentEvent; @@ -110,23 +114,60 @@ function AlternativeMpdController() { altPlayer.on(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); } - if (useDashEventsForScheduling) { - _startDashEventPlaybackTimeMonitoring(); + document.addEventListener('fullscreenchange', () => { + if (document.fullscreenElement === videoModel.getElement()) { + console.log('Ay ay'); + // document.exitFullscreen(); + // fullscreenDiv.requestFullscreen(); + } else { + console.log('Esto no se va a triggerear nunca'); + // document.exitFullscreen(); + } + }); + + // Borrar por el amor de dios + if (!fullscreenDiv) { + fullscreenDiv = document.createElement('div'); + fullscreenDiv.id = 'fullscreenDiv'; + videoModel.getElement().parentNode.insertBefore(fullscreenDiv, videoModel.getElement()); + fullscreenDiv.appendChild(videoModel.getElement()); } + + eventBus.on(Constants.ALTERNATIVE_MPD.URI, _onAlternativeLoad); + } + + function _onAlternativeLoad(e) { + console.log(e); + console.log('I\'m coming from an alternative based event'); } function _onManifestLoaded(e) { - console.log('I\'m about to experience brain damange') - console.log(e) const manifest = e.data; const events = _parseAlternativeMPDEvents(manifest) if (scheduledEvents && scheduledEvents.length > 0) { - scheduledEvents.push(events) + scheduledEvents.push(...events) } else { scheduledEvents = events } - if (!useDashEventsForScheduling) { - _scheduleAlternativeMPDEvents(); + + scheduledEvents.forEach((d) => { if (d.alternativeMPD.uri == e.data.originalUrl) { d.type = e.data.type } }); + + switch (manifest.type) { + case 'dynamic': + if (!currentEvent && !altPlayer) { + isMainDynamic = true; + _scheduleAlternativeMPDEvents(); + } + break; + case 'static': + if (!isMainDynamic) { + _prebufferNextAlternative(); + _startDashEventPlaybackTimeMonitoring(); + } + break; + default: + console.log('Unknown manifest type') + break; } } @@ -139,51 +180,58 @@ function AlternativeMpdController() { function _onDashPlaybackTimeUpdated(e) { try { - if (e.streamId == 'defaultId_0') { + if (currentEvent) { + if (currentEvent.type == 'dynamic') { return; } + if (currentEvent.duration <= e.time && Math.round(e.time - lastTimestamp) != 0) { + _switchBackToMainContent(currentEvent); + } + } + else { const currentTime = e.time; lastTimestamp = e.time; - console.log(`Time elapsed for main player ${e.time}`); - const event = _getCurrentEvent(currentTime); if (event && !isSwitching && !currentEvent) { + currentEvent = event; _switchToAlternativeContent(event); - } else if (!event && !isSwitching && currentEvent) { - _switchBackToMainContent(currentEvent); - } - } else { - console.log('I\'m not being played during main content'); - console.log(lastTimestamp); - console.log(`Time elapsed for alternative player ${e.time}`); - // Check of end of event and dispatch switch to mainPlayer - if (currentEvent && currentEvent.duration <= e.time) { - _switchBackToMainContent(currentEvent); } } } catch (err) { + console.log(lastTimestamp); console.error('Error in onDashPlaybackTimeUpdated:', err); } } function _getCurrentEvent(currentTime) { return scheduledEvents.find(event => { + if (event.watched && event.mode === 'insert') { + return false; + } return currentTime >= event.presentationTime && - currentTime < event.presentationTime + event.duration; + currentTime < event.presentationTime + event.duration - event.returnOffset; }); } - function _initializeAlternativePlayer(event) { - if (altVideoElement && altPlayer) { return }; + function _initializeAlternativePlayerElement(event) { + if (!altVideoElement) { + // Create a new video element for the alternative content + altVideoElement = document.createElement('video'); + altVideoElement.style.display = 'none'; // Hide the alternative video element initially + altVideoElement.autoplay = false; + altVideoElement.controls = !hideAlternativePlayerControls; + fullscreenDiv.appendChild(altVideoElement); - // Create a new video element for the alternative content - altVideoElement = document.createElement('video'); - altVideoElement.style.display = 'none'; // Hide the alternative video element initially - altVideoElement.autoplay = false; - altVideoElement.controls = !hideAlternativePlayerControls; + // Insert the alternative video element into the DOM + videoModel.getElement().parentNode.insertBefore(altVideoElement, videoModel.getElement().nextSibling); + }; - // Insert the alternative video element into the DOM - videoModel.getElement().parentNode.insertBefore(altVideoElement, videoModel.getElement().nextSibling); + // Initialize alternative player + if (event != bufferedEvent) { + _initializeAlternativePlayer(event); + } + } + function _initializeAlternativePlayer(event) { // Initialize alternative player altPlayer = MediaPlayer().create(); altPlayer.initialize(altVideoElement, event.alternativeMPD.uri, false, NaN, alternativeContext); @@ -195,7 +243,6 @@ function AlternativeMpdController() { } function _parseAlternativeMPDEvents(manifest) { - // This should not be done instead using DashManifestModel and DashAdaptor const events = []; const periods = manifest.Period || []; @@ -205,18 +252,24 @@ function AlternativeMpdController() { if (eventStream.schemeIdUri === dashConstants.ALTERNATIVE_MPD_SCHEME_ID) { const timescale = eventStream.timescale || 1; const eventsArray = eventStream.Event || []; - eventsArray.forEach(event => { - if (event && event.AlternativeMPD) { - const alternativeMPDNode = event.AlternativeMPD; - events.push({ - presentationTime: event.presentationTime / timescale, - duration: event.duration / timescale, + eventsArray.forEach(ev => { + if (ev && ev.AlternativeMPD) { + const alternativeMPDNode = ev.AlternativeMPD; + const mode = alternativeMPDNode.mode || 'insert'; + const eventObj = { //This should be casted using an AlternativeMpdObject + presentationTime: ev.presentationTime / timescale, + duration: ev.duration / timescale, alternativeMPD: { uri: alternativeMPDNode.uri, - earliestResolutionTimeOffset: parseInt(alternativeMPDNode.earliestResolutionTimeOffset || '0', 10) / 1000, // Convert to seconds + earliestResolutionTimeOffset: parseInt(alternativeMPDNode.earliestResolutionTimeOffset || '0', 10) / 1000, }, + mode: mode, + returnOffset: parseInt(alternativeMPDNode.returnOffset || '0', 10) / 1000, triggered: false, - }); + watched: false, + type: 'static' + }; + events.push(eventObj); } }); } @@ -228,6 +281,9 @@ function AlternativeMpdController() { function _scheduleAlternativeMPDEvents() { scheduledEvents.forEach(event => { + if (event.length == 0) { + return; + } const currentTime = videoModel.getElement().currentTime; const timeToEvent = event.presentationTime - currentTime; const timeToPrebuffer = (event.presentationTime - event.alternativeMPD.earliestResolutionTimeOffset) - currentTime; @@ -267,11 +323,38 @@ function AlternativeMpdController() { }); } + function _descheduleAlternativeMPDEvents(event) { + if (currentEvent) { + setTimeout(() => { + _switchBackToMainContent(event); + }, event.duration * 1000); + // currentEvent = null; + } + } + + function _prebufferNextAlternative() { + const nextEvent = scheduledEvents.find(event => { + if (event.watched && event.mode === 'insert') { + return false; + } + return !event.triggered; + }); + + if (nextEvent && !bufferedEvent) { + console.log(`Preloading event starting at ${nextEvent.presentationTime}`) + _prebufferAlternativeContent(nextEvent); + } + } + function _prebufferAlternativeContent(event) { if (event.triggered) { return }; - event.triggered = true; + const idx = scheduledEvents.findIndex(e => e == event); + if (idx !== -1) { + scheduledEvents[idx].triggered = true; + } - _initializeAlternativePlayer(event); + _initializeAlternativePlayerElement(event); + bufferedEvent = event; altPlayer.on(Events.STREAM_INITIALIZED, () => { console.log('I\'m buffering') @@ -283,10 +366,15 @@ function AlternativeMpdController() { function _switchToAlternativeContent(event) { if (isSwitching) { return }; isSwitching = true; - currentEvent = {...event, triggered: true}; + const idx = scheduledEvents.findIndex(e => e === event); + if (idx !== -1) { + scheduledEvents[idx].triggered = true; + } - if (!altVideoElement || !altPlayer) { - _initializeAlternativePlayer(event); + _initializeAlternativePlayerElement(event); + + if (event.type == 'dynamic') { + _descheduleAlternativeMPDEvents(currentEvent); } videoModel.pause(); @@ -297,6 +385,7 @@ function AlternativeMpdController() { altPlayer.play(); isSwitching = false; + bufferedEvent = null; } function _switchBackToMainContent(event) { @@ -304,15 +393,28 @@ function AlternativeMpdController() { isSwitching = true; altPlayer.pause(); - altVideoElement.style.display = 'none'; videoModel.getElement().style.display = 'block'; - // If we implement more depth juggling this is necessary - // @TODO: This seek should be changed according to the mode - const returnTime = event.presentationTime + event.duration; - playbackController.seek(returnTime, false, false); - console.log(event); + let seekTime; + if (event.mode === 'replace') { + seekTime = event.presentationTime + event.duration - event.returnOffset; + } else if (event.mode === 'insert') { + if (!event.watched) { + const idx = scheduledEvents.findIndex(e => e === event); + if (idx !== -1) { + scheduledEvents[idx].watched = true; + } + } + seekTime = event.presentationTime; + } + + if (playbackController.getIsDynamic()) { + playbackController.seekToOriginalLive(true, false, false); + } else { + playbackController.seek(seekTime, false, false); + } + videoModel.play(); altPlayer.reset(); @@ -323,6 +425,8 @@ function AlternativeMpdController() { isSwitching = false; currentEvent = null; + + _prebufferNextAlternative(); } @@ -332,6 +436,7 @@ function AlternativeMpdController() { eventTimeouts = []; if (altPlayer) { + altPlayer.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); altPlayer.reset(); altPlayer = null; } diff --git a/src/streaming/controllers/EventController.js b/src/streaming/controllers/EventController.js index 837bcabf2e..e86d5ca149 100644 --- a/src/streaming/controllers/EventController.js +++ b/src/streaming/controllers/EventController.js @@ -474,6 +474,7 @@ function EventController() { if (mode === MediaPlayerEvents.EVENT_MODE_ON_RECEIVE && !event.triggeredReceivedEvent) { logger.debug(`Received event ${eventId}`); event.triggeredReceivedEvent = true; + console.log(`Triggered: ${event.eventStream.schemeIdUri}`); eventBus.trigger(event.eventStream.schemeIdUri, { event: event }, { mode }); return; } From 0d5600923eeceef1663b55db0f8d25b269ac8086 Mon Sep 17 00:00:00 2001 From: mhargain Date: Thu, 19 Dec 2024 12:05:57 -0300 Subject: [PATCH 006/175] @AlternativeMpd: added manifest highlight for demo page --- samples/demo.html | 102 +++++++++++++++++++++++++++++++++++++++++-- samples/demo_bu.html | 56 ++++++++++++++++++++---- 2 files changed, 146 insertions(+), 12 deletions(-) diff --git a/samples/demo.html b/samples/demo.html index 5ae2a761ab..ddc54a5006 100644 --- a/samples/demo.html +++ b/samples/demo.html @@ -11,6 +11,28 @@ width: 640px; height: 360px; } + + #manifestViewer { + font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + background: #1e1e1e; + color: #dcdcdc; + padding: 1em; + white-space: pre; + overflow: auto; + border: 1px solid #444; + border-radius: 4px; + } + #manifestViewer .token\.tag { color: #569CD6; } + #manifestViewer .token\.attr-name { color: #9CDCFE; } + #manifestViewer .token\.attr-value { color: #CE9178; } + #manifestViewer .token\.text { color: #D4D4D4; } + #manifestViewer .token\.altmpd { color: #06ff06; } + #manifestViewer .token\.altmpd-bg { background-color: rgb(36, 179, 36); + } + + .highlight { + background-color: yellow; + } @@ -43,9 +65,12 @@
-
+
+
+
Manifest will appear here...
+
© DASH-IF @@ -98,9 +123,6 @@ document.body.appendChild(overlay); - - - document.addEventListener("DOMContentLoaded", function () { var video = document.querySelector("video"); var manifestUrlInput = document.getElementById('manifestUrl'); @@ -108,6 +130,7 @@ var addAltBtn = document.getElementById('addAlt'); var addRecommendedBtn = document.getElementById('addRecommended'); var loadPlayerBtn = document.getElementById('loadPlayer'); + var manifestViewer = document.getElementById('manifestViewer'); var recommendedUris = [ 'https://media.axprod.net/TestVectors/v7-Clear/Manifest_1080p.mpd', @@ -258,6 +281,77 @@ var player = dashjs.MediaPlayer().create(); player.initialize(newVideo, finalUrl, true); + + fetch(finalUrl) + .then(response => response.text()) + .then(rawText => { + let xmlString = rawText.trim(); + + let parser = new DOMParser(); + let xmlDoc = parser.parseFromString(xmlString, "application/xml"); + + let serializer = new XMLSerializer(); + let serialized = serializer.serializeToString(xmlDoc); + + let PADDING = ' '; + let reg = /(>)(<)(\/*)/g; + let xmlFormatted = serialized.replace(reg, '$1\r\n$2$3'); + let formatted = ''; + let pad = 0; + xmlFormatted.split('\r\n').forEach((node) => { + let indent = 0; + if (node.match(/.+<\/\w[^>]*>$/)) { + // a complete tag in one line + indent = 0; + } else if (node.match(/^<\/\w/)) { + // closing tag + if (pad > 0) pad -= 1; + } else if (node.match(/^<\w([^>]*[^/])?>.*$/)) { + // opening tag + indent = 1; + } else { + // text node or something else + indent = 0; + } + formatted += PADDING.repeat(pad) + node + '\n'; + pad += indent; + }); + formatted = formatted.trim(); + + let escaped = formatted + .replace(/&/g, '&') + .replace(//g, '>'); + + let highlighted = escaped + .replace(/(<\/?)(\??)(\w+)(.*?)(\/?>)/g, (match, open, question, tagName, attrs, close) => { + let isAltMpd = (tagName === "AlternativeMPD"); + let coloredTag = `${open}${question}${tagName}`; + if (attrs.trim().length > 0) { + attrs = attrs.replace(/(\w+)="(.*?)"/g, `$1="$2"`); + coloredTag += attrs; + } + coloredTag += `${close}`; + return coloredTag; + }) + .replace(/(>)([^<]*)(?=<)/g, (match, close, text) => { + if (text.trim()) { + return `${close}${text}`; + } + return match; + }); + + highlighted = highlighted.replace(/(<AlternativeMPD[\s\S]*?<\/AlternativeMPD>)/, '$1'); + + document.getElementById('manifestViewer').innerHTML = highlighted; + + }) + .catch(err => { + document.getElementById('manifestViewer').textContent = 'Error loading manifest: ' + err; + }); + + + }); var defaultRow = createAltRow('', '', '', '', '', ''); diff --git a/samples/demo_bu.html b/samples/demo_bu.html index 8b3bdceb9d..5ae2a761ab 100644 --- a/samples/demo_bu.html +++ b/samples/demo_bu.html @@ -3,11 +3,9 @@ Alternative Config - - - - - - - -
-
-
-
- - -
-
-
-
- - -
-
-
-
URI
-
presentationTime(ms)
-
duration(ms)
-
eRTO(ms)
-
mode(insert/replace)
-
returnOffset(ms)
-
-
-
-
- -
-
-
-
- -
-
-
Manifest will appear here...
-
-
-
- © DASH-IF -
-
-
- - - - diff --git a/samples/demo_bu.html b/samples/demo_bu.html deleted file mode 100644 index 5ae2a761ab..0000000000 --- a/samples/demo_bu.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - Alternative Config - - - - - - -
-
-
-
- - -
-
-
-
- - -
-
-
-
URI
-
presentationTime(ms)
-
duration(ms)
-
eRTO(ms)
-
mode(insert/replace)
-
returnOffset(ms)
-
-
-
-
- -
-
-
-
- -
-
-
- © DASH-IF -
-
-
- - - - diff --git a/samples/develop.html b/samples/develop.html deleted file mode 100644 index e3ec0e9dac..0000000000 --- a/samples/develop.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - Buffer target - - - - - - - - - - -
-
-
- -
-
-
- © DASH-IF -
-
-
- - - - \ No newline at end of file diff --git a/src/streaming/ManifestLoader.js b/src/streaming/ManifestLoader.js index 86cf001a11..df26e78a3a 100644 --- a/src/streaming/ManifestLoader.js +++ b/src/streaming/ManifestLoader.js @@ -222,8 +222,6 @@ function ManifestLoader(config) { } } - // This should be done recursively until every alternative is parsed - manifest.baseUri = baseUri; manifest.loadedTime = new Date(); xlinkController.resolveManifestOnLoad(manifest); diff --git a/src/streaming/controllers/AlternativeMpdController.js b/src/streaming/controllers/AlternativeMpdController.js index 09314d1b0f..32b1708eaf 100644 --- a/src/streaming/controllers/AlternativeMpdController.js +++ b/src/streaming/controllers/AlternativeMpdController.js @@ -35,14 +35,6 @@ import EventBus from './../../core/EventBus.js'; import FactoryMaker from '../../core/FactoryMaker.js'; import Constants from '../constants/Constants.js'; -/* -TODOS: --> Prebuffering could be done in a queue style, where at initialization you preload the first one, when that is done you preload the following, etc - -> This is specially good for the timeupdate scheduling approach --> I should save the last mainPlayer time reference in order to search current event while in alternative and to easily calculate time back --> When going to the alternative you could seek into the content taking into account the duration of the alternative or go back to the time before the alternative -*/ - function AlternativeMpdController() { const DEFAULT_EARLIEST_RESOULTION_TIME_OFFSET = 60; @@ -52,7 +44,6 @@ function AlternativeMpdController() { let instance, scheduledEvents = [], - eventTimeouts = [], videoModel, bufferedEvent = null, currentEvent = null, @@ -64,7 +55,6 @@ function AlternativeMpdController() { playbackController, altVideoElement, alternativeContext, - isMainDynamic = false, actualEventPresentationTime = 0, timeToSwitch = 0, manifestInfo = {}, @@ -112,11 +102,11 @@ function AlternativeMpdController() { function initialize() { eventBus.on(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); - eventBus.on(Events.ALTERNATIVE_EVENT_RECEIVED, _onAlternativeEventeReceived, this); + eventBus.on(Events.ALTERNATIVE_EVENT_RECEIVED, _onAlternativeEventReceived, this); if (altPlayer) { altPlayer.on(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); - altPlayer.on(Events.ALTERNATIVE_EVENT_RECEIVED, _onAlternativeEventeReceived, this); + altPlayer.on(Events.ALTERNATIVE_EVENT_RECEIVED, _onAlternativeEventReceived, this); } document.addEventListener('fullscreenchange', () => { @@ -150,7 +140,7 @@ function AlternativeMpdController() { }); } - function _onAlternativeEventeReceived(event) { + function _onAlternativeEventReceived(event) { // Only Alternative MPD replace events can be used used for dynamic MPD if (manifestInfo.type === DashConstants.DYNAMIC && event?.alternativeMpd.mode === Constants.ALTERNATIVE_MPD.MODES.INSERT) { logger.warn('Insert mode not supported for dynamic manifests - ignoring event'); @@ -166,24 +156,8 @@ function AlternativeMpdController() { logger.info('First alternative event scheduled'); } - switch (manifestInfo.type) { - case DashConstants.DYNAMIC: - if (!currentEvent && !altPlayer) { - isMainDynamic = true; - logger.info('Starting alternative MPD event scheduling for dynamic manifest'); - _scheduleAlternativeMPDEvents(); - } - break; - case DashConstants.STATIC: - if (!isMainDynamic) { - logger.info('Starting playback time monitoring for static manifest'); - _startPlaybackTimeMonitoring(); - } - break; - default: - logger.warn(`Unknown manifest type: ${manifestInfo.type}`); - break; - } + logger.info('Starting playback time monitoring for static manifest'); + _startPlaybackTimeMonitoring(); } function _startPlaybackTimeMonitoring() { @@ -308,16 +282,24 @@ function AlternativeMpdController() { } function _initializeAlternativePlayer(event) { + // Clean up previous error listener if any + if (altPlayer) { + altPlayer.off(Events.ERROR, _onAlternativePlayerError, this); + } // Initialize alternative player altPlayer = MediaPlayer().create(); altPlayer.initialize(altVideoElement, event.alternativeMPD.url, false, NaN, alternativeContext); altPlayer.setAutoPlay(false); + altPlayer.on(Events.ERROR, _onAlternativePlayerError, this); + } - altPlayer.on(Events.ERROR, (e) => { + function _onAlternativePlayerError(e) { + if (logger) { logger.error('Alternative player error:', e); - }, this); + } } + function _parseAlternativeMPDEvent(event) { if (event.alternativeMpd) { const timescale = event.eventStream.timescale || 1; @@ -344,58 +326,6 @@ function AlternativeMpdController() { } } - function _scheduleAlternativeMPDEvents() { - scheduledEvents.forEach(event => { - if (event.length == 0) { - return; - } - const currentTime = videoModel.getElement().currentTime; - const timeToEvent = event.presentationTime - currentTime; - const timeToPrebuffer = (event.presentationTime - event.alternativeMPD.earliestResolutionTimeOffset) - currentTime; - - if (timeToPrebuffer >= 0) { - const prebufferTimeoutId = setTimeout(() => { - _prebufferAlternativeContent(event); - }, timeToPrebuffer * 1000); - - eventTimeouts.push(prebufferTimeoutId); - } else { - _prebufferAlternativeContent(event); - } - - if (timeToEvent >= 0) { - const switchToAltTimeoutId = setTimeout(() => { - _switchToAlternativeContent(event); - }, timeToEvent * 1000); - - eventTimeouts.push(switchToAltTimeoutId); - - const switchToMainTimeoutId = setTimeout(() => { - _switchBackToMainContent(event); - }, (timeToEvent + event.duration) * 1000); - - eventTimeouts.push(switchToMainTimeoutId); - } else if (currentTime < event.presentationTime + event.duration) { - _switchToAlternativeContent(event); - - const timeLeft = (event.presentationTime + event.duration - currentTime) * 1000; - const switchToMainTimeoutId = setTimeout(() => { - _switchBackToMainContent(event); - }, timeLeft); - - eventTimeouts.push(switchToMainTimeoutId); - } - }); - } - - function _descheduleAlternativeMPDEvents(event) { - if (currentEvent) { - setTimeout(() => { - _switchBackToMainContent(event); - }, event.duration * 1000); - } - } - function _prebufferNextAlternative(nextEvent) { if (nextEvent && !bufferedEvent) { logger.info(`Preloading event starting at ${nextEvent.presentationTime}`) @@ -428,11 +358,6 @@ function AlternativeMpdController() { _initializeAlternativePlayerElement(event); - if (event.type == DashConstants.DYNAMIC) { - logger.debug('Descheduling events for dynamic manifest'); - _descheduleAlternativeMPDEvents(currentEvent); - } - videoModel.pause(); logger.debug('Main video paused'); @@ -502,7 +427,6 @@ function AlternativeMpdController() { videoModel.play(); logger.info('Main content playback resumed'); - // Cleanup altPlayer.reset(); altPlayer = null; @@ -519,17 +443,17 @@ function AlternativeMpdController() { function reset() { scheduledEvents = []; - eventTimeouts.forEach(timeoutId => clearTimeout(timeoutId)); - eventTimeouts = []; if (altPlayer) { altPlayer.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); - altPlayer.off(Events.ALTERNATIVE_EVENT_RECEIVED, _onAlternativeEventeReceived, this); + altPlayer.off(Events.ALTERNATIVE_EVENT_RECEIVED, _onAlternativeEventReceived, this); + altPlayer.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onAlternativePlaybackTimeUpdated, this); + altPlayer.off(Events.ERROR, _onAlternativePlayerError, this); altPlayer.reset(); altPlayer = null; } - if (altVideoElement) { + if (altVideoElement && altVideoElement.parentNode) { altVideoElement.parentNode.removeChild(altVideoElement); altVideoElement = null; } @@ -538,15 +462,11 @@ function AlternativeMpdController() { videoModel.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onMainPlaybackTimeUpdated, this); } - if (altPlayer) { - altPlayer.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onAlternativePlaybackTimeUpdated, this); - } - isSwitching = false; currentEvent = null; eventBus.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); - eventBus.off(Events.ALTERNATIVE_EVENT_RECEIVED, _onAlternativeEventeReceived, this); + eventBus.off(Events.ALTERNATIVE_EVENT_RECEIVED, _onAlternativeEventReceived, this); } instance = { diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js deleted file mode 100644 index 99a06401cb..0000000000 --- a/src/streaming/controllers/StreamController.js +++ /dev/null @@ -1,1776 +0,0 @@ -/** - * 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 MetricsConstants from '../constants/MetricsConstants.js'; -import Stream from '../Stream.js'; -import ManifestUpdater from '../ManifestUpdater.js'; -import EventBus from '../../core/EventBus.js'; -import Events from '../../core/events/Events.js'; -import FactoryMaker from '../../core/FactoryMaker.js'; -import {PlayList, PlayListTrace} from '../vo/metrics/PlayList.js'; -import Debug from '../../core/Debug.js'; -import InitCache from '../utils/InitCache.js'; -import MediaPlayerEvents from '../MediaPlayerEvents.js'; -import TimeSyncController from './TimeSyncController.js'; -import MediaSourceController from './MediaSourceController.js'; -import DashJSError from '../vo/DashJSError.js'; -import Errors from '../../core/errors/Errors.js'; -import EventController from './EventController.js'; -import ConformanceViolationConstants from '../constants/ConformanceViolationConstants.js'; -import ExtUrlQueryInfoController from './ExtUrlQueryInfoController.js'; -import ProtectionEvents from '../protection/ProtectionEvents.js'; -import ProtectionErrors from '../protection/errors/ProtectionErrors.js'; - -const PLAYBACK_ENDED_TIMER_INTERVAL = 200; -const DVR_WAITING_OFFSET = 2; - -function StreamController() { - - const context = this.context; - const eventBus = EventBus(context).getInstance(); - - let instance, logger, capabilities, capabilitiesFilter, manifestUpdater, manifestLoader, manifestModel, adapter, - dashMetrics, mediaSourceController, timeSyncController, contentSteeringController, baseURLController, - segmentBaseController, uriFragmentModel, abrController, throughputController, mediaController, eventController, - initCache, errHandler, timelineConverter, streams, activeStream, protectionController, textController, - protectionData, extUrlQueryInfoController, - autoPlay, isStreamSwitchingInProgress, hasMediaError, hasInitialisationError, mediaSource, videoModel, - playbackController, serviceDescriptionController, mediaPlayerModel, customParametersModel, isPaused, - initialPlayback, initialSteeringRequest, playbackEndedTimerInterval, bufferSinks, preloadingStreams, settings, - firstLicenseIsFetched, waitForPlaybackStartTimeout, providedStartTime, errorInformation; - - function setup() { - logger = Debug(context).getInstance().getLogger(instance); - timeSyncController = TimeSyncController(context).getInstance(); - mediaSourceController = MediaSourceController(context).getInstance(); - initCache = InitCache(context).getInstance(); - - resetInitialSettings(); - } - - function initialize(autoPl, protData) { - _checkConfig(); - - autoPlay = autoPl; - protectionData = protData; - timelineConverter.initialize(); - - manifestUpdater = ManifestUpdater(context).create(); - manifestUpdater.setConfig({ - manifestModel, - adapter, - manifestLoader, - errHandler, - settings, - contentSteeringController - }); - manifestUpdater.initialize(); - - eventController = EventController(context).getInstance(); - eventController.setConfig({ - manifestUpdater: manifestUpdater, playbackController: playbackController, settings - }); - eventController.start(); - - extUrlQueryInfoController = ExtUrlQueryInfoController(context).getInstance(); - - timeSyncController.setConfig({ - dashMetrics, baseURLController, errHandler, settings - }); - timeSyncController.initialize(); - - mediaSourceController.setConfig({ settings }); - - if (protectionController) { - eventBus.trigger(Events.PROTECTION_CREATED, { - controller: protectionController - }); - protectionController.setMediaElement(videoModel.getElement()); - if (protectionData) { - protectionController.setProtectionData(protectionData); - } - } - - registerEvents(); - } - - function registerEvents() { - eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_SEEKING, _onPlaybackSeeking, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_ERROR, _onPlaybackError, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_PAUSED, _onPlaybackPaused, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance); - eventBus.on(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance); - eventBus.on(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, _onManifestValidityChanged, instance); - eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, instance); - eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_REQUESTED, _onQualityChanged, instance); - eventBus.on(MediaPlayerEvents.CONTENT_STEERING_REQUEST_COMPLETED, _onSteeringManifestUpdated, instance); - - - if (Events.KEY_SESSION_UPDATED) { - eventBus.on(Events.KEY_SESSION_UPDATED, _onKeySessionUpdated, instance); - } - - eventBus.on(Events.MANIFEST_UPDATED, _onManifestUpdated, instance); - eventBus.on(Events.STREAM_BUFFERING_COMPLETED, _onStreamBufferingCompleted, instance); - eventBus.on(Events.TIME_SYNCHRONIZATION_COMPLETED, _onTimeSyncCompleted, instance); - eventBus.on(Events.CURRENT_TRACK_CHANGED, _onCurrentTrackChanged, instance); - eventBus.on(Events.SETTING_UPDATED_LIVE_DELAY, _onLiveDelaySettingUpdated, instance); - eventBus.on(Events.SETTING_UPDATED_LIVE_DELAY_FRAGMENT_COUNT, _onLiveDelaySettingUpdated, instance); - - eventBus.on(ProtectionEvents.INTERNAL_KEY_STATUSES_CHANGED, _onInternalKeyStatusesChanged, instance); - } - - function unRegisterEvents() { - eventBus.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_SEEKING, _onPlaybackSeeking, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_ERROR, _onPlaybackError, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_PAUSED, _onPlaybackPaused, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance); - eventBus.off(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance); - eventBus.off(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, _onManifestValidityChanged, instance); - eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, instance); - eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_REQUESTED, _onQualityChanged, instance); - eventBus.off(MediaPlayerEvents.CONTENT_STEERING_REQUEST_COMPLETED, _onSteeringManifestUpdated, instance); - - if (Events.KEY_SESSION_UPDATED) { - eventBus.off(Events.KEY_SESSION_UPDATED, _onKeySessionUpdated, instance); - } - - eventBus.off(Events.MANIFEST_UPDATED, _onManifestUpdated, instance); - eventBus.off(Events.STREAM_BUFFERING_COMPLETED, _onStreamBufferingCompleted, instance); - eventBus.off(Events.TIME_SYNCHRONIZATION_COMPLETED, _onTimeSyncCompleted, instance); - eventBus.off(Events.CURRENT_TRACK_CHANGED, _onCurrentTrackChanged, instance); - eventBus.off(Events.SETTING_UPDATED_LIVE_DELAY, _onLiveDelaySettingUpdated, instance); - eventBus.off(Events.SETTING_UPDATED_LIVE_DELAY_FRAGMENT_COUNT, _onLiveDelaySettingUpdated, instance); - - eventBus.off(ProtectionEvents.INTERNAL_KEY_STATUSES_CHANGED, _onInternalKeyStatusesChanged, instance); - } - - function _checkConfig() { - if (!manifestLoader || !manifestLoader.hasOwnProperty('load') || !timelineConverter || !timelineConverter.hasOwnProperty('initialize') || !timelineConverter.hasOwnProperty('reset') || !timelineConverter.hasOwnProperty('getClientTimeOffset') || !manifestModel || !errHandler || !dashMetrics || !playbackController) { - throw new Error(Constants.MISSING_CONFIG_ERROR); - } - } - - function _checkInitialize() { - if (!manifestUpdater || !manifestUpdater.hasOwnProperty('setManifest')) { - throw new Error('initialize function has to be called previously'); - } - } - - /** - * Start the streaming session by loading the target manifest - * @param {string} url - * @param {number} startTime - */ - function load(url, startTime = NaN) { - _checkConfig(); - providedStartTime = startTime; - manifestLoader.load(url); - } - - /** - * Start the streaming session by using the provided manifest object - * @param {object} manifest - * @param {number} startTime - */ - function loadWithManifest(manifest, startTime = NaN) { - _checkInitialize(); - providedStartTime = startTime; - manifestUpdater.setManifest(manifest); - } - - /** - * When the UTC snychronization is completed we can compose the streams - * @private - */ - function _onTimeSyncCompleted( /*e*/) { - _composePeriods(); - } - - /** - * - * @private - */ - function _onKeySessionUpdated() { - firstLicenseIsFetched = true; - } - - /** - * Setup the stream objects after the stream start and each MPD reload. This function is called after the UTC sync has been done (TIME_SYNCHRONIZATION_COMPLETED) - * @private - */ - function _composePeriods() { - try { - const streamsInfo = adapter.getStreamsInfo(); - - if (!activeStream && streamsInfo.length === 0) { - throw new Error('There are no periods in the MPD'); - } - - if (activeStream && streamsInfo.length > 0) { - dashMetrics.updateManifestUpdateInfo({ - currentTime: playbackController.getTime(), - buffered: videoModel.getBufferRange(), - presentationStartTime: streamsInfo[0].start, - clientTimeOffset: timelineConverter.getClientTimeOffset() - }); - } - - // Filter streams that are outdated and not included in the MPD anymore - if (streams.length > 0) { - _filterOutdatedStreams(streamsInfo); - } - - const promises = []; - for (let i = 0, ln = streamsInfo.length; i < ln; i++) { - const streamInfo = streamsInfo[i]; - promises.push(_initializeOrUpdateStream(streamInfo)); - dashMetrics.addManifestUpdateStreamInfo(streamInfo); - } - - Promise.all(promises) - .then(() => { - return new Promise((resolve, reject) => { - if (!activeStream) { - _initializeForFirstStream(streamsInfo, resolve, reject); - } else { - resolve(); - } - }); - }) - .then(() => { - eventBus.trigger(Events.STREAMS_COMPOSED); - // Additional periods might have been added after an MPD update. Check again if we can start prebuffering. - _checkIfPrebufferingCanStart(); - }) - .catch((e) => { - throw e; - }) - - } catch (e) { - errHandler.error(new DashJSError(Errors.MANIFEST_ERROR_ID_NOSTREAMS_CODE, e.message + ' nostreamscomposed', manifestModel.getValue())); - hasInitialisationError = true; - reset(); - } - } - - /** - * Called for each stream when composition is performed. Either a new instance of Stream is created or the existing one is updated. - * @param {object} streamInfo - * @private - */ - function _initializeOrUpdateStream(streamInfo) { - let stream = getStreamById(streamInfo.id); - - // If the Stream object does not exist we probably loaded the manifest the first time or it was - // introduced in the updated manifest, so we need to create a new Stream and perform all the initialization operations - if (!stream) { - stream = Stream(context).create({ - manifestModel, - mediaPlayerModel, - dashMetrics, - manifestUpdater, - adapter, - timelineConverter, - capabilities, - capabilitiesFilter, - errHandler, - baseURLController, - segmentBaseController, - textController, - abrController, - playbackController, - throughputController, - eventController, - mediaController, - protectionController, - videoModel, - streamInfo, - settings - }); - streams.push(stream); - stream.initialize(); - return Promise.resolve(); - } else { - return stream.updateData(streamInfo); - } - } - - /** - * Initialize playback for the first period. - * @param {array} streamsInfo - * @private - */ - function _initializeForFirstStream(streamsInfo, resolve, reject) { - try { - - // Add the DVR window so we can calculate the right starting point - addDVRMetric(); - - // If the start is in the future we need to wait - const dvrRange = dashMetrics.getCurrentDVRInfo().range; - if (dvrRange.end < dvrRange.start) { - if (waitForPlaybackStartTimeout) { - clearTimeout(waitForPlaybackStartTimeout); - } - const waitingTime = Math.min((((dvrRange.end - dvrRange.start) * -1) + DVR_WAITING_OFFSET) * 1000, 2147483647); - logger.debug(`Waiting for ${waitingTime} ms before playback can start`); - eventBus.trigger(Events.AST_IN_FUTURE, { delay: waitingTime }); - waitForPlaybackStartTimeout = setTimeout(() => { - _initializeForFirstStream(streamsInfo, resolve, reject); - }, waitingTime); - return; - } - - - // Calculate the producer reference time offsets if given - if (settings.get().streaming.applyProducerReferenceTime) { - serviceDescriptionController.calculateProducerReferenceTimeOffsets(streamsInfo); - } - - // Apply Service description parameters. - const manifestInfo = streamsInfo[0].manifestInfo; - if (settings.get().streaming.applyServiceDescription) { - serviceDescriptionController.applyServiceDescription(manifestInfo); - } - - // Compute and set the live delay - if (adapter.getIsDynamic()) { - const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); - } - - // Apply content steering - _applyContentSteeringBeforeStart() - .then(() => { - const manifest = manifestModel.getValue(); - if (manifest) { - baseURLController.update(manifest) - } - _calculateStartTimeAndSwitchStream() - resolve(); - }) - .catch((e) => { - logger.error(e); - _calculateStartTimeAndSwitchStream(); - resolve(); - }) - } catch (e) { - reject(e); - } - } - - function _applyContentSteeringBeforeStart() { - if (settings.get().streaming.applyContentSteering && contentSteeringController.shouldQueryBeforeStart()) { - return contentSteeringController.loadSteeringData(); - } - return Promise.resolve(); - } - - function _calculateStartTimeAndSwitchStream() { - // Figure out the correct start time and the correct start period - const startTime = _getInitialStartTime(); - let initialStream = getStreamForTime(startTime); - const startStream = initialStream !== null ? initialStream : streams[0]; - eventBus.trigger(Events.INITIAL_STREAM_SWITCH, { startTime }); - _switchStream(startStream, null, startTime); - _startPlaybackEndedTimerInterval(); - } - - /** - * Switch from the current stream (period) to the next stream (period). - * @param {object} stream - * @param {object} previousStream - * @param {number} seekTime - * @private - */ - function _switchStream(stream, previousStream, seekTime) { - try { - if (isStreamSwitchingInProgress || !stream || (previousStream === stream && stream.getIsActive())) { - return; - } - - isStreamSwitchingInProgress = true; - eventBus.trigger(Events.PERIOD_SWITCH_STARTED, { - fromStreamInfo: previousStream ? previousStream.getStreamInfo() : null, - toStreamInfo: stream.getStreamInfo() - }); - - let keepBuffers = false; - let representationsFromPreviousPeriod = []; - activeStream = stream; - - if (previousStream) { - keepBuffers = _canSourceBuffersBeKept(stream, previousStream); - representationsFromPreviousPeriod = _getRepresentationsFromPreviousPeriod(previousStream); - previousStream.deactivate(keepBuffers); - } - - // Determine seek time when switching to new period - // - seek at given seek time - // - or seek at period start if upcoming period is not prebuffered - seekTime = !isNaN(seekTime) ? seekTime : (!keepBuffers && previousStream ? stream.getStreamInfo().start : NaN); - logger.info(`Switch to stream ${stream.getId()}. Seektime is ${seekTime}, current playback time is ${playbackController.getTime()}. Seamless period switch is set to ${keepBuffers}`); - - preloadingStreams = preloadingStreams.filter((s) => { - return s.getId() !== activeStream.getId(); - }); - playbackController.initialize(getActiveStreamInfo(), !!previousStream); - - // If we have a video element we are not preloading into a virtual buffer - if (videoModel.getElement()) { - _openMediaSource({ seekTime, keepBuffers, streamActivated: false, representationsFromPreviousPeriod }); - } else { - _activateStream({ seekTime, keepBuffers }); - } - } catch (e) { - isStreamSwitchingInProgress = false; - } - } - - /** - * Setup the Media Source. Open MSE and attach event listeners - * @private - * @param inputParameters - */ - function _openMediaSource(inputParameters) { - let sourceUrl; - - function _onMediaSourceOpen() { - // Manage situations in which a call to reset happens while MediaSource is being opened - if (!mediaSource || mediaSource.readyState !== 'open') { - return; - } - - logger.debug('MediaSource is open!'); - window.URL.revokeObjectURL(sourceUrl); - mediaSource.removeEventListener('sourceopen', _onMediaSourceOpen); - mediaSource.removeEventListener('webkitsourceopen', _onMediaSourceOpen); - - _setMediaDuration(); - const dvrInfo = dashMetrics.getCurrentDVRInfo(); - mediaSourceController.setSeekable(dvrInfo.range.start, dvrInfo.range.end); - if (inputParameters.streamActivated) { - if (!isNaN(inputParameters.seekTime)) { - playbackController.seek(inputParameters.seekTime, true, true); - } - // Set the media source for all StreamProcessors - activeStream.setMediaSource(mediaSource) - .then(() => { - // Start text processing now that we have a video element - activeStream.initializeForTextWithMediaSource(mediaSource); - }) - } else { - _activateStream(inputParameters); - } - } - - function _open() { - mediaSource.addEventListener('sourceopen', _onMediaSourceOpen, false); - mediaSource.addEventListener('webkitsourceopen', _onMediaSourceOpen, false); - sourceUrl = mediaSourceController.attachMediaSource(videoModel); - logger.debug('MediaSource attached to element. Waiting on open...'); - } - - if (!mediaSource) { - mediaSource = mediaSourceController.createMediaSource(); - _open(); - } else { - if (inputParameters.keepBuffers) { - _activateStream(inputParameters); - } else { - mediaSourceController.detachMediaSource(videoModel); - _open(); - } - } - } - - /** - * Activates a new stream. - * @param {number} seekTime - * @param {boolean} keepBuffers - */ - function _activateStream(inputParameters) { - const representationsFromPreviousPeriod = inputParameters.representationsFromPreviousPeriod || []; - activeStream.activate(mediaSource, inputParameters.keepBuffers ? bufferSinks : undefined, representationsFromPreviousPeriod) - .then((sinks) => { - if (sinks) { - bufferSinks = sinks; - } - - // Set the initial time for this stream in the StreamProcessor - if (!isNaN(inputParameters.seekTime)) { - eventBus.trigger(Events.SEEK_TARGET, { time: inputParameters.seekTime }, { streamId: activeStream.getId() }); - playbackController.seek(inputParameters.seekTime, false, true); - activeStream.startScheduleControllers(); - } - - isStreamSwitchingInProgress = false; - eventBus.trigger(Events.PERIOD_SWITCH_COMPLETED, { toStreamInfo: getActiveStreamInfo() }); - }); - } - - function _getRepresentationsFromPreviousPeriod(previousStream) { - const previousStreamProcessors = previousStream ? previousStream.getStreamProcessors() : []; - return previousStreamProcessors.map((streamProcessor) => { - return streamProcessor.getRepresentation(); - }) - } - - /** - * A playback seeking event was triggered. We need to disable the preloading streams and call the respective seeking handler. - * We distinguish between inner period seeks and outer period seeks - * @param {object} e - * @private - */ - function _onPlaybackSeeking(e) { - const newTime = e.seekTime; - const seekToStream = getStreamForTime(newTime); - - if (!seekToStream || seekToStream === activeStream) { - _cancelPreloading(); - _handleInnerPeriodSeek(e); - } else if (seekToStream && seekToStream !== activeStream) { - _cancelPreloading(seekToStream); - _handleOuterPeriodSeek(e, seekToStream); - } - - _createPlaylistMetrics(PlayList.SEEK_START_REASON); - } - - /** - * Cancels the preloading of certain streams based on the position we are seeking to. - * @param {object} seekToStream - * @private - */ - function _cancelPreloading(seekToStream = null) { - // Inner period seek - if (!seekToStream) { - _deactivateAllPreloadingStreams(); - } - - // Outer period seek: Deactivate everything for now - else { - _deactivateAllPreloadingStreams(); - } - - } - - /** - * Deactivates all preloading streams - * @private - */ - function _deactivateAllPreloadingStreams() { - if (preloadingStreams && preloadingStreams.length > 0) { - preloadingStreams.forEach((s) => { - s.deactivate(true); - }); - preloadingStreams = []; - } - } - - /** - * Handle an inner period seek. Prepare all StreamProcessors for the seek. - * @param {object} e - * @private - */ - function _handleInnerPeriodSeek(e) { - const streamProcessors = activeStream.getStreamProcessors(); - - streamProcessors.forEach((sp) => { - return sp.prepareInnerPeriodPlaybackSeeking(e); - }); - - _flushPlaylistMetrics(PlayListTrace.USER_REQUEST_STOP_REASON); - } - - /** - * Handle an outer period seek. Dispatch the corresponding event to be handled in the BufferControllers and the ScheduleControllers - * @param {object} e - * @param {object} seekToStream - * @private - */ - function _handleOuterPeriodSeek(e, seekToStream) { - // Stop segment requests - const seekTime = e && !isNaN(e.seekTime) ? e.seekTime : NaN; - const streamProcessors = activeStream.getStreamProcessors(); - - const promises = streamProcessors.map((sp) => { - // Cancel everything in case the active stream is still buffering - return sp.prepareOuterPeriodPlaybackSeeking(e); - }); - - Promise.all(promises) - .then(() => { - _switchStream(seekToStream, activeStream, seekTime); - }) - .catch((e) => { - errHandler.error(e); - }); - } - - /** - * A track change occured. We deactivate the preloading streams - * @param {object} e - * @private - */ - function _onCurrentTrackChanged(e) { - // Track was changed in non-active stream. No need to do anything, this only happens when a stream starts preloading - if (e.newMediaInfo.streamInfo.id !== activeStream.getId()) { - return; - } - - // If the track was changed in the active stream we need to stop preloading and remove the already prebuffered stuff. Since we do not support preloading specific handling of specific AdaptationSets yet. - _deactivateAllPreloadingStreams(); - - if (settings.get().streaming.buffer.resetSourceBuffersForTrackSwitch && e.oldMediaInfo && e.oldMediaInfo.codec !== e.newMediaInfo.codec) { - const seekTime = playbackController.getTime(); - activeStream.deactivate(false); - _openMediaSource({ seekTime, keepBuffers: false, streamActivated: false }); - return; - } - - activeStream.prepareTrackChange(e); - } - - /** - * If the source buffer can be reused we can potentially start buffering the next period - * @param {object} nextStream - * @param {object} previousStream - * @return {boolean} - * @private - */ - function _canSourceBuffersBeKept(nextStream, previousStream) { - try { - // Seamless period switch allowed only if: - // - none of the periods uses contentProtection. - // - AND changeType method is implemented - return (settings.get().streaming.buffer.reuseExistingSourceBuffers - && (capabilities.isProtectionCompatible(previousStream.getStreamInfo(), nextStream.getStreamInfo()) || firstLicenseIsFetched) - && (capabilities.supportsChangeType() && settings.get().streaming.buffer.useChangeType)); - } catch (e) { - return false; - } - } - - /** - * Initiate the preloading of the next stream - * @param {object} nextStream - * @param {object} previousStream - * @private - */ - function _onStreamCanLoadNext(nextStream, previousStream = null) { - - if (mediaSource && !nextStream.getPreloaded()) { - let seamlessPeriodSwitch = _canSourceBuffersBeKept(nextStream, previousStream); - - if (seamlessPeriodSwitch) { - const representationsFromPreviousPeriod = _getRepresentationsFromPreviousPeriod(previousStream); - nextStream.startPreloading(mediaSource, bufferSinks, representationsFromPreviousPeriod) - .then(() => { - preloadingStreams.push(nextStream); - }); - } - } - } - - /** - * Returns the corresponding stream object for a specific presentation time. - * @param {number} time - * @return {null|object} - */ - function getStreamForTime(time) { - - if (isNaN(time)) { - return null; - } - - const ln = streams.length; - - for (let i = 0; i < ln; i++) { - const stream = streams[i]; - const streamEnd = parseFloat((stream.getStartTime() + stream.getDuration()).toFixed(5)); - - if (time < streamEnd) { - return stream; - } - } - - return null; - } - - /** - * Add the DVR window to the metric list. We need the DVR window to restrict the seeking and calculate the right start time. - */ - function addDVRMetric() { - try { - const isDynamic = adapter.getIsDynamic(); - const streamsInfo = adapter.getStreamsInfo(); - const manifestInfo = streamsInfo[0].manifestInfo; - const time = playbackController.getTime(); - const range = timelineConverter.calcTimeShiftBufferWindow(streams, isDynamic); - const activeStreamProcessors = getActiveStreamProcessors(); - - if (typeof range.start === 'undefined' || typeof range.end === 'undefined') { - return; - } - - if (!activeStreamProcessors || activeStreamProcessors.length === 0) { - dashMetrics.addDVRInfo(Constants.VIDEO, time, manifestInfo, range); - } else { - activeStreamProcessors.forEach((sp) => { - dashMetrics.addDVRInfo(sp.getType(), time, manifestInfo, range); - }); - } - } catch (e) { - } - } - - /** - * The buffer level for a certain media type has been updated. If this is the initial playback and we want to autoplay the content we check if we can start playback now. - * For livestreams we might have a drift of the target live delay compared to the current live delay because reaching the initial buffer level took time. - * @param {object} e - * @private - */ - function _onBufferLevelUpdated(e) { - - // check if this is the initial playback and we reached the buffer target. If autoplay is true we start playback - if (initialPlayback && autoPlay) { - const initialBufferLevel = mediaPlayerModel.getInitialBufferLevel(); - const excludedStreamProcessors = [Constants.TEXT]; - if (isNaN(initialBufferLevel) || initialBufferLevel <= playbackController.getBufferLevel(excludedStreamProcessors) || (adapter.getIsDynamic() && initialBufferLevel > playbackController.getLiveDelay())) { - initialPlayback = false; - _createPlaylistMetrics(PlayList.INITIAL_PLAYOUT_START_REASON); - playbackController.play(); - } - } - - if (e && e.mediaType) { - dashMetrics.addBufferLevel(e.mediaType, new Date(), e.bufferLevel * 1000); - } - } - - /** - * When the quality is changed in the currently active stream we stop the prebuffering to avoid inconsistencies in the buffer settings like codec and append window - * @param e - * @private - */ - function _onQualityChanged(e) { - if (e.streamInfo.id === activeStream.getId()) { - _deactivateAllPreloadingStreams(); - } - - const stream = getStreamById(e.streamInfo.id); - - stream.prepareQualityChange(e); - } - - /** - * A setting related to the live delay was updated. Check if one of the latency values changed. If so, recalculate the live delay. - * @private - */ - function _onLiveDelaySettingUpdated() { - if (adapter.getIsDynamic() && playbackController.getOriginalLiveDelay() !== 0 && activeStream) { - const streamsInfo = adapter.getStreamsInfo() - if (streamsInfo.length > 0) { - const manifestInfo = streamsInfo[0].manifestInfo; - const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - - playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); - } - } - } - - /** - * When the playback time is updated we add the droppedFrames metric to the dash metric object - * @private - */ - function _onPlaybackTimeUpdated(/*e*/) { - if (hasVideoTrack()) { - const playbackQuality = videoModel.getPlaybackQuality(); - if (playbackQuality) { - dashMetrics.addDroppedFrames(playbackQuality); - } - } - } - - /** - * Once playback starts add playlist metrics depending on whether this was the first playback or playback resumed after pause - * @private - */ - function _onPlaybackStarted( /*e*/) { - logger.debug('[onPlaybackStarted]'); - if (!initialPlayback && isPaused) { - _createPlaylistMetrics(PlayList.RESUME_FROM_PAUSE_START_REASON); - } - if (initialPlayback) { - initialPlayback = false; - } - if (initialSteeringRequest) { - initialSteeringRequest = false; - // If this is the initial playback attempt and we have not yet triggered content steering now is the time - if (settings.get().streaming.applyContentSteering && !contentSteeringController.shouldQueryBeforeStart()) { - contentSteeringController.loadSteeringData(); - } - - } - isPaused = false; - } - - /** - * Once playback is paused flush metrics - * @param {object} e - * @private - */ - function _onPlaybackPaused(e) { - logger.debug('[onPlaybackPaused]'); - if (!e.ended) { - isPaused = true; - _flushPlaylistMetrics(PlayListTrace.USER_REQUEST_STOP_REASON); - } - } - - /** - * Callback once a stream/period is completely buffered. We can either signal the end of the stream or start prebuffering the next period. - * @param {object} e - * @private - */ - function _onStreamBufferingCompleted(e) { - logger.debug(`Stream with id ${e.streamInfo.id} finished buffering`); - const isLast = e.streamInfo.isLast; - if (mediaSource && isLast) { - logger.info('[onStreamBufferingCompleted] calls signalEndOfStream of mediaSourceController.'); - mediaSourceController.signalEndOfStream(mediaSource); - } else { - _checkIfPrebufferingCanStart(); - } - } - - /** - * Check if we can start prebuffering the next period. - * @private - */ - function _checkIfPrebufferingCanStart() { - - if (!activeStream) { - return; - } - - // Check if we are finished buffering. In case this is the case the prebuffering will be triggered automatically - if (!activeStream.getHasFinishedBuffering()) { - activeStream.checkAndHandleCompletedBuffering(); - return; - } - - // In case we have finished buffering already we can preload - const upcomingStreams = _getNextStreams(activeStream); - let i = 0; - - while (i < upcomingStreams.length) { - const stream = upcomingStreams[i]; - const previousStream = i === 0 ? activeStream : upcomingStreams[i - 1]; - - // If the preloading for the current stream is not scheduled, but its predecessor has finished buffering we can start prebuffering this stream - if (!stream.getPreloaded() && previousStream.getHasFinishedBuffering()) { - if (mediaSource) { - _onStreamCanLoadNext(stream, previousStream); - } - } - i += 1; - } - } - - /** - * In some cases we need to fire the playback ended event manually - * @private - */ - function _startPlaybackEndedTimerInterval() { - if (!playbackEndedTimerInterval) { - playbackEndedTimerInterval = setInterval(function () { - if (!isStreamSwitchingInProgress && playbackController.getTimeToStreamEnd() <= 0 && !playbackController.isSeeking()) { - eventBus.trigger(Events.PLAYBACK_ENDED, { 'isLast': getActiveStreamInfo().isLast }); - } - }, PLAYBACK_ENDED_TIMER_INTERVAL); - } - } - - /** - * Stop the check if the playback has ended - * @private - */ - function _stopPlaybackEndedTimerInterval() { - if (playbackEndedTimerInterval) { - clearInterval(playbackEndedTimerInterval); - playbackEndedTimerInterval = null; - } - } - - /** - * Returns a playhead time, in seconds, converted to be relative - * to the start of an identified stream/period or null if no such stream - * @param {number} time - * @param {string} id - * @returns {number|null} - */ - function getTimeRelativeToStreamId(time, id) { - let stream = null; - let baseStart = 0; - let streamStart = 0; - let streamDur = null; - - for (let i = 0; i < streams.length; i++) { - stream = streams[i]; - streamStart = stream.getStartTime(); - streamDur = stream.getDuration(); - - // use start time, if not undefined or NaN or similar - if (Number.isFinite(streamStart)) { - baseStart = streamStart; - } - - if (stream.getId() === id) { - return time - baseStart; - } else { - // use duration if not undefined or NaN or similar - if (Number.isFinite(streamDur)) { - baseStart += streamDur; - } - } - } - - return null; - } - - /** - * Returns the streamProcessors of the active stream. - * @return {array} - */ - function getActiveStreamProcessors() { - return activeStream ? activeStream.getStreamProcessors() : []; - } - - /** - * Once playback has ended we switch to the next stream - * @param {object} e - */ - function _onPlaybackEnded(e) { - if (activeStream && !activeStream.getIsEndedEventSignaled()) { - activeStream.setIsEndedEventSignaled(true); - const nextStream = _getNextStream(); - if (nextStream) { - logger.debug(`StreamController onEnded, found next stream with id ${nextStream.getStreamInfo().id}. Switching from ${activeStream.getStreamInfo().id} to ${nextStream.getStreamInfo().id}`); - _switchStream(nextStream, activeStream, NaN); - } else { - logger.debug('StreamController no next stream found'); - activeStream.setIsEndedEventSignaled(false); - } - _flushPlaylistMetrics(nextStream ? PlayListTrace.END_OF_PERIOD_STOP_REASON : PlayListTrace.END_OF_CONTENT_STOP_REASON); - } - if (e && e.isLast) { - _stopPlaybackEndedTimerInterval(); - contentSteeringController.stopSteeringRequestTimer(); - } - } - - /** - * Returns the next stream to be played relative to the stream provided. If no stream is provided we use the active stream. - * In order to avoid rounding issues we should not use the duration of the periods. Instead find the stream with starttime closest to startTime of the previous stream. - * @param {object} stream - * @return {null|object} - */ - function _getNextStream(stream = null) { - const refStream = stream ? stream : activeStream ? activeStream : null; - - if (!refStream) { - return null; - } - - const refStreamInfo = refStream.getStreamInfo(); - const start = refStreamInfo.start; - let i = 0; - let targetIndex = -1; - let lastDiff = NaN; - - while (i < streams.length) { - const s = streams[i]; - const sInfo = s.getStreamInfo(); - const diff = sInfo.start - start; - - if (diff > 0 && (isNaN(lastDiff) || diff < lastDiff) && refStreamInfo.id !== sInfo.id) { - lastDiff = diff; - targetIndex = i; - } - - i += 1; - } - - if (targetIndex >= 0) { - return streams[targetIndex]; - } - - return null; - } - - /** - * Returns all upcoming streams relative to the provided stream. If no stream is provided we use the active stream. - * @param {object} stream - * @return {array} - */ - function _getNextStreams(stream = null) { - try { - const refStream = stream ? stream : activeStream ? activeStream : null; - - if (refStream) { - const refStreamInfo = refStream.getStreamInfo(); - - return streams.filter(function (stream) { - const sInfo = stream.getStreamInfo(); - return sInfo.start > refStreamInfo.start && refStreamInfo.id !== sInfo.id; - }); - } - } catch (e) { - return []; - } - } - - /** - * Sets the duration attribute of the MediaSource using the MediaSourceController. - * @param {number} duration - * @private - */ - function _setMediaDuration(duration) { - const manifestDuration = duration ? duration : getActiveStreamInfo().manifestInfo.duration; - mediaSourceController.setDuration(manifestDuration); - } - - /** - * Returns the active stream - * @return {object} - */ - function getActiveStream() { - return activeStream; - } - - /** - * Initial playback indicates if we have called play() for the first time yet. - * @return {*} - */ - function getInitialPlayback() { - return initialPlayback; - } - - /** - * Auto Play indicates if the stream starts automatically as soon as it is initialized. - * @return {boolean} - */ - function getAutoPlay() { - return autoPlay; - } - - /** - * Called once the first stream has been initialized. We only use this function to seek to the right start time. - * @return {number} - * @private - */ - function _getInitialStartTime() { - // Seek new stream in priority order: - // - at start time provided via the application - // - at start time provided in URI parameters - // - at stream/period start time (for static streams) or live start time (for dynamic streams) - let startTime; - const isDynamic = adapter.getIsDynamic(); - if (isDynamic) { - // For dynamic stream, start by default at (live edge - live delay) - const dvrInfo = dashMetrics.getCurrentDVRInfo(); - const liveEdge = dvrInfo && dvrInfo.range ? dvrInfo.range.end : 0; - // we are already in the right start period. so time should not be smaller than period@start and should not be larger than period@end - startTime = liveEdge - playbackController.getOriginalLiveDelay(); - // If start time in URI, take min value between live edge time and time from URI (capped by DVR window range) - const dvrWindow = dvrInfo ? dvrInfo.range : null; - if (dvrWindow) { - // If start time was provided by the application as part of the call to initialize() or attachSource() use this value - if (!isNaN(providedStartTime) || providedStartTime.toString().indexOf('posix:') !== -1) { - logger.info(`Start time provided by the app: ${providedStartTime}`); - const providedStartTimeAsPresentationTime = _getStartTimeFromProvidedData(true, providedStartTime) - if (!isNaN(providedStartTimeAsPresentationTime)) { - // Do not move closer to the live edge as defined by live delay - startTime = Math.min(startTime, providedStartTimeAsPresentationTime); - } - } else { - // #t shall be relative to period start - const startTimeFromUri = _getStartTimeFromUriParameters(true); - if (!isNaN(startTimeFromUri)) { - logger.info(`Start time from URI parameters: ${startTimeFromUri}`); - // Do not move closer to the live edge as defined by live delay - startTime = Math.min(startTime, startTimeFromUri); - } - } - // If calcFromSegmentTimeline is enabled we saw problems caused by the MSE.seekableRange when starting at dvrWindow.start. Apply a small offset to avoid this problem. - const offset = settings.get().streaming.timeShiftBuffer.calcFromSegmentTimeline ? 0.1 : 0; - startTime = Math.max(startTime, dvrWindow.start + offset); - } - } else { - // For static stream, start by default at period start - const streams = getStreams(); - const streamInfo = streams[0].getStreamInfo(); - startTime = streamInfo.start; - - // If start time was provided by the application as part of the call to initialize() or attachSource() use this value - if (!isNaN(providedStartTime)) { - logger.info(`Start time provided by the app: ${providedStartTime}`); - const providedStartTimeAsPresentationTime = _getStartTimeFromProvidedData(false, providedStartTime) - if (!isNaN(providedStartTimeAsPresentationTime)) { - // Do not play earlier than the start of the first period - startTime = Math.max(startTime, providedStartTimeAsPresentationTime); - } - } else { - // If start time in URI, take max value between period start and time from URI (if in period range) - const startTimeFromUri = _getStartTimeFromUriParameters(false); - if (!isNaN(startTimeFromUri)) { - logger.info(`Start time from URI parameters: ${startTimeFromUri}`); - // Do not play earlier than the start of the first period - startTime = Math.max(startTime, startTimeFromUri); - } - } - } - - return startTime; - } - - /** - * 23009-1 Annex C.4 defines MPD anchors to use URI fragment syntax to start a presentation at a given time and a given state - * @param {boolean} isDynamic - * @return {number} - * @private - */ - function _getStartTimeFromUriParameters(isDynamic) { - const fragData = uriFragmentModel.getURIFragmentData(); - if (!fragData || !fragData.t) { - return NaN; - } - const refStream = getStreams()[0]; - const referenceTime = refStream.getStreamInfo().start; - fragData.t = fragData.t.split(',')[0]; - - return _getStartTimeFromString(isDynamic, fragData.t, referenceTime); - } - - /** - * Calculate start time using the value that was provided via the application as part of attachSource() or initialize() - * @param {boolean} isDynamic - * @param {number | string} providedStartTime - * @return {number} - * @private - */ - function _getStartTimeFromProvidedData(isDynamic, providedStartTime) { - let referenceTime = 0; - - if (!isDynamic) { - const refStream = getStreams()[0]; - referenceTime = refStream.getStreamInfo().start; - } - - return _getStartTimeFromString(isDynamic, providedStartTime, referenceTime); - } - - - function _getStartTimeFromString(isDynamic, targetValue, referenceTime) { - // Consider only start time of MediaRange - // TODO: consider end time of MediaRange to stop playback at provided end time - // "t=