diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1fe5f517ab..0dd6a53512 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -21,6 +21,7 @@ - [RazeLighter777](https://github.com/RazeLighter777) - [LogicalPhallacy](https://github.com/LogicalPhallacy) - [thornbill](https://github.com/thornbill) + - [redSpoutnik](https://github.com/redSpoutnik) # Emby Contributors diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js index d25b49163b..7d5fcaeda6 100644 --- a/src/components/htmlvideoplayer/plugin.js +++ b/src/components/htmlvideoplayer/plugin.js @@ -188,6 +188,9 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa var currentAssRenderer; var customTrackIndex = -1; + var showTrackOffset = false; + var currentTrackOffset; + var videoSubtitlesElem; var currentTrackEvents; @@ -543,6 +546,81 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa setCurrentTrackElement(index); }; + self.enableShowingSubtitleOffset = function() { + showTrackOffset = true; + } + + self.disableShowingSubtitleOffset = function() { + showTrackOffset = false; + } + + self.isShowingSubtitleOffsetEnabled = function() { + return showTrackOffset; + } + + self.setSubtitleOffset = function(offset) { + + var offsetValue = parseFloat(offset); + var videoElement = self._mediaElement; + var mediaStreamTextTracks = getMediaStreamTextTracks(self._currentPlayOptions.mediaSource); + + Array.from(videoElement.textTracks) + .filter(function(trackElement) { + if (customTrackIndex === -1 ) { + // get showing .vtt textTacks + return trackElement.mode === 'showing'; + } else { + // get current .ass textTrack + return ("textTrack" + customTrackIndex) === trackElement.id; + } + }) + .forEach(function(trackElement) { + + var track = mediaStreamTextTracks.filter(function(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(function(cue) { + cue.startTime -= offsetValue; + cue.endTime -= offsetValue; + }); + } + + } + + self.getSubtitleOffset = function() { + return currentTrackOffset; + } + function isAudioStreamSupported(stream, deviceProfile) { var codec = (stream.Codec || '').toLowerCase(); @@ -1167,6 +1245,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 fedf7924ba..d9ea0a905b 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1635,6 +1635,46 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla getPlayerData(player).subtitleStreamIndex = index; }; + self.supportSubtitleOffset = function(player) { + player = player || self._currentPlayer; + return player && 'setSubtitleOffset' in player; + } + + self.enableShowingSubtitleOffset = function(player) { + player = player || self._currentPlayer; + player.enableShowingSubtitleOffset(); + } + + self.disableShowingSubtitleOffset = function(player) { + player = player || self._currentPlayer; + player.disableShowingSubtitleOffset(); + } + + self.isShowingSubtitleOffsetEnabled = function(player) { + player = player || self._currentPlayer; + return player.isShowingSubtitleOffsetEnabled(); + } + + self.isSubtitleStreamExternal = function(index, player) { + var stream = getSubtitleStream(player, index); + return stream ? getDeliveryMethod(stream) === 'External' : false; + } + + self.setSubtitleOffset = function (value, player) { + player = player || self._currentPlayer; + player.setSubtitleOffset(value); + }; + + self.getPlayerSubtitleOffset = function(player) { + player = player || self._currentPlayer; + return player.getSubtitleOffset(); + } + + self.canHandleOffsetOnCurrentSubtitle = function(player) { + var index = self.getSubtitleStreamIndex(player); + return index !== -1 && self.isSubtitleStreamExternal(index, player); + } + self.seek = function (ticks, player) { ticks = Math.max(0, ticks); diff --git a/src/components/playback/playersettingsmenu.js b/src/components/playback/playersettingsmenu.js index acfba81812..9d5b1f08ba 100644 --- a/src/components/playback/playersettingsmenu.js +++ b/src/components/playback/playersettingsmenu.js @@ -213,6 +213,15 @@ define(['connectionManager', 'actionsheet', 'datetime', 'playbackManager', 'glob }); } + if (options.suboffset) { + + menuItems.push({ + name: globalize.translate('SubtitleOffset'), + id: 'suboffset', + asideText: null + }); + } + return actionsheet.show({ items: menuItems, positionTo: options.positionTo @@ -248,6 +257,11 @@ define(['connectionManager', 'actionsheet', 'datetime', 'playbackManager', 'glob options.onOption('stats'); } return Promise.resolve(); + case 'suboffset': + if (options.onOption) { + options.onOption('suboffset'); + } + return Promise.resolve(); default: break; } diff --git a/src/components/subtitlesync/subtitlesync.css b/src/components/subtitlesync/subtitlesync.css new file mode 100644 index 0000000000..112e624722 --- /dev/null +++ b/src/components/subtitlesync/subtitlesync.css @@ -0,0 +1,48 @@ +.subtitleSyncContainer { + width: 40%; + margin-left: 30%; + margin-right: 30%; + height: 4.2em; + background: rgba(28,28,28,0.8); + border-radius: .3em; + color: #fff; + position: absolute; +} + +.subtitleSync-closeButton { + position: absolute; + top: 0; + right: 0; + color: #ccc; + z-index: 2; +} + +.subtitleSyncTextField { + position: absolute; + left: 0; + width: 40%; + margin-left: 30%; + margin-right: 30%; + top: 0.1em; + text-align: center; + font-size: 20px; + color: white; + z-index: 2; +} + +#prompt { + flex-shrink: 0; +} + +.subtitleSyncSliderContainer { + width: 98%; + margin-left: 1%; + margin-right: 1%; + top: 2.5em; + height: 1.4em; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + flex-grow: 1; + border-radius: .3em; + z-index: 1; +} \ No newline at end of file diff --git a/src/components/subtitlesync/subtitlesync.js b/src/components/subtitlesync/subtitlesync.js new file mode 100644 index 0000000000..3389b7bf89 --- /dev/null +++ b/src/components/subtitlesync/subtitlesync.js @@ -0,0 +1,163 @@ +define(['playbackManager', 'text!./subtitlesync.template.html', 'css!./subtitlesync'], function (playbackManager, template, css) { + "use strict"; + + var player; + var subtitleSyncSlider; + var subtitleSyncTextField; + var subtitleSyncCloseButton; + var subtitleSyncContainer; + + function init(instance) { + + var parent = document.createElement('div'); + parent.innerHTML = template; + + subtitleSyncSlider = parent.querySelector(".subtitleSyncSlider"); + subtitleSyncTextField = parent.querySelector(".subtitleSyncTextField"); + subtitleSyncCloseButton = parent.querySelector(".subtitleSync-closeButton"); + subtitleSyncContainer = parent.querySelector(".subtitleSyncContainer"); + + subtitleSyncContainer.classList.add("hide"); + + subtitleSyncTextField.updateOffset = function(offset) { + this.textContent = offset + "s"; + } + + subtitleSyncTextField.addEventListener("keypress", function(event) { + + if(event.key === "Enter"){ + // if input key is enter search for float pattern + var inputOffset = /[-+]?\d+\.?\d*/g.exec(this.textContent); + if(inputOffset) { + inputOffset = inputOffset[0]; + + // replace current text by considered offset + this.textContent = inputOffset + "s"; + + inputOffset = parseFloat(inputOffset); + // set new offset + playbackManager.setSubtitleOffset(inputOffset, player); + // synchronize with slider value + subtitleSyncSlider.updateOffset( + getPercentageFromOffset(inputOffset)); + } else { + this.textContent = (playbackManager.getPlayerSubtitleOffset(player) || 0) + "s"; + } + this.hasFocus = false; + event.preventDefault(); + } else { + // keep focus to prevent fade with bottom layout + this.hasFocus = true; + if(event.key.match(/[+-\d.s]/) === null) { + event.preventDefault(); + } + } + }); + + subtitleSyncSlider.updateOffset = function(percent) { + // default value is 0s = 50% + this.value = percent === undefined ? 50 : percent; + } + + subtitleSyncSlider.addEventListener("change", function () { + // set new offset + playbackManager.setSubtitleOffset(getOffsetFromPercentage(this.value), player); + // synchronize with textField value + subtitleSyncTextField.updateOffset( + getOffsetFromPercentage(this.value)); + }); + + subtitleSyncSlider.addEventListener("touchmove", function () { + // set new offset + playbackManager.setSubtitleOffset(getOffsetFromPercentage(this.value), player); + // synchronize with textField value + subtitleSyncTextField.updateOffset( + getOffsetFromPercentage(this.value)); + }); + + subtitleSyncSlider.getBubbleHtml = function (value) { + var newOffset = getOffsetFromPercentage(value); + return '