From 42026b0ee8a97000598769262cd8422c56dc4045 Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:44:04 +0100 Subject: [PATCH] test revert --- .../Web/mediaBarEnhanced.js | 437 ++++++------------ 1 file changed, 146 insertions(+), 291 deletions(-) diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js index 42e0eab..b5aa2d6 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js @@ -1403,8 +1403,6 @@ const ApiUtils = { return { id: trailer.Id, - // static=true forces Jellyfin to direct-stream (no transcoding) which enables - // HTTP Range Requests (Accept-Ranges: bytes) — required by Safari for video playback url: `${STATE.jellyfinData.serverAddress}/Videos/${trailer.Id}/stream.mp4?mediaSourceId=${mediaSourceId}&api_key=${STATE.jellyfinData.accessToken}&static=true` }; } @@ -1444,7 +1442,7 @@ const ApiUtils = { return { id: video.Id, - url: `${STATE.jellyfinData.serverAddress}/Videos/${video.Id}/stream.mp4?api_key=${STATE.jellyfinData.accessToken}` + url: `${STATE.jellyfinData.serverAddress}/Videos/${video.Id}/stream.mp4?api_key=${STATE.jellyfinData.accessToken}&static=true` }; } } @@ -1670,7 +1668,7 @@ const SlideCreator = { trailerUrl = { id: videoId, - url: `${STATE.jellyfinData.serverAddress}/Videos/${videoId}/stream.mp4?api_key=${STATE.jellyfinData.accessToken}` + url: `${STATE.jellyfinData.serverAddress}/Videos/${videoId}/stream.mp4?api_key=${STATE.jellyfinData.accessToken}&static=true` }; } else { // Assume it's a standard URL (YouTube, etc.) @@ -1736,275 +1734,153 @@ const SlideCreator = { // Create a wrapper for opacity transition videoBackdrop = SlideUtils.createElement("div", { className: `backdrop video-backdrop ${videoClass}`, - style: "opacity: 0; transition: opacity 1.2s ease-in-out;" + style: "opacity: 0; transition: opacity 1.2s ease-in-out;" // Start interrupted/transparent }); - // Detect Safari/WebKit — the YouTube IFrame API causes Error 153 on WebKit - // due to cross-origin postMessage restrictions. Use a plain iframe embed instead. - const isSafariWebKit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) && !/Chromium/.test(navigator.userAgent); + const ytPlayerDiv = SlideUtils.createElement("div", { + id: `youtube-player-${itemId}`, + style: "width: 100%; height: 100%;" + }); + + videoBackdrop.appendChild(ytPlayerDiv); - if (isSafariWebKit) { - // ── Safari: plain iframe embed ─────────────────────────────────────────── - // Fetch SponsorBlock data and apply as URL params (start= / end=) + // Initialize YouTube Player + SlideUtils.loadYouTubeIframeAPI().then(() => { + // Fetch SponsorBlock data ApiUtils.fetchSponsorBlockData(videoId).then(segments => { - let startParam = ''; - let endParam = ''; + const playerVars = { + autoplay: 0, + mute: STATE.slideshow.isMuted ? 1 : 0, + controls: 0, + disablekb: 1, + fs: 0, + iv_load_policy: 3, + rel: 0, + loop: 0, + playsinline: 1, + origin: window.location.origin, + enablejsapi: 1 + }; + + // Determine video quality + let quality = 'hd1080'; + if (CONFIG.preferredVideoQuality === 'Maximum') { + quality = 'highres'; + } else if (CONFIG.preferredVideoQuality === '720p') { + quality = 'hd720'; + } else if (CONFIG.preferredVideoQuality === '1080p') { + quality = 'hd1080'; + } else { // Auto or fallback + // If screen is wider than 1920, prefer highres, otherwise 1080p + quality = window.screen.width > 1920 ? 'highres' : 'hd1080'; + } + + playerVars.suggestedQuality = quality; + + // Apply SponsorBlock start/end times if (segments.intro) { - startParam = `&start=${Math.ceil(segments.intro[1])}`; - console.info(`SponsorBlock (Safari) intro skip: starting at ${Math.ceil(segments.intro[1])}s`); + playerVars.start = Math.ceil(segments.intro[1]); + console.info(`SponsorBlock intro detected for video ${videoId}: skipping to ${playerVars.start}s`); } if (segments.outro) { - endParam = `&end=${Math.floor(segments.outro[0])}`; - console.info(`SponsorBlock (Safari) outro skip: ending at ${Math.floor(segments.outro[0])}s`); + playerVars.end = Math.floor(segments.outro[0]); + console.info(`SponsorBlock outro detected for video ${videoId}: ending at ${playerVars.end}s`); } - // enablejsapi=1 needed for postMessage commands — does NOT trigger IFrame API handshake - const embedUrl = `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=1&controls=0&playsinline=1&rel=0&iv_load_policy=3&enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}${startParam}${endParam}`; + STATE.slideshow.videoPlayers[itemId] = new YT.Player(`youtube-player-${itemId}`, { + height: '100%', + width: '100%', + videoId: videoId, + host: 'https://www.youtube-nocookie.com', + playerVars: playerVars, + events: { + 'onReady': (event) => { + const iframe = event.target.getIframe(); + if (iframe) { + iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); + } - const ytIframe = document.createElement('iframe'); - ytIframe.style.cssText = 'width:100%;height:100%;border:0;pointer-events:none;'; - ytIframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'); - ytIframe.setAttribute('allowfullscreen', ''); - ytIframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); - ytIframe.src = embedUrl; - videoBackdrop.appendChild(ytIframe); + // Store start/end time and videoId for later use + event.target._startTime = playerVars.start || 0; + event.target._endTime = playerVars.end || undefined; + event.target._videoId = videoId; + + // Store reference to wrapper for fading + event.target._wrapperDiv = videoBackdrop; - // Show immediately — no onStateChange available for plain iframes - videoBackdrop.style.opacity = '1'; + if (STATE.slideshow.isMuted) { + event.target.mute(); + } else { + event.target.unMute(); + event.target.setVolume(40); + } - // Helper: send postMessage command to the iframe player - const ytCmd = (func, args = []) => { - try { - ytIframe.contentWindow?.postMessage( - JSON.stringify({ event: 'command', func, args }), - 'https://www.youtube-nocookie.com' - ); - } catch(e) { /* cross-origin access may fail on some iOS versions */ } - }; + if (typeof event.target.setPlaybackQuality === 'function') { + event.target.setPlaybackQuality(quality); + } - // YouTube won't send onStateChange events unless we explicitly subscribe. - // The IFrame API does this automatically; we must do it manually for plain iframes. - const subscribeToYtEvents = () => { - try { - // Step 1: Announce we're listening - ytIframe.contentWindow?.postMessage( - JSON.stringify({ event: 'listening' }), - 'https://www.youtube-nocookie.com' - ); - // Step 2: Subscribe to state change events - ytIframe.contentWindow?.postMessage( - JSON.stringify({ event: 'command', func: 'addEventListener', args: ['onStateChange'] }), - 'https://www.youtube-nocookie.com' - ); - } catch(e) {} - }; + // Only play if this is the active slide + const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); + const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); - // Subscribe when iframe has finished loading - ytIframe.addEventListener('load', subscribeToYtEvents); + if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) { + event.target.playVideo(); + + // Check if it actually started playing after a short delay (handling autoplay blocks) + const timeoutId = setTimeout(() => { + // Re-check conditions before processing fallback + const isVideoPlayerOpenNow = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); + if (document.hidden || (isVideoPlayerOpenNow && !isVideoPlayerOpenNow.classList.contains('hide')) || !slide.classList.contains('active')) { + console.log(`Navigation detected during autoplay check for ${itemId}, stopping video.`); + try { + event.target.stopVideo(); + } catch (e) { console.warn("Error stopping video in timeout:", e); } + return; + } - // Listen for YouTube state changes (video ended → advance slide) - const handleYtMessage = (event) => { - if (!event.origin.includes('youtube')) return; - try { - const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; + if (event.target.getPlayerState() !== YT.PlayerState.PLAYING && + event.target.getPlayerState() !== YT.PlayerState.BUFFERING) { + console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`); + event.target.mute(); + event.target.playVideo(); + } + }, 1000); - // Player is ready — re-subscribe in case the first attempt was too early - if (data.event === 'onReady') { - subscribeToYtEvents(); - } + if (!STATE.slideshow.autoplayTimeouts) STATE.slideshow.autoplayTimeouts = []; + STATE.slideshow.autoplayTimeouts.push(timeoutId); - if (data.event === 'onStateChange') { - console.log(`🍎 Safari YT state: ${data.info} for ${itemId}`); - if (data.info === 0) { // 0 = ENDED + // Pause slideshow timer when video starts if configured + if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { + STATE.slideshow.slideInterval.stop(); + } + } + }, + 'onStateChange': (event) => { + // Fade in when playing + if (event.data === YT.PlayerState.PLAYING) { + if (event.target._wrapperDiv) { + event.target._wrapperDiv.style.opacity = "1"; + } + } + + if (event.data === YT.PlayerState.ENDED) { const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); if (slide && slide.classList.contains('active')) { SlideshowManager.nextSlide(); } } - } - } catch(e) {} - }; - window.addEventListener('message', handleYtMessage); - - // Create a postMessage-based stub compatible with all slide management code - // Key: we NEVER clear ytIframe.src — that would break the YouTube session and cause Error 153. - // Instead we use postMessage pause/play/seek to control playback state. - STATE.slideshow.videoPlayers[itemId] = { - _isSafariIframe: true, - _iframe: ytIframe, - _videoId: videoId, - _embedUrl: embedUrl, - _msgHandler: handleYtMessage, - pauseVideo() { ytCmd('pauseVideo'); }, - stopVideo() { ytCmd('pauseVideo'); ytCmd('seekTo', [0, true]); }, - playVideo() { ytCmd('playVideo'); }, - mute() { ytCmd('mute'); }, - unMute() { ytCmd('unMute'); }, - setVolume(v) { ytCmd('setVolume', [v]); }, - getIframe() { return ytIframe; }, - getPlayerState() { return 1; }, // approximate: avoids triggering fallback timeouts - loadVideoById({ videoId: vid, startSeconds = 0 }) { - if (vid === this._videoId) { - // Same video — seek to start and resume. NEVER change src (would cause Error 153). - ytCmd('seekTo', [startSeconds, true]); - ytCmd('playVideo'); - } else { - // Different video — need a fresh embed URL - const url = `https://www.youtube-nocookie.com/embed/${vid}?autoplay=1&mute=1&controls=0&playsinline=1&rel=0&iv_load_policy=3&enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`; - ytIframe.src = url; - this._videoId = vid; - this._embedUrl = url; - } - }, - destroy() { - window.removeEventListener('message', this._msgHandler); - ytIframe.remove(); - } - }; - - console.log(`🍎 Safari detected — using plain iframe embed for YouTube video ${videoId}`); - }); - - } else { - // ── Non-Safari: YouTube IFrame API ────────────────────────────────────── - const ytPlayerDiv = SlideUtils.createElement("div", { - id: `youtube-player-${itemId}`, - style: "width: 100%; height: 100%;" - }); - - videoBackdrop.appendChild(ytPlayerDiv); - - SlideUtils.loadYouTubeIframeAPI().then(() => { - ApiUtils.fetchSponsorBlockData(videoId).then(segments => { - const playerVars = { - autoplay: 1, - mute: 1, // need to be muted for Safari, because apple makes life difficult... - controls: 0, - disablekb: 1, - fs: 0, - iv_load_policy: 3, - rel: 0, - loop: 0, - playsinline: 1, - origin: window.location.origin, - enablejsapi: 1 - }; - - // Determine video quality - let quality = 'hd1080'; - if (CONFIG.preferredVideoQuality === 'Maximum') { - quality = 'highres'; - } else if (CONFIG.preferredVideoQuality === '720p') { - quality = 'hd720'; - } else if (CONFIG.preferredVideoQuality === '1080p') { - quality = 'hd1080'; - } else { - quality = window.screen.width > 1920 ? 'highres' : 'hd1080'; - } - - playerVars.suggestedQuality = quality; - - // Apply SponsorBlock start/end times - if (segments.intro) { - playerVars.start = Math.ceil(segments.intro[1]); - console.info(`SponsorBlock intro detected for video ${videoId}: skipping to ${playerVars.start}s`); - } - if (segments.outro) { - playerVars.end = Math.floor(segments.outro[0]); - console.info(`SponsorBlock outro detected for video ${videoId}: ending at ${playerVars.end}s`); - } - - STATE.slideshow.videoPlayers[itemId] = new YT.Player(`youtube-player-${itemId}`, { - height: '100%', - width: '100%', - videoId: videoId, - host: 'https://www.youtube-nocookie.com', - playerVars: playerVars, - events: { - 'onReady': (event) => { - const iframe = event.target.getIframe(); - if (iframe) { - iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); - // Full allow attribute matching what YouTube sets on their own embed pages - iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'); - } - - // Store start/end time and videoId for later use - event.target._startTime = playerVars.start || 0; - event.target._endTime = playerVars.end || undefined; - event.target._videoId = videoId; - - // Store reference to wrapper for fading - event.target._wrapperDiv = videoBackdrop; - - // Unmute now if user wants sound. - if (!STATE.slideshow.isMuted) { - event.target.unMute(); - event.target.setVolume(40); - } - - if (typeof event.target.setPlaybackQuality === 'function') { - event.target.setPlaybackQuality(quality); - } - - // Stop playback if slide was navigated away from - const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); - const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); - - if (!slide || !slide.classList.contains('active') || document.hidden || (isVideoPlayerOpen && !isVideoPlayerOpen.classList.contains('hide'))) { - event.target.stopVideo(); - } else { - if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { - STATE.slideshow.slideInterval.stop(); - } - - const timeoutId = setTimeout(() => { - const isVideoPlayerOpenNow = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); - if (document.hidden || (isVideoPlayerOpenNow && !isVideoPlayerOpenNow.classList.contains('hide')) || !slide.classList.contains('active')) { - console.log(`Navigation detected during autoplay check for ${itemId}, stopping video.`); - try { - event.target.stopVideo(); - } catch (e) { console.warn("Error stopping video:", e); } - return; - } - - const state = event.target.getPlayerState(); - if (state !== YT.PlayerState.PLAYING && state !== YT.PlayerState.BUFFERING) { - console.warn(`Autoplay stalled for ${itemId}, attempting muted fallback`); - event.target.mute(); - event.target.playVideo(); - } - }, 1000); - - if (!STATE.slideshow.autoplayTimeouts) STATE.slideshow.autoplayTimeouts = []; - STATE.slideshow.autoplayTimeouts.push(timeoutId); - } - }, - 'onStateChange': (event) => { - if (event.data === YT.PlayerState.PLAYING) { - if (event.target._wrapperDiv) { - event.target._wrapperDiv.style.opacity = "1"; - } - } - - if (event.data === YT.PlayerState.ENDED) { - const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); - if (slide && slide.classList.contains('active')) { - SlideshowManager.nextSlide(); - } - } - }, - 'onError': (event) => { - console.warn(`YouTube player error ${event.data} for video ${videoId}`); - if (CONFIG.waitForTrailerToEnd) { - SlideshowManager.nextSlide(); - } + }, + 'onError': (event) => { + console.warn(`YouTube player error ${event.data} for video ${videoId}`); + // Fallback to next slide on error + if (CONFIG.waitForTrailerToEnd) { + SlideshowManager.nextSlide(); } } - }); + } }); }); - } // end non-Safari + }); // 2. Check for local video trailers in MediaSources if yt is not available } else if (!isYoutube) { @@ -2020,7 +1896,6 @@ const SlideCreator = { }; videoAttributes.muted = ""; - videoAttributes.playsinline = ""; // again Safari needs extra treatment... videoBackdrop = SlideUtils.createElement("video", videoAttributes); videoBackdrop.volume = 0.4; @@ -2052,10 +1927,7 @@ const SlideCreator = { }); videoBackdrop.addEventListener('error', (event) => { - const src = event.target.src || event.target.getAttribute('data-src') || 'unknown'; - const errCode = event.target.error ? event.target.error.code : 'n/a'; - const errMsg = event.target.error ? event.target.error.message : 'n/a'; - console.warn(`Local video error for item ${itemId} | code=${errCode} | msg=${errMsg} | url=${src}`); + console.warn(`Local video error for item ${itemId}`); const slide = event.target.closest('.slide'); if (slide && slide.classList.contains('active')) { SlideshowManager.nextSlide(); @@ -2594,65 +2466,48 @@ const SlideshowManager = { if (videoBackdrop.tagName === 'VIDEO') { // Restore src from data-src if it was deactivated to release connections const lazySrc = videoBackdrop.getAttribute('data-src'); - const isFreshLoad = lazySrc && !videoBackdrop.src; + if (lazySrc && !videoBackdrop.src) { + videoBackdrop.src = lazySrc; + } + + videoBackdrop.currentTime = 0; videoBackdrop.muted = STATE.slideshow.isMuted; if (!STATE.slideshow.isMuted) { videoBackdrop.volume = 0.4; } - const doPlay = () => { - if (!currentSlide.classList.contains('active')) return; - videoBackdrop.play().catch(e => { - 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); - }); - }; - - if (isFreshLoad) { - // Safari: set src, then wait for loadedmetadata before seeking/playing - videoBackdrop.src = lazySrc; - videoBackdrop.load(); - videoBackdrop.addEventListener('loadedmetadata', () => { - videoBackdrop.currentTime = 0; - doPlay(); - }, { once: true }); - } else { - // src already set (e.g. paused slide resuming) - videoBackdrop.currentTime = 0; - doPlay(); - } + videoBackdrop.play().catch(e => { + // Check if it actually started playing after a short delay (handling autoplay blocks) + 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 - // load always starts muted first, then unmute if needed player.loadVideoById({ videoId: player._videoId, startSeconds: player._startTime || 0, endSeconds: player._endTime }); - player.mute(); - if (!STATE.slideshow.isMuted) { - setTimeout(() => { - // Only unmute if still on the same slide - if (currentSlide.classList.contains('active')) { - player.unMute(); - player.setVolume(40); - } - }, 600); + + if (STATE.slideshow.isMuted) { + player.mute(); + } else { + player.unMute(); + player.setVolume(40); } // Check if playback successfully started, otherwise fallback to muted - // (Only for real YT.Player instances — Safari stub's getPlayerState() always returns 1) setTimeout(() => { if (!currentSlide.classList.contains('active')) return; - if (player.getPlayerState && typeof YT !== 'undefined' && + if (player.getPlayerState && player.getPlayerState() !== YT.PlayerState.PLAYING && player.getPlayerState() !== YT.PlayerState.BUFFERING) { console.log("YouTube loadVideoById didn't start playback, retrying muted...");