From acff072f5c9a2954ac90c166ce29081cd0321dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 5 Aug 2026 02:01:04 +0000 Subject: [PATCH] a resting mouse is not the user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chrome came back for a couple of seconds every few minutes during a film with nobody touching anything. Only three things wake it, and none of them is periodic — so the culprit is a stray pointermove. A mouse sitting on a desk still emits the odd one-pixel event, a bumped table emits a few, and the browser synthesises a zero-delta move of its own when the cursor style changes, which this player does every single time it hides the cursor. A move now has to travel eight manhattan pixels from wherever the pointer last genuinely woke it. The anchor only advances on a real wake, so a slow deliberate drift still accumulates past the threshold — it is jitter around a fixed point that stops counting. Co-Authored-By: Claude Opus 5 --- .../src/apps/Jellyfin/VideoPlayer.tsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/workspaces/officerdev/src/apps/Jellyfin/VideoPlayer.tsx b/src/workspaces/officerdev/src/apps/Jellyfin/VideoPlayer.tsx index 320e4657..041e3bef 100644 --- a/src/workspaces/officerdev/src/apps/Jellyfin/VideoPlayer.tsx +++ b/src/workspaces/officerdev/src/apps/Jellyfin/VideoPlayer.tsx @@ -40,6 +40,8 @@ import Hls from './vendor/hls.mjs'; const PROGRESS_INTERVAL_MS = 10_000; const CHROME_HIDE_MS = 2_500; +/** Manhattan pixels a pointer must travel before it counts as the user, rather than as a resting mouse. */ +const WAKE_THRESHOLD_PX = 8; const VOLUME_KEY = 'jellyfin:volume'; type Mode = 'file' | 'hls-mse' | 'progressive'; @@ -110,6 +112,8 @@ export const VideoPlayer = ({ id }: { id: string }) => { // re-run) every time one of them changes. const audioRef = useRef(null); const bitrateRef = useRef(null); + // Where the pointer was when it last counted as movement — see `onPointerMove`. + const pointerRef = useRef<{ x: number; y: number } | null>(null); const mode = useMemo(() => (plan ? transportFor(plan) : null), [plan]); const hasTimeline = mode !== null && mode !== 'progressive'; @@ -305,6 +309,25 @@ export const VideoPlayer = ({ id }: { id: string }) => { const wake = () => setChrome(true); + /** + * Wake on real movement only. + * + * A bare `pointermove` handler is too credulous to sit in front of a two-hour film: a mouse resting on a + * desk still emits the odd one-pixel event, a bumped table emits a few, and a browser will synthesise a + * zero-delta move of its own when the cursor style changes — which this player does every time it hides the + * cursor. Any one of those read as "the user is here" and put the scrubber back for two and a half seconds, + * minutes apart, with nobody having touched anything. + * + * The anchor only moves when a wake actually happens, so a slow deliberate drift still accumulates past the + * threshold and wakes; it is jitter around a fixed point that never does. + */ + const onPointerMove = (ev: React.PointerEvent) => { + const last = pointerRef.current; + if (last && Math.abs(ev.clientX - last.x) + Math.abs(ev.clientY - last.y) < WAKE_THRESHOLD_PX) return; + pointerRef.current = { x: ev.clientX, y: ev.clientY }; + wake(); + }; + // Keyboard on the container rather than on window: the player is a panel inside a workspace, and stealing // space or the arrow keys from whatever else is on screen would be wrong. Autofocus puts it in reach. const onKeyDown = (ev: React.KeyboardEvent) => { @@ -355,7 +378,7 @@ export const VideoPlayer = ({ id }: { id: string }) => { tabIndex={-1} autoFocus onKeyDown={onKeyDown} - onPointerMove={wake} + onPointerMove={onPointerMove} onPointerDown={wake} className={`absolute inset-0 z-30 bg-black outline-none ${idle ? 'cursor-none' : ''}`} >