Locate the player's window instead of assuming fullscreen

The corners and trail animations had stopped showing stills. Neither was at
fault: capture.js gated every screenshot on

  window.innerWidth === screen.width && window.innerHeight === screen.height

and that compares two different units. innerWidth is device pixels divided by
devicePixelRatio; screen.width is not divided. At any zoom but 100%, or any
fractional display scale, the two can never be equal — measured here at dpr
1.25, fullscreen gives 1536x864 against a reported 1920x1080. The gate stayed
shut, grab() returned null, and shoot()/drop() bailed out before making a
request. The only symptom was an F11 notice that stayed wrong after F11.

Forcing the gate open would not have helped: the rect went out in CSS pixels
and was read as logical screen pixels, so every crop would have been 1.25x too
small and offset.

Stop guessing from browser metrics and ask the compositor, which knows exactly.
hyprctl gives the window's position and size in logical pixels; against the
viewport the page reports, their ratio is the CSS-pixel scale outright, and
since CSS pixels are square that one number does for both axes. The vertical
room it leaves over is the tab and address bars, measured rather than assumed —
which is what lets the player run in an ordinary window. Fullscreen becomes
just the case where the leftover is nothing, so Start still asks for it, for the
projector's sake, but says nothing when refused.

The window is found by matching document.title as a prefix. The queue page was
titled "Party Player — Queue", which would have matched the player's prefix and
aimed the crop at the wrong window, so it is now "Queue — Party Player".
Candidates are ranked on screen first, then most recently focused, then largest:
hyprctl clients has no `focused` field, it has focusHistoryID.

The old F11 notice is replaced by one that fires only when the server genuinely
cannot find the window, debounced on resize so dragging does not spawn a burst
of grim processes.

Verified against the live player window: capture() returns bytes identical to a
grim -g of the independently predicted region, the same physical region
described at 100% and 125% zoom resolves to identical output, and the route
answers 422 for an unknown window and 400 for a missing title or viewport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zyhxe2w7eEMXvz8wekCyc
This commit is contained in:
Si
2026-08-13 21:56:31 +02:00
co-authored by Claude Opus 5
parent 049a269885
commit 3f00897f0e
6 changed files with 255 additions and 60 deletions
+5 -6
View File
@@ -64,12 +64,11 @@ async function start() {
// chosen without one, so this ordering has to hold either way.
const capturing = requestCapture({ onIssue: showNotice });
// Screenshots only line up while the viewport covers the whole screen, since
// that is what makes a CSS pixel a screen pixel. Chrome's tab capture is the
// one exception, and fullscreen does it no harm.
document.documentElement.requestFullscreen?.().catch(() => {
showNotice('Could not go fullscreen — press F11 so screenshots line up.');
});
// Cosmetic now rather than required: the server path locates the window
// itself, so screenshots line up windowed too. It is still what you want on a
// projector, so ask — but say nothing if it is refused, since nothing breaks.
// (The getDisplayMedia fallback does still need it; that path says so itself.)
document.documentElement.requestFullscreen?.().catch(() => {});
gate.classList.add('hidden');