feat: synchronize live TV/Radio pages with global floating player
This commit is contained in:
parent
a2d5604d38
commit
031e8f4523
@ -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 />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 }),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user