Implement soft fullscreen mode with timeout handling in VideoPlayer component; update fullscreen state management and styling accordingly.

This commit is contained in:
Nikita
2025-12-23 14:25:07 -08:00
parent 937f13581e
commit 9233104381
2 changed files with 77 additions and 11 deletions

View File

@@ -19,9 +19,18 @@ export function VideoPlayer({
const [isMuted, setIsMuted] = useState(muted) const [isMuted, setIsMuted] = useState(muted)
const [volume, setVolume] = useState(1) const [volume, setVolume] = useState(1)
const [isFullscreen, setIsFullscreen] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false)
const [isSoftFullscreen, setIsSoftFullscreen] = useState(false)
const [showControls, setShowControls] = useState(true) const [showControls, setShowControls] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const hideControlsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null) const hideControlsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const softFullscreenTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const clearSoftFullscreenTimeout = () => {
if (softFullscreenTimeoutRef.current) {
clearTimeout(softFullscreenTimeoutRef.current)
softFullscreenTimeoutRef.current = null
}
}
useEffect(() => { useEffect(() => {
const video = videoRef.current const video = videoRef.current
@@ -80,6 +89,21 @@ export function VideoPlayer({
} }
}, [src, autoPlay]) }, [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(() => { useEffect(() => {
const video = videoRef.current const video = videoRef.current
if (!video) return if (!video) return
@@ -94,11 +118,25 @@ export function VideoPlayer({
const updateFullscreenState = () => { const updateFullscreenState = () => {
const isDocFullscreen = !!doc.fullscreenElement || !!doc.webkitFullscreenElement const isDocFullscreen = !!doc.fullscreenElement || !!doc.webkitFullscreenElement
const isVideoFullscreen = !!videoEl.webkitDisplayingFullscreen const isVideoFullscreen = !!videoEl.webkitDisplayingFullscreen
setIsFullscreen(isDocFullscreen || isVideoFullscreen) const isNowFullscreen = isDocFullscreen || isVideoFullscreen
setIsFullscreen(isNowFullscreen)
if (isNowFullscreen) {
clearSoftFullscreenTimeout()
setIsSoftFullscreen(false)
}
} }
const onWebkitBegin = () => setIsFullscreen(true) const onWebkitBegin = () => {
const onWebkitEnd = () => setIsFullscreen(false) clearSoftFullscreenTimeout()
video.controls = true
setIsFullscreen(true)
setIsSoftFullscreen(false)
}
const onWebkitEnd = () => {
video.controls = false
setIsFullscreen(false)
setIsSoftFullscreen(false)
}
document.addEventListener("fullscreenchange", updateFullscreenState) document.addEventListener("fullscreenchange", updateFullscreenState)
document.addEventListener("webkitfullscreenchange", updateFullscreenState) document.addEventListener("webkitfullscreenchange", updateFullscreenState)
@@ -153,6 +191,7 @@ export function VideoPlayer({
const video = videoRef.current const video = videoRef.current
const container = containerRef.current const container = containerRef.current
if (!video || !container) return if (!video || !container) return
clearSoftFullscreenTimeout()
const doc = document as Document & { const doc = document as Document & {
webkitFullscreenElement?: Element | null webkitFullscreenElement?: Element | null
@@ -193,11 +232,33 @@ export function VideoPlayer({
return 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) { if (isAppleMobile && videoEl.webkitEnterFullscreen) {
try { try {
video.controls = true
if (video.paused) {
video.play().then(() => setIsPlaying(true)).catch(() => {})
}
videoEl.webkitEnterFullscreen() videoEl.webkitEnterFullscreen()
scheduleSoftFullscreenFallback()
return return
} catch { } catch {
video.controls = false
// Fall back to other fullscreen methods. // Fall back to other fullscreen methods.
} }
} }
@@ -220,6 +281,8 @@ export function VideoPlayer({
setIsFullscreen(true) setIsFullscreen(true)
return return
} }
setIsSoftFullscreen(true)
return
} }
} catch { } catch {
// Fall through to video fullscreen methods. // Fall through to video fullscreen methods.
@@ -235,9 +298,12 @@ export function VideoPlayer({
} else if (videoEl.webkitEnterFullscreen) { } else if (videoEl.webkitEnterFullscreen) {
videoEl.webkitEnterFullscreen() videoEl.webkitEnterFullscreen()
setIsFullscreen(true) setIsFullscreen(true)
scheduleSoftFullscreenFallback()
} else {
setIsSoftFullscreen(true)
} }
} catch { } catch {
// Ignore fullscreen errors to avoid breaking playback. setIsSoftFullscreen(true)
} }
} }
@@ -251,6 +317,8 @@ export function VideoPlayer({
}, 3000) }, 3000)
} }
const isFullscreenActive = isFullscreen || isSoftFullscreen
if (error) { if (error) {
return ( return (
<div className="flex h-full w-full items-center justify-center bg-neutral-900 text-neutral-400"> <div className="flex h-full w-full items-center justify-center bg-neutral-900 text-neutral-400">
@@ -262,7 +330,9 @@ export function VideoPlayer({
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className="group relative h-full w-full bg-black" className={`group bg-black ${
isSoftFullscreen ? "fixed inset-0 z-50 h-screen w-screen" : "relative h-full w-full"
}`}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseLeave={() => isPlaying && setShowControls(false)} onMouseLeave={() => isPlaying && setShowControls(false)}
> >
@@ -344,7 +414,7 @@ export function VideoPlayer({
onClick={handleFullscreen} onClick={handleFullscreen}
className="text-white transition-transform hover:scale-110" className="text-white transition-transform hover:scale-110"
> >
{isFullscreen ? ( {isFullscreenActive ? (
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 24 24"> <svg className="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" /> <path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" />
</svg> </svg>

View File

@@ -120,11 +120,7 @@ function StreamPage() {
const stream = data?.stream ?? null const stream = data?.stream ?? null
const playback = stream?.playback ?? null const playback = stream?.playback ?? null
const fallbackPlayback = stream?.hls_url const fallbackPlayback = stream?.hls_url
? resolveStreamPlayback({ ? { type: "hls", url: stream.hls_url }
hlsUrl: stream.hls_url,
webrtcUrl: null,
preferWebRtc: false,
})
: null : null
const activePlayback = const activePlayback =
playback?.type === "webrtc" && webRtcFailed playback?.type === "webrtc" && webRtcFailed