Compare commits

...

5 Commits

Author SHA1 Message Date
mani
3dd1de3d9c Fix CRT shader: use plain &crtShader=true query param in TranscodingUrl
Some checks failed
Push & Release 🌍 / Automation 🎛️ (push) Has been cancelled
Push & Release 🌍 / Unstable release 🚀⚠️ (push) Has been cancelled
Push & Release 🌍 / Quality checks 👌🧪 (push) Has been cancelled
Push & Release 🌍 / GitHub CodeQL 🔬 (push) Has been cancelled
Push & Release 🌍 / Deploy 🚀 (push) Has been cancelled
Previous approach used streamOptions[crtShader]=true (wrong format).
ParseStreamOptions on the server reads IQueryCollection directly and
stores keys verbatim — so streamOptions[crtShader] becomes the key,
not crtShader, and TryGetValue("crtShader") always returns false.

Plain &crtShader=true works because ParseStreamOptions adds any
lowercase-starting query param directly to the StreamOptions dict.

Also remove the dead PlaybackInfoDto.StreamOptions code — that DTO
has no StreamOptions field, so it was silently ignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 22:23:24 +01:00
mani
d14f15a2f8 Fix CRT shader: pass streamOptions via PlaybackInfo body, not URL
Some checks failed
Push & Release 🌍 / Automation 🎛️ (push) Has been cancelled
Push & Release 🌍 / Unstable release 🚀⚠️ (push) Has been cancelled
Push & Release 🌍 / Quality checks 👌🧪 (push) Has been cancelled
Push & Release 🌍 / GitHub CodeQL 🔬 (push) Has been cancelled
Push & Release 🌍 / Deploy 🚀 (push) Has been cancelled
The previous approach of appending streamOptions[crtShader]=true to
the transcoding URL didn't work — the server only reads StreamOptions
from the PlaybackInfo request body (PlaybackInfoDto), not from the
streaming endpoint URL query string.

Pass streamOptions as part of the getPlaybackInfo query so the server
embeds them in the TranscodingUrl. Also propagate crtShadowMask the
same way. Remove the broken URL-appending code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 21:36:59 +01:00
mani
de2a29621d Fix CRT shader: add AllowVideoStreamCopy=false to force re-encode
Some checks failed
Push & Release 🌍 / GitHub CodeQL 🔬 (push) Has been cancelled
Push & Release 🌍 / Deploy 🚀 (push) Has been cancelled
Push & Release 🌍 / Automation 🎛️ (push) Has been cancelled
Push & Release 🌍 / Unstable release 🚀⚠️ (push) Has been cancelled
Push & Release 🌍 / Quality checks 👌🧪 (push) Has been cancelled
Without this the server chooses codec:copy (container remux MKV→HLS)
which runs no filter chain at all. The CRT shader requires actual
video decoding and re-encoding through program_opencl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 02:51:45 +01:00
mani
f6fce9eae8 Fix CRT shader: force transcoding when enabling filter
When setCrtShader(true) was called the stream restart passed an empty
params object to changeStream(), which left EnableDirectPlay/EnableDirectStream
undefined. Due to JavaScript loose equality (undefined != null === false)
these were never sent to the server, so the server could return a Direct Play
MediaSource and our streamOptions[crtShader] append on TranscodingUrl was
never reached — resulting in no visual effect.

Fix: pass EnableDirectPlay:false, EnableDirectStream:false when enabling CRT
to force the server to transcode. When disabling, empty params restore the
default negotiation so the server can resume direct play if eligible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:19:31 +01:00
mani
0b4b125519 Add CRT shader toggle to player settings menu
Some checks failed
Push & Release 🌍 / Automation 🎛️ (push) Has been cancelled
Push & Release 🌍 / Unstable release 🚀⚠️ (push) Has been cancelled
Push & Release 🌍 / Quality checks 👌🧪 (push) Has been cancelled
Push & Release 🌍 / GitHub CodeQL 🔬 (push) Has been cancelled
Push & Release 🌍 / Deploy 🚀 (push) Has been cancelled
Adds a per-session CRT-Lottes shader toggle in the video player settings
menu (shown when transcoding is available). Toggling restarts the stream
with streamOptions[crtShader]=true appended to the TranscodingUrl, which
the server picks up to apply the OpenCL CRT post-processing filter.

