feat: synchronize live TV/Radio pages with global floating player

This commit is contained in:
Carlos (Almeira) 2026-03-30 08:26:32 -05:00
parent a2d5604d38
commit 031e8f4523
3 changed files with 94 additions and 27 deletions

View File

@ -10,7 +10,7 @@ import 'video.js/dist/video-js.css';
export const GlobalPlayer = () => {
const {
audioUrl, isAudioPlaying, audioVolume, toggleAudio, setAudioVolume,
isVideoActive, videoUrl, setVideoActive
isVideoActive, videoUrl, videoType, setVideoActive
} = usePlayerStore();
const audioRef = useRef<HTMLAudioElement>(null);
@ -36,9 +36,16 @@ export const GlobalPlayer = () => {
// Handle Video Player Initialization
useEffect(() => {
if (isVideoActive && videoRef.current && !playerRef.current) {
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, {
@ -47,7 +54,7 @@ export const GlobalPlayer = () => {
responsive: true,
fluid: true,
sources: [{
src: videoUrl || 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8', // Fallback para test
src: videoUrl || 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8',
type: 'application/x-mpegURL'
}]
});
@ -62,7 +69,7 @@ export const GlobalPlayer = () => {
playerRef.current = null;
}
};
}, [isVideoActive, videoUrl]);
}, [isVideoActive, videoUrl, videoType]);
// Sync Volume
useEffect(() => {
@ -91,7 +98,17 @@ export const GlobalPlayer = () => {
{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">
<div ref={videoRef} className="w-full h-full" />
{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 />

View File

@ -1,7 +1,8 @@
'use client';
import { Tv, PlayCircle, RadioTower, WifiOff } from 'lucide-react';
import { Tv, PlayCircle, RadioTower, WifiOff, PauseCircle, Volume2 } from 'lucide-react';
import { useState, useEffect } from 'react';
import { usePlayerStore } from '@/store/usePlayerStore';
interface LivePlayerProps {
url?: string;
@ -10,6 +11,11 @@ interface LivePlayerProps {
}
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);
@ -26,14 +32,25 @@ export function LivePlayer({ url, type, title }: LivePlayerProps) {
: url.split('/').pop();
if (videoId) {
setProcessedUrl(`https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`);
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]);
}, [url, type, setAudioUrl, setVideoUrl, setVideoType]);
if (!processedUrl) {
return (
@ -60,13 +77,20 @@ export function LivePlayer({ url, type, title }: LivePlayerProps) {
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' ? (
<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 className="w-full h-full relative group">
<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"}
/>
{/* Overlay to catch interaction and sync state */}
<div
className="absolute inset-0 bg-transparent cursor-pointer"
onClick={() => setVideoActive(true)}
></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" />
@ -74,7 +98,14 @@ export function LivePlayer({ url, type, title }: LivePlayerProps) {
{/* 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 animate-pulse" style={{ height: `${20 + Math.random() * 80}%`, animationDuration: `${0.4 + Math.random()}s` }}></div>
<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>
@ -85,19 +116,34 @@ export function LivePlayer({ url, type, title }: LivePlayerProps) {
</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">Sintonizando Almeira Radio HD</p>
<p className="text-rose-400 text-[10px] font-black uppercase tracking-widest">
{isAudioPlaying ? 'Sintonizando Almeira Radio HD' : 'Radio Pulsa para Escuchar'}
</p>
</div>
</div>
{/* Standard Audio Player or Custom UI for Direct Stream */}
<audio
controls
autoPlay
className="w-full h-10 accent-rose-500"
src={processedUrl}
>
Tu navegador no soporta la reproducción de audio.
</audio>
{/* 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}

View File

@ -7,10 +7,11 @@ interface PlayerState {
isAudioPlaying: boolean;
audioVolume: number;
// Video Streaming (Restreamer)
// Video Streaming (Restreamer / YT)
videoUrl: string;
isVideoActive: boolean;
videoVolume: number;
videoType: 'm3u8' | 'youtube';
// Overlays & Content
isOverlayVisible: boolean;
@ -24,6 +25,7 @@ interface PlayerState {
setVideoUrl: (url: string) => void;
setVideoActive: (active: boolean) => void;
setVideoVolume: (vol: number) => void;
setVideoType: (type: 'm3u8' | 'youtube') => void;
setOverlayVisible: (visible: boolean) => void;
setOverlayTab: (tab: 'whatsapp' | 'classifieds' | 'events') => void;
@ -40,6 +42,7 @@ export const usePlayerStore = create<PlayerState>()(
videoUrl: 'https://tv.almeira.com.co/hls/stream.m3u8',
isVideoActive: false,
videoVolume: 0.5,
videoType: 'm3u8',
isOverlayVisible: true,
activeOverlayTab: 'whatsapp',
@ -52,6 +55,7 @@ export const usePlayerStore = create<PlayerState>()(
setVideoUrl: (url) => set({ videoUrl: url }),
setVideoActive: (active) => set({ isVideoActive: active }),
setVideoVolume: (vol) => set({ videoVolume: vol }),
setVideoType: (type) => set({ videoType: type }),
setOverlayVisible: (visible) => set({ isOverlayVisible: visible }),
setOverlayTab: (tab) => set({ activeOverlayTab: tab }),