diff --git a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js index b6cb6ff..a71c61b 100644 --- a/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js +++ b/Jellyfin.Plugin.MediaBarEnhanced/Web/mediaBarEnhanced.js @@ -1745,43 +1745,96 @@ const SlideCreator = { if (isSafariWebKit) { // ── Safari: plain iframe embed ─────────────────────────────────────────── - const embedUrl = `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=1&controls=0&playsinline=1&rel=0&iv_load_policy=3&enablejsapi=0&origin=${encodeURIComponent(window.location.origin)}`; + // Fetch SponsorBlock data and apply as URL params (start= / end=) + ApiUtils.fetchSponsorBlockData(videoId).then(segments => { + let startParam = ''; + let endParam = ''; + if (segments.intro) { + startParam = `&start=${Math.ceil(segments.intro[1])}`; + console.info(`SponsorBlock (Safari) intro skip: starting at ${Math.ceil(segments.intro[1])}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`); + } - 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); + // 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}`; - // Show immediately — no onStateChange available for plain iframes - videoBackdrop.style.opacity = '1'; + 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); - // Create a stub player compatible with all slide management code - STATE.slideshow.videoPlayers[itemId] = { - _isSafariIframe: true, - _iframe: ytIframe, - _videoId: videoId, - _embedUrl: embedUrl, - pauseVideo() { ytIframe.src = ''; }, - stopVideo() { ytIframe.src = ''; }, - playVideo() { if (!ytIframe.src) ytIframe.src = this._embedUrl; }, - mute() { /* cannot mute plain iframe mid-play */ }, - unMute() { /* cannot unmute plain iframe mid-play */ }, - setVolume() { /* not available */ }, - getIframe() { return ytIframe; }, - getPlayerState() { return 1; }, // always report PLAYING so fallback timeouts don't fire - loadVideoById({ videoId: vid }) { - const url = `https://www.youtube-nocookie.com/embed/${vid}?autoplay=1&mute=1&controls=0&playsinline=1&rel=0&iv_load_policy=3&enablejsapi=0`; - ytIframe.src = url; - this._videoId = vid; - this._embedUrl = url; - }, - destroy() { ytIframe.remove(); } - }; + // Show immediately — no onStateChange available for plain iframes + videoBackdrop.style.opacity = '1'; - console.log(`🍎 Safari detected — using plain iframe embed for YouTube video ${videoId}`); + // 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 */ } + }; + + // Listen for YouTube state changes (e.g. 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 (data.event === 'onStateChange' && data.info === 0) { // 0 = 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 ──────────────────────────────────────