Enhance TV mode support by preventing iframe focus stealing and ensuring container focus continuity; refactor video playback management for improved clarity and functionality.
This commit is contained in:
@@ -1696,6 +1696,12 @@ const SlideCreator = {
|
||||
playerVars: playerVars,
|
||||
events: {
|
||||
'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
|
||||
event.target._startTime = playerVars.start || 0;
|
||||
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
|
||||
if (CONFIG.waitForTrailerToEnd) {
|
||||
SlideshowManager.nextSlide();
|
||||
@@ -2293,109 +2300,27 @@ const SlideshowManager = {
|
||||
|
||||
currentSlide.classList.add("active");
|
||||
|
||||
if (focusSelector) {
|
||||
// Restore focus for TV mode navigation continuity
|
||||
requestAnimationFrame(() => {
|
||||
if (focusSelector) {
|
||||
const target = currentSlide.querySelector(focusSelector);
|
||||
if (target) {
|
||||
requestAnimationFrame(() => {
|
||||
target.focus();
|
||||
});
|
||||
target.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Manage Video Playback: Stop others, Play current
|
||||
|
||||
// 1. Pause 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();
|
||||
} 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) {}
|
||||
}
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Play and Reset current video
|
||||
const videoBackdrop = currentSlide.querySelector('.video-backdrop');
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Manage Video Playback: Stop others, Play current
|
||||
this.pauseOtherVideos(currentItemId);
|
||||
this.playCurrentVideo(currentSlide, currentItemId);
|
||||
|
||||
const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled);
|
||||
|
||||
@@ -2404,7 +2329,8 @@ const SlideshowManager = {
|
||||
if (backdrop && !backdrop.classList.contains("video-backdrop")) {
|
||||
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;
|
||||
@@ -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)
|
||||
* 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");
|
||||
if (container) {
|
||||
container.querySelectorAll('video').forEach(video => {
|
||||
try {
|
||||
video.pause();
|
||||
video.muted = true;
|
||||
video.currentTime = 0;
|
||||
} 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}"]`);
|
||||
if (!currentSlide) return;
|
||||
|
||||
// 1. Try YouTube Player
|
||||
const ytPlayer = STATE.slideshow.videoPlayers[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));
|
||||
}
|
||||
// Use playCurrentVideo to properly restore video with correct mute state
|
||||
this.playCurrentVideo(currentSlide, currentItemId);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2822,7 +2846,7 @@ const SlideshowManager = {
|
||||
// Determine if we should handle navigation keys (Arrows, Space, M)
|
||||
// TV Mode: Strict focus required (must be on slideshow)
|
||||
// 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)
|
||||
const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable);
|
||||
@@ -3040,7 +3064,7 @@ const SlideshowManager = {
|
||||
finalIds.push(id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Error resolving item ${id}:`, e);
|
||||
console.warn(`Error resolving item ${rawId}:`, e);
|
||||
}
|
||||
}
|
||||
return finalIds;
|
||||
@@ -3438,75 +3462,25 @@ const MediaBarEnhancedSettingsManager = {
|
||||
* Initialize page visibility handling to pause when tab is inactive
|
||||
*/
|
||||
const initPageVisibilityHandler = () => {
|
||||
let wasVideoPlayingBeforeHide = false;
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
console.log("Tab inactive - pausing slideshow and videos");
|
||||
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
|
||||
|
||||
console.log("Tab inactive - stopping all slideshow playback");
|
||||
if (STATE.slideshow.slideInterval) {
|
||||
STATE.slideshow.slideInterval.stop();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
SlideshowManager.stopAllPlayback();
|
||||
} else {
|
||||
console.log("Tab active - resuming slideshow");
|
||||
if (!STATE.slideshow.isPaused) {
|
||||
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
|
||||
|
||||
if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
|
||||
const player = STATE.slideshow.videoPlayers[currentItemId];
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
// Only resume if we're on the home page and not paused
|
||||
const isOnHome = window.location.hash === "#/home.html" || window.location.hash === "#/home";
|
||||
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer:not(.hide)') || document.querySelector('.youtubePlayerContainer:not(.hide)');
|
||||
|
||||
if (isOnHome && !STATE.slideshow.isPaused && !isVideoPlayerOpen) {
|
||||
SlideshowManager.resumeActivePlayback();
|
||||
|
||||
if (STATE.slideshow.slideInterval && !CONFIG.waitForTrailerToEnd) {
|
||||
STATE.slideshow.slideInterval.start();
|
||||
}
|
||||
wasVideoPlayingBeforeHide = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user