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>
This commit is contained in:
mani
2026-02-24 13:53:56 +01:00
parent c66db4e18d
commit 0b4b125519
2 changed files with 68 additions and 1 deletions

View File

@@ -1422,6 +1422,21 @@ 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;
}
changeStream(player, getCurrentTicks(player), {});
};
self.isFullscreen = function (player) {
player = player || self._currentPlayer;
if (!player.isLocalPlayer || player.isFullscreen) {
@@ -2856,7 +2871,19 @@ export class PlaybackManager {
playMethod = mediaSource.SupportsDirectPlay ? 'DirectPlay' : 'DirectStream';
} else if (mediaSource.SupportsTranscoding) {
mediaUrl = apiClient.getUrl(mediaSource.TranscodingUrl);
let transcodingUrl = mediaSource.TranscodingUrl;
// Append CRT-Lottes shader option when enabled for this session.
if (getPlayerData(player).enableCrtShader) {
const sep = transcodingUrl.includes('?') ? '&' : '?';
transcodingUrl += sep + 'streamOptions%5BcrtShader%5D=true';
const maskVal = getPlayerData(player).crtShadowMask;
if (maskVal !== undefined && maskVal !== null) {
transcodingUrl += '&streamOptions%5BcrtShadowMask%5D=' + 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');