import { useState, useRef, useEffect } from 'react' import { Play, Pause, Volume2, VolumeX, Maximize, Minimize, SkipBack, SkipForward, Download } from 'lucide-react' import { invoke } from '@tauri-apps/api/core' interface VideoInfo { url: string // HTTP URL from local server content_type: string cached: boolean } interface VideoPlayerProps { src: string poster?: string filename?: string autoPlay?: boolean loop?: boolean className?: string } export default function VideoPlayer({ src, poster, filename, autoPlay = false, loop = true, className = '' }: VideoPlayerProps) { const videoRef = useRef(null) const containerRef = useRef(null) const progressRef = useRef(null) // Proxied video URL from local server const [proxiedSrc, setProxiedSrc] = useState(null) const [proxyError, setProxyError] = useState(null) const [isPlaying, setIsPlaying] = useState(autoPlay) const [isMuted, setIsMuted] = useState(false) // Start with sound enabled const [volume, setVolume] = useState(0.5) const [progress, setProgress] = useState(0) const [duration, setDuration] = useState(0) const [currentTime, setCurrentTime] = useState(0) const [isFullscreen, setIsFullscreen] = useState(false) const [showControls, setShowControls] = useState(true) const [isLoading, setIsLoading] = useState(true) const hideControlsTimeout = useRef() // Fetch video through proxy - returns localhost HTTP URL useEffect(() => { async function loadVideo() { try { setIsLoading(true) setProxyError(null) const result = await invoke('proxy_video', { url: src }) // Result.url is already an HTTP URL from local server setProxiedSrc(result.url) setIsLoading(false) } catch (error) { console.error('Failed to proxy video:', error) setProxyError(String(error)) setIsLoading(false) } } if (src) { loadVideo() } }, [src]) // Reset hide timeout on mouse move const resetHideTimeout = () => { setShowControls(true) if (hideControlsTimeout.current) { clearTimeout(hideControlsTimeout.current) } hideControlsTimeout.current = setTimeout(() => { if (isPlaying) setShowControls(false) }, 2500) } // Handle play/pause const togglePlay = () => { const video = videoRef.current if (!video) return if (isPlaying) { video.pause() } else { video.play() } setIsPlaying(!isPlaying) } // Handle mute const toggleMute = () => { const video = videoRef.current if (!video) return video.muted = !isMuted setIsMuted(!isMuted) } // Handle volume change const handleVolumeChange = (e: React.ChangeEvent) => { const video = videoRef.current if (!video) return const newVolume = parseFloat(e.target.value) video.volume = newVolume setVolume(newVolume) setIsMuted(newVolume === 0) } // Handle seek const handleSeek = (e: React.MouseEvent) => { const video = videoRef.current const progressBar = progressRef.current if (!video || !progressBar) return const rect = progressBar.getBoundingClientRect() const pos = (e.clientX - rect.left) / rect.width video.currentTime = pos * duration } // Skip forward/backward const skip = (seconds: number) => { const video = videoRef.current if (!video) return video.currentTime = Math.max(0, Math.min(duration, video.currentTime + seconds)) } // Handle fullscreen const toggleFullscreen = async () => { const container = containerRef.current if (!container) return if (!document.fullscreenElement) { await container.requestFullscreen() setIsFullscreen(true) } else { await document.exitFullscreen() setIsFullscreen(false) } } // Handle download const handleDownload = () => { const a = document.createElement('a') a.href = src a.download = filename || 'video' a.click() } // Format time const formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Only handle if video is focused or in fullscreen if (!containerRef.current?.contains(document.activeElement) && !isFullscreen) return switch (e.key) { case ' ': case 'k': e.preventDefault() togglePlay() break case 'm': toggleMute() break case 'f': toggleFullscreen() break case 'ArrowLeft': e.preventDefault() skip(-5) break case 'ArrowRight': e.preventDefault() skip(5) break case 'ArrowUp': e.preventDefault() if (videoRef.current) { const newVol = Math.min(1, volume + 0.1) videoRef.current.volume = newVol setVolume(newVol) setIsMuted(false) } break case 'ArrowDown': e.preventDefault() if (videoRef.current) { const newVol = Math.max(0, volume - 0.1) videoRef.current.volume = newVol setVolume(newVol) } break } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [isPlaying, isMuted, volume, isFullscreen]) // Video event handlers useEffect(() => { const video = videoRef.current if (!video) return const onTimeUpdate = () => { setCurrentTime(video.currentTime) setProgress((video.currentTime / video.duration) * 100) } const onLoadedMetadata = () => { setDuration(video.duration) setIsLoading(false) } const onEnded = () => { if (!loop) setIsPlaying(false) } const onPlay = () => setIsPlaying(true) const onPause = () => setIsPlaying(false) const onWaiting = () => setIsLoading(true) const onCanPlay = () => setIsLoading(false) video.addEventListener('timeupdate', onTimeUpdate) video.addEventListener('loadedmetadata', onLoadedMetadata) video.addEventListener('ended', onEnded) video.addEventListener('play', onPlay) video.addEventListener('pause', onPause) video.addEventListener('waiting', onWaiting) video.addEventListener('canplay', onCanPlay) return () => { video.removeEventListener('timeupdate', onTimeUpdate) video.removeEventListener('loadedmetadata', onLoadedMetadata) video.removeEventListener('ended', onEnded) video.removeEventListener('play', onPlay) video.removeEventListener('pause', onPause) video.removeEventListener('waiting', onWaiting) video.removeEventListener('canplay', onCanPlay) } }, [loop]) return (
isPlaying && setShowControls(false)} tabIndex={0} > {/* Error State */} {proxyError && (

Failed to load video

{proxyError}

)} {/* Video Element - only render when we have proxied src */} {proxiedSrc && !proxyError && (