From 9233104381dcfd411249f1ae5c1155796f1cdd61 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 23 Dec 2025 14:25:07 -0800 Subject: [PATCH] Implement soft fullscreen mode with timeout handling in VideoPlayer component; update fullscreen state management and styling accordingly. --- packages/web/src/components/VideoPlayer.tsx | 82 +++++++++++++++++++-- packages/web/src/routes/$username.tsx | 6 +- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/packages/web/src/components/VideoPlayer.tsx b/packages/web/src/components/VideoPlayer.tsx index 89b8133b..a6177bb4 100644 --- a/packages/web/src/components/VideoPlayer.tsx +++ b/packages/web/src/components/VideoPlayer.tsx @@ -19,9 +19,18 @@ export function VideoPlayer({ const [isMuted, setIsMuted] = useState(muted) const [volume, setVolume] = useState(1) const [isFullscreen, setIsFullscreen] = useState(false) + const [isSoftFullscreen, setIsSoftFullscreen] = useState(false) const [showControls, setShowControls] = useState(true) const [error, setError] = useState(null) const hideControlsTimeoutRef = useRef | null>(null) + const softFullscreenTimeoutRef = useRef | null>(null) + + const clearSoftFullscreenTimeout = () => { + if (softFullscreenTimeoutRef.current) { + clearTimeout(softFullscreenTimeoutRef.current) + softFullscreenTimeoutRef.current = null + } + } useEffect(() => { const video = videoRef.current @@ -80,6 +89,21 @@ export function VideoPlayer({ } }, [src, autoPlay]) + useEffect(() => { + return () => { + clearSoftFullscreenTimeout() + } + }, []) + + useEffect(() => { + if (!isSoftFullscreen || typeof document === "undefined") return + const previousOverflow = document.body.style.overflow + document.body.style.overflow = "hidden" + return () => { + document.body.style.overflow = previousOverflow + } + }, [isSoftFullscreen]) + useEffect(() => { const video = videoRef.current if (!video) return @@ -94,11 +118,25 @@ export function VideoPlayer({ const updateFullscreenState = () => { const isDocFullscreen = !!doc.fullscreenElement || !!doc.webkitFullscreenElement const isVideoFullscreen = !!videoEl.webkitDisplayingFullscreen - setIsFullscreen(isDocFullscreen || isVideoFullscreen) + const isNowFullscreen = isDocFullscreen || isVideoFullscreen + setIsFullscreen(isNowFullscreen) + if (isNowFullscreen) { + clearSoftFullscreenTimeout() + setIsSoftFullscreen(false) + } } - const onWebkitBegin = () => setIsFullscreen(true) - const onWebkitEnd = () => setIsFullscreen(false) + const onWebkitBegin = () => { + clearSoftFullscreenTimeout() + video.controls = true + setIsFullscreen(true) + setIsSoftFullscreen(false) + } + const onWebkitEnd = () => { + video.controls = false + setIsFullscreen(false) + setIsSoftFullscreen(false) + } document.addEventListener("fullscreenchange", updateFullscreenState) document.addEventListener("webkitfullscreenchange", updateFullscreenState) @@ -153,6 +191,7 @@ export function VideoPlayer({ const video = videoRef.current const container = containerRef.current if (!video || !container) return + clearSoftFullscreenTimeout() const doc = document as Document & { webkitFullscreenElement?: Element | null @@ -193,11 +232,33 @@ export function VideoPlayer({ return } + if (isSoftFullscreen) { + setIsSoftFullscreen(false) + return + } + + const scheduleSoftFullscreenFallback = () => { + softFullscreenTimeoutRef.current = setTimeout(() => { + const isDocFullscreenNow = !!doc.fullscreenElement || !!doc.webkitFullscreenElement + const isVideoFullscreenNow = !!videoEl.webkitDisplayingFullscreen + if (!isDocFullscreenNow && !isVideoFullscreenNow) { + video.controls = false + setIsSoftFullscreen(true) + } + }, 400) + } + if (isAppleMobile && videoEl.webkitEnterFullscreen) { try { + video.controls = true + if (video.paused) { + video.play().then(() => setIsPlaying(true)).catch(() => {}) + } videoEl.webkitEnterFullscreen() + scheduleSoftFullscreenFallback() return } catch { + video.controls = false // Fall back to other fullscreen methods. } } @@ -220,6 +281,8 @@ export function VideoPlayer({ setIsFullscreen(true) return } + setIsSoftFullscreen(true) + return } } catch { // Fall through to video fullscreen methods. @@ -235,9 +298,12 @@ export function VideoPlayer({ } else if (videoEl.webkitEnterFullscreen) { videoEl.webkitEnterFullscreen() setIsFullscreen(true) + scheduleSoftFullscreenFallback() + } else { + setIsSoftFullscreen(true) } } catch { - // Ignore fullscreen errors to avoid breaking playback. + setIsSoftFullscreen(true) } } @@ -251,6 +317,8 @@ export function VideoPlayer({ }, 3000) } + const isFullscreenActive = isFullscreen || isSoftFullscreen + if (error) { return (
@@ -262,7 +330,9 @@ export function VideoPlayer({ return (
isPlaying && setShowControls(false)} > @@ -344,7 +414,7 @@ export function VideoPlayer({ onClick={handleFullscreen} className="text-white transition-transform hover:scale-110" > - {isFullscreen ? ( + {isFullscreenActive ? ( diff --git a/packages/web/src/routes/$username.tsx b/packages/web/src/routes/$username.tsx index d4982f91..be43b023 100644 --- a/packages/web/src/routes/$username.tsx +++ b/packages/web/src/routes/$username.tsx @@ -120,11 +120,7 @@ function StreamPage() { const stream = data?.stream ?? null const playback = stream?.playback ?? null const fallbackPlayback = stream?.hls_url - ? resolveStreamPlayback({ - hlsUrl: stream.hls_url, - webrtcUrl: null, - preferWebRtc: false, - }) + ? { type: "hls", url: stream.hls_url } : null const activePlayback = playback?.type === "webrtc" && webRtcFailed