Compare commits

..

17 Commits

Author SHA1 Message Date
CodeDevMLH
92fc8d72f7 Update manifest.json for release v1.6.1.2 [skip ci] 2026-02-11 19:13:25 +00:00
CodeDevMLH
cfe9dec550 Bump version to 1.6.1.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-11 20:12:30 +01:00
CodeDevMLH
1bef573aaf Enhance video playback handling and improve slide preloading logic 2026-02-11 20:12:16 +01:00
CodeDevMLH
29a365b690 Update manifest.json for release v1.6.1.1 [skip ci] 2026-02-11 18:57:36 +00:00
CodeDevMLH
0ee0a65309 Bump version to 1.6.1.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-11 19:56:42 +01:00
CodeDevMLH
152d22b709 Refine video playback logic in slideshow and update preload description 2026-02-11 19:56:20 +01:00
CodeDevMLH
216dddad94 Update manifest.json for release v1.6.1.0 [skip ci] 2026-02-11 17:24:36 +00:00
CodeDevMLH
a6de148ca1 Bump version to 1.6.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-02-11 18:23:42 +01:00
CodeDevMLH
1f9378d74d Enhance video backdrop styling for TV layout and improve focus management in slideshow 2026-02-11 18:22:44 +01:00
CodeDevMLH
cc025779dc Update manifest.json for release v1.6.0.2 [skip ci] 2026-02-10 22:07:34 +00:00
CodeDevMLH
3d10fd59b5 Enhance manual trailer/video override instructions to support Jellyfin Item IDs and clarify usage
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-10 23:06:40 +01:00
CodeDevMLH
bf261eba96 Update manifest.json for release v1.6.0.2 [skip ci] 2026-02-10 21:44:36 +00:00
CodeDevMLH
e3116c30cf Bump version to 1.6.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-10 22:43:47 +01:00
CodeDevMLH
bb39c91d32 Enhance custom trailer URL handling to support Jellyfin Item IDs and standard URLs 2026-02-10 22:43:40 +01:00
CodeDevMLH
4e0c74614a Update manifest.json for release v1.6.0.1 [skip ci] 2026-02-10 21:27:25 +00:00
CodeDevMLH
b61bf92437 Bump version to 1.6.0.1 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-10 22:26:32 +01:00
CodeDevMLH
dfbd6ce964 Enable autoplay for video playback in SlideUtils 2026-02-10 22:26:28 +01:00
5 changed files with 125 additions and 30 deletions

View File

