diff --git a/frontend/js/player.js b/frontend/js/player.js index 1f4ed46..ad8b6b0 100644 --- a/frontend/js/player.js +++ b/frontend/js/player.js @@ -23,6 +23,7 @@ App.player = App.player || {}; historyPushed: false, idleTimer: null, originEl: null, + hudHovered: false, // mouse resting on the controls (desktop) attemptToken: 0 // bumps on every open()/format switch to void stale async callbacks }; @@ -145,6 +146,11 @@ App.player = App.player || {}; clearIdleTimer(); cp.idleTimer = setTimeout(() => { cp.idleTimer = null; + // Never fade out from under a mouse resting on the controls. + if (cp.hudHovered) { + scheduleHudHide(); + return; + } if (cp.container) cp.container.classList.add('cp-hud-idle'); }, HUD_IDLE_MS); } @@ -449,6 +455,64 @@ App.player = App.player || {}; addCleanup(destroy); } + // --------------------------------------------------------------------- + // Mouse presence (desktop): moving the mouse anywhere over the player + // brings the HUD back, and it stays up while the pointer rests on the + // controls. Touch input is deliberately excluded -- taps already wake the + // HUD, and a finger dragging for volume or a dismiss swipe shouldn't count + // as "the viewer went looking for the controls". + // --------------------------------------------------------------------- + function bindHoverHud() { + const container = cp.container; + if (!container) return; + const isMouse = (event) => !event.pointerType || event.pointerType === 'mouse'; + + let lastWake = 0; + const onMove = (event) => { + if (!isMouse(event)) return; + // wakeHud() re-arms the idle timer; once every 150ms is plenty and + // keeps a fast mouse from churning timers on every pixel. + const now = (window.performance && performance.now()) ? performance.now() : Date.now(); + if (now - lastWake < 150) return; + lastWake = now; + wakeHud(); + }; + const onEnterHud = (event) => { + if (!isMouse(event)) return; + cp.hudHovered = true; + wakeHud(); + }; + const onLeaveHud = (event) => { + if (!isMouse(event)) return; + cp.hudHovered = false; + scheduleHudHide(); + }; + const onLeaveContainer = (event) => { + if (!isMouse(event)) return; + cp.hudHovered = false; + }; + + container.addEventListener('pointermove', onMove); + container.addEventListener('pointerleave', onLeaveContainer); + // The bars, not .cp-hud itself: the HUD wrapper is pointer-events:none + // (so it never eats gestures over the video) and only its children are + // hit-testable. + const bars = [q('.cp-top-bar'), q('.cp-bottom-bar')].filter(Boolean); + bars.forEach((bar) => { + bar.addEventListener('pointerenter', onEnterHud); + bar.addEventListener('pointerleave', onLeaveHud); + }); + addCleanup(() => { + cp.hudHovered = false; + container.removeEventListener('pointermove', onMove); + container.removeEventListener('pointerleave', onLeaveContainer); + bars.forEach((bar) => { + bar.removeEventListener('pointerenter', onEnterHud); + bar.removeEventListener('pointerleave', onLeaveHud); + }); + }); + } + // --------------------------------------------------------------------- // Keyboard shortcuts (desktop) // --------------------------------------------------------------------- @@ -840,6 +904,7 @@ App.player = App.player || {}; }); } bindGestures(cp.video); + bindHoverHud(); bindKeyboard(cp.video); bindClose(); bindBufferingIndicator(cp.video);