diff --git a/src/controllers/playback/video/index.html b/src/controllers/playback/video/index.html index a460ee8f6a3..d7b344d4b1b 100644 --- a/src/controllers/playback/video/index.html +++ b/src/controllers/playback/video/index.html @@ -6,6 +6,12 @@
+
+ +
diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 2adad5708c3..5b81eebc7f1 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -365,7 +365,7 @@ export default function (view) { toggleSubtitleSync('hide'); // Firefox does not blur by itself - if (document.activeElement) { + if (document.activeElement && !skipButton.contains(document.activeElement)) { document.activeElement.blur(); } } @@ -517,9 +517,95 @@ export default function (view) { updatePlaylist(); enableStopOnBack(true); updatePlaybackRate(player); + getIntroTimestamps(state.NowPlayingItem); } } + function secureFetch(url) { + const apiClient = ServerConnections.currentApiClient(); + const address = apiClient.serverAddress(); + const reqInit = { + headers: { + "Authorization": `MediaBrowser Token=${apiClient.accessToken()}` + } + }; + return fetch(`${address}${url}`, reqInit).then(r => { + return r.ok ? r.json() : null; + }); + } + + function getIntroTimestamps(item) { + secureFetch(`/Episode/${item.Id}/IntroSkipperSegments`).then(segments => { + skipSegments = segments; + hasCreditsSegment = Object.keys(segments).some(key => key === "Credits"); + }).catch(err => { + skipSegments = {}; + hasCreditsSegment = false; }); + secureFetch(`/Intros/UserInterfaceConfiguration`).then(config => { + skipButton.dataset.Introduction = config.SkipButtonIntroText; + skipButton.dataset.Credits = config.SkipButtonEndCreditsText; + }).catch(err => { + skipButton.dataset.Introduction = 'Skip Intro'; + skipButton.dataset.Credits = 'Next'; }); + } + + function getCurrentSegment(position) { + for (const [key, segment] of Object.entries(skipSegments)) { + if ((position > segment.ShowSkipPromptAt && position < segment.HideSkipPromptAt - 1) || + (currentVisibleMenu === 'osd' && position > segment.IntroStart && position < segment.IntroEnd - 1)) { + segment.SegmentType = key; + return segment; + } + } + return { SegmentType: "None" }; + } + + function videoPositionChanged(currentTime) { + const embyButton = skipButton.querySelector(".emby-button"); + const segmentType = getCurrentSegment(currentTime / TICKS_PER_SECOND).SegmentType; + if (segmentType === "None") { + if (!skipButton.classList.contains('show')) return; + skipButton.classList.remove('show'); + embyButton.addEventListener("transitionend", () => { + skipButton.classList.add("hide"); + if (!currentVisibleMenu) { + embyButton.blur(); + } else { + _focus(osdBottomElement.querySelector('.btnPause')); + } + }, { once: true }); + return; + } + skipButton.querySelector("#btnSkipSegmentText").textContent = skipButton.dataset[segmentType]; + if (!skipButton.classList.contains("hide")) { + if (!currentVisibleMenu && !embyButton.contains(document.activeElement)) _focus(embyButton); + return; + } + requestAnimationFrame(() => { + skipButton.classList.remove("hide"); + requestAnimationFrame(() => { + skipButton.classList.add('show'); + _focus(embyButton); + }); + }); + } + + function doSkip() { + const segment = getCurrentSegment(playbackManager.currentTime(currentPlayer) / 1000); + if (segment.SegmentType === "None") { + console.warn("[intro skipper] doSkip() called without an active segment"); + return; + } + playbackManager.seek(segment.IntroEnd * TICKS_PER_SECOND, currentPlayer); + } + + function eventHandler(e) { + if (e.key !== "Enter") return; + e.stopPropagation(); + e.preventDefault(); + doSkip(); + } + function onPlayPauseStateChanged() { if (isEnabled) { updatePlayPauseState(this.paused()); @@ -637,12 +723,13 @@ export default function (view) { const item = currentItem; refreshProgramInfoIfNeeded(player, item); showComingUpNextIfNeeded(player, item, currentTime, currentRuntimeTicks); + videoPositionChanged(currentTime); } } } function showComingUpNextIfNeeded(player, currentItem, currentTimeTicks, runtimeTicks) { - if (runtimeTicks && currentTimeTicks && !comingUpNextDisplayed && !currentVisibleMenu && currentItem.Type === 'Episode' && userSettings.enableNextVideoInfoOverlay()) { + if (!hasCreditsSegment && runtimeTicks && currentTimeTicks && !comingUpNextDisplayed && !currentVisibleMenu && currentItem.Type === 'Episode' && userSettings.enableNextVideoInfoOverlay()) { let showAtSecondsLeft = 30; if (runtimeTicks >= 50 * TICKS_PER_MINUTE) { showAtSecondsLeft = 40; @@ -1543,7 +1630,10 @@ export default function (view) { let programEndDateMs = 0; let playbackStartTimeTicks = 0; let subtitleSyncOverlay; + let skipSegments = {}; + let hasCreditsSegment; let trickplayResolution = null; + const skipButton = document.querySelector(".skipIntro"); const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider'); const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer'); const nowPlayingPositionSlider = view.querySelector('.osdPositionSlider'); @@ -1699,6 +1789,10 @@ export default function (view) { let lastPointerDown = 0; /* eslint-disable-next-line compat/compat */ dom.addEventListener(view, window.PointerEvent ? 'pointerdown' : 'click', function (e) { + if (dom.parentWithClass(e.target, ['btnSkipIntro'])) { + return; + } + if (dom.parentWithClass(e.target, ['videoOsdBottom', 'upNextContainer'])) { showOsd(); return; @@ -1854,6 +1948,8 @@ export default function (view) { }); view.querySelector('.btnAudio').addEventListener('click', showAudioTrackSelection); view.querySelector('.btnSubtitles').addEventListener('click', showSubtitleTrackSelection); + skipButton.addEventListener('click', doSkip); + skipButton.addEventListener("keydown", eventHandler); // HACK: Remove `emby-button` from the rating button to make it look like the other buttons view.querySelector('.btnUserRating').classList.remove('emby-button'); @@ -1964,4 +2060,3 @@ export default function (view) { }); } } - diff --git a/src/styles/videoosd.scss b/src/styles/videoosd.scss index 2c8c00e2601..336b2bacad3 100644 --- a/src/styles/videoosd.scss +++ b/src/styles/videoosd.scss @@ -346,3 +346,44 @@ transform: rotate(-360deg); } } + +:root { + --rounding: 4px; + --accent: 0, 164, 220; +} +.skipIntro { + position: absolute; + bottom: 7.5em; + right: 5em; + background-color: transparent; +} +.skipIntro .emby-button { + color: #ffffff; + font-size: 110%; + background: rgba(0, 0, 0, 0.7); + border-radius: var(--rounding); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.6); + transition: opacity 0.3s cubic-bezier(0.4,0,0.2,1), + transform 0.3s cubic-bezier(0.4,0,0.2,1), + background-color 0.2s ease-out, + box-shadow 0.2s ease-out; + opacity: 0; + transform: translateY(50%); +} +.skipIntro.show .emby-button { + opacity: 1; + transform: translateY(0); +} +.skipIntro .emby-button:hover { + background: rgb(var(--accent)); + box-shadow: 0 0 8px rgba(var(--accent), 0.6); + filter: brightness(1.2); +} +.skipIntro .emby-button:focus { + background: rgb(var(--accent)); + box-shadow: 0 0 8px rgba(var(--accent), 0.6); +} +.btnSkipSegmentText { + letter-spacing: 0.5px; + padding: 0 5px 0 5px; +}