From 4c6619a02fe43f18ea398690528129062ff3441a Mon Sep 17 00:00:00 2001 From: Alan Date: Sun, 2 Aug 2026 04:39:21 +0200 Subject: [PATCH] fix: resolve player controls not waking up on mousemove in Firefox Wayland --- src/components/HLSPlayer.tsx | 47 +++++++++++++++++++++++---- src/components/LiveTVPlayer.tsx | 38 +++++++++++++++++++++- src/pages/FranceTV/FranceTVPlayer.tsx | 37 ++++++++++++++++++++- 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/src/components/HLSPlayer.tsx b/src/components/HLSPlayer.tsx index b4453ac..b28c1a9 100644 --- a/src/components/HLSPlayer.tsx +++ b/src/components/HLSPlayer.tsx @@ -4120,9 +4120,10 @@ const HLSPlayer = forwardRef(({ setTranslationLang(null); }, []); - const handleMouseMove = () => { - // Do not show controls via mousemove on touch devices or while a touch gesture is active - if (isTouchDevice || touchActiveRef.current) return; + const handleMouseMove = useCallback((e?: Event | React.MouseEvent | PointerEvent) => { + // Ignore touch drag events (touch taps/toggles are handled separately) + if (touchActiveRef.current) return; + if (e && 'pointerType' in e && (e as PointerEvent).pointerType === 'touch') return; setShowControls(true); if (controlsTimeoutRef.current) { @@ -4137,7 +4138,7 @@ const HLSPlayer = forwardRef(({ } }, 5000); } - }; + }, [isPlaying, showCastMenu, showForwardAnimation, showRewindAnimation, showLeftTapAnimation, showRightTapAnimation]); const togglePlay = () => { @@ -5330,6 +5331,40 @@ const HLSPlayer = forwardRef(({ }; }, [isCastDragging, castDuration, handleCastProgressDragMove, handleCastProgressDragEnd]); + // Native listener binding for pointer & mouse movement to ensure controls wake up + // across all platforms (including Firefox on Linux Wayland) + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const onUserPointerActivity = (e: Event) => { + handleMouseMove(e); + }; + + const events = ['pointermove', 'mousemove', 'pointerenter', 'mouseenter']; + + events.forEach(eventName => { + container.addEventListener(eventName, onUserPointerActivity, { passive: true }); + }); + + const handleDocumentPointerMove = (e: Event) => { + if (document.fullscreenElement || container.contains(e.target as Node)) { + onUserPointerActivity(e); + } + }; + + document.addEventListener('pointermove', handleDocumentPointerMove, { passive: true }); + document.addEventListener('mousemove', handleDocumentPointerMove, { passive: true }); + + return () => { + events.forEach(eventName => { + container.removeEventListener(eventName, onUserPointerActivity); + }); + document.removeEventListener('pointermove', handleDocumentPointerMove); + document.removeEventListener('mousemove', handleDocumentPointerMove); + }; + }, [handleMouseMove]); + // Add event listener for PIP changes initiated outside our button useEffect(() => { const video = videoRef.current; @@ -9046,7 +9081,6 @@ const HLSPlayer = forwardRef(({ }; const shouldHideCursor = - !isTouchDevice && !isCasting && isFullscreen && isPlaying && @@ -9065,7 +9099,8 @@ const HLSPlayer = forwardRef(({
isPlaying && !showCastMenu && setShowControls(false)} onClick={handleVideoClick} onTouchStart={handleZoomTouchStart} diff --git a/src/components/LiveTVPlayer.tsx b/src/components/LiveTVPlayer.tsx index d235fe7..f5de901 100644 --- a/src/components/LiveTVPlayer.tsx +++ b/src/components/LiveTVPlayer.tsx @@ -1619,11 +1619,46 @@ const LiveTVPlayer: React.FC = ({ }; }, [clearControlsTimeout, hideControlsTimeout, isFullscreen, isPlaying, isUserPaused, showControls, showSettings]); - const handleMouseMove = useCallback(() => { + const handleMouseMove = useCallback((e?: Event | React.MouseEvent | PointerEvent) => { + if (e && 'pointerType' in e && (e as PointerEvent).pointerType === 'touch') return; setShowControls(true); hideControlsTimeout(); }, [hideControlsTimeout]); + // Native listener binding for pointer & mouse movement in LiveTVPlayer + useEffect(() => { + if (isEmbedStream) return; + const container = containerRef.current; + if (!container) return; + + const onUserPointerActivity = (e: Event) => { + handleMouseMove(e); + }; + + const events = ['pointermove', 'mousemove', 'pointerenter', 'mouseenter']; + + events.forEach(eventName => { + container.addEventListener(eventName, onUserPointerActivity, { passive: true }); + }); + + const handleDocumentPointerMove = (e: Event) => { + if (document.fullscreenElement || container.contains(e.target as Node)) { + onUserPointerActivity(e); + } + }; + + document.addEventListener('pointermove', handleDocumentPointerMove, { passive: true }); + document.addEventListener('mousemove', handleDocumentPointerMove, { passive: true }); + + return () => { + events.forEach(eventName => { + container.removeEventListener(eventName, onUserPointerActivity); + }); + document.removeEventListener('pointermove', handleDocumentPointerMove); + document.removeEventListener('mousemove', handleDocumentPointerMove); + }; + }, [isEmbedStream, handleMouseMove]); + // Tap on video area to toggle play/pause (essential for mobile) const handleVideoClick = useCallback((e: React.MouseEvent) => { e.stopPropagation(); @@ -1876,6 +1911,7 @@ const LiveTVPlayer: React.FC = ({ exit={{ opacity: 0 }} className={`fixed inset-0 z-[12000] flex items-center justify-center ${isEmbedStream ? 'bg-black/80 p-4 backdrop-blur-md sm:p-6' : 'bg-black'} ${shouldHideCursor ? 'cursor-none' : ''}`} ref={containerRef} + onPointerMove={isEmbedStream ? undefined : handleMouseMove} onMouseMove={isEmbedStream ? undefined : handleMouseMove} onClick={isEmbedStream ? undefined : handleMouseMove} onMouseLeave={isEmbedStream ? undefined : (() => { diff --git a/src/pages/FranceTV/FranceTVPlayer.tsx b/src/pages/FranceTV/FranceTVPlayer.tsx index 96ce7ac..a1bb197 100644 --- a/src/pages/FranceTV/FranceTVPlayer.tsx +++ b/src/pages/FranceTV/FranceTVPlayer.tsx @@ -480,10 +480,44 @@ const FranceTVPlayer: React.FC = () => { }, 5000); }, [isPlaying, showSettings]); - const handleMouseMove = useCallback(() => { + const handleMouseMove = useCallback((e?: Event | React.MouseEvent | PointerEvent) => { + if (e && 'pointerType' in e && (e as PointerEvent).pointerType === 'touch') return; resetControlsTimeout(); }, [resetControlsTimeout]); + // Native listener binding for pointer & mouse movement in FranceTVPlayer + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const onUserPointerActivity = (e: Event) => { + handleMouseMove(e); + }; + + const events = ['pointermove', 'mousemove', 'pointerenter', 'mouseenter']; + + events.forEach(eventName => { + container.addEventListener(eventName, onUserPointerActivity, { passive: true }); + }); + + const handleDocumentPointerMove = (e: Event) => { + if (document.fullscreenElement || container.contains(e.target as Node)) { + onUserPointerActivity(e); + } + }; + + document.addEventListener('pointermove', handleDocumentPointerMove, { passive: true }); + document.addEventListener('mousemove', handleDocumentPointerMove, { passive: true }); + + return () => { + events.forEach(eventName => { + container.removeEventListener(eventName, onUserPointerActivity); + }); + document.removeEventListener('pointermove', handleDocumentPointerMove); + document.removeEventListener('mousemove', handleDocumentPointerMove); + }; + }, [handleMouseMove]); + const handleMouseLeave = useCallback(() => { if (isPlaying) { if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); @@ -759,6 +793,7 @@ const FranceTVPlayer: React.FC = () => { ref={containerRef} style={{ minHeight: 'calc(var(--vh, 1vh) * 100)', overflow: 'hidden' }} className="w-full bg-black text-white overflow-hidden fixed inset-0 z-40" + onPointerMove={handleMouseMove} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} >