diff --git a/client/src/views/player/Player.ts b/client/src/views/player/Player.ts index 283b166..3e8f939 100644 --- a/client/src/views/player/Player.ts +++ b/client/src/views/player/Player.ts @@ -525,20 +525,30 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli scheduleControlsHide(); } - function onBottomBarPointerEnter() { + // Mouse only (pointerType check) - not just an optimization. A touch + // pointer has no real "hovering": tapping the bar fires a pointerenter + // with no matching pointerleave ever coming (there's no cursor left to + // lift away), which used to latch isPointerOverBottomBar permanently + // true after the very first tap on the bar - the whole thing could + // never auto-hide again from that point on, on any touch device. + // Touch's own "wake the bar up" path is already covered below by the + // plain pointerdown/pointermove listeners on the whole player. + function onBottomBarPointerEnter(e: PointerEvent) { + if (e.pointerType !== 'mouse') return; isPointerOverBottomBar = true; showControls(); } - function onBottomBarPointerLeave() { + function onBottomBarPointerLeave(e: PointerEvent) { + if (e.pointerType !== 'mouse') return; isPointerOverBottomBar = false; scheduleControlsHide(); } playerWrap.addEventListener('pointermove', showControls); playerWrap.addEventListener('pointerdown', showControls); - bottomBar.addEventListener('mouseenter', onBottomBarPointerEnter); - bottomBar.addEventListener('mouseleave', onBottomBarPointerLeave); + bottomBar.addEventListener('pointerenter', onBottomBarPointerEnter); + bottomBar.addEventListener('pointerleave', onBottomBarPointerLeave); scheduleControlsHide(); /** @@ -1108,8 +1118,8 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli document.removeEventListener('fullscreenchange', onFullscreenChange); playerWrap.removeEventListener('pointermove', showControls); playerWrap.removeEventListener('pointerdown', showControls); - bottomBar.removeEventListener('mouseenter', onBottomBarPointerEnter); - bottomBar.removeEventListener('mouseleave', onBottomBarPointerLeave); + bottomBar.removeEventListener('pointerenter', onBottomBarPointerEnter); + bottomBar.removeEventListener('pointerleave', onBottomBarPointerLeave); unsubscribeCastAvailability(); unsubscribeCastSessionEnded(); // Deliberately NOT stopping an active cast here - leaving this page diff --git a/docs/architecture.md b/docs/architecture.md index ba010e6..ace0d2e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2769,6 +2769,27 @@ inactivity, does hovering genuinely prevent it) wasn't driven through real wall-clock waits here - the CSS transition and class-toggling logic were checked directly instead. +**Reported broken on mobile, right after shipping**: it never actually +auto-hid there. Root cause - a classic mobile-web trap, not a typo: +`mouseenter`/`mouseleave` on the bar tracked "is the pointer resting on +the controls" so hovering could hold them open, but a touch pointer has +no real hover - tapping the bar fires a synthetic `pointerenter` with +no `pointerleave` ever coming after it (there's no cursor left to lift +away), so the very first tap anywhere on the bar latched +`isPointerOverBottomBar` permanently `true` and the bar could never +auto-hide again for the rest of that session. Fixed by checking +`event.pointerType === 'mouse'` in both handlers - touch/pen never even +attempt the "hovering holds it open" path at all, and correctly fall +back to the same plain activity-based wake-up (`pointerdown`/ +`pointermove` on the whole player) every device already gets. Confirmed +by reproducing the exact failure directly - dispatched a synthetic +touch `pointerenter` with no matching `pointerleave` (the real-world +case) against the fixed logic and watched it still transition to +`.minimal` on schedule, where the original `mouseenter`-based version +would have latched open. A real touchscreen's own event behavior still +isn't independently reproducible here, so this is the strongest +confirmation available without one. + ## Known limitations - Playback always goes through `hls.js` against whatever Emby's