diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js index d25b49163b..ca93034260 100644 --- a/src/components/htmlvideoplayer/plugin.js +++ b/src/components/htmlvideoplayer/plugin.js @@ -188,6 +188,8 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa var currentAssRenderer; var customTrackIndex = -1; + var currentTrackOffset; + var videoSubtitlesElem; var currentTrackEvents; @@ -543,6 +545,65 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa setCurrentTrackElement(index); }; + self.setSubtitleOffset = function(offset) { + + var offsetValue = parseFloat(offset); + var videoElement = self._mediaElement; + var mediaStreamTextTracks = getMediaStreamTextTracks(self._currentPlayOptions.mediaSource); + + Array.from(videoElement.textTracks) + .filter(trackElement => { + if (customTrackIndex === -1 ) { + // get showing .vtt textTacks + return trackElement.mode === 'showing'; + } else { + // get current .ass textTrack + return ("textTrack" + customTrackIndex) === trackElement.id; + } + }) + .forEach(trackElement => { + + var track = mediaStreamTextTracks.filter(stream => { + return ("textTrack" + stream.Index) === trackElement.id; + })[0]; + + if(track) { + offsetValue = updateCurrentTrackOffset(offsetValue); + var format = (track.Codec || '').toLowerCase(); + if (format !== 'ass' && format !== 'ssa') { + setVttSubtitleOffset(trackElement, offsetValue); + } + } else { + console.log("No available track, cannot apply offset : " + offsetValue); + } + + }); + }; + + function updateCurrentTrackOffset(offsetValue) { + + var relativeOffset = offsetValue; + var newTrackOffset = offsetValue; + if(currentTrackOffset){ + relativeOffset -= currentTrackOffset; + } + currentTrackOffset = newTrackOffset; + // relative to currentTrackOffset + return relativeOffset; + } + + function setVttSubtitleOffset(currentTrack, offsetValue) { + + if(currentTrack.cues) { + Array.from(currentTrack.cues) + .forEach(cue => { + cue.startTime -= offsetValue; + cue.endTime -= offsetValue; + }); + } + + } + function isAudioStreamSupported(stream, deviceProfile) { var codec = (stream.Codec || '').toLowerCase(); @@ -1167,6 +1228,11 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa function updateSubtitleText(timeMs) { + // handle offset for ass tracks + if(currentTrackOffset) { + timeMs += (currentTrackOffset * 1000); + } + var clock = currentClock; if (clock) { try { diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 33b0b2b793..1588be5d22 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1635,6 +1635,28 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla getPlayerData(player).subtitleStreamIndex = index; }; + self.supportSubtitleOffset = function(player) { + player = player || self._currentPlayer; + return 'setSubtitleOffset' in player; + } + + self.isSubtitleStreamExternal = function(index, player) { + var stream = getSubtitleStream(player, index); + return stream ? getDeliveryMethod(stream) === 'External' : false; + } + + self.setSubtitleOffset = function (sliderValue, player) { + player = player || self._currentPlayer; + player.setSubtitleOffset(self.getOffsetFromSliderValue(sliderValue)); + }; + + self.getOffsetFromSliderValue = function(value) { + var offset = (value - 50) / 50; + // multiply by offset min/max range value (-x to +x) : + offset *= 30; + return offset.toFixed(1); + }; + self.seek = function (ticks, player) { ticks = Math.max(0, ticks); diff --git a/src/controllers/videoosd.js b/src/controllers/videoosd.js index c9befe03c5..00ce862f23 100644 --- a/src/controllers/videoosd.js +++ b/src/controllers/videoosd.js @@ -280,6 +280,7 @@ define(["playbackManager", "dom", "inputmanager", "datetime", "itemHelper", "med btnFastForward.disabled = true; btnRewind.disabled = true; view.querySelector(".btnSubtitles").classList.add("hide"); + view.querySelector(".subtitleSyncSliderContainer").classList.add("hide"); view.querySelector(".btnAudio").classList.add("hide"); view.querySelector(".osdTitle").innerHTML = ""; view.querySelector(".osdMediaInfo").innerHTML = ""; @@ -295,8 +296,23 @@ define(["playbackManager", "dom", "inputmanager", "datetime", "itemHelper", "med if (playbackManager.subtitleTracks(player).length) { view.querySelector(".btnSubtitles").classList.remove("hide"); + + if(playbackManager.supportSubtitleOffset()) { + var index = playbackManager.getSubtitleStreamIndex(player); + // if there is an external subtitle stream enabled + if(index !== -1 && playbackManager.isSubtitleStreamExternal(index, player)){ + // show subtitle sync slider + subtitleSyncSliderContainer.classList.remove("hide"); + }else{ + // hide subtitle sync slider + subtitleSyncSliderContainer.classList.add("hide"); + } + } + } else { view.querySelector(".btnSubtitles").classList.add("hide"); + // hide subtitle sync slider + subtitleSyncSliderContainer.classList.add("hide"); } if (playbackManager.audioTracks(player).length > 1) { @@ -1008,6 +1024,28 @@ define(["playbackManager", "dom", "inputmanager", "datetime", "itemHelper", "med if (index !== currentIndex) { playbackManager.setSubtitleStreamIndex(index, player); } + return id; + + }).then(function (id) { + var index = parseInt(id); + // on subtitle stream change + if (playbackManager.supportSubtitleOffset() && index !== currentIndex) { + + /// if there is an external subtitle stream enabled + if (index !== -1 && playbackManager.isSubtitleStreamExternal(index, player)){ + + // set default offset to '0' (slider's middle value) + var subtitleSyncSliderMiddleValue = 50; + subtitleSyncSlider.value = subtitleSyncSliderMiddleValue.toString(); + playbackManager.setSubtitleOffset(subtitleSyncSliderMiddleValue, player); + + // show subtitle sync slider + subtitleSyncSliderContainer.classList.remove("hide"); + } else { + // hide subtitle sync slider + subtitleSyncSliderContainer.classList.add("hide"); + } + } }); }); } @@ -1146,6 +1184,8 @@ define(["playbackManager", "dom", "inputmanager", "datetime", "itemHelper", "med var programStartDateMs = 0; var programEndDateMs = 0; var playbackStartTimeTicks = 0; + var subtitleSyncSlider = view.querySelector(".subtitleSyncSlider"); + var subtitleSyncSliderContainer = view.querySelector(".subtitleSyncSliderContainer"); var nowPlayingVolumeSlider = view.querySelector(".osdVolumeSlider"); var nowPlayingVolumeSliderContainer = view.querySelector(".osdVolumeSliderContainer"); var nowPlayingPositionSlider = view.querySelector(".osdPositionSlider"); @@ -1268,6 +1308,21 @@ define(["playbackManager", "dom", "inputmanager", "datetime", "itemHelper", "med nowPlayingVolumeSlider.addEventListener("touchmove", function () { playbackManager.setVolume(this.value, currentPlayer); }); + + subtitleSyncSlider.addEventListener("change", function () { + playbackManager.setSubtitleOffset(this.value, currentPlayer); + }); + subtitleSyncSlider.addEventListener("touchmove", function () { + playbackManager.setSubtitleOffset(this.value, currentPlayer); + }); + + subtitleSyncSlider.getBubbleHtml = function (value) { + var newOffset = playbackManager.getOffsetFromSliderValue(value); + return '

' + + (newOffset > 0 ? "+" : "") + parseFloat(newOffset) + "s" + + "

"; + }; + nowPlayingPositionSlider.addEventListener("change", function () { var player = currentPlayer; diff --git a/src/css/videoosd.css b/src/css/videoosd.css index bd24e41309..5151f87d63 100644 --- a/src/css/videoosd.css +++ b/src/css/videoosd.css @@ -144,6 +144,13 @@ flex-grow: 1 } +.subtitleSyncSliderContainer { + width: 12em; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + flex-grow: 1 +} + .osdMediaInfo, .volumeButtons { display: -webkit-box; diff --git a/src/videoosd.html b/src/videoosd.html index e70adbe39c..d5c117a4d4 100644 --- a/src/videoosd.html +++ b/src/videoosd.html @@ -59,6 +59,13 @@ + +
+
+ +
+
+