Compare commits

..

6 Commits

Author SHA1 Message Date
CodeDevMLH
ccba1857e1 Update manifest.json for release v1.6.1.3 [skip ci] 2026-02-12 00:43:53 +00:00
CodeDevMLH
ff56c9370b Bump version to 1.6.1.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-12 01:43:05 +01:00
CodeDevMLH
162600c43f Enhance TV mode support by preventing iframe focus stealing and ensuring container focus continuity; refactor video playback management for improved clarity and functionality. 2026-02-12 01:42:22 +01:00
CodeDevMLH
a21549af47 Update manifest.json for release v1.6.1.2 [skip ci] 2026-02-11 23:50:41 +00:00
CodeDevMLH
1b319ade40 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-12 00:49:49 +01:00
CodeDevMLH
75757d67e7 Adjust backdrop container positioning for TV layout 2026-02-11 20:15:17 +01:00
4 changed files with 164 additions and 186 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.2</Version> <Version>1.6.1.3</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

@@ -996,7 +996,11 @@
margin: 0; margin: 0;
} }
.layout-tv .video-backdrop { .layout-tv .backdrop-container{
top: -5%;
}
/* .layout-tv .video-backdrop {
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, #fff0 2%,
rgb(0 0 0 / 0.5) 6%, rgb(0 0 0 / 0.5) 6%,
@@ -1005,4 +1009,4 @@
#fff0 2%, #fff0 2%,
rgb(0 0 0 / 0.5) 6%, rgb(0 0 0 / 0.5) 6%,
#000000 8%); #000000 8%);
} } */

View File

@@ -1696,6 +1696,12 @@ 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');
}
// 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;
@@ -1764,7 +1770,8 @@ const SlideCreator = {
} }
} }
}, },
'onError': () => { 'onError': (event) => {
console.warn(`YouTube player error ${event.data} for video ${videoId}`);
// Fallback to next slide on error // Fallback to next slide on error
if (CONFIG.waitForTrailerToEnd) { if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
@@ -2293,109 +2300,27 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
if (focusSelector) { // Restore focus for TV mode navigation continuity
requestAnimationFrame(() => {
if (focusSelector) {
const target = currentSlide.querySelector(focusSelector); const target = currentSlide.querySelector(focusSelector);
if (target) { if (target) {
requestAnimationFrame(() => { target.focus();
target.focus(); return;
});
} }
} }
// Always ensure container has focus in TV mode to keep keyboard navigation working
// Manage Video Playback: Stop others, Play current const isTvMode = (window.layoutManager && window.layoutManager.tv) ||
document.documentElement.classList.contains('layout-tv') ||
// 1. Pause all other YouTube players document.body.classList.contains('layout-tv');
if (STATE.slideshow.videoPlayers) { if (isTvMode) {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => { container.focus({ preventScroll: true });
if (id !== currentItemId) {
const p = STATE.slideshow.videoPlayers[id];
if (p) {
try {
if (typeof p.pauseVideo === 'function') {
p.pauseVideo();
} else if (p.tagName === 'VIDEO') {
p.pause();
}
} catch (e) { console.warn("Error pausing player", id, e); }
}
}
});
}
// 2. Pause all other HTML5 videos e.g. local trailers
document.querySelectorAll('video').forEach(video => {
const slideParent = video.closest('.slide');
if (slideParent && slideParent.dataset.itemId !== currentItemId) {
try {
video.pause();
} catch (e) {}
} }
}); });
// 3. Play and Reset current video // Manage Video Playback: Stop others, Play current
const videoBackdrop = currentSlide.querySelector('.video-backdrop'); this.pauseOtherVideos(currentItemId);
this.playCurrentVideo(currentSlide, currentItemId);
// 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) {
console.warn(`Autoplay blocked for ${itemId}, 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 (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();
}
}
}
const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled); const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled);
@@ -2404,7 +2329,8 @@ const SlideshowManager = {
if (backdrop && !backdrop.classList.contains("video-backdrop")) { if (backdrop && !backdrop.classList.contains("video-backdrop")) {
backdrop.classList.add("animate"); backdrop.classList.add("animate");
} }
currentSlide.querySelector(".logo").classList.add("animate"); const logo = currentSlide.querySelector(".logo");
if (logo) logo.classList.add("animate");
} }
STATE.slideshow.currentSlideIndex = index; STATE.slideshow.currentSlideIndex = index;
@@ -2682,6 +2608,114 @@ 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
* @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) {
const videoBackdrop = slide.querySelector('.video-backdrop');
// Update mute button visibility
const muteButton = document.querySelector('.mute-button');
if (muteButton) {
muteButton.style.display = videoBackdrop ? 'block' : 'none';
}
if (!videoBackdrop) return false;
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) {
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?.[itemId];
if (player && typeof player.loadVideoById === 'function' && player._videoId) {
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 (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;
} else if (player && typeof player.seekTo === 'function') {
player.seekTo(player._startTime || 0);
player.playVideo();
return true;
}
return false;
},
/** /**
* 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
@@ -2711,14 +2745,16 @@ const SlideshowManager = {
}); });
} }
// 2. Pause all HTML5 videos // 2. Stop and mute all HTML5 videos
const container = document.getElementById("slides-container"); const container = document.getElementById("slides-container");
if (container) { if (container) {
container.querySelectorAll('video').forEach(video => { container.querySelectorAll('video').forEach(video => {
try { try {
video.pause(); video.pause();
video.muted = true;
video.currentTime = 0;
} catch (e) { } catch (e) {
console.warn("Error pausing HTML5 video:", e); console.warn("Error stopping HTML5 video:", e);
} }
}); });
} }
@@ -2736,20 +2772,8 @@ const SlideshowManager = {
const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`); const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`);
if (!currentSlide) return; if (!currentSlide) return;
// 1. Try YouTube Player // Use playCurrentVideo to properly restore video with correct mute state
const ytPlayer = STATE.slideshow.videoPlayers[currentItemId]; this.playCurrentVideo(currentSlide, currentItemId);
if (ytPlayer && typeof ytPlayer.playVideo === 'function') {
ytPlayer.playVideo();
}
// 2. Try HTML5 Video
const html5Video = currentSlide.querySelector('video');
if (html5Video) {
if (STATE.slideshow.isMuted) {
html5Video.muted = true;
}
html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e));
}
}, },
/** /**
@@ -2822,7 +2846,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)
const canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused); let 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);
@@ -3040,7 +3064,7 @@ const SlideshowManager = {
finalIds.push(id); finalIds.push(id);
} }
} catch (e) { } catch (e) {
console.warn(`Error resolving item ${id}:`, e); console.warn(`Error resolving item ${rawId}:`, e);
} }
} }
return finalIds; return finalIds;
@@ -3438,75 +3462,25 @@ 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 = () => {
let wasVideoPlayingBeforeHide = false;
document.addEventListener("visibilitychange", () => { document.addEventListener("visibilitychange", () => {
if (document.hidden) { if (document.hidden) {
console.log("Tab inactive - pausing slideshow and videos"); console.log("Tab inactive - stopping all slideshow playback");
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
SlideshowManager.stopAllPlayback();
// Pause active video if playing
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
if (currentItemId) {
// YouTube
if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
const player = STATE.slideshow.videoPlayers[currentItemId];
if (typeof player.pauseVideo === "function") {
try {
player.pauseVideo();
STATE.slideshow.isVideoPlaying = false;
} 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");
if (!STATE.slideshow.isPaused) { // Only resume if we're on the home page and not paused
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; const isOnHome = window.location.hash === "#/home.html" || window.location.hash === "#/home";
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer:not(.hide)') || document.querySelector('.youtubePlayerContainer:not(.hide)');
if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) { if (isOnHome && !STATE.slideshow.isPaused && !isVideoPlayerOpen) {
const player = STATE.slideshow.videoPlayers[currentItemId]; SlideshowManager.resumeActivePlayback();
// YouTube if (STATE.slideshow.slideInterval && !CONFIG.waitForTrailerToEnd) {
if (typeof player.playVideo === "function") { STATE.slideshow.slideInterval.start();
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;
} }
} }
}); });

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.2", "version": "1.6.1.3",
"changelog": "- fix tv mode issue", "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.2/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "186c08c80091c24270fda41f0908e951", "checksum": "e5c4f3d11b22621e013bdd0784cf35a6",
"timestamp": "2026-02-11T19:13:24Z" "timestamp": "2026-02-12T00:43:52Z"
}, },
{ {
"version": "1.6.0.2", "version": "1.6.0.2",