import { useEffect, useRef, useState } from "react" import Hls from "hls.js" interface VideoPlayerProps { src: string autoPlay?: boolean muted?: boolean } export function VideoPlayer({ src, autoPlay = true, muted = false, }: 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 [showControls, setShowControls] = useState(true) const [error, setError] = useState(null) const hideControlsTimeoutRef = useRef | null>(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().catch(() => setIsPlaying(false)) 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().catch(() => setIsPlaying(false)) }) 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]) 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 setIsFullscreen(isDocFullscreen || isVideoFullscreen) } const onWebkitBegin = () => setIsFullscreen(true) const onWebkitEnd = () => setIsFullscreen(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 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 (isAppleMobile && videoEl.webkitEnterFullscreen) { try { videoEl.webkitEnterFullscreen() return } catch { // 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 } } } 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) } } catch { // Ignore fullscreen errors to avoid breaking playback. } } const handleMouseMove = () => { setShowControls(true) if (hideControlsTimeoutRef.current) { clearTimeout(hideControlsTimeoutRef.current) } hideControlsTimeoutRef.current = setTimeout(() => { if (isPlaying) setShowControls(false) }, 3000) } if (error) { return (

{error}

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