diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js index b7cd049..8411771 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js @@ -1696,6 +1696,12 @@ const SlideCreator = { playerVars: playerVars, events: { 'onReady': (event) => { + // Prevent iframe from stealing focus (critical for TV mode) + const iframe = event.target.getIframe(); + if (iframe) { + iframe.setAttribute('tabindex', '-1'); + } + // Store start/end time and videoId for later use event.target._startTime = playerVars.start || 0; event.target._endTime = playerVars.end || undefined; @@ -1764,7 +1770,8 @@ const SlideCreator = { } } }, - 'onError': () => { + 'onError': (event) => { + console.warn(`YouTube player error ${event.data} for video ${videoId}`); // Fallback to next slide on error if (CONFIG.waitForTrailerToEnd) { SlideshowManager.nextSlide(); @@ -2293,109 +2300,27 @@ const SlideshowManager = { currentSlide.classList.add("active"); - if (focusSelector) { + // Restore focus for TV mode navigation continuity + requestAnimationFrame(() => { + if (focusSelector) { const target = currentSlide.querySelector(focusSelector); if (target) { - requestAnimationFrame(() => { - target.focus(); - }); + target.focus(); + return; } - } - - // Manage Video Playback: Stop others, Play current - - // 1. Pause all other YouTube players - if (STATE.slideshow.videoPlayers) { - Object.keys(STATE.slideshow.videoPlayers).forEach(id => { - if (id !== currentItemId) { - const p = STATE.slideshow.videoPlayers[id]; - if (p) { - try { - if (typeof p.pauseVideo === 'function') { - p.pauseVideo(); - } else if (p.tagName === 'VIDEO') { - p.pause(); - } - } catch (e) { console.warn("Error pausing player", id, e); } - } - } - }); - } - - // 2. Pause all other HTML5 videos e.g. local trailers - document.querySelectorAll('video').forEach(video => { - const slideParent = video.closest('.slide'); - if (slideParent && slideParent.dataset.itemId !== currentItemId) { - try { - video.pause(); - } catch (e) {} + } + // Always ensure container has focus in TV mode to keep keyboard navigation working + const isTvMode = (window.layoutManager && window.layoutManager.tv) || + document.documentElement.classList.contains('layout-tv') || + document.body.classList.contains('layout-tv'); + if (isTvMode) { + container.focus({ preventScroll: true }); } }); - // 3. Play and Reset current video - const videoBackdrop = currentSlide.querySelector('.video-backdrop'); - - // Update mute button visibility - const muteButton = document.querySelector('.mute-button'); - if (muteButton) { - const hasVideo = !!videoBackdrop; - muteButton.style.display = hasVideo ? 'block' : 'none'; - } - - if (videoBackdrop) { - if (videoBackdrop.tagName === 'VIDEO') { - videoBackdrop.currentTime = 0; - - videoBackdrop.muted = STATE.slideshow.isMuted; - if (!STATE.slideshow.isMuted) { - videoBackdrop.volume = 0.4; - } - - videoBackdrop.play().catch(e => { - // Check if it actually started playing after a short delay (handling autoplay blocks) - setTimeout(() => { - if (videoBackdrop.paused) { - console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`); - videoBackdrop.muted = true; - videoBackdrop.play().catch(err => console.error("Muted fallback failed", err)); - } - }, 1000); - }); - } else if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) { - const player = STATE.slideshow.videoPlayers[currentItemId]; - if (player && typeof player.loadVideoById === 'function' && player._videoId) { - // Use loadVideoById to enforce start and end times - player.loadVideoById({ - videoId: player._videoId, - startSeconds: player._startTime || 0, - endSeconds: player._endTime - }); - - if (STATE.slideshow.isMuted) { - player.mute(); - } else { - player.unMute(); - player.setVolume(40); - } - - // Check if playback successfully started, otherwise fallback to muted - setTimeout(() => { - if (player.getPlayerState && - player.getPlayerState() !== YT.PlayerState.PLAYING && - player.getPlayerState() !== YT.PlayerState.BUFFERING) { - console.log("YouTube loadVideoById didn't start playback, retrying muted..."); - player.mute(); - player.playVideo(); - } - }, 1000); - } else if (player && typeof player.seekTo === 'function') { - // Fallback if loadVideoById is not available or videoId missing - const startTime = player._startTime || 0; - player.seekTo(startTime); - player.playVideo(); - } - } - } + // Manage Video Playback: Stop others, Play current + this.pauseOtherVideos(currentItemId); + this.playCurrentVideo(currentSlide, currentItemId); const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled); @@ -2404,7 +2329,8 @@ const SlideshowManager = { if (backdrop && !backdrop.classList.contains("video-backdrop")) { backdrop.classList.add("animate"); } - currentSlide.querySelector(".logo").classList.add("animate"); + const logo = currentSlide.querySelector(".logo"); + if (logo) logo.classList.add("animate"); } STATE.slideshow.currentSlideIndex = index; @@ -2682,6 +2608,114 @@ const SlideshowManager = { } }, + /** + * Pauses all video players except the one with the given item ID + * @param {string} excludeItemId - Item ID to exclude from pausing + */ + pauseOtherVideos(excludeItemId) { + // Pause YouTube players + if (STATE.slideshow.videoPlayers) { + Object.keys(STATE.slideshow.videoPlayers).forEach(id => { + if (id !== excludeItemId) { + const p = STATE.slideshow.videoPlayers[id]; + if (p) { + try { + if (typeof p.pauseVideo === 'function') { + p.pauseVideo(); + if (typeof p.mute === 'function') { + p.mute(); + } + } + else if (p.tagName === 'VIDEO') { + p.pause(); + p.muted = true; + } + } catch (e) { console.warn("Error pausing player", id, e); } + } + } + }); + } + // Pause HTML5 videos + document.querySelectorAll('video').forEach(video => { + const slideParent = video.closest('.slide'); + if (slideParent && slideParent.dataset.itemId !== excludeItemId) { + try { + video.pause(); + video.muted = true; + } catch (e) {} + } + }); + }, + + /** + * Plays the video backdrop on the given slide and updates mute button visibility + * @param {Element} slide - The slide DOM element + * @param {string} itemId - The item ID of the slide + * @returns {boolean} Whether a video was found and playback attempted + */ + playCurrentVideo(slide, itemId) { + const videoBackdrop = slide.querySelector('.video-backdrop'); + + // Update mute button visibility + const muteButton = document.querySelector('.mute-button'); + if (muteButton) { + muteButton.style.display = videoBackdrop ? 'block' : 'none'; + } + + if (!videoBackdrop) return false; + + if (videoBackdrop.tagName === 'VIDEO') { + videoBackdrop.currentTime = 0; + videoBackdrop.muted = STATE.slideshow.isMuted; + if (!STATE.slideshow.isMuted) videoBackdrop.volume = 0.4; + + videoBackdrop.play().catch(() => { + setTimeout(() => { + if (videoBackdrop.paused) { + console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`); + videoBackdrop.muted = true; + videoBackdrop.play().catch(err => console.error("Muted fallback failed", err)); + } + }, 1000); + }); + return true; + } + + // YouTube player + const player = STATE.slideshow.videoPlayers?.[itemId]; + if (player && typeof player.loadVideoById === 'function' && player._videoId) { + player.loadVideoById({ + videoId: player._videoId, + startSeconds: player._startTime || 0, + endSeconds: player._endTime + }); + + if (STATE.slideshow.isMuted) { + player.mute(); + } else { + player.unMute(); + player.setVolume(40); + } + + setTimeout(() => { + if (player.getPlayerState && + player.getPlayerState() !== YT.PlayerState.PLAYING && + player.getPlayerState() !== YT.PlayerState.BUFFERING) { + console.log("YouTube loadVideoById didn't start playback, retrying muted..."); + player.mute(); + player.playVideo(); + } + }, 1000); + return true; + } else if (player && typeof player.seekTo === 'function') { + player.seekTo(player._startTime || 0); + player.playVideo(); + return true; + } + + return false; + }, + /** * Stops all video playback (YouTube and HTML5) * Used when navigating away from the home screen @@ -2711,14 +2745,16 @@ const SlideshowManager = { }); } - // 2. Pause all HTML5 videos + // 2. Stop and mute all HTML5 videos const container = document.getElementById("slides-container"); if (container) { container.querySelectorAll('video').forEach(video => { try { video.pause(); + video.muted = true; + video.currentTime = 0; } catch (e) { - console.warn("Error pausing HTML5 video:", e); + console.warn("Error stopping HTML5 video:", e); } }); } @@ -2736,20 +2772,8 @@ const SlideshowManager = { const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`); if (!currentSlide) return; - // 1. Try YouTube Player - const ytPlayer = STATE.slideshow.videoPlayers[currentItemId]; - if (ytPlayer && typeof ytPlayer.playVideo === 'function') { - ytPlayer.playVideo(); - } - - // 2. Try HTML5 Video - const html5Video = currentSlide.querySelector('video'); - if (html5Video) { - if (STATE.slideshow.isMuted) { - html5Video.muted = true; - } - html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e)); - } + // Use playCurrentVideo to properly restore video with correct mute state + this.playCurrentVideo(currentSlide, currentItemId); }, /** @@ -2822,7 +2846,7 @@ const SlideshowManager = { // Determine if we should handle navigation keys (Arrows, Space, M) // TV Mode: Strict focus required (must be on slideshow) // Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused) - const canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused); + let canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused); // Check for Input Fields (always ignore typing) const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable); @@ -3040,7 +3064,7 @@ const SlideshowManager = { finalIds.push(id); } } catch (e) { - console.warn(`Error resolving item ${id}:`, e); + console.warn(`Error resolving item ${rawId}:`, e); } } return finalIds; @@ -3438,75 +3462,25 @@ const MediaBarEnhancedSettingsManager = { * Initialize page visibility handling to pause when tab is inactive */ const initPageVisibilityHandler = () => { - let wasVideoPlayingBeforeHide = false; - document.addEventListener("visibilitychange", () => { if (document.hidden) { - console.log("Tab inactive - pausing slideshow and videos"); - wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying; - + console.log("Tab inactive - stopping all slideshow playback"); if (STATE.slideshow.slideInterval) { STATE.slideshow.slideInterval.stop(); } - - // Pause active video if playing - const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; - if (currentItemId) { - // YouTube - if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) { - const player = STATE.slideshow.videoPlayers[currentItemId]; - if (typeof player.pauseVideo === "function") { - try { - player.pauseVideo(); - STATE.slideshow.isVideoPlaying = false; - } catch (e) { - console.warn("Error pausing video on tab hide:", e); - } - } else if (player.tagName === 'VIDEO') { // HTML5 Video - player.pause(); - STATE.slideshow.isVideoPlaying = false; - } - } - } + SlideshowManager.stopAllPlayback(); } else { console.log("Tab active - resuming slideshow"); - if (!STATE.slideshow.isPaused) { - const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; - - if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) { - const player = STATE.slideshow.videoPlayers[currentItemId]; - - // YouTube - if (typeof player.playVideo === "function") { - try { - player.playVideo(); - STATE.slideshow.isVideoPlaying = true; - } catch (e) { - console.warn("Error resuming video on tab show:", e); - if (STATE.slideshow.slideInterval) { - STATE.slideshow.slideInterval.start(); - } - } - } else if (player.tagName === 'VIDEO') { // HTML5 Video - try { - player.play().catch(e => console.warn("Error resuming HTML5 video:", e)); - STATE.slideshow.isVideoPlaying = true; - } catch(e) { console.warn(e); } - } - } else { - // No video was playing, just restart interval - const activeSlide = document.querySelector('.slide.active'); - const hasVideo = activeSlide && activeSlide.querySelector('.video-backdrop'); - - if (CONFIG.waitForTrailerToEnd && hasVideo) { - // Don't restart interval if waiting for trailer - } else { - if (STATE.slideshow.slideInterval) { - STATE.slideshow.slideInterval.start(); - } - } + // Only resume if we're on the home page and not paused + const isOnHome = window.location.hash === "#/home.html" || window.location.hash === "#/home"; + const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer:not(.hide)') || document.querySelector('.youtubePlayerContainer:not(.hide)'); + + if (isOnHome && !STATE.slideshow.isPaused && !isVideoPlayerOpen) { + SlideshowManager.resumeActivePlayback(); + + if (STATE.slideshow.slideInterval && !CONFIG.waitForTrailerToEnd) { + STATE.slideshow.slideInterval.start(); } - wasVideoPlayingBeforeHide = false; } } });