From d88f910b5821faa73803e06d65a624916aea391d Mon Sep 17 00:00:00 2001 From: Karol Labus Date: Thu, 17 Sep 2026 23:45:19 +0200 Subject: [PATCH] fix: update client-side subtitle positioning logic and vertical offset alignment --- JellyfinPlugin/Api/BlackBarService.cs | 159 ++++++++++++++++++++------ 1 file changed, 123 insertions(+), 36 deletions(-) diff --git a/JellyfinPlugin/Api/BlackBarService.cs b/JellyfinPlugin/Api/BlackBarService.cs index faf6521..8e896b8 100644 --- a/JellyfinPlugin/Api/BlackBarService.cs +++ b/JellyfinPlugin/Api/BlackBarService.cs @@ -72,31 +72,96 @@ namespace JellyfinPlugin.Api let interceptedItemId = null; let currentCropData = null; window._currentPlayMethod = null; + window._isVideoDirect = null; function isTranscoding() { - if (window._currentPlayMethod === 'Transcode' || window._currentPlayMethod === 'DirectStream') { + if (window._isVideoDirect === false) { return true; } - try { - if (window.playbackManager) { - const currentPlayer = window.playbackManager.getCurrentPlayer(); - if (currentPlayer && typeof currentPlayer.getPlayerState === 'function') { - const state = currentPlayer.getPlayerState(); - if (state && state.PlayMethod) { - return state.PlayMethod === 'Transcode' || state.PlayMethod === 'DirectStream'; + if (window._isVideoDirect === true) { + return false; + } + return false; + } + + function ensureBoundingBoxHook(video) { + if (!video._origGetBoundingClientRect) { + video._origGetBoundingClientRect = video.getBoundingClientRect; + video.getBoundingClientRect = function() { + if (video._isCroppedByPlugin && video.parentElement) { + const rect = video.parentElement.getBoundingClientRect(); + return { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + x: rect.x !== undefined ? rect.x : rect.left, + y: rect.y !== undefined ? rect.y : rect.top, + toJSON: function() { return this; } + }; + } + return video._origGetBoundingClientRect.apply(this, arguments); + }; + } + } + + let lastBottomPercent = 0; + + function updateSubtitleStyles(bottomPercent) { + lastBottomPercent = bottomPercent; + let styleEl = document.getElementById('black-bar-remover-sub-style'); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = 'black-bar-remover-sub-style'; + document.head.appendChild(styleEl); + } + + if (bottomPercent > 0) { + const offsetVh = (bottomPercent * 0.4 + 2.5).toFixed(2); + styleEl.textContent = ` + .videoSubtitles { + display: flex !important; + flex-direction: column !important; + justify-content: flex-end !important; + transform: translateY(-${offsetVh}vh) !important; + } + .track-text-output, + .custom-subtitle-overlay, + .subtitleSyncContainer, + .videoSubtitlesContainer, + .libassjs-canvas-parent, + canvas.libassjs-canvas, + .subtitles-octopus-canvas, + .htmlvideoplayer::-webkit-media-text-track-container, + .htmlvideoplayer::-webkit-media-text-track-display { + transform: translateY(-${offsetVh}vh) !important; + } + `; + console.log('Black Bar Remover: Subtitle CSS style updated translateY(-' + offsetVh + 'vh)'); + } else { + styleEl.textContent = ''; + } + } + + function adjustVttCues(bottomPercent) { + const video = document.querySelector('video'); + if (!video || !video.textTracks) return; + const targetLine = Math.max(10, 92 - Math.round(bottomPercent * 0.4 + 2.5)); + for (let i = 0; i < video.textTracks.length; i++) { + const track = video.textTracks[i]; + if (track && track.cues) { + for (let j = 0; j < track.cues.length; j++) { + const cue = track.cues[j]; + if (cue && !cue._pluginAdjusted) { + cue.snapToLines = false; + cue.line = targetLine; + cue._pluginAdjusted = true; } } } - } catch (e) {} - - const video = document.querySelector('video'); - if (video && video.src) { - const src = video.src.toLowerCase(); - if (src.includes('transcode') || src.includes('.m3u8') || src.includes('transcodereasons')) { - return true; - } } - return false; } function applyCrop(cropData) { @@ -104,9 +169,16 @@ namespace JellyfinPlugin.Api if (!video) return; if (isTranscoding()) { - console.log('Black Bar Remover: Video is transcoded by server (PlayMethod:', window._currentPlayMethod, '), skipping CSS crop.'); + console.log('Black Bar Remover: Video is transcoded by server (IsVideoDirect: false), skipping CSS crop.'); + video.style.position = ''; + video.style.objectFit = ''; + video.style.width = ''; + video.style.height = ''; + video.style.top = ''; + video.style.left = ''; video.style.clipPath = ''; video.style.transform = ''; + updateSubtitleStyles(0); return; } @@ -117,21 +189,28 @@ namespace JellyfinPlugin.Api const x = parseInt(parts[2]); const y = parseInt(parts[3]); - const topPercent = (y / 1080) * 100; - const bottomPercent = ((1080 - (y + h)) / 1080) * 100; - const leftPercent = (x / 1920) * 100; - const rightPercent = ((1920 - (x + w)) / 1920) * 100; - - video.style.clipPath = `inset(${topPercent}% ${rightPercent}% ${bottomPercent}% ${leftPercent}%)`; - - const scaleY = 1 / (1 - (topPercent + bottomPercent)/100); - const scaleX = 1 / (1 - (leftPercent + rightPercent)/100); - const scale = Math.max(scaleY, scaleX); - - if (scale > 1.01) { - video.style.transform = `scale(${scale})`; + if (h > 0 && h < 1080) { + const heightPercent = (1080 / h) * 100; + const topPercent = -(y / h) * 100; + const bottomPercent = ((1080 - (y + h)) / 1080) * 100; + + video.style.position = 'absolute'; + video.style.objectFit = 'fill'; + video.style.width = '100%'; + video.style.height = heightPercent.toFixed(2) + '%'; + video.style.top = topPercent.toFixed(2) + '%'; + video.style.left = '0%'; + video.style.transform = 'none'; + video.style.clipPath = 'none'; + + if (video.parentElement) { + video.parentElement.style.overflow = 'hidden'; + } + + updateSubtitleStyles(bottomPercent); + adjustVttCues(bottomPercent); + console.log('Black Bar Remover: Applied non-scaling crop height:', heightPercent.toFixed(2) + '% top:', topPercent.toFixed(2) + '% subOffset:', (bottomPercent * 0.4 + 2.5).toFixed(2) + 'vh'); } - console.log('Black Bar Remover: Applied crop', cropData, 'scale', scale); } } @@ -161,6 +240,7 @@ namespace JellyfinPlugin.Api if (id && id !== interceptedItemId) { interceptedItemId = id; window._currentPlayMethod = null; + window._isVideoDirect = null; console.log('Black Bar Remover: Intercepted itemId from fetch:', id); } } @@ -174,8 +254,9 @@ namespace JellyfinPlugin.Api clone.json().then(data => { if (data && data.MediaSources && data.MediaSources[0]) { const ms = data.MediaSources[0]; - window._currentPlayMethod = ms.PlayMethod || (ms.TranscodingUrl ? 'Transcode' : 'DirectPlay'); - console.log('Black Bar Remover: PlayMethod from fetch PlaybackInfo:', window._currentPlayMethod); + window._currentPlayMethod = ms.PlayMethod; + window._isVideoDirect = ms.IsVideoDirect !== false; + console.log('Black Bar Remover: PlaybackInfo detected -> PlayMethod:', window._currentPlayMethod, 'IsVideoDirect:', window._isVideoDirect); if (currentCropData) { applyCrop(currentCropData); } @@ -201,6 +282,7 @@ namespace JellyfinPlugin.Api if (id && id !== interceptedItemId) { interceptedItemId = id; window._currentPlayMethod = null; + window._isVideoDirect = null; console.log('Black Bar Remover: Intercepted itemId from XHR:', id); } } @@ -210,8 +292,9 @@ namespace JellyfinPlugin.Api const data = JSON.parse(xhr.responseText); if (data && data.MediaSources && data.MediaSources[0]) { const ms = data.MediaSources[0]; - window._currentPlayMethod = ms.PlayMethod || (ms.TranscodingUrl ? 'Transcode' : 'DirectPlay'); - console.log('Black Bar Remover: PlayMethod from XHR PlaybackInfo:', window._currentPlayMethod); + window._currentPlayMethod = ms.PlayMethod; + window._isVideoDirect = ms.IsVideoDirect !== false; + console.log('Black Bar Remover: XHR PlaybackInfo detected -> PlayMethod:', window._currentPlayMethod, 'IsVideoDirect:', window._isVideoDirect); if (currentCropData) { applyCrop(currentCropData); } @@ -299,8 +382,12 @@ namespace JellyfinPlugin.Api console.log('Black Bar Remover: Video element found for intercepted item:', currentItemId); fetchCrop(currentItemId); checkServerCrop(currentItemId); + } else if (video && lastBottomPercent > 0) { + updateSubtitleStyles(lastBottomPercent); + adjustVttCues(lastBottomPercent); } else if (!video) { currentItemId = null; + lastBottomPercent = 0; window._isServerCropping = false; } }, 2000);