import { useEffect, useRef, useState } from "react" import Hls from "hls.js" interface VideoPlayerProps { src: string autoPlay?: boolean muted?: boolean onReady?: () => void } export function VideoPlayer({ src, autoPlay = true, muted = false, onReady, }: VideoPlayerProps) { const containerRef = useRef(null) const videoRef = useRef(null) const hlsRef = useRef(null) const [isPlaying, setIsPlaying] = useState(autoPlay) 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 if (!video || !src) return // Check if native HLS is supported (Safari) if (video.canPlayType("application/vnd.apple.mpegurl")) { video.src = src if (autoPlay) { video.play() .then(() => { setIsPlaying(true) onReady?.() }) .catch(() => setIsPlaying(false)) } else { // Even if not autoplay, notify ready when we can play video.addEventListener("canplay", () => onReady?.(), { once: true }) } return } // Use HLS.js for other browsers if (Hls.isSupported()) { const hls = new Hls({ enableWorker: true, lowLatencyMode: true, liveSyncDurationCount: 3, liveMaxLatencyDurationCount: 6, }) hls.loadSource(src) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () => { if (autoPlay) { video.play() .then(() => { setIsPlaying(true) onReady?.() }) .catch(() => setIsPlaying(false)) } else { onReady?.() } }) hls.on(Hls.Events.ERROR, (_, data) => { if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: setError("Network error - retrying...") hls.startLoad() break case Hls.ErrorTypes.MEDIA_ERROR: setError("Media error - recovering...") hls.recoverMediaError() break default: setError("Stream error") hls.destroy() break } } }) hlsRef.current = hls return () => { hls.destroy() hlsRef.current = null } } else { setError("HLS playback not supported in this browser") } }, [src, autoPlay, onReady]) 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 const doc = document as Document & { webkitFullscreenElement?: Element | null } const videoEl = video as HTMLVideoElement & { webkitDisplayingFullscreen?: boolean } const updateFullscreenState = () => { const isDocFullscreen = !!doc.fullscreenElement || !!doc.webkitFullscreenElement const isVideoFullscreen = !!videoEl.webkitDisplayingFullscreen const isNowFullscreen = isDocFullscreen || isVideoFullscreen setIsFullscreen(isNowFullscreen) if (isNowFullscreen) { clearSoftFullscreenTimeout() setIsSoftFullscreen(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) video.addEventListener("webkitbeginfullscreen", onWebkitBegin as EventListener) video.addEventListener("webkitendfullscreen", onWebkitEnd as EventListener) return () => { document.removeEventListener("fullscreenchange", updateFullscreenState) document.removeEventListener("webkitfullscreenchange", updateFullscreenState) video.removeEventListener("webkitbeginfullscreen", onWebkitBegin as EventListener) video.removeEventListener("webkitendfullscreen", onWebkitEnd as EventListener) } }, []) const handlePlayPause = () => { const video = videoRef.current if (!video) return if (video.paused) { video.play().then(() => setIsPlaying(true)) } else { video.pause() setIsPlaying(false) } } const handleMute = () => { const video = videoRef.current if (!video) return video.muted = !video.muted setIsMuted(video.muted) } const handleVolumeChange = (e: React.ChangeEvent) => { const video = videoRef.current if (!video) return const newVolume = parseFloat(e.target.value) video.volume = newVolume setVolume(newVolume) if (newVolume === 0) { setIsMuted(true) video.muted = true } else if (isMuted) { setIsMuted(false) video.muted = false } } const handleFullscreen = async () => { const video = videoRef.current const container = containerRef.current if (!video || !container) return clearSoftFullscreenTimeout() const doc = document as Document & { webkitFullscreenElement?: Element | null webkitExitFullscreen?: () => void } const videoEl = video as HTMLVideoElement & { webkitEnterFullscreen?: () => void webkitExitFullscreen?: () => void webkitRequestFullscreen?: () => Promise | void webkitDisplayingFullscreen?: boolean } const containerEl = container as HTMLElement & { webkitRequestFullscreen?: () => Promise | void } const isDocFullscreen = !!doc.fullscreenElement || !!doc.webkitFullscreenElement const isVideoFullscreen = !!videoEl.webkitDisplayingFullscreen const isAppleMobile = typeof navigator !== "undefined" && (/iP(ad|hone|od)/.test(navigator.userAgent) || (navigator.userAgent.includes("Mac") && navigator.maxTouchPoints > 1)) if (isDocFullscreen) { if (document.exitFullscreen) { await document.exitFullscreen() } else if (doc.webkitExitFullscreen) { doc.webkitExitFullscreen() } setIsFullscreen(false) return } if (isVideoFullscreen) { if (videoEl.webkitExitFullscreen) { videoEl.webkitExitFullscreen() } setIsFullscreen(false) 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. } } const requestContainerFullscreen = async () => { if (containerEl.requestFullscreen) { await containerEl.requestFullscreen() return true } if (containerEl.webkitRequestFullscreen) { await containerEl.webkitRequestFullscreen() return true } return false } try { if (await requestContainerFullscreen()) { if (!!doc.fullscreenElement || !!doc.webkitFullscreenElement) { setIsFullscreen(true) return } setIsSoftFullscreen(true) return } } catch { // Fall through to video fullscreen methods. } try { if (video.requestFullscreen) { await video.requestFullscreen() setIsFullscreen(true) } else if (videoEl.webkitRequestFullscreen) { await videoEl.webkitRequestFullscreen() setIsFullscreen(true) } else if (videoEl.webkitEnterFullscreen) { videoEl.webkitEnterFullscreen() setIsFullscreen(true) scheduleSoftFullscreenFallback() } else { setIsSoftFullscreen(true) } } catch { setIsSoftFullscreen(true) } } const handleMouseMove = () => { setShowControls(true) if (hideControlsTimeoutRef.current) { clearTimeout(hideControlsTimeoutRef.current) } hideControlsTimeoutRef.current = setTimeout(() => { if (isPlaying) setShowControls(false) }, 3000) } const isFullscreenActive = isFullscreen || isSoftFullscreen if (error) { return (

{error}

) } return (
isPlaying && setShowControls(false)} >