Compare commits

..

10 Commits

Author SHA1 Message Date
CodeDevMLH
0682967591 Update manifest.json for release v1.6.1.28 [skip ci] 2026-02-14 14:38:41 +00:00
CodeDevMLH
7938728f8e Bump version to 1.6.1.28 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-14 15:37:50 +01:00
CodeDevMLH
a0773c66eb Refactor video playback logic to manage mute state more effectively and improve autoplay handling 2026-02-14 15:37:44 +01:00
CodeDevMLH
10f2a38add Update manifest.json for release v1.6.1.27 [skip ci] 2026-02-14 14:22:59 +00:00
CodeDevMLH
9bfa3ba5ea Bump version to 1.6.1.26
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-14 15:22:09 +01:00
CodeDevMLH
5c00c07b8a Refactor video playback logic and enhance slide management 2026-02-14 15:21:32 +01:00
CodeDevMLH
773c49a228 Update manifest.json for release v1.6.1.25 [skip ci] 2026-02-14 02:21:15 +00:00
CodeDevMLH
41a309e0d1 Bump version to 1.6.1.25
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-14 03:20:25 +01:00
CodeDevMLH
43797fbb98 Refactor video playback handling and improve tab visibility management 2026-02-14 03:20:04 +01:00
CodeDevMLH
f13a1ba1af test [skip ci] 2026-02-14 02:18:13 +01:00
4 changed files with 3852 additions and 370 deletions

View File

@@ -12,7 +12,7 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> --> <!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Media Bar Enhanced Plugin</Title> <Title>Jellyfin Media Bar Enhanced Plugin</Title>
<Authors>CodeDevMLH</Authors> <Authors>CodeDevMLH</Authors>
<Version>1.6.1.24</Version> <Version>1.6.1.28</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl> <RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl>
</PropertyGroup> </PropertyGroup>

View File

