253 lines
12 KiB
TypeScript
253 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { usePlayerStore } from '@/store/usePlayerStore';
|
|
import { Play, Pause, Volume2, VolumeX, Radio, Music, Maximize2, Minimize2, Video, X } from 'lucide-react';
|
|
import { StreamOverlays } from './StreamOverlays';
|
|
import videojs from 'video.js';
|
|
import 'video.js/dist/video-js.css';
|
|
|
|
export const GlobalPlayer = ({ initialConfig }: { initialConfig?: any }) => {
|
|
const {
|
|
audioUrl, isAudioPlaying, audioVolume, toggleAudio, setAudioUrl, setAudioVolume,
|
|
isVideoActive, videoUrl, videoType, setVideoActive, setVideoUrl, setVideoType
|
|
} = usePlayerStore();
|
|
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
const videoRef = useRef<HTMLDivElement>(null);
|
|
const playerRef = useRef<any>(null);
|
|
const [isMuted, setIsMuted] = useState(false);
|
|
const [lastVolume, setLastVolume] = useState(audioVolume);
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const [hasHydrated, setHasHydrated] = useState(false);
|
|
|
|
// Sync from Layout Config on First Load
|
|
useEffect(() => {
|
|
if (initialConfig) {
|
|
if (initialConfig.radio_url && audioUrl === '') {
|
|
setAudioUrl(initialConfig.radio_url);
|
|
}
|
|
if (initialConfig.tv_url && videoUrl === '') {
|
|
setVideoUrl(initialConfig.tv_url);
|
|
setVideoType(initialConfig.tv_url.includes('youtube.com') || initialConfig.tv_url.includes('youtu.be') ? 'youtube' : 'm3u8');
|
|
}
|
|
}
|
|
setHasHydrated(true);
|
|
}, [initialConfig, setAudioUrl, setVideoUrl, setVideoType]);
|
|
|
|
// Sync Audio State
|
|
useEffect(() => {
|
|
if (!audioRef.current) return;
|
|
|
|
if (isAudioPlaying) {
|
|
audioRef.current.play().catch(e => {
|
|
console.error("Audio playback error:", e);
|
|
toggleAudio(false);
|
|
});
|
|
} else {
|
|
audioRef.current.pause();
|
|
}
|
|
}, [isAudioPlaying, audioUrl, toggleAudio]);
|
|
|
|
// Handle Video Player Initialization
|
|
useEffect(() => {
|
|
if (isVideoActive && videoRef.current && videoType === 'm3u8') {
|
|
// Cleanup previous if exists
|
|
if (playerRef.current) {
|
|
playerRef.current.dispose();
|
|
playerRef.current = null;
|
|
}
|
|
|
|
const videoElement = document.createElement('video-js');
|
|
videoElement.classList.add('vjs-big-play-centered', 'vjs-almeira');
|
|
videoRef.current.innerHTML = '';
|
|
videoRef.current.appendChild(videoElement);
|
|
|
|
const player = playerRef.current = videojs(videoElement, {
|
|
autoplay: true,
|
|
controls: true,
|
|
responsive: true,
|
|
fluid: true,
|
|
sources: [{
|
|
src: videoUrl || 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8',
|
|
type: 'application/x-mpegURL'
|
|
}]
|
|
});
|
|
|
|
// Cuando el video está activo, bajamos el volumen de la radio
|
|
if (isAudioPlaying) setAudioVolume(0.1);
|
|
}
|
|
|
|
return () => {
|
|
if (playerRef.current) {
|
|
playerRef.current.dispose();
|
|
playerRef.current = null;
|
|
}
|
|
};
|
|
}, [isVideoActive, videoUrl, videoType]);
|
|
|
|
// Sync Volume
|
|
useEffect(() => {
|
|
if (audioRef.current) {
|
|
audioRef.current.volume = isMuted ? 0 : audioVolume;
|
|
}
|
|
}, [audioVolume, isMuted]);
|
|
|
|
const handleToggleMute = () => {
|
|
if (isMuted) {
|
|
setIsMuted(false);
|
|
setAudioVolume(lastVolume);
|
|
} else {
|
|
setLastVolume(audioVolume);
|
|
setIsMuted(true);
|
|
setAudioVolume(0);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Hidden Audio Element */}
|
|
<audio ref={audioRef} src={audioUrl} preload="auto" />
|
|
|
|
{/* VOLUMETRIC VIDEO OVERLAY (When Active) */}
|
|
{isVideoActive && (
|
|
<div className="fixed inset-0 z-[100] bg-black flex items-center justify-center animate-in fade-in zoom-in duration-300">
|
|
<div className="relative w-full h-full max-w-6xl aspect-video bg-slate-900 shadow-2xl overflow-hidden md:rounded-3xl border border-white/10">
|
|
{videoType === 'm3u8' ? (
|
|
<div ref={videoRef} className="w-full h-full" />
|
|
) : (
|
|
<iframe
|
|
src={videoUrl}
|
|
className="w-full h-full border-0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
allowFullScreen
|
|
title="Live TV Stream"
|
|
/>
|
|
)}
|
|
|
|
{/* Interactive Data Layer */}
|
|
<StreamOverlays />
|
|
|
|
{/* Top Controls */}
|
|
<div className="absolute top-6 right-6 z-20">
|
|
<button
|
|
onClick={() => setVideoActive(false)}
|
|
className="p-3 bg-white/10 backdrop-blur-md hover:bg-white/20 text-white rounded-full transition-all border border-white/20"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Floating Global Controller */}
|
|
<div className={`fixed bottom-6 left-6 z-[60] flex items-center gap-3 transition-opacity duration-500 ${isVideoActive ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}>
|
|
|
|
{/* ESTADO COLAPSADO */}
|
|
{!isExpanded && (audioUrl !== '' || videoUrl !== '') && (
|
|
<button
|
|
onClick={() => setIsExpanded(true)}
|
|
className="group relative h-14 px-5 bg-red-600 hover:bg-red-500 text-white rounded-full flex items-center justify-center gap-3 shadow-[0_0_20px_rgba(220,38,38,0.4)] transition-all animate-in zoom-in slide-in-from-bottom-5"
|
|
>
|
|
<div className="absolute inset-0 border-2 border-red-500 rounded-full animate-[ping_2s_ease-out_infinite] opacity-50 pointer-events-none"></div>
|
|
<Radio size={20} className="relative z-10" />
|
|
<span className="font-black tracking-widest uppercase text-xs whitespace-nowrap">En Vivo</span>
|
|
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse relative z-10 shadow-[0_0_8px_rgba(255,255,255,0.8)]"></div>
|
|
</button>
|
|
)}
|
|
|
|
{/* ESTADO EXPANDIDO */}
|
|
{isExpanded && (
|
|
<>
|
|
<div className="bg-slate-900/40 backdrop-blur-2xl border border-white/10 p-2 rounded-2xl shadow-2xl flex items-center gap-4 pr-3 group hover:bg-slate-900/60 transition-all animate-in fade-in slide-in-from-left-4">
|
|
{/* Play/Pause Main Button */}
|
|
<button
|
|
onClick={() => toggleAudio()}
|
|
className={`w-12 h-12 rounded-xl flex items-center justify-center transition-all shadow-lg overflow-hidden relative ${
|
|
isAudioPlaying
|
|
? 'bg-cyan-500 text-white shadow-cyan-500/20'
|
|
: 'bg-white/5 text-slate-400 hover:text-white hover:bg-white/10'
|
|
}`}
|
|
>
|
|
{isAudioPlaying ? (
|
|
<div className="flex items-end gap-[2px] h-4">
|
|
<div className="w-1 bg-white animate-[music-bar_0.6s_ease-in-out_infinite]"></div>
|
|
<div className="w-1 bg-white animate-[music-bar_0.8s_ease-in-out_infinite]"></div>
|
|
<div className="w-1 bg-white animate-[music-bar_0.5s_ease-in-out_infinite]"></div>
|
|
<div className="w-1 bg-white animate-[music-bar_0.9s_ease-in-out_infinite]"></div>
|
|
</div>
|
|
) : (
|
|
<Play size={20} fill="currentColor" />
|
|
)}
|
|
</button>
|
|
|
|
<div className="flex flex-col pr-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className={`text-[10px] font-black uppercase tracking-[0.2em] transition-colors ${isAudioPlaying ? 'text-cyan-400' : 'text-rose-400'}`}>
|
|
{isAudioPlaying ? 'Al Aire' : 'Señal en Vivo'}
|
|
</span>
|
|
<div className={`w-1.5 h-1.5 rounded-full ${isAudioPlaying ? 'bg-red-500 animate-pulse shadow-[0_0_8px_rgba(239,68,68,0.5)]' : 'bg-rose-500'}`}></div>
|
|
</div>
|
|
<h4 className="text-xs font-bold text-white tracking-wider font-outfit truncate max-w-[100px]">ALMEIRA RADIO</h4>
|
|
</div>
|
|
|
|
{/* Volume Control (Desktop Only) */}
|
|
<div className="hidden md:flex items-center gap-3 border-l border-white/5 pl-4 ml-2">
|
|
<button onClick={handleToggleMute} className="text-slate-500 hover:text-white transition-colors">
|
|
{isMuted || audioVolume === 0 ? <VolumeX size={16} /> : <Volume2 size={16} />}
|
|
</button>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={audioVolume}
|
|
onChange={(e) => {
|
|
const val = parseFloat(e.target.value);
|
|
setAudioVolume(val);
|
|
if(val > 0) setIsMuted(false);
|
|
}}
|
|
className="w-20 h-1 bg-slate-800 rounded-full appearance-none cursor-pointer accent-cyan-500 hover:accent-cyan-400 transition-all"
|
|
/>
|
|
|
|
{/* Botón para colapsar */}
|
|
<button
|
|
onClick={() => setIsExpanded(false)}
|
|
title="Ocultar Player"
|
|
className="ml-2 w-8 h-8 flex items-center justify-center bg-white/5 hover:bg-white/10 border border-white/5 rounded-xl text-slate-400 hover:text-white transition-all"
|
|
>
|
|
<Minimize2 size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Video Switcher Mini (If Stream is Active) */}
|
|
<button
|
|
onClick={() => setVideoActive(!isVideoActive)}
|
|
className={`w-12 h-12 rounded-2xl flex items-center justify-center transition-all border shadow-2xl relative overflow-hidden ${
|
|
isVideoActive
|
|
? 'bg-pink-600 border-pink-500 text-white shadow-pink-500/20'
|
|
: 'bg-slate-900/40 backdrop-blur-2xl border-white/10 text-slate-500 hover:text-white'
|
|
}`}
|
|
>
|
|
<Video size={18} />
|
|
{isVideoActive && (
|
|
<div className="absolute inset-x-0 bottom-0 h-1 bg-white/30 animate-pulse"></div>
|
|
)}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Add Music Bar Animation to globals.css */}
|
|
<style jsx global>{`
|
|
@keyframes music-bar {
|
|
0%, 100% { height: 4px; }
|
|
50% { height: 16px; }
|
|
}
|
|
`}</style>
|
|
</>
|
|
);
|
|
};
|