Player: sharp video and subtitles at screen size; dev tools: danmaku preview, subtitle lift with a stand-in chat line

- "Sharp video and subtitles" (Settings → Player and the player settings
  card, on by default): the video output renders at the view's physical
  size (debounced, GPU rendering only), so libass, mpv's scaler and the
  shaders work at screen resolution; re-sent after media_kit's own reset
  on new video params, redrawn while paused.
- Dev builds only (Settings → About → Developer): a danmaku preview
  (DanmakuLayer: free lanes in an upper band, density cap with a queue,
  equal crossing time, outlined text, avatars, own-line frame) over the
  real player or a stand-in picture, with samples, a random stream, live
  toasts, every knob and mock subtitles; and "Lift subtitles above the
  chat line and controls": Enter opens a stand-in chat line, bottom
  subtitles move up via mpv sub-pos while it or the control bar is up.
- tool/make_sub_test.sh: a test video with a top \an8 sign, bottom
  dialogue and an SRT track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-18 02:19:40 +03:00
1 parent 638a30e16c
commit 5bc252bf02
18 files changed
+2253 -16

No files matched your search

+28 -2
View File
@@ -567,6 +567,30 @@ server without the relay's header rule, or the connection reuse above)
fails every seek until a fresh open, so the coordinator runs "Reload
video" once per media for it (`mpvStreamLost`, `onStreamLost`).
Output size ("Sharp video and subtitles", `AppSettings.screenResVideo`,
on by default, live; Settings → Player and the player settings card):
media_kit renders mpv into a texture the size of the video and Flutter
scales it, so libass subtitles, mpv's scaler and the shaders all work on
a 720p image for a 720p file. With the setting on and the GPU renderer
(`renderMode` gpu; never in software rendering, where every pixel is CPU
work, nor before the probe knows), the session view
(`shell/video_view.dart`, `VideoOutputSizer` around the `Video`) sends its
size in physical pixels to the host (`PlayerHost.setOutputSize`): at
once the first time, then once a resize has held 180 ms
(`outputResizeDelay`: a window drag, fullscreen, full ↔ mini). The host
applies it to every live output and to each new one once it is up
(`NativeVideoController.setSize`); mpv then letterboxes inside it and
draws subtitles at that size (text subtitles may sit in the black bars,
as in mpv). media_kit's controller sets an output back to the video's
size whenever the video's parameters change (each new file), so the host
sends the size again after it, under the controller's own lock; a fresh
player's first frame or two show at the video's size. A paused player
draws no frame for the new size: the host nudges `video-pan-x` by a
millionth and back so mpv redraws. Off: each output at its video's own
size again (`setOutputSize(null)`). Measured on a GTX 1080, 1280x720
H.264 at 2560x1080 fullscreen: no difference past the noise in the
process's GPU (`nvidia-smi pmon`, 9 to 12 % either way) or CPU.
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 host therefore requests
@@ -606,7 +630,8 @@ their own): creates players by role (`session`: the full cache, `warn`;
no outputs, no cache, no log) with every pin set before the caller's
first command (`pinMismatches` lists the read-backs that differ), gives
a player its video output (`HostedVideo`, with the hwdec policy:
`SYNC_RELAY_HWDEC`, else the setting, `auto-copy` on Windows), runs the
`SYNC_RELAY_HWDEC`, else the setting, `auto-copy` on Windows) at the
view's size when asked (`setOutputSize`, see "Output size"), runs the
render-mode probe and its diagnostics (`renderMode`, also after leaving),
routes mpv's log, and owns the order players go in: `retire` (pause, let
the view drop the texture: two frames and a route pop, then dispose; a
@@ -2112,7 +2137,8 @@ panel) and chat messages (Copy message, and a link in it).
between kinds. The mini shows its short line in its hover bar. ⚙ →
Player settings (both skins) opens the player settings card (`controls/
player_settings.dart`, `showPopover`, on `AppGlass`): player design, progress line
when hidden, upscaling shader, hardware decoding (set live on mpv's
when hidden, upscaling shader, sharp video and subtitles (the output
at screen size, see "Media"), hardware decoding (set live on mpv's
`hwdec`), subtitle and audio track. The rows (`playerSettingRows`) are
the same widgets Settings → Player shows; all apply live. Settings →
Player also has the controls' own choices (`controlsSettingRows`, see
+58 -1
View File
@@ -75,6 +75,61 @@ Harnesses in `tool/` (own `pubspec.yaml`, `dart run tool/<name>.dart`):
`range_logger` (serve a file and log every Range request), `shaper`
(bandwidth cap, latency, `--kill-after` for outage tests), `fake_viewer`
and `e2e_check` (simulated viewers against a live server).
`tool/make_sub_test.sh [out.mkv] [seconds]` (ffmpeg) writes a 720p test
video for subtitle placement and sharpness (default
`~/Videos/starsync-sub-test.mkv`): an .ass track with a top sign
(`\an8`) and bottom dialogue (English and Japanese), and an SRT track.
## Dev tools
Test builds and runs from source (a version with `-dev`) and debug runs
(`devTools`, `screens/home/ui.dart`) get Settings → About → Developer;
releases never show it. What is there is an experiment to look at, not
a decided feature:
- **Danmaku preview** (`lib/src/danmaku/`): chat lines and short notices
(joins, leaves, Up next adds, "starting now") flying right to left
across the upper part of the picture. `DanmakuLayer` is the reusable
widget: lanes in a band of the height (`DanmakuStyle.bandTop` /
`bandBottom`, default 10 to 45 %, under top signs, never down into the
dialogue); each line takes the topmost free lane (`DanmakuLanes`, pure:
the last line in a lane must have fully entered with a gap, and the
new one, at its own speed, cannot catch it before it leaves); at most
`maxOnScreen` fly, the rest wait in order (dropped after 15 s, 60 at
most); every line crosses in `crossSecs`, so long ones move faster;
white text with a dark outline and no box, the sender's avatar
(`StatusAvatar`: picture, else initial) and coloured name first, the
app's own notices without one, own lines in an accent frame; the
layer's own clock, so lines fly on while paused. Each line is a widget
moved by a `Flow`: per frame only paint runs. The button opens it over
the full player when one is up (under the controls; the panel top
right holds them up), else on a page with a moving stand-in picture
and a mock control bar of the skin in use. The panel (`DanmakuPanel`)
fires samples (chat, own, join, leave, Up next, starting, a burst), a
random stream (Poisson arrivals at a rate, short to long lines, CJK
and emoji; in a session half the senders are the party's real
members), this session's own chat and notice toasts, and every
`DanmakuStyle` knob live (crossing time, cap, lane band, opacity,
size, outline, avatars, own frame), plus mock subtitles (a top sign
and bottom dialogue). Tests: `danmaku_test`.
- **Lift subtitles above the chat line and controls**
(`AppSettings.liftSubtitles`, off): Enter in the full player opens a
stand-in for the planned chat line (a one-line box at the bottom in
place of the control bar; Enter sends to the chat, Esc closes). While
it is open, or the control bar is up, bottom subtitles move above it
and back down when it goes (`SubtitleLifter`, `video_view.dart`): mpv
`sub-pos`, animated over the shell's 320 ms curve in 0.1 steps, only as
far as the letterbox under the picture leaves no room
(`subtitleLiftPos`). What each mpv property does, on
`make_sub_test.sh`'s file (mpv 0.41, libmpv as the app uses it):
`sub-pos` moves bottom ASS dialogue and SRT lines up and leaves `\an8`
signs in place (with `sub-ass-override` at its default `scale` or
`yes`; `no` ignores it for ASS); `sub-margin-y` moves SRT only (ASS
only with `sub-ass-override=force`, which restyles every line, signs
too); `sub-ass-style-overrides=MarginV=...` moves top signs down as
well; `video-margin-ratio-bottom` shrinks and moves the picture
itself (with `sub-ass-force-margins` the dialogue goes into the new
black band).
## Tests
@@ -109,7 +164,9 @@ Three suites, run from each package's folder:
version gate, the media plan's table with the Jellyfin relay rule and
the "Waiting for" status (`media_plan_test`), watch tracking and the
device history (`watch_tracker_test`), what continues a file of a
local show (`queue_logic_test`)), `singletons_test` (the `.instance` count, see
local show (`queue_logic_test`), the video view's output size and
subtitle lift and the dev-tools gate (`video_view_test`), danmaku lanes
(`danmaku_test`)), `singletons_test` (the `.instance` count, see
ARCHITECTURE "Composition root"; lower its baseline when a use goes) and the real-mpv `*_e2e`
tests plus `coord_presence`. The whole suite takes a couple of minutes;
run it only before a restructure step lands, otherwise only the files