From 456e20ce4edcc3cbab5e4a006a52ef29840f9d8e Mon Sep 17 00:00:00 2001 From: Sebastian Piquerez Date: Tue, 23 Sep 2025 15:59:52 -0300 Subject: [PATCH 1/2] feat: Implement live to live switch back --- samples/alternative/index.html | 1 - src/streaming/MediaManager.js | 29 ++++++-------- .../controllers/AlternativeMediaController.js | 38 +++++++++---------- src/streaming/controllers/EventController.js | 29 +++++++++++++- .../controllers/PlaybackController.js | 22 +++++++++++ 5 files changed, 79 insertions(+), 40 deletions(-) diff --git a/samples/alternative/index.html b/samples/alternative/index.html index dc9eaf6942..de1f772cc1 100644 --- a/samples/alternative/index.html +++ b/samples/alternative/index.html @@ -35,7 +35,6 @@ video = document.getElementById('video-element'); alternativeVideo = document.getElementById('alternative-video-element'); player = dashjs.MediaPlayer().create(); - alternativeVideo = document.querySelector('#alternativeVideo'); player.initialize(video, url, false); player.setAlternativeVideoElement(alternativeVideo); } diff --git a/src/streaming/MediaManager.js b/src/streaming/MediaManager.js index 2700ae737c..17bba0389d 100644 --- a/src/streaming/MediaManager.js +++ b/src/streaming/MediaManager.js @@ -40,7 +40,6 @@ function MediaManager() { isSwitching = false, hideAlternativePlayerControls = false, altPlayer, - fullscreenDiv, playbackController, altVideoElement, alternativeContext, @@ -84,17 +83,6 @@ function MediaManager() { logger = debug.getLogger(instance); - if (!fullscreenDiv) { - fullscreenDiv = document.createElement('div'); - fullscreenDiv.id = 'fullscreenDiv'; - const videoElement = videoModel.getElement(); - const parentNode = videoElement && videoElement.parentNode; - if (parentNode) { - parentNode.insertBefore(fullscreenDiv, videoElement); - fullscreenDiv.appendChild(videoElement); - } - } - document.addEventListener('fullscreenchange', () => { if (document.fullscreenElement === videoModel.getElement()) { // TODO: Implement fullscreen @@ -117,7 +105,9 @@ function MediaManager() { const prebufferedPlayer = MediaPlayer().create(); prebufferedPlayer.initialize(null, alternativeMpdUrl, false, NaN); prebufferedPlayer.updateSettings({ - streaming: {cacheInitSegments: true} + streaming: { + cacheInitSegments: true + } }); prebufferedPlayer.preload(); prebufferedPlayer.setAutoPlay(false); @@ -162,7 +152,6 @@ function MediaManager() { if (altPlayer) { altPlayer.off(Events.ERROR, onAlternativePlayerError, this); } - altPlayer = MediaPlayer().create(); altPlayer.updateSettings({ streaming: { @@ -201,14 +190,13 @@ function MediaManager() { } if (altPlayer && altVideoElement) { + altVideoElement.style.display = 'block'; altPlayer.attachView(altVideoElement); } videoModel.pause(); - logger.debug('Main video paused'); - videoModel.getElement().style.display = 'none'; - altVideoElement.style.display = 'block'; + logger.debug('Main video paused'); if (time) { logger.debug(`Seeking alternative content to time: ${time}`); @@ -242,7 +230,12 @@ function MediaManager() { if (playbackController.getIsDynamic()) { logger.debug('Seeking to original live point for dynamic manifest'); - playbackController.seekToOriginalLive(true, false, false); + if (seekTime > playbackController.getDvrWindowStart()) { + playbackController.seek(seekTime, false, false); + } else { + logger.warn('Seek time is before DVR window start, seeking to start of DVR window'); + playbackController.seekToDvrWindowStart(); + } } else { logger.debug(`Seeking main content to time: ${seekTime}`); playbackController.seek(seekTime, false, false); diff --git a/src/streaming/controllers/AlternativeMediaController.js b/src/streaming/controllers/AlternativeMediaController.js index 894518d349..ff3abfa9bb 100644 --- a/src/streaming/controllers/AlternativeMediaController.js +++ b/src/streaming/controllers/AlternativeMediaController.js @@ -41,25 +41,6 @@ function AlternativeMediaController() { const context = this.context; const eventBus = EventBus(context).getInstance(); - function _calculateSeekTime(currentEvent, altPlayer) { - let seekTime; - if (currentEvent.mode === Constants.ALTERNATIVE_MPD.MODES.REPLACE) { - if (currentEvent.returnOffset || currentEvent.returnOffset === 0) { - seekTime = currentEvent.presentationTime + currentEvent.returnOffset; - logger.debug(`Using return offset - seeking to: ${seekTime}`); - } else { - const alternativeDuration = altPlayer.duration() - const alternativeEffectiveDuration = !isNaN(currentEvent.maxDuration) ? Math.min(currentEvent.maxDuration, alternativeDuration) : alternativeDuration - seekTime = currentEvent.presentationTime + alternativeEffectiveDuration; - logger.debug(`Using alternative duration - seeking to: ${seekTime}`); - } - } else if (currentEvent.mode === Constants.ALTERNATIVE_MPD.MODES.INSERT) { - seekTime = currentEvent.presentationTime; - logger.debug(`Insert mode - seeking to original presentation time: ${seekTime}`); - } - return seekTime; - } - let instance, debug, logger, @@ -314,6 +295,25 @@ function AlternativeMediaController() { } } + function _calculateSeekTime(currentEvent, altPlayer) { + let seekTime; + if (currentEvent.mode === Constants.ALTERNATIVE_MPD.MODES.REPLACE) { + if (currentEvent.returnOffset || currentEvent.returnOffset === 0) { + seekTime = currentEvent.presentationTime + currentEvent.returnOffset; + logger.debug(`Using return offset - seeking to: ${seekTime}`); + } else { + const alternativeDuration = altPlayer.duration() + const alternativeEffectiveDuration = !isNaN(currentEvent.maxDuration) ? Math.min(currentEvent.maxDuration, alternativeDuration) : alternativeDuration + seekTime = currentEvent.presentationTime + alternativeEffectiveDuration; + logger.debug(`Using alternative duration - seeking to: ${seekTime}`); + } + } else if (currentEvent.mode === Constants.ALTERNATIVE_MPD.MODES.INSERT) { + seekTime = currentEvent.presentationTime; + logger.debug(`Insert mode - seeking to original presentation time: ${seekTime}`); + } + return seekTime; + } + function _resetAlternativeSwitchStates() { currentEvent = null; actualEventPresentationTime = 0; diff --git a/src/streaming/controllers/EventController.js b/src/streaming/controllers/EventController.js index 2bb08f31c8..9256e965eb 100644 --- a/src/streaming/controllers/EventController.js +++ b/src/streaming/controllers/EventController.js @@ -502,6 +502,29 @@ function EventController() { } } + /** + * Auxiliary method to check for earliest resolution time events and return alternative MPD + * @param {object} event + * @return {object|null} - Returns the alternative MPD if it exists and has earliestResolutionTimeOffset, null otherwise + * @private + */ + function _checkForEarliestResolutionTimeEvents(event) { + try { + if (!event || !event.alternativeMpd) { + return null; + } + + if (event.alternativeMpd.earliestResolutionTimeOffset !== undefined) { + return event.alternativeMpd; + } + + return null; + } catch (e) { + logger.error(e); + return null; + } + } + /** * Checks if the event has an earliestResolutionTimeOffset and if it's ready to resolve * @param {object} event @@ -511,11 +534,13 @@ function EventController() { */ function _checkEventReadyToResolve(event, currentVideoTime) { try { - if (!event.alternativeMpd.earliestResolutionTimeOffset || event.triggeredReadyToResolve) { + const erarlyToResolveEvent = _checkForEarliestResolutionTimeEvents(event); + + if (!erarlyToResolveEvent || event.triggeredReadyToResolve) { return false; } - const resolutionTime = event.calculatedPresentationTime - event.alternativeMpd .earliestResolutionTimeOffset; + const resolutionTime = event.calculatedPresentationTime - erarlyToResolveEvent.earliestResolutionTimeOffset; return currentVideoTime >= resolutionTime; } catch (e) { logger.error(e); diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index fad48959d5..99cfee5a00 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -267,6 +267,25 @@ function PlaybackController() { seek(seektime, stickToBuffered, internal, adjustLiveDelay); } + function seekToStartDvrWindow(stickToBuffered = false, internal = false, adjustLiveDelay = false) { + const dvrWindowStart = getDvrWindowStart(); + + if (dvrWindowStart === 0) { + return; + } + + seek(dvrWindowStart, stickToBuffered, internal, adjustLiveDelay); + } + + function getDvrWindowStart() { + if (!streamInfo || !videoModel || !isDynamic) { + return; + } + const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; + const dvrInfo = dashMetrics.getCurrentDVRInfo(type); + return dvrInfo && dvrInfo.range ? dvrInfo.range.start : 0; + } + function _getDvrWindowEnd() { if (!streamInfo || !videoModel || !isDynamic) { return; @@ -274,6 +293,7 @@ function PlaybackController() { const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; const dvrInfo = dashMetrics.getCurrentDVRInfo(type); + console.log(dvrInfo); return dvrInfo && dvrInfo.range ? dvrInfo.range.end : 0; } @@ -922,6 +942,7 @@ function PlaybackController() { getAvailabilityStartTime, getBufferLevel, getCurrentLiveLatency, + getDvrWindowStart, getEnded, getInitialCatchupModeActivated, getIsDynamic, @@ -947,6 +968,7 @@ function PlaybackController() { seek, seekToCurrentLive, seekToOriginalLive, + seekToStartDvrWindow, setConfig, updateCurrentTime, }; From a909d2fb6c8350a862a748ed47e337a23d9e313f Mon Sep 17 00:00:00 2001 From: Sebastian Piquerez Date: Thu, 25 Sep 2025 15:26:56 -0300 Subject: [PATCH 2/2] fix: Correct variable name typo in EventController and remove console log in PlaybackController --- src/streaming/controllers/EventController.js | 6 +++--- src/streaming/controllers/PlaybackController.js | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/streaming/controllers/EventController.js b/src/streaming/controllers/EventController.js index 9256e965eb..3a2abf9ec7 100644 --- a/src/streaming/controllers/EventController.js +++ b/src/streaming/controllers/EventController.js @@ -534,13 +534,13 @@ function EventController() { */ function _checkEventReadyToResolve(event, currentVideoTime) { try { - const erarlyToResolveEvent = _checkForEarliestResolutionTimeEvents(event); + const earlyToResolveEvent = _checkForEarliestResolutionTimeEvents(event); - if (!erarlyToResolveEvent || event.triggeredReadyToResolve) { + if (!earlyToResolveEvent || event.triggeredReadyToResolve) { return false; } - const resolutionTime = event.calculatedPresentationTime - erarlyToResolveEvent.earliestResolutionTimeOffset; + const resolutionTime = event.calculatedPresentationTime - earlyToResolveEvent.earliestResolutionTimeOffset; return currentVideoTime >= resolutionTime; } catch (e) { logger.error(e); diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 99cfee5a00..f6b68cb2e6 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -293,7 +293,6 @@ function PlaybackController() { const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; const dvrInfo = dashMetrics.getCurrentDVRInfo(type); - console.log(dvrInfo); return dvrInfo && dvrInfo.range ? dvrInfo.range.end : 0; }