- playersettingsmenu: showCrtMenu() (On/Off actionsheet), menu entry with
  current state as aside text, handleSelectedOption case
- playbackmanager: isCrtShaderEnabled(), setCrtShader(enabled, shadowMask)
  methods; createStreamInfo appends CRT stream options to TranscodingUrl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:53:56 +01:00
2 changed files with 75 additions and 1 deletions

View File

@@ -1422,6 +1422,28 @@ export class PlaybackManager {
});
};
self.isCrtShaderEnabled = function (player) {
player = player || self._currentPlayer;
return !!(getPlayerData(player).enableCrtShader);
};
self.setCrtShader = function (enabled, shadowMask, player) {
player = player || self._currentPlayer;
const pd = getPlayerData(player);
pd.enableCrtShader = !!enabled;
if (shadowMask !== undefined && shadowMask !== null) {
pd.crtShadowMask = shadowMask;
}
// When enabling CRT we must force full re-encode — the filter only runs
// server-side via program_opencl and requires actual video decoding/encoding.
// AllowVideoStreamCopy:false prevents the server from choosing codec:copy
// (container remux), which would bypass the filter chain entirely.
const streamParams = enabled
? { EnableDirectPlay: false, EnableDirectStream: false, AllowVideoStreamCopy: false }
: {};
changeStream(player, getCurrentTicks(player), streamParams);
};
self.isFullscreen = function (player) {
player = player || self._currentPlayer;
if (!player.isLocalPlayer || player.isFullscreen) {
@@ -2856,7 +2878,19 @@ export class PlaybackManager {
playMethod = mediaSource.SupportsDirectPlay ? 'DirectPlay' : 'DirectStream';
} else if (mediaSource.SupportsTranscoding) {
mediaUrl = apiClient.getUrl(mediaSource.TranscodingUrl);
let transcodingUrl = mediaSource.TranscodingUrl;
// Append CRT shader options as plain query params so ParseStreamOptions
// on the server picks them up (key must start lowercase, no streamOptions[] wrapper).
if (getPlayerData(player).enableCrtShader) {
transcodingUrl += '&crtShader=true';
const maskVal = getPlayerData(player).crtShadowMask;
if (maskVal !== undefined && maskVal !== null) {
transcodingUrl += '&crtShadowMask=' + maskVal;
}
}
mediaUrl = apiClient.getUrl(transcodingUrl);
if (mediaSource.TranscodingSubProtocol === 'hls') {
contentType = 'application/x-mpegURL';

View File

@@ -4,6 +4,34 @@ import globalize from 'lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import qualityoptions from '../qualityOptions';
function showCrtMenu(player, btn) {
const isEnabled = playbackManager.isCrtShaderEnabled(player);
const menuItems = [
{
name: globalize.translate('On'),
id: 'crt_on',
selected: isEnabled
},
{
name: globalize.translate('Off'),
id: 'crt_off',
selected: !isEnabled
}
];
return actionsheet.show({
items: menuItems,
positionTo: btn
}).then(function (id) {
if (id === 'crt_on' && !isEnabled) {
playbackManager.setCrtShader(true, null, player);
} else if (id === 'crt_off' && isEnabled) {
playbackManager.setCrtShader(false, null, player);
}
});
}
function showQualityMenu(player, btn) {
const videoStream = playbackManager.currentMediaSource(player).MediaStreams.filter(function (stream) {
return stream.Type === 'Video';
@@ -214,6 +242,16 @@ function showWithUser(options, player, user) {
});
}
if (options.quality && user?.Policy?.EnableVideoPlaybackTranscoding) {
menuItems.push({
name: 'CRT Shader',
id: 'crtshader',
asideText: playbackManager.isCrtShaderEnabled(player)
? globalize.translate('On')
: globalize.translate('Off')
});
}
const repeatMode = playbackManager.getRepeatMode(player);
if (supportedCommands.indexOf('SetRepeatMode') !== -1 && playbackManager.currentMediaSource(player).RunTimeTicks) {
@@ -272,6 +310,8 @@ function handleSelectedOption(id, options, player) {
return showPlaybackRateMenu(player, options.positionTo);
case 'repeatmode':
return showRepeatModeMenu(player, options.positionTo);
case 'crtshader':
return showCrtMenu(player, options.positionTo);
case 'stats':
if (options.onOption) {
options.onOption('stats');