@@ -1435,8 +1435,6 @@ 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') {
this._lastVisibleState = 'player-active';
const container = document.getElementById("slides-container"); const container = document.getElementById("slides-container");
if (container) { if (container) {
container.style.display = "none"; container.style.display = "none";
@@ -1447,7 +1445,6 @@ const VisibilityObserver = {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
SlideshowManager.stopAllPlayback(); SlideshowManager.stopAllPlayback();
}
return; return;
} }
@@ -1462,12 +1459,6 @@ const VisibilityObserver = {
activeTab && activeTab &&
activeTab.getAttribute("data-index") === "0"; activeTab.getAttribute("data-index") === "0";
const newState = isVisible ? 'visible' : 'hidden';
// Only update DOM and trigger actions when state actually changes
if (this._lastVisibleState !== newState) {
this._lastVisibleState = newState;
container.style.display = isVisible ? "block" : "none"; container.style.display = isVisible ? "block" : "none";
container.style.visibility = isVisible ? "visible" : "hidden"; container.style.visibility = isVisible ? "visible" : "hidden";
container.style.pointerEvents = isVisible ? "auto" : "none"; container.style.pointerEvents = isVisible ? "auto" : "none";
@@ -1483,7 +1474,6 @@ const VisibilityObserver = {
} }
SlideshowManager.stopAllPlayback(); SlideshowManager.stopAllPlayback();
} }
}
}, },
/** /**
@@ -1491,11 +1481,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 +1607,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,25 +1704,21 @@ 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;
event.target._videoId = videoId; event.target._videoId = videoId;
if (typeof event.target.setPlaybackQuality === 'function') {
event.target.setPlaybackQuality(quality);
}
// 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');
if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) {
// Set mute state only for active slide
if (STATE.slideshow.isMuted) { if (STATE.slideshow.isMuted) {
event.target.mute(); event.target.mute();
} else { } else {
@@ -1740,22 +1726,6 @@ const SlideCreator = {
event.target.setVolume(40); event.target.setVolume(40);
} }
if (typeof event.target.setPlaybackQuality === 'function') {
event.target.setPlaybackQuality(quality);
}
// 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');
const isActive = slide && slide.classList.contains('active');
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)
@@ -1785,14 +1755,17 @@ const SlideCreator = {
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
} else {
event.target.mute();
} }
}, },
'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) {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
if (slide && slide.classList.contains('active')) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} }
}
}, },
'onError': (event) => { 'onError': (event) => {
console.warn(`YouTube player error ${event.data} for video ${videoId}`); console.warn(`YouTube player error ${event.data} for video ${videoId}`);
@@ -1832,16 +1805,17 @@ 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();
} }
@@ -2283,16 +2257,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 +2281,134 @@ 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");
currentSlide.removeAttribute("inert");
currentSlide.setAttribute("tabindex", "0");
// Restore focus for TV mode navigation continuity // Update Play/Pause Button State if it was paused
requestAnimationFrame(() => { if (STATE.slideshow.isPaused) {
if (focusSelector) {
const target = currentSlide.querySelector(focusSelector);
if (target) {
target.focus();
return;
}
}
// 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 });
}
});
// Manage Video Playback: Stop others, Play current
this.pauseOtherVideos(currentItemId);
if (!STATE.slideshow.isPaused) {
this.playCurrentVideo(currentSlide, currentItemId);
} 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);
} }
this.playCurrentVideo(currentSlide, currentItemId);
} }
// Manage Video Playback: Stop others, Play current
// 1. Pause and mute 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();
if (typeof p.mute === 'function') {
p.mute();
}
}
} catch (e) { console.warn("Error pausing player", id, e); }
}
}
});
}
// 2. Pause all other HTML5 videos e.g. local trailers
document.querySelectorAll('video').forEach(video => {
if (!video.closest(`.slide[data-item-id="${currentItemId}"]`)) {
video.pause();
video.muted = true;
}
});
// 3. Play and Reset current video
const videoBackdrop = currentSlide.querySelector('.video-backdrop');
// Update mute button visibility // Update mute button visibility
const muteButton = document.querySelector('.mute-button'); const muteButton = document.querySelector('.mute-button');
if (muteButton) { if (muteButton) {
muteButton.style.display = videoBackdrop ? 'block' : 'none'; 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) {
// If same video is already loaded, just seek and play (much faster)
let isAlreadyLoaded = false;
if (typeof player.getVideoData === 'function') {
try {
const data = player.getVideoData();
if (data && data.video_id === player._videoId) {
isAlreadyLoaded = true;
}
} catch (e) { /* player not fully ready */ }
}
if (isAlreadyLoaded) {
player.seekTo(player._startTime || 0);
player.playVideo();
} else {
// Full load needed (first time or different video)
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();
}
} }
} }
@@ -2458,6 +2505,7 @@ 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++) {
@@ -2465,8 +2513,11 @@ const SlideshowManager = {
if (nextIndex === currentIndex) break; if (nextIndex === currentIndex) break;
const itemId = STATE.slideshow.itemIds[nextIndex]; const itemId = STATE.slideshow.itemIds[nextIndex];
if (!preloadedIds.has(itemId)) {
preloadedIds.add(itemId);
SlideCreator.createSlideForItemId(itemId); SlideCreator.createSlideForItemId(itemId);
} }
}
// Preload previous slides // Preload previous slides
for (let i = 1; i <= preloadCount; i++) { for (let i = 1; i <= preloadCount; i++) {
@@ -2474,8 +2525,11 @@ const SlideshowManager = {
if (prevIndex === currentIndex) break; if (prevIndex === currentIndex) break;
const prevItemId = STATE.slideshow.itemIds[prevIndex]; const prevItemId = STATE.slideshow.itemIds[prevIndex];
if (!preloadedIds.has(prevItemId)) {
preloadedIds.add(prevItemId);
SlideCreator.createSlideForItemId(prevItemId); SlideCreator.createSlideForItemId(prevItemId);
} }
}
}, },
nextSlide() { nextSlide() {
@@ -2510,8 +2564,6 @@ 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);
@@ -2541,13 +2593,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 +2724,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 +2872,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)
let canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused); const 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,70 +3516,76 @@ 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");
SlideshowManager.stopAllPlayback();
} else {
// Simple tab switch: stop all others, pause only the current
console.log("Tab inactive. Pausing current video, stopping others");
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) { if (currentItemId) {
const player = STATE.slideshow.videoPlayers?.[currentItemId]; // YouTube
if (player) { if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
const player = STATE.slideshow.videoPlayers[currentItemId];
if (typeof player.pauseVideo === "function") {
try { try {
if (typeof player.pauseVideo === 'function') {
player.pauseVideo(); player.pauseVideo();
} else if (player.tagName === 'VIDEO') { STATE.slideshow.isVideoPlaying = false;
player.pause(); } catch (e) {
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. Resuming slideshow"); console.log("Tab active - resuming 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) { if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
SlideshowManager.resumeActivePlayback(); const player = STATE.slideshow.videoPlayers[currentItemId];
if (STATE.slideshow.slideInterval && !CONFIG.waitForTrailerToEnd) { // 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(); 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;
}
} }
}); });
}; };

View File

@@ -9,12 +9,12 @@
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [ "versions": [
{ {
"version": "1.6.1.24", "version": "1.6.1.28",
"changelog": "- fix tv mode issue\n- refactor video playback management", "changelog": "- fix tv mode issue\n- refactor video playback management",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.24/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.28/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "466a2504753288ac48d3a9fd6b697f27", "checksum": "2b44f2ee7399b70c4bfdaae33ff80437",
"timestamp": "2026-02-14T01:12:44Z" "timestamp": "2026-02-14T14:38:40Z"
}, },
{ {
"version": "1.6.0.2", "version": "1.6.0.2",

3607
tmp.js Normal file

File diff suppressed because it is too large Load Diff