fix(player): re-arm the cursor idle timer when playback changes

The idle countdown that hides the cursor and the play/pause overlay was only
started from mousemove. A cursor that stayed still over the player while a
click, a key or a scrub release started playback never got a countdown, so
the overlay stayed on the video until the mouse moved again.

Arm the timer from one place and rerun it whenever playback or fullscreen
changes, keeping the cursor-over-player state in a ref so the same rule
applies from every entry point.
This commit is contained in:
2026-09-08 12:55:30 +03:00
parent 79bba5e7a1
commit b2070c1030
3 changed files with 107 additions and 14 deletions
-1
View File
@@ -214,7 +214,6 @@ export function VideoPageContent({
setActiveVersionId,
});
// Cursor idle detection: hide overlay when cursor idle for 3s while playing
// Memoize version selection handler to prevent recreating on each render
const handleVersionSelect = useCallback(
(versionId: string) => {
+29 -13
View File
@@ -34,6 +34,9 @@ import {
timeFromClientX as timeFromClientXWithin,
} from '@/components/video-page/hooks/video-player-utils';
/** How long the cursor has to sit still over the player before it and the overlay hide. */
const CURSOR_IDLE_DELAY_MS = 1000;
interface UseVideoPlayerParams {
activeVersion: Version | undefined;
activeVersionId: string | null;
@@ -116,6 +119,7 @@ export function useVideoPlayer({
const [bunnyPortraitFrameWidth, setBunnyPortraitFrameWidth] = useState<number>(0);
const [cursorIdle, setCursorIdle] = useState(false);
const cursorIdleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isCursorOverPlayerRef = useRef(false);
const bunnyRetryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const bunnyFrameCallbackIdRef = useRef<number | null>(null);
const bunnyFrameSampleRef = useRef<{ mediaTime: number; presentedFrames: number } | null>(null);
@@ -212,29 +216,41 @@ export function useVideoPlayer({
return () => observer.disconnect();
}, [activeVersionId, bunnyViewportRef]);
const handleVideoMouseMove = useCallback(() => {
setCursorIdle(false);
// Restart the idle countdown from "cursor active". The cursor only counts as
// idle while it is over the player and there is something to hide (playback
// running, or fullscreen chrome); otherwise it stays visible.
const armCursorIdleTimer = useCallback(() => {
if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current);
cursorIdleTimerRef.current = null;
setCursorIdle(false);
const shouldHideControls = isFullscreenMode;
if (!isCursorOverPlayerRef.current) return;
if (!isPlaying && !isFullscreenMode) return;
if (isPlaying || shouldHideControls) {
cursorIdleTimerRef.current = setTimeout(() => {
setCursorIdle(true);
}, 1000);
}
cursorIdleTimerRef.current = setTimeout(() => {
setCursorIdle(true);
}, CURSOR_IDLE_DELAY_MS);
}, [isFullscreenMode, isPlaying]);
const handleVideoMouseLeave = useCallback(() => {
if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current);
setCursorIdle(false);
}, []);
const handleVideoMouseMove = useCallback(() => {
isCursorOverPlayerRef.current = true;
armCursorIdleTimer();
}, [armCursorIdleTimer]);
const handleVideoMouseLeave = useCallback(() => {
isCursorOverPlayerRef.current = false;
armCursorIdleTimer();
}, [armCursorIdleTimer]);
// Playback can change without the cursor moving: a click or a key starts it,
// and a scrub pauses then resumes it. Each of those needs a fresh countdown,
// or a still cursor over the player never goes idle and the overlay stays.
useEffect(() => {
armCursorIdleTimer();
return () => {
if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current);
};
}, []);
}, [armCursorIdleTimer]);
useEffect(() => {
if (isApiLoaded) return;