diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js index c5bde24..472a116 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js @@ -1744,18 +1744,11 @@ const SlideCreator = { event.target.setPlaybackQuality(quality); } - // Only play if this is the active slide AND the slideshow is visible + // Only play if this is the active slide and not paused const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); - // Check _pendingPlay flag (set by playCurrentVideo when player wasn't ready yet) - const container = document.getElementById(`youtube-player-${itemId}`); - const hasPendingPlay = container && container._pendingPlay; - if (container) container._pendingPlay = false; - - const isActiveAndVisible = slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide')); - - if ((isActiveAndVisible || hasPendingPlay) && !STATE.slideshow.isPaused) { + if (slide && slide.classList.contains('active') && !document.hidden && !STATE.slideshow.isPaused && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) { event.target.playVideo(); // Check if it actually started playing after a short delay (handling autoplay blocks) @@ -2338,16 +2331,39 @@ const SlideshowManager = { }); // Manage Video Playback: Stop others, Play current - this.pauseOtherVideos(currentItemId); - // Check for video backdrop (also check by YouTube player ID since YT replaces div with iframe) - const hasVideoBackdrop = !!(currentSlide.querySelector('.video-backdrop') || - currentSlide.querySelector(`#youtube-player-${currentItemId}`) || - (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId])); + // 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 && typeof p.pauseVideo === 'function') { + p.pauseVideo(); + } + } + }); + } - // If paused and new slide has video, un-pause for video playback. + // 2. Pause all other HTML5 videos + document.querySelectorAll('video').forEach(video => { + if (!video.closest(`.slide[data-item-id="${currentItemId}"]`)) { + video.pause(); + } + }); + + // 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'; + } + + // Option B: If paused and new slide has video, un-pause for video playback. // If paused and new slide has only images, stay paused. - if (STATE.slideshow.isPaused && hasVideoBackdrop) { + if (STATE.slideshow.isPaused && videoBackdrop) { STATE.slideshow.isPaused = false; const pauseButton = document.querySelector('.pause-button'); if (pauseButton) { @@ -2358,14 +2374,63 @@ const SlideshowManager = { } } - if (!STATE.slideshow.isPaused) { - this.playCurrentVideo(currentSlide, currentItemId); - } else { - // Still update mute button visibility based on video presence - const muteButton = document.querySelector('.mute-button'); - if (muteButton) { - muteButton.style.display = hasVideoBackdrop ? 'block' : 'none'; + if (videoBackdrop && !STATE.slideshow.isPaused) { + 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 && currentSlide.classList.contains('active')) { + console.warn(`Autoplay blocked for ${currentItemId}, 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 (!currentSlide.classList.contains('active')) return; + 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(); + } + // If player exists but is NOT ready yet (no loadVideoById), do nothing here. + // The onReady handler will fire later, see the slide is active, and auto-play. } + // If videoBackdrop is a YouTube div but player hasn't been created yet, + // do nothing — onReady will handle it when the player becomes ready. } const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled); @@ -2401,9 +2466,7 @@ const SlideshowManager = { this.updateDots(); // Only restart interval if we are NOT waiting for a video to end - const hasVideo = currentSlide.querySelector('.video-backdrop') || - currentSlide.querySelector(`#youtube-player-${currentItemId}`) || - (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]); + const hasVideo = videoBackdrop; if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) { if (CONFIG.waitForTrailerToEnd && hasVideo) { STATE.slideshow.slideInterval.stop(); @@ -2665,9 +2728,7 @@ const SlideshowManager = { // Only restart interval if we are NOT waiting for a video to end const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`); - const hasVideo = currentSlide && (currentSlide.querySelector('.video-backdrop') || - currentSlide.querySelector(`#youtube-player-${currentItemId}`) || - (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId])); + const hasVideo = currentSlide && currentSlide.querySelector('.video-backdrop'); if (!CONFIG.waitForTrailerToEnd || !hasVideo) { STATE.slideshow.slideInterval.start(); @@ -2719,140 +2780,7 @@ const SlideshowManager = { }); }, - /** - * 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) { - // Try finding by class first, then fall back to YouTube player container by ID - let videoBackdrop = slide.querySelector('.video-backdrop'); - if (!videoBackdrop) { - // YouTube API replaces the div with an iframe, which may not have the class yet - videoBackdrop = slide.querySelector(`#youtube-player-${itemId}`); - } - // Also check if a player exists in the registry even if no DOM element found - const hasRegisteredPlayer = !!(STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[itemId]); - // Update mute button visibility - const muteButton = document.querySelector('.mute-button'); - if (muteButton) { - muteButton.style.display = (videoBackdrop || hasRegisteredPlayer) ? 'block' : 'none'; - } - - if (!videoBackdrop && !hasRegisteredPlayer) return false; - - if (videoBackdrop && 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 && slide.classList.contains('active')) { - 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 && STATE.slideshow.videoPlayers[itemId]; - const playerIsReady = player && typeof player.loadVideoById === 'function' && player._videoId; - - if (playerIsReady) { - 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 (!slide.classList.contains('active')) return; - - 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; - } - - // YouTube player exists but NOT fully ready yet. - // new YT.Player() returns an object immediately, but API methods like - // loadVideoById and _videoId aren't available until onReady fires. - // onReady may have ALREADY fired (during preload), so we can't rely on - // _pendingPlay alone. Instead, poll for readiness. - const ytContainer = videoBackdrop || slide.querySelector(`[id^="youtube-player-"]`); - if (ytContainer && (ytContainer.tagName === 'IFRAME' || (ytContainer.id && ytContainer.id.startsWith('youtube-player-')))) { - console.log(`YouTube player for ${itemId} not ready yet, polling for readiness...`); - - // Also set _pendingPlay as fallback in case onReady hasn't fired yet - ytContainer._pendingPlay = true; - - let attempts = 0; - const maxAttempts = 25; // 25 * 200ms = 5 seconds max - const pollInterval = setInterval(() => { - attempts++; - - // Stop if slide is no longer active (user navigated away) - if (!slide.classList.contains('active')) { - clearInterval(pollInterval); - return; - } - - const p = STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[itemId]; - if (p && typeof p.loadVideoById === 'function' && p._videoId) { - clearInterval(pollInterval); - console.log(`YouTube player for ${itemId} now ready (after ${attempts * 200}ms), starting playback`); - - if (STATE.slideshow.isPaused) return; - - p.loadVideoById({ - videoId: p._videoId, - startSeconds: p._startTime || 0, - endSeconds: p._endTime - }); - - if (STATE.slideshow.isMuted) { - p.mute(); - } else { - p.unMute(); - p.setVolume(40); - } - - // Pause slideshow timer when video starts if configured - if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { - STATE.slideshow.slideInterval.stop(); - } - return; - } - - if (attempts >= maxAttempts) { - clearInterval(pollInterval); - console.warn(`YouTube player for ${itemId} failed to become ready after ${maxAttempts * 200}ms`); - } - }, 200); - - return true; - } - - return false; - }, /** * Stops all video playback (YouTube and HTML5)