|
import React, { useRef, useEffect, useState } from 'react' |
|
import Hls from 'hls.js' |
|
import { Channel } from '../types/channel' |
|
import { channelService } from '../services/channelService' |
|
import { Play, Pause, Volume2, VolumeX, Maximize, AlertCircle } from 'lucide-react' |
|
|
|
interface VideoPlayerProps { |
|
channel: Channel |
|
} |
|
|
|
export default function VideoPlayer({ channel }: VideoPlayerProps) { |
|
const videoRef = useRef<HTMLVideoElement>(null) |
|
const hlsRef = useRef<Hls | null>(null) |
|
const [isPlaying, setIsPlaying] = useState(false) |
|
const [isMuted, setIsMuted] = useState(false) |
|
const [volume, setVolume] = useState(1) |
|
const [error, setError] = useState<string | null>(null) |
|
const [isLoading, setIsLoading] = useState(true) |
|
|
|
useEffect(() => { |
|
if (!videoRef.current || !channel) return |
|
|
|
const video = videoRef.current |
|
setError(null) |
|
setIsLoading(true) |
|
|
|
|
|
if (hlsRef.current) { |
|
hlsRef.current.destroy() |
|
hlsRef.current = null |
|
} |
|
|
|
const streamUrl = channelService.getProxyUrl(channel.url) |
|
|
|
if (Hls.isSupported()) { |
|
const hls = new Hls({ |
|
enableWorker: true, |
|
lowLatencyMode: true, |
|
backBufferLength: 90 |
|
}) |
|
|
|
hls.loadSource(streamUrl) |
|
hls.attachMedia(video) |
|
|
|
hls.on(Hls.Events.MANIFEST_PARSED, () => { |
|
setIsLoading(false) |
|
video.play().catch(console.error) |
|
}) |
|
|
|
hls.on(Hls.Events.ERROR, (event, data) => { |
|
console.error('HLS Error:', data) |
|
if (data.fatal) { |
|
setError('Error al cargar el canal. Inténtalo de nuevo.') |
|
setIsLoading(false) |
|
} |
|
}) |
|
|
|
hlsRef.current = hls |
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { |
|
|
|
video.src = streamUrl |
|
video.addEventListener('loadedmetadata', () => { |
|
setIsLoading(false) |
|
video.play().catch(console.error) |
|
}) |
|
video.addEventListener('error', () => { |
|
setError('Error al cargar el canal. Inténtalo de nuevo.') |
|
setIsLoading(false) |
|
}) |
|
} else { |
|
setError('Tu navegador no soporta la reproducción de este tipo de contenido.') |
|
setIsLoading(false) |
|
} |
|
|
|
return () => { |
|
if (hlsRef.current) { |
|
hlsRef.current.destroy() |
|
hlsRef.current = null |
|
} |
|
} |
|
}, [channel]) |
|
|
|
useEffect(() => { |
|
const video = videoRef.current |
|
if (!video) return |
|
|
|
const handlePlay = () => setIsPlaying(true) |
|
const handlePause = () => setIsPlaying(false) |
|
const handleVolumeChange = () => { |
|
setVolume(video.volume) |
|
setIsMuted(video.muted) |
|
} |
|
|
|
video.addEventListener('play', handlePlay) |
|
video.addEventListener('pause', handlePause) |
|
video.addEventListener('volumechange', handleVolumeChange) |
|
|
|
return () => { |
|
video.removeEventListener('play', handlePlay) |
|
video.removeEventListener('pause', handlePause) |
|
video.removeEventListener('volumechange', handleVolumeChange) |
|
} |
|
}, []) |
|
|
|
const togglePlay = () => { |
|
if (!videoRef.current) return |
|
|
|
if (isPlaying) { |
|
videoRef.current.pause() |
|
} else { |
|
videoRef.current.play() |
|
} |
|
} |
|
|
|
const toggleMute = () => { |
|
if (!videoRef.current) return |
|
videoRef.current.muted = !videoRef.current.muted |
|
} |
|
|
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
if (!videoRef.current) return |
|
const newVolume = parseFloat(e.target.value) |
|
videoRef.current.volume = newVolume |
|
setVolume(newVolume) |
|
} |
|
|
|
const toggleFullscreen = () => { |
|
if (!videoRef.current) return |
|
|
|
if (document.fullscreenElement) { |
|
document.exitFullscreen() |
|
} else { |
|
videoRef.current.requestFullscreen() |
|
} |
|
} |
|
|
|
if (error) { |
|
return ( |
|
<div className="flex-1 flex items-center justify-center bg-black"> |
|
<div className="text-center text-white"> |
|
<AlertCircle className="h-12 w-12 mx-auto mb-4 text-red-500" /> |
|
<h3 className="text-lg font-semibold mb-2">Error de reproducción</h3> |
|
<p className="text-gray-300 mb-4">{error}</p> |
|
<button |
|
onClick={() => window.location.reload()} |
|
className="btn-primary" |
|
> |
|
Reintentar |
|
</button> |
|
</div> |
|
</div> |
|
) |
|
} |
|
|
|
return ( |
|
<div className="flex-1 relative bg-black group"> |
|
<video |
|
ref={videoRef} |
|
className="w-full h-full object-contain" |
|
controls={false} |
|
autoPlay |
|
muted={false} |
|
playsInline |
|
/> |
|
|
|
{isLoading && ( |
|
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-75"> |
|
<div className="text-center text-white"> |
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto mb-2"></div> |
|
<p>Cargando canal...</p> |
|
</div> |
|
</div> |
|
)} |
|
|
|
{/* Controles */} |
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300"> |
|
<div className="flex items-center space-x-4"> |
|
<button |
|
onClick={togglePlay} |
|
className="p-2 hover:bg-white hover:bg-opacity-20 rounded-full transition-colors" |
|
> |
|
{isPlaying ? ( |
|
<Pause className="h-6 w-6 text-white" /> |
|
) : ( |
|
<Play className="h-6 w-6 text-white" /> |
|
)} |
|
</button> |
|
|
|
<div className="flex items-center space-x-2"> |
|
<button |
|
onClick={toggleMute} |
|
className="p-2 hover:bg-white hover:bg-opacity-20 rounded-full transition-colors" |
|
> |
|
{isMuted ? ( |
|
<VolumeX className="h-5 w-5 text-white" /> |
|
) : ( |
|
<Volume2 className="h-5 w-5 text-white" /> |
|
)} |
|
</button> |
|
|
|
<input |
|
type="range" |
|
min="0" |
|
max="1" |
|
step="0.1" |
|
value={isMuted ? 0 : volume} |
|
onChange={handleVolumeChange} |
|
className="w-20 h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer" |
|
/> |
|
</div> |
|
|
|
<div className="flex-1" /> |
|
|
|
<div className="text-white text-sm font-medium"> |
|
{channel.name} |
|
</div> |
|
|
|
<button |
|
onClick={toggleFullscreen} |
|
className="p-2 hover:bg-white hover:bg-opacity-20 rounded-full transition-colors" |
|
> |
|
<Maximize className="h-5 w-5 text-white" /> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
) |
|
} |