@@ -123,9 +123,15 @@
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow. <div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow.
You can separate them by new line or comma. You can separate them by new line or comma.
<br><br> <br><br>
<b>Manual Trailer Override:</b> You can specify a YouTube URL for an item by adding it in <b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [https://youtu.be/...] DESCRIPTION</code> brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [Method] DESCRIPTION</code>.
<br><br> <br>
Methods:
<ul>
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li>
</ul>
<br>
You can also add a description after the ID using any separator like space, pipe You can also add a description after the ID using any separator like space, pipe
(|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code> (|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
<br><br> <br><br>
@@ -331,7 +337,7 @@
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label> <label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label>
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" /> <input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
<div class="fieldDescription">Number of images to preload.</div> <div class="fieldDescription">Number of slides to preload.</div>
</div> </div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination <label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination

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.0.0</Version> <Version>1.6.1.2</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

@@ -995,3 +995,14 @@
.dots-container .slide-counter { .dots-container .slide-counter {
margin: 0; margin: 0;
} }
.layout-tv .video-backdrop {
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
}

View File

@@ -737,7 +737,7 @@ const SlideUtils = {
width: '100%', width: '100%',
videoId: videoId, videoId: videoId,
playerVars: { playerVars: {
autoplay: 0, autoplay: 1,
controls: 1, controls: 1,
iv_load_policy: 3, iv_load_policy: 3,
rel: 0, rel: 0,
@@ -1576,9 +1576,25 @@ const SlideCreator = {
// 1a. Custom URL override // 1a. Custom URL override
if (STATE.slideshow.customTrailerUrls && STATE.slideshow.customTrailerUrls[itemId]) { if (STATE.slideshow.customTrailerUrls && STATE.slideshow.customTrailerUrls[itemId]) {
trailerUrl = STATE.slideshow.customTrailerUrls[itemId]; const customValue = STATE.slideshow.customTrailerUrls[itemId];
// Check if the custom value is a Jellyfin Item ID (GUID)
const guidMatch = customValue.match(/^([0-9a-f]{32})$/i);
if (guidMatch) {
const videoId = guidMatch[1];
console.log(`Using custom local video ID for ${itemId}: ${videoId}`);
trailerUrl = {
id: videoId,
url: `${STATE.jellyfinData.serverAddress}/Videos/${videoId}/stream.mp4?Static=true&api_key=${STATE.jellyfinData.accessToken}`
};
} else {
// Assume it's a standard URL (YouTube, etc.)
trailerUrl = customValue;
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`); console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
} }
}
// 1b. Check Local Trailer if preferred // 1b. Check Local Trailer if preferred
else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) { else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) {
trailerUrl = item.localTrailerUrl; trailerUrl = item.localTrailerUrl;
@@ -1696,11 +1712,18 @@ const SlideCreator = {
event.target.setPlaybackQuality(quality); event.target.setPlaybackQuality(quality);
} }
// Only play if this is the active slide // Only play if this is the active slide AND the slideshow is visible
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');
if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) { if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) {
const currentIndex = STATE.slideshow.currentSlideIndex;
const currentItemId = STATE.slideshow.itemIds[currentIndex];
if (currentItemId !== itemId) {
console.log(`Slide ${itemId} is no longer active (current: ${currentItemId}), aborting playback.`);
event.target.mute(); // Mute just in case
return;
}
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)
@@ -1757,7 +1780,6 @@ const SlideCreator = {
isVideo = true; isVideo = true;
const videoAttributes = { const videoAttributes = {
className: "backdrop video-backdrop",
className: "backdrop video-backdrop", className: "backdrop video-backdrop",
src: (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl), src: (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl),
autoplay: false, autoplay: false,
@@ -1778,7 +1800,18 @@ const SlideCreator = {
STATE.slideshow.videoPlayers[itemId] = backdrop; STATE.slideshow.videoPlayers[itemId] = backdrop;
backdrop.addEventListener('play', () => { backdrop.addEventListener('play', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
const currentIndex = STATE.slideshow.currentSlideIndex;
const currentItemId = STATE.slideshow.itemIds[currentIndex];
if (!slide || !slide.classList.contains('active') || currentItemId !== itemId) {
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();
} }
@@ -2222,6 +2255,16 @@ 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));
@@ -2250,6 +2293,15 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
if (focusSelector) {
const target = currentSlide.querySelector(focusSelector);
if (target) {
requestAnimationFrame(() => {
target.focus();
});
}
}
// Manage Video Playback: Stop others, Play current // Manage Video Playback: Stop others, Play current
// 1. Pause all other YouTube players // 1. Pause all other YouTube players
@@ -2257,8 +2309,14 @@ const SlideshowManager = {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => { Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
if (id !== currentItemId) { if (id !== currentItemId) {
const p = STATE.slideshow.videoPlayers[id]; const p = STATE.slideshow.videoPlayers[id];
if (p && typeof p.pauseVideo === 'function') { if (p) {
try {
if (typeof p.pauseVideo === 'function') {
p.pauseVideo(); p.pauseVideo();
} else if (p.tagName === 'VIDEO') {
p.pause();
}
} catch (e) { console.warn("Error pausing player", id, e); }
} }
} }
}); });
@@ -2266,8 +2324,11 @@ const SlideshowManager = {
// 2. Pause all other HTML5 videos e.g. local trailers // 2. Pause all other HTML5 videos e.g. local trailers
document.querySelectorAll('video').forEach(video => { document.querySelectorAll('video').forEach(video => {
if (!video.closest(`.slide[data-item-id="${currentItemId}"]`)) { const slideParent = video.closest('.slide');
if (slideParent && slideParent.dataset.itemId !== currentItemId) {
try {
video.pause(); video.pause();
} catch (e) {}
} }
}); });
@@ -2426,17 +2487,19 @@ const SlideshowManager = {
*/ */
async preloadAdjacentSlides(currentIndex) { async preloadAdjacentSlides(currentIndex) {
const totalItems = STATE.slideshow.totalItems; const totalItems = STATE.slideshow.totalItems;
const preloadCount = CONFIG.preloadCount; const preloadCount = Math.min(Math.max(CONFIG.preloadCount || 1, 1), 5);
const nextIndex = (currentIndex + 1) % totalItems; // Preload next slides
for (let i = 1; i <= preloadCount; i++) {
const nextIndex = (currentIndex + i) % totalItems;
const itemId = STATE.slideshow.itemIds[nextIndex]; const itemId = STATE.slideshow.itemIds[nextIndex];
SlideCreator.createSlideForItemId(itemId);
}
await SlideCreator.createSlideForItemId(itemId); // Preload previous slides
for (let i = 1; i <= preloadCount; i++) {
if (preloadCount > 1) { const prevIndex = (currentIndex - i + totalItems) % totalItems;
const prevIndex = (currentIndex - 1 + totalItems) % totalItems;
const prevItemId = STATE.slideshow.itemIds[prevIndex]; const prevItemId = STATE.slideshow.itemIds[prevIndex];
SlideCreator.createSlideForItemId(prevItemId); SlideCreator.createSlideForItemId(prevItemId);
} }
}, },
@@ -2471,7 +2534,14 @@ const SlideshowManager = {
const index = STATE.slideshow.itemIds.indexOf(itemId); const index = STATE.slideshow.itemIds.indexOf(itemId);
if (index === -1) return; if (index === -1) return;
const distance = Math.abs(index - currentIndex); const totalItems = STATE.slideshow.itemIds.length;
// Calculate wrapped distance
let distance = Math.abs(index - currentIndex);
if (totalItems > keepRange * 2) {
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]) {

View File

@@ -9,12 +9,20 @@
"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.0.0", "version": "1.6.1.2",
"changelog": "- fix tv mode issue",
"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",
"checksum": "186c08c80091c24270fda41f0908e951",
"timestamp": "2026-02-11T19:13:24Z"
},
{
"version": "1.6.0.2",
"changelog": "- add local trailer support on trailer button\nfix: iOS/MacOS playback issue?", "changelog": "- add local trailer support on trailer button\nfix: iOS/MacOS playback issue?",
"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.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "d16997010900e2b18c1a8b33d4ecb9be", "checksum": "cdd0208f8cc4f4b04f50e7138e508370",
"timestamp": "2026-02-10T21:18:25Z" "timestamp": "2026-02-10T22:07:33Z"
}, },
{ {
"version": "1.5.1.3", "version": "1.5.1.3",