206 lines
10 KiB
TypeScript
206 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { Tv, PlayCircle, RadioTower, WifiOff, PauseCircle, Volume2 } from 'lucide-react';
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { usePlayerStore } from '@/store/usePlayerStore';
|
|
import videojs from 'video.js';
|
|
import 'video.js/dist/video-js.css';
|
|
|
|
interface LivePlayerProps {
|
|
url?: string;
|
|
type: 'tv' | 'radio';
|
|
title?: string;
|
|
}
|
|
|
|
export function LivePlayer({ url, type, title }: LivePlayerProps) {
|
|
const {
|
|
isAudioPlaying, toggleAudio, setAudioUrl, audioVolume, setAudioVolume,
|
|
isVideoActive, setVideoActive, setVideoUrl, setVideoType
|
|
} = usePlayerStore();
|
|
|
|
const [processedUrl, setProcessedUrl] = useState<string | null>(null);
|
|
const [isYouTube, setIsYouTube] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!url) {
|
|
setProcessedUrl(null);
|
|
return;
|
|
}
|
|
|
|
// YT Detection & Conversion
|
|
if (url.includes('youtube.com/watch?v=') || url.includes('youtu.be/')) {
|
|
const videoId = url.includes('watch?v=')
|
|
? url.split('v=')[1]?.split('&')[0]
|
|
: url.split('/').pop();
|
|
|
|
if (videoId) {
|
|
const ytUrl = `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`;
|
|
setProcessedUrl(ytUrl);
|
|
setIsYouTube(true);
|
|
if (type === 'tv') {
|
|
setVideoUrl(ytUrl);
|
|
setVideoType('youtube');
|
|
}
|
|
}
|
|
} else {
|
|
setProcessedUrl(url);
|
|
setIsYouTube(false);
|
|
if (type === 'tv') {
|
|
setVideoUrl(url);
|
|
setVideoType('m3u8');
|
|
} else {
|
|
setAudioUrl(url);
|
|
}
|
|
}
|
|
}, [url, type, setAudioUrl, setVideoUrl, setVideoType]);
|
|
|
|
// Handle inner Video Player Initialization
|
|
const innerVideoRef = useRef<HTMLDivElement>(null);
|
|
const innerPlayerRef = useRef<any>(null);
|
|
|
|
useEffect(() => {
|
|
if (type === 'tv' && !isYouTube && processedUrl && innerVideoRef.current) {
|
|
if (innerPlayerRef.current) {
|
|
innerPlayerRef.current.dispose();
|
|
innerPlayerRef.current = null;
|
|
}
|
|
const videoElement = document.createElement('video-js');
|
|
videoElement.classList.add('vjs-fill', 'vjs-almeira');
|
|
innerVideoRef.current.innerHTML = '';
|
|
innerVideoRef.current.appendChild(videoElement);
|
|
|
|
innerPlayerRef.current = videojs(videoElement, {
|
|
autoplay: 'muted',
|
|
controls: false,
|
|
fluid: true,
|
|
responsive: true,
|
|
sources: [{
|
|
src: processedUrl,
|
|
type: 'application/x-mpegURL'
|
|
}]
|
|
});
|
|
}
|
|
return () => {
|
|
if (innerPlayerRef.current) {
|
|
innerPlayerRef.current.dispose();
|
|
innerPlayerRef.current = null;
|
|
}
|
|
};
|
|
}, [type, isYouTube, processedUrl]);
|
|
|
|
if (!processedUrl) {
|
|
return (
|
|
<div className={`relative w-full rounded-3xl overflow-hidden border border-slate-800 shadow-2xl bg-slate-900 flex flex-col items-center justify-center p-12 transition-all ${type === 'tv' ? 'aspect-video' : 'h-64'}`}>
|
|
{/* Static Background Effect */}
|
|
<div className="absolute inset-0 opacity-[0.03] pointer-events-none bg-[url('https://media.giphy.com/media/oEI9uWUic6Rji/giphy.gif')] bg-cover mix-blend-overlay"></div>
|
|
|
|
<div className="relative z-10 flex flex-col items-center animate-in fade-in zoom-in duration-700">
|
|
<div className={`p-6 rounded-full mb-6 border-2 border-dashed animate-pulse ${type === 'tv' ? 'bg-cyan-500/10 border-cyan-500/30 text-cyan-400' : 'bg-rose-500/10 border-rose-500/30 text-rose-400'}`}>
|
|
<WifiOff size={64} />
|
|
</div>
|
|
<h2 className="text-4xl font-black font-outfit text-white tracking-tighter uppercase mb-2">SEÑAL PERDIDA</h2>
|
|
<p className="text-slate-500 font-bold uppercase tracking-widest text-[10px] md:text-xs text-center max-w-sm">
|
|
La transmisión de {type.toUpperCase()} no está disponible en este momento. Por favor, intenta más tarde.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Decorative scanning line */}
|
|
<div className="absolute top-0 left-0 w-full h-1 bg-white/10 animate-[scan_3s_linear_infinite] pointer-events-none"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`relative w-full rounded-3xl overflow-hidden border border-slate-800 shadow-2xl bg-black transition-all ${type === 'tv' ? 'aspect-video' : 'h-48'}`}>
|
|
{type === 'tv' ? (
|
|
<div className="w-full h-full relative group">
|
|
{isYouTube ? (
|
|
<iframe
|
|
src={processedUrl}
|
|
className="w-full h-full border-0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
allowFullScreen
|
|
title={title || "Live TV Stream"}
|
|
/>
|
|
) : (
|
|
<div ref={innerVideoRef} className="w-full h-full pointer-events-none" />
|
|
)}
|
|
{/* Overlay to catch interaction and sync state */}
|
|
<div
|
|
className="absolute inset-0 bg-transparent cursor-pointer flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity bg-black/40"
|
|
onClick={() => setVideoActive(true)}
|
|
>
|
|
<div className="w-20 h-20 bg-pink-600 rounded-full flex items-center justify-center shadow-[0_0_30px_rgba(219,39,119,0.5)] animate-pulse">
|
|
<PlayCircle size={40} className="text-white translate-x-1" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="w-full h-full flex flex-col items-center justify-center relative p-8">
|
|
<div className="absolute inset-0 bg-gradient-to-r from-rose-600/10 to-orange-600/10 pointer-events-none" />
|
|
|
|
{/* Visualizer bars for Radio */}
|
|
<div className="absolute bottom-4 left-0 right-0 h-16 flex items-end justify-center gap-1 opacity-20 overflow-hidden px-8 pointer-events-none">
|
|
{Array.from({length: 40}).map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className={`w-2 bg-rose-500 rounded-t-sm ${isAudioPlaying ? 'animate-pulse' : ''}`}
|
|
style={{
|
|
height: isAudioPlaying ? `${20 + Math.random() * 80}%` : '10%',
|
|
animationDuration: `${0.4 + Math.random()}s`
|
|
}}
|
|
></div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="relative z-10 flex flex-col items-center w-full max-w-lg">
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="p-3 bg-rose-500 rounded-full text-white shadow-xl shadow-rose-500/20">
|
|
<RadioTower size={24} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xl font-black text-white uppercase tracking-tight leading-none mb-1">En Vivo: {title || 'Radio Online'}</h3>
|
|
<p className="text-rose-400 text-[10px] font-black uppercase tracking-widest">
|
|
{isAudioPlaying ? 'Sintonizando Almeira Radio HD' : 'Radio Pulsa para Escuchar'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Store-driven Controls */}
|
|
<div className="flex items-center gap-6 w-full bg-white/5 backdrop-blur-md rounded-2xl p-4 border border-white/10">
|
|
<button
|
|
onClick={() => toggleAudio()}
|
|
className="w-14 h-14 bg-rose-500 hover:bg-rose-400 text-white rounded-xl flex items-center justify-center shadow-lg transition-all active:scale-95"
|
|
>
|
|
{isAudioPlaying ? <PauseCircle size={32} fill="white" /> : <PlayCircle size={32} fill="white" />}
|
|
</button>
|
|
|
|
<div className="flex-1 flex items-center gap-3">
|
|
<Volume2 size={18} className="text-rose-400" />
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={audioVolume}
|
|
onChange={(e) => setAudioVolume(parseFloat(e.target.value))}
|
|
className="flex-1 h-1.5 bg-slate-800 rounded-full appearance-none cursor-pointer accent-rose-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<a
|
|
href={processedUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-[9px] font-bold text-slate-500 hover:text-white transition-colors mt-4 uppercase tracking-widest border border-slate-800 px-3 py-1 rounded-full"
|
|
>
|
|
Enlace Directo de Audio ↗
|
|
</a>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|