Stabilisation quick fixes: orderly close, visible version and update band, chat pop-ups on, Windows shaders and hwdec, render logs
ci / core (push) Successful in 27s
ci / server (push) Successful in 1m4s
ci / updater-windows (push) Successful in 5m50s

- Close: hide the window, dispose every player, thumbnail job and the
  sound engine, leave, then close normally; Linux runner _exit (the
  NVIDIA EGL exit handler crashed 10/10 closes, 0/41 now); Windows tears
  down inside the message loop, then TerminateProcess; watchdogs.
- Version under the Settings tab; an update band instead of a dot,
  never over a video; checks at start and every 4 hours.
- Chat pop-ups default to All again.
- Shader read-back compared as a list (old mpv joins with ','), so
  Windows stops turning shaders off; Windows hwdec auto-copy.
- Render diagnostics (`mpv: render` lines); RAM-only demuxer cache pin
  silences the file-cache warning; a volume readout test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-15 18:01:44 +03:00
1 parent e537f9d5f8
commit ea607c1a52
29 files changed
+873 -168

No files matched your search

+75 -14
View File
@@ -193,7 +193,7 @@ Loosely modeled on Syncplay, with a different readiness design.
the server state and reconnect every 1.5 s until cancelled (an epoch counter makes
a cancelled attempt stale). After a reconnect the ready flag is
restored from the server's state. Cancel during setup aborts the join.
Closing the window leaves gracefully (2 s timeout).
Closing the window leaves gracefully (see "App close" under Desktop UI).
- **Idle** (`IdleTracker`): measured from the later of the last input and the
moment playback stopped. While HELD and self ready: warning at 8 min,
auto-unready at 10 min, and never less than 2 min after the warning.
@@ -294,21 +294,29 @@ page yt-dlp knows", network errors → "Couldn't reach the site", anything
else as is). It rides in `report.error` (see "Player error" in the sync
model) until the media opens (a new duration) or the server's `mediaId`
moves on; the pill shows "Can't play this: ..." with Retry (Reload
video). The cache is in RAM:
media_kit sets `cache-on-disk` without a `cache-dir`, and mpv falls back. mpv
video). The cache is in RAM: `cache-on-disk=no` is pinned (media_kit
sets it on without a `cache-dir`, so mpv fell back to RAM anyway, warning
"Failed to create file cache." on every open). mpv
ignores invalid values silently, so every pin is read back and a mismatch is
logged and kept in `pinMismatches`. Measured against the `/m` endpoint
(`readahead_e2e_test`): a paused player buffers ~147 s ahead (10 s before).
media_kit has no public `setProperty`/`getProperty`; the code calls them
through a guarded dynamic call on `player.platform`. Hardware decoding comes from Settings (auto, off, custom)
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 media,
overrides it. Auto is mpv `auto` on Linux and `auto-copy` on Windows
(`autoHwdec`, also for a blank custom value): media_kit renders through
ANGLE there, where `auto` picks the dxva2 interop, which cannot create its
EGL surface ("Failed to create EGL surface"); `auto-copy` decodes on the
GPU (d3d11va-copy, dxva2-copy, ... whichever works) and copies frames
back, needing no interop. It is read per player, so a change applies on the next media,
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`
(`:` joined, `;` on Windows). The read-back is compared as a list
(`shaderListMatches`): older mpv (0.37 and before, the bundled Windows
libmpv among them) prints it `,` joined. One saved choice in `AppSettings`
(`shaderPreset`, `shaderQuality`, `customShaders`): applied on every player
creation (join, new media, Reload video) and live when changed from the session (the
controls' ⚙ → Upscaling shader menu, Ctrl+0..6, both also save it). The Anime4K
@@ -330,7 +338,17 @@ mpv logs at `v` while the controller is created: OpenGL init logs
(`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). Otherwise the session player logs at `warn`
menu and Settings say so). Into the app log (`mpv`, INFO, all starting
`render`): once per app start on Linux `render env:` (`XDG_SESSION_TYPE`,
`WAYLAND_DISPLAY`, `DISPLAY`, `GDK_BACKEND`, `FLUTTER_LINUX_RENDERER`,
`LIBGL_ALWAYS_SOFTWARE`, `__EGL_VENDOR_LIBRARY_FILENAMES`, `-` when
unset); from that `v` window only mpv's GL init lines (`GL_VENDOR`,
`GL_RENDERER`, `GL_VERSION`, `GL_SHADING_LANGUAGE_VERSION`, `Detected
...`); the mode decided at the end of it; and per player, at its first
frame, `hwdec-current` and the `hwdec` asked for (the first one per app
start also `mpv-version` and `ffmpeg-version`). Why media_kit fell back to
software it only prints on stderr (`media_kit: VideoOutput: ...`), which
the app does not capture. Otherwise the session player logs at `warn`
(`PlaybackController.logLevel`, switched with `mpv_request_log_messages`
on media_kit's handle): media_kit drains mpv's log on the UI thread, one
message at a time, so a chatty level, or ffmpeg logging every retry of a
@@ -1105,7 +1123,8 @@ panel) and chat messages (Copy message, and a link in it).
- **Party strip** (`party_strip.dart`, 60 px): your avatar (profile card; 42 px like the party's),
the Library / Schedule / Settings tabs with a growing underline (an
`HScroll` when narrow), the update indicator, then offline a grey dot,
`HScroll` when narrow; the version in small text under Settings,
`shortVersion`, the full one in its tooltip), then offline a grey dot,
"Offline", Join a friend and Host (connect cards; icon-less "Join" and
"Host" under 1100 px), solo a "Solo · only you" chip, Invite friends
(the Host card, titled "Invite friends", "Start hosting") and Stop (no
@@ -1270,7 +1289,7 @@ panel) and chat messages (Copy message, and a link in it).
switched off in `soundsOff`. The device opens 2 s after startup
(`warmUp`), so neither startup nor the first sound waits on it; if it
can't open, the engine logs `no sounds: audio init failed` once and drops
every play. The close path disposes the engine (deinit, 500 ms bound)
every play. App close disposes the engine (deinit, 500 ms bound)
before leaving the server. Settings → Notifications & sounds has the volume, the
overall sound set (`soundPreset`), and per sound a preview, its own set
(`soundPresetFor`, else the overall one) and a switch; a preview always
@@ -1376,8 +1395,10 @@ panel) and chat messages (Copy message, and a link in it).
own have none); each
slides in, stays 5 s, then slides out and collapses its gap. A click on
a chat toast opens the chat; the others let clicks through. Chat toasts
follow `AppSettings.chatPopups` (Settings → Notifications: All, Only
when paused (default), Off); the message is in the chat either way.
follow `AppSettings.chatPopups` (Settings → Notifications: All
(default), Only when paused, Off); the message is in the chat either
way, and its sound plays whatever this setting (its own switch in
Sounds).
Settings persist in two Hive boxes: `connect_form` (last form, recent
servers and links, the profile picture and colour; saved even when a join
@@ -1534,6 +1555,36 @@ Window chrome is frameless (`window_manager`): the party strip is the drag
area and holds the min/max/close buttons (`widgets/window_buttons.dart`);
fullscreen has no drag area and shows the buttons in the player's top bar.
App close (`platform/app_shutdown.dart`, `shutDownApp`; the close button,
Alt+F4, and Update and restart): the window position is saved and the
window hides at once. Then every mpv player goes, before the engine does
(a live one calls its wakeup callback into a dead isolate): the session
player, the preload and the player still retiring
(`PlaybackController.disposeAll`, 1.5 s each, via
`SessionCoordinator.releasePlayers`, which keeps the position for the
Jellyfin stop report), the Files thumbnail jobs (`cancelAllThumbnails`)
and the sound engine (500 ms), side by side; on Windows 300 ms more,
since media_kit_video frees a video output on a thread of its own after
dispose returns, and an output still up when the engine stops blocks the
plugin's teardown (`~VideoOutput` waits on a texture callback that never
comes). Then `SessionState.leave`
(Jellyfin stop report, last diagnostics, the goodbye, so others see
"left") and the hosted server's stop, 3 s in all; an update restart then
closes Hive and starts the new binary. Last the window closes the normal
way (`setPreventClose(false)` + `close`), so the engine and plugins are
torn down inside the platform's main loop, and the runners end the
process without `exit()`'s handlers: Linux `_exit` after
`g_application_run` (NVIDIA's EGL exit handler segfaults inside `exit()`),
Windows `TerminateProcess` after the message loop (no DLL detach
handlers, which can hang on threads already gone). Every step is bounded
and logs (`close: ...`) when it is slow or fails. Backstops, so no process
stays behind a hidden window: 3 s after asking the window to close the
app kills itself (`Process.killPid`, no exit handlers; it only gets there
if the close did not go through); the runners end the process 15 s after
the first close request (Linux: a `delete-event` handler connected before
window_manager's; Windows: the first `WM_CLOSE`), and Windows also 3 s
after the window is destroyed, while the engine and plugins go.
## Logging
`packages/core/lib/src/logging.dart`, one setup for the app and the
@@ -1690,10 +1741,20 @@ defines for every build and run) with "dev run". CLI: `watch-together-server --u
the shipped `update.sh` / `update.bat` run it), `--check-update` prints only;
it ends with "restart the server to run <version>". `watch_together --upgrade`
does the same from the app binary. In the app, `UpdateController`
(`apps/desktop/lib/src/update/`) runs the same code for the strip's update
indicator, which opens the update card, and restarts the app ("Update and
restart", closing Hive first).
Settings has Check now and an auto-check toggle. The manifest is `updates/manifest.json` in
(`apps/desktop/lib/src/update/`) runs the same code for the update bar
(`UpdateBar`, `screens/home/update_indicator.dart`): a band across the
window under the strip while an update is available: "Starsync X.Y.Z is
ready", the notes, Later (hidden until the next check or start) and
"Update and restart" (download, apply, then the app close above with Hive
closed and the new binary started), its progress in the band. A release
that is behind also says which version it runs; a copy that is not a
packaged install says to replace the folder by hand. The shell keeps the
band off the full player while a video is on (not finished); it shows on
the tabs, the mini player's views and once the video ends. A required
update has no Later. Nothing downloads before the click. The app checks at
start and every 4 hours while running (`updateCheckEvery`), both with the
auto-check setting on; a re-check never touches an install under way.
Settings has Check now and the auto-check toggle. The manifest is `updates/manifest.json` in
this repo, written by `tool/release.sh`. Test builds (`X.Y.Z-dev.N`,
`isDevBuild`) also read `devManifestUrl`, the `manifest.json` of the `dev`
prerelease (`tool/dev_release.sh`), and take the newer of the two