@@ -1258,9 +1258,20 @@ const ApiUtils = {
*/
*/
async fetchSponsorBlockData ( videoId ) {
async fetchSponsorBlockData ( videoId ) {
if ( ! CONFIG . useSponsorBlock ) return { intro : null , outro : null } ;
if ( ! CONFIG . useSponsorBlock ) return { intro : null , outro : null } ;
// Return cached result if available
if ( ! this . _sponsorBlockCache ) this . _sponsorBlockCache = { } ;
if ( this . _sponsorBlockCache [ videoId ] ) {
return this . _sponsorBlockCache [ videoId ] ;
}
try {
try {
const response = await fetch ( ` https://sponsor.ajay.app/api/skipSegments?videoID= ${ videoId } &categories=["intro","outro"] ` ) ;
const response = await fetch ( ` https://sponsor.ajay.app/api/skipSegments?videoID= ${ videoId } &categories=["intro","outro"] ` ) ;
if ( ! response . ok ) return { intro : null , outro : null } ;
if ( ! response . ok ) {
const result = { intro : null , outro : null } ;
this . _sponsorBlockCache [ videoId ] = result ;
return result ;
}
const segments = await response . json ( ) ;
const segments = await response . json ( ) ;
let intro = null ;
let intro = null ;
@@ -1274,7 +1285,9 @@ const ApiUtils = {
}
}
} ) ;
} ) ;
return { intro , outro } ;
const result = { intro , outro } ;
this . _sponsorBlockCache [ videoId ] = result ;
return result ;
} catch ( error ) {
} catch ( error ) {
console . warn ( 'Error fetching SponsorBlock data:' , error ) ;
console . warn ( 'Error fetching SponsorBlock data:' , error ) ;
return { intro : null , outro : null } ;
return { intro : null , outro : null } ;
@@ -1435,19 +1448,16 @@ const VisibilityObserver = {
// If a full screen video player is active, hide slideshow and stop playback
// If a full screen video player is active, hide slideshow and stop playback
if ( ( videoPlayer && ! videoPlayer . classList . contains ( 'hide' ) ) || ( trailerPlayer && ! trailerPlayer . classList . contains ( 'hide' ) ) ) {
if ( ( videoPlayer && ! videoPlayer . classList . contains ( 'hide' ) ) || ( trailerPlayer && ! trailerPlayer . classList . contains ( 'hide' ) ) ) {
if ( this . _lastVisibleState !== 'player-active' ) {
const container = document . getElementById ( "slides-container" ) ;
this . _lastVisibleState = 'player-active' ;
if ( container ) {
const container = document . getElementById ( "slides-container" ) ;
container . style . display = "none" ;
if ( container ) {
container . style . visibility = "hidden" ;
container . style . display = "none" ;
container . style . pointerEvents = "none" ;
container . style . visibility = "hidden" ;
container . style . pointerEvents = "none" ;
}
if ( STATE . slideshow . slideInterval ) {
STATE . slideshow . slideInterval . stop ( ) ;
}
SlideshowManager . stopAllPlayback ( ) ;
}
}
if ( STATE . slideshow . slideInterval ) {
STATE . slideshow . slideInterval . stop ( ) ;
}
SlideshowManager . stopAllPlayback ( ) ;
return ;
return ;
}
}
@@ -1462,27 +1472,20 @@ const VisibilityObserver = {
activeTab &&
activeTab &&
activeTab . getAttribute ( "data-index" ) === "0" ;
activeTab . getAttribute ( "data-index" ) === "0" ;
const newState = isVisible ? 'visible' : 'hidden' ;
container . style . display = isVisible ? "block" : "none" ;
container . style . visibility = isVisible ? "visible" : "hidden" ;
// Only update DOM and trigger actions when state actually changes
container . style . pointerEvents = isVisible ? "auto" : "none" ;
if ( this . _lastVisibleState !== newState ) {
this . _lastVisibleState = newState ;
container . style . display = isVisible ? "block" : "none" ;
container . style . visibility = isVisible ? "visible" : "hidden" ;
container . style . pointerEvents = isVisible ? "auto" : "none" ;
if ( isVisible ) {
if ( isVisible ) {
if ( STATE . slideshow . slideInterval && ! STATE . slideshow . isPaused ) {
if ( STATE . slideshow . slideInterval && ! STATE . slideshow . isPaused ) {
STATE . slideshow . slideInterval . start ( ) ;
STATE . slideshow . slideInterval . start ( ) ;
SlideshowManager . resumeActivePlayback ( ) ;
SlideshowManager . resumeActivePlayback ( ) ;
}
} else {
if ( STATE . slideshow . slideInterval ) {
STATE . slideshow . slideInterval . stop ( ) ;
}
SlideshowManager . stopAllPlayback ( ) ;
}
}
} else {
if ( STATE . slideshow . slideInterval ) {
STATE . slideshow . slideInterval . stop ( ) ;
}
SlideshowManager . stopAllPlayback ( ) ;
}
}
} ,
} ,
@@ -1491,11 +1494,6 @@ const VisibilityObserver = {
*/
*/
init ( ) {
init ( ) {
const observer = new MutationObserver ( ( ) => this . updateVisibility ( ) ) ;
const observer = new MutationObserver ( ( ) => this . updateVisibility ( ) ) ;
// let debounceTimer = null;
// const observer = new MutationObserver(() => {
// if (debounceTimer) clearTimeout(debounceTimer);
// debounceTimer = setTimeout(() => this.updateVisibility(), 250);
// });
observer . observe ( document . body , { childList : true , subtree : true } ) ;
observer . observe ( document . body , { childList : true , subtree : true } ) ;
document . body . addEventListener ( "click" , ( ) => this . updateVisibility ( ) ) ;
document . body . addEventListener ( "click" , ( ) => this . updateVisibility ( ) ) ;
@@ -1622,6 +1620,11 @@ const SlideCreator = {
else if ( item . RemoteTrailers && item . RemoteTrailers . length > 0 ) {
else if ( item . RemoteTrailers && item . RemoteTrailers . length > 0 ) {
trailerUrl = item . RemoteTrailers [ 0 ] . Url ;
trailerUrl = item . RemoteTrailers [ 0 ] . Url ;
}
}
// 1d. Final Fallback to Local Trailer (even if not preferred)
else if ( item . LocalTrailerCount > 0 && item . localTrailerUrl ) {
trailerUrl = item . localTrailerUrl ;
console . log ( ` Using local trailer fallback for ${ itemId } : ${ trailerUrl } ` ) ;
}
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i . test ( navigator . userAgent ) ;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i . test ( navigator . userAgent ) ;
@@ -1714,20 +1717,6 @@ const SlideCreator = {
playerVars : playerVars ,
playerVars : playerVars ,
events : {
events : {
'onReady' : ( event ) => {
'onReady' : ( event ) => {
// Prevent iframe from stealing focus (critical for TV mode)
const iframe = event . target . getIframe ( ) ;
if ( iframe ) {
iframe . setAttribute ( 'tabindex' , '-1' ) ;
iframe . setAttribute ( 'inert' , '' ) ;
// Preserve video-backdrop class on the iframe (YT API replaces the original div)
iframe . classList . add ( 'backdrop' , 'video-backdrop' ) ;
if ( CONFIG . fullWidthVideo ) {
iframe . classList . add ( 'video-backdrop-full' ) ;
} else {
iframe . classList . add ( 'video-backdrop-default' ) ;
}
}
// Store start/end time and videoId for later use
// Store start/end time and videoId for later use
event . target . _startTime = playerVars . start || 0 ;
event . target . _startTime = playerVars . start || 0 ;
event . target . _endTime = playerVars . end || undefined ;
event . target . _endTime = playerVars . end || undefined ;
@@ -1744,18 +1733,11 @@ const SlideCreator = {
event . target . setPlaybackQuality ( quality ) ;
event . target . setPlaybackQuality ( quality ) ;
}
}
// Only play if this is the active slide and not paused
// Only play if this is the active slide
const slide = document . querySelector ( ` .slide[data-item-id=" ${ itemId } "] ` ) ;
const slide = document . querySelector ( ` .slide[data-item-id=" ${ itemId } "] ` ) ;
const isVideoPlayerOpen = document . querySelector ( '.videoPlayerContainer' ) || document . querySelector ( '.youtubePlayerContainer' ) ;
const isVideoPlayerOpen = document . querySelector ( '.videoPlayerContainer' ) || document . querySelector ( '.youtubePlayerContainer' ) ;
const isActive = slide && slide . classList . contains ( 'activ e' ) ;
if ( slide && slide . classList . contains ( 'active' ) && ! document . hidden && ( ! isVideoPlayerOpen || isVideoPlayerOpen . classList . contains ( 'hid e' ) ) ) {
const isHidden = document . hidden ;
const isPaused = STATE . slideshow . isPaused ;
const isPlayerOpen = isVideoPlayerOpen && ! isVideoPlayerOpen . classList . contains ( 'hide' ) ;
console . log ( ` [MBE-READY] onReady for ${ itemId } : active= ${ isActive } , hidden= ${ isHidden } , paused= ${ isPaused } , playerOpen= ${ ! ! isPlayerOpen } ` ) ;
if ( isActive && ! isHidden && ! isPaused && ! isPlayerOpen ) {
console . log ( ` [MBE-READY] → Playing video for ${ itemId } ` ) ;
event . target . playVideo ( ) ;
event . target . playVideo ( ) ;
// Check if it actually started playing after a short delay (handling autoplay blocks)
// Check if it actually started playing after a short delay (handling autoplay blocks)
@@ -1788,10 +1770,15 @@ const SlideCreator = {
}
}
} ,
} ,
'onStateChange' : ( event ) => {
'onStateChange' : ( event ) => {
const stateNames = { [ - 1 ] : 'UNSTARTED' , 0 : 'ENDED' , 1 : 'PLAYING' , 2 : 'PAUSED' , 3 : 'BUFFERING' , 5 : 'CUED' } ;
console . log ( ` [MBE-STATE] ${ itemId } : ${ stateNames [ event . data ] || event . data } ` ) ;
if ( event . data === YT . PlayerState . ENDED ) {
if ( event . data === YT . PlayerState . ENDED ) {
SlideshowManager . nextSlide ( ) ;
const slide = document . querySelector ( ` .slide[data-item-id=" ${ itemId } "] ` ) ;
if ( slide && slide . classList . contains ( 'active' ) ) {
if ( CONFIG . waitForTrailerToEnd ) {
SlideshowManager . nextSlide ( ) ;
} else {
event . target . playVideo ( ) ; // Loop if trailer is shorter than slide duration
}
}
}
}
} ,
} ,
'onError' : ( event ) => {
'onError' : ( event ) => {
@@ -1832,23 +1819,24 @@ const SlideCreator = {
STATE . slideshow . videoPlayers [ itemId ] = backdrop ;
STATE . slideshow . videoPlayers [ itemId ] = backdrop ;
backdrop . addEventListener ( 'play' , ( event ) => {
backdrop . addEventListener ( 'play' , ( ) => {
const slide = document . querySelector ( ` .slide[data-item-id=" ${ itemId } "] ` ) ;
if ( ! slide || ! slide . classList . contains ( 'active' ) ) {
// backdrop.addEventListener('play', (event) => {
console . log ( ` Local video ${ itemId } started playing but is not active, pausing. ` ) ;
// const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
event . target . pause ( ) ;
event . target . currentTime = 0 ;
return ;
}
// if (!slide || !slide.classList.contains('active')) {
// console.log(`Local video ${itemId} started playing but is not active, pausing.`);
// event.target.pause();
// event.target.currentTime = 0;
// return;
// }
if ( CONFIG . waitForTrailerToEnd && STATE . slideshow . slideInterval ) {
if ( CONFIG . waitForTrailerToEnd && STATE . slideshow . slideInterval ) {
STATE . slideshow . slideInterval . stop ( ) ;
STATE . slideshow . slideInterval . stop ( ) ;
}
}
} ) ;
} ) ;
backdrop . addEventListener ( 'ended' , ( ) => {
backdrop . addEventListener ( 'ended' , ( ) => {
SlideshowManager . nextSlide ( ) ;
SlideshowManager . nextSlide ( ) ;
} ) ;
} ) ;
backdrop . addEventListener ( 'error' , ( ) => {
backdrop . addEventListener ( 'error' , ( ) => {
@@ -2283,16 +2271,6 @@ const SlideshowManager = {
let previousVisibleSlide ;
let previousVisibleSlide ;
try {
try {
const container = SlideUtils . getOrCreateSlidesContainer ( ) ;
const container = SlideUtils . getOrCreateSlidesContainer ( ) ;
const activeElement = document . activeElement ;
let focusSelector = null ;
if ( container . contains ( activeElement ) ) {
if ( activeElement . classList . contains ( 'play-button' ) ) focusSelector = '.play-button' ;
else if ( activeElement . classList . contains ( 'detail-button' ) ) focusSelector = '.detail-button' ;
else if ( activeElement . classList . contains ( 'favorite-button' ) ) focusSelector = '.favorite-button' ;
else if ( activeElement . classList . contains ( 'trailer-button' ) ) focusSelector = '.trailer-button' ;
}
const totalItems = STATE . slideshow . totalItems ;
const totalItems = STATE . slideshow . totalItems ;
index = Math . max ( 0 , Math . min ( index , totalItems - 1 ) ) ;
index = Math . max ( 0 , Math . min ( index , totalItems - 1 ) ) ;
@@ -2317,51 +2295,119 @@ const SlideshowManager = {
if ( previousVisibleSlide ) {
if ( previousVisibleSlide ) {
previousVisibleSlide . classList . remove ( "active" ) ;
previousVisibleSlide . classList . remove ( "active" ) ;
// previousVisibleSlide.setAttribute("inert", "");
// previousVisibleSlide.setAttribute("tabindex", "-1");
}
}
currentSlide . classList . add ( "active" ) ;
currentSlide . classList . add ( "active" ) ;
// // Update Play/Pause Button State if it was paused
// if (STATE.slideshow.isPaused) {
// STATE.slideshow.isPaused = false;
// const pauseButton = document.querySelector('.pause-button');
// if (pauseButton) {
// pauseButton.innerHTML = '<i class="material-icons">pause</i>';
// const pauseLabel = LocalizationUtils.getLocalizedString('ButtonPause', 'Pause');
// pauseButton.setAttribute("aria-label", pauseLabel);
// pauseButton.setAttribute("title", pauseLabel);
// }
// }
// Restore focus for TV mode navigation continuity
// Manage Video Playback: Stop others, Play current
requestAnimationFrame ( ( ) => {
if ( focusSelector ) {
// 1. Stop all other YouTube players
const target = currentSlide . querySelector ( focusSelector ) ;
if ( STATE . slideshow . videoPlayers ) {
if ( target ) {
Object . keys ( STATE . slideshow . videoPlayers ) . forEach ( id => {
target . focus ( ) ;
if ( id !== currentItemId ) {
return ;
const p = STATE . slideshow . videoPlayers [ id ] ;
if ( p && typeof p . pauseVideo === 'function' ) {
p . pauseVideo ( ) ;
}
}
}
}
} ) ;
// 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' ) ||
// 2. Pause all other HTML5 videos e.g. local trailers
document . body . classList . contains ( 'layout-tv' ) ;
document . querySelectorAll ( 'video' ) . forEach ( video => {
if ( isTvMode ) {
if ( ! video . closest ( ` .slide[data-item-id=" ${ currentItemId } "] ` ) ) {
container . foc us( { preventScroll : true } );
video . pa use ( ) ;
}
}
} ) ;
} ) ;
// Manage Video Playback: Stop others, Play current
// 3. Play and Reset current video
this . pauseOtherV ideos ( currentItemId ) ;
const videoBackdrop = currentSl ide. querySelector ( '.video-backdrop' ) ;
if ( ! STATE . slideshow . isPaused ) {
// Auto-unpause when a video slide becomes active
this . playCurrentVideo ( currentSlide , currentItemI d) ;
if ( videoBackdrop && STATE . slideshow . isPause d) {
} else {
// Check if new slide has video — Option B: un-pause for video slides
const videoBackdrop = currentSlide . querySelector ( '.video-backdrop' ) ;
if ( videoBackdrop ) {
STATE . slideshow . isPaused = false ;
STATE . slideshow . isPaused = false ;
const pauseButton = document . querySelector ( '.pause-button' ) ;
const pauseButton = document . querySelector ( '.pause-button' ) ;
if ( pauseButton ) {
if ( pauseButton ) {
pauseButton . innerHTML = '<i class="material-icons">pause</i>' ;
pauseButton . innerHTML = '<i class="material-icons">pause</i>' ;
const pauseLabel = LocalizationUtils . getLocalizedString ( 'ButtonPause' , 'Pause' ) ;
const pauseLabel = LocalizationUtils . getLocalizedString ( 'ButtonPause' , 'Pause' ) ;
pauseButton . setAttribute ( ' aria-label' , pauseLabel ) ;
pauseButton . setAttribute ( " aria-label" , pauseLabel ) ;
pauseButton . setAttribute ( ' title' , pauseLabel ) ;
pauseButton . setAttribute ( " title" , pauseLabel ) ;
}
}
// 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 && 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 ( ) ;
}
}
this . playCurrentVideo ( currentSlide , currentItemId ) ;
}
// Update mute button visibility
const muteButton = document . querySelector ( '.mute-button' ) ;
if ( muteButton ) {
muteButton . style . display = videoBackdrop ? 'block' : 'none' ;
}
}
}
}
@@ -2458,14 +2504,18 @@ const SlideshowManager = {
async preloadAdjacentSlides ( currentIndex ) {
async preloadAdjacentSlides ( currentIndex ) {
const totalItems = STATE . slideshow . totalItems ;
const totalItems = STATE . slideshow . totalItems ;
const preloadCount = Math . min ( Math . max ( CONFIG . preloadCount || 1 , 1 ) , 5 ) ;
const preloadCount = Math . min ( Math . max ( CONFIG . preloadCount || 1 , 1 ) , 5 ) ;
const preloadedIds = new Set ( ) ;
// Preload next slides
// Preload next slides
for ( let i = 1 ; i <= preloadCount ; i ++ ) {
for ( let i = 1 ; i <= preloadCount ; i ++ ) {
const nextIndex = ( currentIndex + i ) % totalItems ;
const nextIndex = ( currentIndex + i ) % totalItems ;
if ( nextIndex === currentIndex ) break ;
if ( nextIndex === currentIndex ) break ;
const itemId = STATE . slideshow . itemIds [ nextIndex ] ;
const itemId = STATE . slideshow . itemIds [ nextIndex ] ;
SlideCreator . c reateSlideForItemId ( itemId ) ;
if ( ! p reloadedIds . has ( itemId ) ) {
preloadedIds . add ( itemId ) ;
SlideCreator . createSlideForItemId ( itemId ) ;
}
}
}
// Preload previous slides
// Preload previous slides
@@ -2474,7 +2524,10 @@ const SlideshowManager = {
if ( prevIndex === currentIndex ) break ;
if ( prevIndex === currentIndex ) break ;
const prevItemId = STATE . slideshow . itemIds [ prevIndex ] ;
const prevItemId = STATE . slideshow . itemIds [ prevIndex ] ;
SlideCreator . c reateSlideForItemId ( prevItemId ) ;
if ( ! p reloadedIds . has ( prevItemId ) ) {
preloadedIds . add ( prevItemId ) ;
SlideCreator . createSlideForItemId ( prevItemId ) ;
}
}
}
} ,
} ,
@@ -2510,13 +2563,11 @@ const SlideshowManager = {
if ( index === - 1 ) return ;
if ( index === - 1 ) return ;
const totalItems = STATE . slideshow . itemIds . length ;
const totalItems = STATE . slideshow . itemIds . length ;
// Calculate wrapped distance
let distance = Math . abs ( index - currentIndex ) ;
let distance = Math . abs ( index - currentIndex ) ;
if ( totalItems > keepRange * 2 ) {
if ( totalItems > keepRange * 2 ) {
distance = Math . min ( distance , totalItems - distance ) ;
distance = Math . min ( distance , totalItems - distance ) ;
}
}
if ( distance > keepRange ) {
if ( distance > keepRange ) {
// Destroy video player if exists
// Destroy video player if exists
if ( STATE . slideshow . videoPlayers [ itemId ] ) {
if ( STATE . slideshow . videoPlayers [ itemId ] ) {
@@ -2541,13 +2592,11 @@ const SlideshowManager = {
}
}
} ) ;
} ) ;
// After pruning, restore focus to container in TV mode
if ( prunedAny ) {
if ( prunedAny ) {
const isTvMode = ( window . layoutManager && window . layoutManager . tv ) ||
const isTvMode = ( window . layoutManager && window . layoutManager . tv ) ||
document . documentElement . classList . contains ( 'layout-tv' ) ||
document . documentElement . classList . contains ( 'layout-tv' ) ||
document . body . classList . contains ( 'layout-tv' ) ;
document . body . classList . contains ( 'layout-tv' ) ;
if ( isTvMode ) {
if ( isTvMode ) {
// Use setTimeout to execute AFTER Jellyfin's focus manager processes the iframe removal
setTimeout ( ( ) => {
setTimeout ( ( ) => {
const container = document . getElementById ( "slides-container" ) ;
const container = document . getElementById ( "slides-container" ) ;
if ( container && container . style . display !== 'none' ) {
if ( container && container . style . display !== 'none' ) {
@@ -2674,187 +2723,6 @@ 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.
* Includes a retry mechanism for YouTube players that aren't ready yet.
* @param {Element} slide - The slide DOM element
* @param {string} itemId - The item ID of the slide
*/
playCurrentVideo ( slide , itemId ) {
// Find video element — check class (covers both original div and iframe with class restored by onReady)
const videoBackdrop = slide . querySelector ( '.video-backdrop' ) ;
const ytPlayer = STATE . slideshow . videoPlayers && STATE . slideshow . videoPlayers [ itemId ] ;
const hasAnyVideo = ! ! ( videoBackdrop || ytPlayer ) ;
console . log ( ` [MBE-PLAY] playCurrentVideo for ${ itemId } : videoBackdrop= ${ videoBackdrop ? . tagName || 'null' } , ytPlayer= ${ ! ! ytPlayer } , ytReady= ${ ytPlayer && typeof ytPlayer . loadVideoById === 'function' } ` ) ;
// Update mute button visibility
const muteButton = document . querySelector ( '.mute-button' ) ;
if ( muteButton ) {
muteButton . style . display = hasAnyVideo ? 'block' : 'none' ;
}
if ( ! hasAnyVideo ) {
console . log ( ` [MBE-PLAY] No video found for ${ itemId } , skipping ` ) ;
return ;
}
// HTML5 <video> element
if ( videoBackdrop && videoBackdrop . tagName === 'VIDEO' ) {
console . log ( ` [MBE-PLAY] Playing HTML5 video for ${ itemId } ` ) ;
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 ( ` [MBE-PLAY] Autoplay blocked for ${ itemId } , muted fallback ` ) ;
videoBackdrop . muted = true ;
videoBackdrop . play ( ) . catch ( err => console . error ( '[MBE-PLAY] Muted fallback failed' , err ) ) ;
}
} , 1000 ) ;
} ) ;
return ;
}
// YouTube player — try to play now if ready
if ( ytPlayer && typeof ytPlayer . loadVideoById === 'function' && ytPlayer . _videoId ) {
console . log ( ` [MBE-PLAY] YouTube player READY for ${ itemId } , calling loadVideoById ` ) ;
ytPlayer . loadVideoById ( {
videoId : ytPlayer . _videoId ,
startSeconds : ytPlayer . _startTime || 0 ,
endSeconds : ytPlayer . _endTime
} ) ;
if ( STATE . slideshow . isMuted ) {
ytPlayer . mute ( ) ;
} else {
ytPlayer . unMute ( ) ;
ytPlayer . setVolume ( 40 ) ;
}
// Pause slideshow timer for video if configured
if ( CONFIG . waitForTrailerToEnd && STATE . slideshow . slideInterval ) {
STATE . slideshow . slideInterval . stop ( ) ;
}
// 1s check: if still not playing, force muted retry
setTimeout ( ( ) => {
if ( ! slide . classList . contains ( 'active' ) ) return ;
try {
const state = ytPlayer . getPlayerState ( ) ;
if ( state !== YT . PlayerState . PLAYING && state !== YT . PlayerState . BUFFERING ) {
console . warn ( ` [MBE-PLAY] loadVideoById didn't start for ${ itemId } (state= ${ state } ), muted retry ` ) ;
ytPlayer . mute ( ) ;
ytPlayer . playVideo ( ) ;
}
} catch ( e ) { console . warn ( '[MBE-PLAY] Error checking player state:' , e ) ; }
} , 1500 ) ;
return ;
}
// YouTube player NOT ready yet (onReady hasn't fired).
// onReady will handle it IF the slide is still active when it fires.
// But as safety net: retry every 500ms for up to 6 seconds.
console . log ( ` [MBE-PLAY] YouTube player NOT READY for ${ itemId } , starting retry loop (onReady will also attempt) ` ) ;
let retryCount = 0 ;
const maxRetries = 12 ; // 12 × 500ms = 6 seconds
const retryTimer = setInterval ( ( ) => {
retryCount ++ ;
// Abort if slide changed or paused
if ( ! slide . classList . contains ( 'active' ) || STATE . slideshow . isPaused ) {
console . log ( ` [MBE-PLAY] Retry aborted for ${ itemId } (slide inactive or paused) ` ) ;
clearInterval ( retryTimer ) ;
return ;
}
const p = STATE . slideshow . videoPlayers && STATE . slideshow . videoPlayers [ itemId ] ;
// Check if player is now playing (onReady may have started it)
if ( p && typeof p . getPlayerState === 'function' ) {
try {
const state = p . getPlayerState ( ) ;
if ( state === YT . PlayerState . PLAYING || state === YT . PlayerState . BUFFERING ) {
console . log ( ` [MBE-PLAY] Player for ${ itemId } is already playing (started by onReady), stopping retry ` ) ;
clearInterval ( retryTimer ) ;
return ;
}
} catch ( e ) { /* player not fully ready yet */ }
}
// Check if player is now ready
if ( p && typeof p . loadVideoById === 'function' && p . _videoId ) {
console . log ( ` [MBE-PLAY] Retry # ${ retryCount } : Player for ${ itemId } now READY, calling loadVideoById ` ) ;
clearInterval ( retryTimer ) ;
p . loadVideoById ( {
videoId : p . _videoId ,
startSeconds : p . _startTime || 0 ,
endSeconds : p . _endTime
} ) ;
if ( STATE . slideshow . isMuted ) {
p . mute ( ) ;
} else {
p . unMute ( ) ;
p . setVolume ( 40 ) ;
}
if ( CONFIG . waitForTrailerToEnd && STATE . slideshow . slideInterval ) {
STATE . slideshow . slideInterval . stop ( ) ;
}
return ;
}
if ( retryCount >= maxRetries ) {
console . warn ( ` [MBE-PLAY] Gave up retrying for ${ itemId } after ${ maxRetries * 500 } ms ` ) ;
clearInterval ( retryTimer ) ;
}
} , 500 ) ;
} ,
/**
/**
* Stops all video playback (YouTube and HTML5)
* Stops all video playback (YouTube and HTML5)
* Used when navigating away from the home screen
* Used when navigating away from the home screen
@@ -3003,7 +2871,7 @@ const SlideshowManager = {
// Determine if we should handle navigation keys (Arrows, Space, M)
// Determine if we should handle navigation keys (Arrows, Space, M)
// TV Mode: Strict focus required (must be on slideshow)
// TV Mode: Strict focus required (must be on slideshow)
// Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused)
// Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused)
le t canControlSlideshow = isTvMode ? hasDirectFocus : ( hasDirectFocus || isBodyFocused ) ;
cons t canControlSlideshow = isTvMode ? hasDirectFocus : ( hasDirectFocus || isBodyFocused ) ;
// Check for Input Fields (always ignore typing)
// Check for Input Fields (always ignore typing)
const isInputElement = activeElement && ( activeElement . tagName === 'INPUT' || activeElement . tagName === 'TEXTAREA' || activeElement . isContentEditable ) ;
const isInputElement = activeElement && ( activeElement . tagName === 'INPUT' || activeElement . tagName === 'TEXTAREA' || activeElement . isContentEditable ) ;
@@ -3647,69 +3515,75 @@ const MediaBarEnhancedSettingsManager = {
* Initialize page visibility handling to pause when tab is inactive
* Initialize page visibility handling to pause when tab is inactive
*/
*/
const initPageVisibilityHandler = ( ) => {
const initPageVisibilityHandler = ( ) => {
document . addEventListener ( "visibilitychange" , ( ) => {
let wasVideoPlayingBeforeHide = false ;
const isVideoPlayerOpen = document . querySelector ( '.videoPlayerContainer:not(.hide)' ) ||
document . querySelector ( '.youtubePlayerContainer:not(.hide)' ) ;
document . addEventListener ( "visibilitychange" , ( ) => {
if ( document . hidden ) {
if ( document . hidden ) {
// Stop slide timer
console . log ( "Tab inactive - pausing slideshow and videos" ) ;
wasVideoPlayingBeforeHide = STATE . slideshow . isVideoPlaying ;
if ( STATE . slideshow . slideInterval ) {
if ( STATE . slideshow . slideInterval ) {
STATE . slideshow . slideInterval . stop ( ) ;
STATE . slideshow . slideInterval . stop ( ) ;
}
}
if ( isVideoPlayerOpen ) {
// Pause active video if playing
// Jellyfin video is playing --> full stop to free all resources
const currentItemId = STATE . slideshow . itemIds [ STATE . slideshow . currentSlideIndex ] ;
console . log ( "Tab inactive and Jellyfin player active - stopping all playback" ) ;
if ( currentItemId ) {
SlideshowManager . stopAllPlayback ( ) ;
// YouTube
} else {
if ( STATE . slideshow . videoPlayers && STATE . slideshow . videoPlayers [ currentItemId ] ) {
// Simple tab switch: stop all others, pause only the current
const player = STATE . slideshow . videoPlayers [ currentItemId ] ;
console . log ( "Tab inactive. Pausing current video, stopping others ") ;
if ( typeof player . pauseVideo === "function " ) {
const currentItemId = STATE . slideshow . itemIds ? . [ STATE . slideshow . currentSlideIndex ] ;
// Stop all players except the current one
if ( STATE . slideshow . videoPlayers ) {
Object . keys ( STATE . slideshow . videoPlayers ) . forEach ( id => {
if ( id === currentItemId ) return ;
const p = STATE . slideshow . videoPlayers [ id ] ;
if ( p ) {
try {
if ( typeof p . stopVideo === 'function' ) {
p . stopVideo ( ) ;
if ( typeof p . clearVideo === 'function' ) p . clearVideo ( ) ;
} else if ( p . tagName === 'VIDEO' ) {
p . pause ( ) ;
p . muted = true ;
p . currentTime = 0 ;
}
} catch ( e ) { console . warn ( "Error stopping background player" , id , e ) ; }
}
} ) ;
}
// Pause only the current video
if ( currentItemId ) {
const player = STATE . slideshow . videoPlayers ? . [ currentItemId ] ;
if ( player ) {
try {
try {
if ( typeof player . pauseVideo === 'function' ) {
player . pauseVideo ( ) ;
player . pauseVideo ( ) ;
STATE . slideshow . isVideoPlaying = false ;
} else if ( player . tagName === 'VIDEO' ) {
} catch ( e ) {
player . pause ( ) ;
console . warn ( "Error pausing video on tab hide:" , e ) ;
}
}
} catch ( e ) { console . warn ( "Error pausing video on tab hide:" , e ) ; }
} else if ( player . tagName === 'VIDEO' ) { // HTML5 Video
player . pause ( ) ;
STATE . slideshow . isVideoPlaying = false ;
}
}
}
}
}
}
} else {
} else {
console . log ( "Tab active. R esuming slideshow" ) ;
console . log ( "Tab active - r esuming slideshow" ) ;
const isOnHome = window . location . hash === "#/home.html" || window . location . hash === "#/home" ;
if ( ! STATE . slideshow . isPaused ) {
const currentItemId = STATE . slideshow . itemIds [ STATE . slideshow . currentSlideIndex ] ;
if ( isOnHome && ! STATE . slideshow . isPaused && ! isVideoPlayerOpen ) {
SlideshowManager . resumeActivePlayback ( ) ;
if ( wasVideoPlayingBeforeHide && currentItemId && STATE . slideshow . videoPlayers && STATE . slideshow . videoPlayers [ currentItemId ] ) {
const player = STATE . slideshow . videoPlayers [ currentItemId ] ;
if ( STATE . slideshow . slideInterval && ! CONFIG . waitForTrailerToEnd ) {
STATE . slideshow . slideInterval . start ( ) ;
// 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 ( ) ;
}
}
}
}
wasVideoPlayingBeforeHide = false ;
}
}
}
}
} ) ;
} ) ;