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>
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>
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>
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>
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>
PlayerChange was firing before the subscriber rebound its player, so the
first media session update could send `isLocalPlayer: false` (player undefined)
and Android treated playback as remote (cast volume UI). Rewire PlaybackSubscriber
so PlayerChange binds the current player before invoking handlers, ensuring media
session updates always have a bound player and report the correct local/remote
state.
Fixes: https://github.com/jellyfin/jellyfin-android/issues/1745
Fixes: https://github.com/jellyfin/jellyfin-android/issues/1854