Shaders: Anime4K presets in-session and in Settings

Vendored Anime4K v4.0.1 GLSL (MIT), extracted to app support once per
version. Presets A/B/C/A+A/B+B/C+A in HQ and Fast per Anime4K's mpv
templates, plus custom files. Shader button in the video controls
(works in fullscreen), Ctrl+1..6 / Ctrl+0, live apply with read-back,
failures fall back to Off with a toast. Render mode detected from
mpv's GL log; warns when on the software path. Docs include the
Jellyfin client section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-07 01:45:54 +03:00
1 parent 4814bfb561
commit fb2927c094
28 files changed
+6516 -53

No files matched your search

+68 -2
View File
@@ -7,7 +7,7 @@ Plain overview first: [HOW-IT-WORKS.md](HOW-IT-WORKS.md). Wire format:
| Path | What | Rule |
|---|---|---|
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging | pure Dart, no Flutter |
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging, Jellyfin client | pure Dart, no Flutter |
| `packages/server` | Relic HTTP + WebSocket server (`SyncServer`, `ServerHub`, `Session`, media endpoint), the `serve()` entrypoint, `bin/server.dart` (dedicated server) | pure Dart, no Flutter |
| `apps/desktop` | Flutter app: UI, `SyncClient`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, updater | ships with `watch-together-server` |
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
@@ -196,6 +196,36 @@ and is passed to the `VideoController`; env `SYNC_RELAY_HWDEC` (e.g. `no`)
overrides it. It is read per player, so a change applies on the next join or
Reload video.
Shaders (per viewer, local only, no wire change): mpv `glsl-shaders`, set
through the same dynamic `setProperty` and read back
(`PlaybackController.applyShaders`). The value is the platform path list
(`:` joined, `;` on Windows). One saved choice in `AppSettings`
(`shaderPreset`, `shaderQuality`, `customShaders`): applied on every player
creation (join, Reload video) and live when changed from the session (the
control bar's shader menu, Ctrl+0..6, both also save it). The Anime4K
v4.0.1 files live in `apps/desktop/assets/shaders/anime4k/` (only the 14
the presets use, plus its MIT LICENSE); mpv needs real paths, so
`extractAnime4k` copies them once per version to
`<app support>/shaders/anime4k-<version>/` (a `.complete` marker is written
last). `anime4kFiles` maps each mode to the file list of Anime4K's mpv
templates: HQ = the high-end GPU set (VL + M networks), Fast = the low-end
set (M + S). A missing file, a path containing the list separator, a
read-back mismatch, or an mpv `libmpv_render` error while shaders are on
turns shaders off (saved) with a toast.
Render mode: media_kit's Linux/Windows video output creates mpv's render
context (OpenGL, else its software renderer) when the `VideoController` is
created, and nothing in its API says which. The player therefore requests
mpv logs at `v`: OpenGL init logs `GL_VERSION` under `libmpv_render`
(`RenderMode.gpu`; "suspected software renderer" means llvmpipe,
`softwareGl`); none within 1.5 s of the controller being ready means the
software path (`software`, where shaders do nothing; the shader toast, the
menu and Settings say so). After startup the `v` level adds no steady
traffic (measured: 0 messages over 5 s of playback). The hardware path is
fragile: media_kit borrows whatever EGL context GDK left current on the main
thread (upstream media-kit#1404), so an X11 session or a Flutter/GDK change
can silently fall back to software.
Player lifecycle: the `Player` and its `VideoController` live for one
session: created on join, disposed on leave, replaced by `recreate()` for
"Reload video" (button or Ctrl+R; volume kept). `openUrl` opens with `play: false`; play/pause/seek/rate come only from
@@ -218,6 +248,42 @@ Never unmount GPU-textured widgets (avatar images) on a transport-gated
branch: teardown races mpv's frame upload and the window goes black. Keep them
mounted and skip painting (`Visibility` with `maintainState`).
## Jellyfin client
`packages/core/lib/src/jellyfin/`, pure Dart over `dart:io` `HttpClient`
behind a `JellyfinHttp` interface (tests inject a fake). Plans:
[ROADMAP.md](ROADMAP.md) "Epic: Jellyfin".
- `JellyfinClient`: one server + one user. Auth (Quick Connect, password,
`/Users/Me` check, logout), browsing (views, Resume, NextUp, Latest, paged
`/Items`, search, seasons, episodes), image URLs (tag + maxWidth, no token
needed), PlaybackInfo, stream URLs, Playing / Progress / Stopped reports,
played / unplayed / resume position.
- `normalizeServerUrl`, `parseJellyfinLink`: pasted addresses and item links
-> `(baseUrl, itemId)`; accounts are keyed by `ServerId`
(`/System/Info/Public`).
- `episodeAfter`: next episode is the successor in `/Shows/{id}/Episodes`
(crosses seasons, skips other copies of the same episode); NextUp means
first unwatched, not next-after-X.
- `matchTrack` / `matchTracks`: carry audio/subtitle choice to the next
episode (language required, then title words, forced/HI flags, codec,
channels).
Gotchas (Jellyfin 10.11):
- Every request sends `Authorization: MediaBrowser Client, Device, DeviceId,
Version[, Token]` (values percent-encoded). Unauthenticated calls missing a
field get 400 with no body.
- A POST must carry `Content-Length` even when empty: behind nginx a
body-less POST without it gets 400 (this was the Quick Connect Initiate
failure).
- Jellyfin overwrites a token's stored device name and app version with the
header's. Only use tokens this app obtained itself; a relay using a
viewer's token must send that viewer's client fields.
- Static streams (`/Videos/{id}/stream?static=true`) serve the original file
with byte ranges. The share URL has no credentials; each side adds its own
`api_key` (`authorizeUrl`).
- Ticks: 10,000,000 per second.
## Desktop UI
One `HomeScreen` (`screens/home/`) with a single top bar (`top_bar.dart`):
@@ -235,7 +301,7 @@ port, password and Host sit in a 340 px column on the right, below the
picker under 860 px. Text fields have fixed or capped widths (720 px max) so
they stay compact on ultrawide screens. Submit state (busy, cancel,
per-tab errors, server crash) is `ConnectFlow`. Settings (profile, hardware
decoding, updates, shortcuts, shaders "coming soon"), Integrations
decoding, updates, video shaders, shortcuts), Integrations
(Jellyfin "coming soon") and About open in the same body. Settings persist
in two Hive boxes: `connect_form` (last form and recents; saved even when a
join fails) and `settings`. Link media names are derived from the URL path
+9 -16
View File
@@ -6,20 +6,7 @@ off by deleting it in the same commit as the code. Wire changes also edit
## Backlog
1. **Shaders.** Per-viewer local setting using mpv `glsl-shaders` (set after
the player is created), shipping Anime4K presets (bloc97, MIT). Shaders only
apply on media_kit's hardware path (verified with a test shader); warn
when the software fallback is active. The hardware path is fragile:
media_kit borrows whatever EGL context GDK left current on the main
thread (upstream media-kit#1404), so a `VideoController` created before
the first frame, an X11 session, or a Flutter/GDK change can silently
fall back to software. Log which path media_kit picked; a vendored
media_kit_video fix (build the EGL display from GDK's native display) is
the durable answer if it regresses. The video freeze after GPU idle is not
explained by this; next suspect is media_kit's context switching on the
raster thread with NVIDIA.
2. **Windows build + CI.** There is no Windows runner. Try
1. **Windows build + CI.** There is no Windows runner. Try
`ubuntu2610/flutter_build` (LLVM-MinGW + Wine `gen_snapshot`) on the Linux
runner, fallback a GitHub Actions mirror. The manifest needs per-platform
URLs and `release_publish.sh` must handle a second platform. The updater's
@@ -27,9 +14,15 @@ off by deleting it in the same commit as the code. Wire changes also edit
`watch-together-server` needs a Windows `dart compile exe` too. Desktop
tests are not in CI either.
3. **Jellyfin** (epic, below).
2. **Jellyfin** (epic, below).
4. **Smaller items**
3. **Smaller items**
- Video output robustness: if media_kit's hardware path regresses to
software (see ARCHITECTURE, Media: render mode), vendor a
media_kit_video fix that builds the EGL display from GDK's native
display. The video freeze after GPU idle is not explained by this;
next suspect is media_kit's context switching on the raster thread
with NVIDIA.
- Link conveniences: a link swap in session updates the remembered media;
show the open link with an open-in-browser button; the session's swap
panel reuses Home's `MediaPicker`.