8
This commit is contained in:
parent
66c1c5f233
commit
5a0edbe51c
@ -33,6 +33,7 @@ import {
|
|||||||
mdiAlertCircle,
|
mdiAlertCircle,
|
||||||
mdiTune,
|
mdiTune,
|
||||||
mdiVolumeVariantOff,
|
mdiVolumeVariantOff,
|
||||||
|
mdiSync,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import BaseIcon from '../components/BaseIcon';
|
import BaseIcon from '../components/BaseIcon';
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
import { useAppDispatch } from '../stores/hooks';
|
||||||
@ -133,6 +134,7 @@ const ObservationPage = () => {
|
|||||||
const wetGainRef = useRef<GainNode | null>(null);
|
const wetGainRef = useRef<GainNode | null>(null);
|
||||||
const monitorGainRef = useRef<GainNode | null>(null);
|
const monitorGainRef = useRef<GainNode | null>(null);
|
||||||
const masterGainRef = useRef<GainNode | null>(null);
|
const masterGainRef = useRef<GainNode | null>(null);
|
||||||
|
const heartbeatOscRef = useRef<OscillatorNode | null>(null);
|
||||||
|
|
||||||
const [isCameraActive, setIsCameraActive] = useState(false);
|
const [isCameraActive, setIsCameraActive] = useState(false);
|
||||||
const [isKaraokeActive, setIsKaraokeActive] = useState(false);
|
const [isKaraokeActive, setIsKaraokeActive] = useState(false);
|
||||||
@ -143,7 +145,7 @@ const ObservationPage = () => {
|
|||||||
const [isMonitorOn, setIsMonitorOn] = useState(false);
|
const [isMonitorOn, setIsMonitorOn] = useState(false);
|
||||||
const [reverbLevel, setReverbLevel] = useState(0.5);
|
const [reverbLevel, setReverbLevel] = useState(0.5);
|
||||||
const [playbackProgress, setPlaybackProgress] = useState(0);
|
const [playbackProgress, setPlaybackProgress] = useState(0);
|
||||||
const [audioStatus, setAudioStatus] = useState<'IDLE' | 'LOADING' | 'ACTIVE' | 'ERROR'>('IDLE');
|
const [audioStatus, setAudioStatus] = useState<'IDLE' | 'LOADING' | 'ACTIVE' | 'ERROR' | 'SYNCING'>('IDLE');
|
||||||
const [volumeLevel, setVolumeLevel] = useState(1.0);
|
const [volumeLevel, setVolumeLevel] = useState(1.0);
|
||||||
const [audioErrorMsg, setAudioErrorMsg] = useState('');
|
const [audioErrorMsg, setAudioErrorMsg] = useState('');
|
||||||
|
|
||||||
@ -166,6 +168,7 @@ const ObservationPage = () => {
|
|||||||
const [customAudioMap, setCustomAudioMap] = useState<Record<string, string>>({});
|
const [customAudioMap, setCustomAudioMap] = useState<Record<string, string>>({});
|
||||||
const [songToUpload, setSongToUpload] = useState<any>(null);
|
const [songToUpload, setSongToUpload] = useState<any>(null);
|
||||||
const [editingSongId, setEditingSongId] = useState<string | null>(null);
|
const [editingSongId, setEditingSongId] = useState<string | null>(null);
|
||||||
|
const [syncProgress, setSyncProgress] = useState(0);
|
||||||
|
|
||||||
const imageCache = useRef<Map<string, HTMLImageElement>>(new Map());
|
const imageCache = useRef<Map<string, HTMLImageElement>>(new Map());
|
||||||
|
|
||||||
@ -174,20 +177,68 @@ const ObservationPage = () => {
|
|||||||
return [...userSongs, ...INITIAL_SONG_DATABASE];
|
return [...userSongs, ...INITIAL_SONG_DATABASE];
|
||||||
}, [userSongs]);
|
}, [userSongs]);
|
||||||
|
|
||||||
// Global Audio Enabler
|
// Global Audio Master Unlock Engine
|
||||||
|
// This ensures that ANY interaction with the screen resumes the audio engine
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleInteraction = () => {
|
const unlockAudio = async () => {
|
||||||
if (audioCtxRef.current?.state === 'suspended') {
|
if (!audioCtxRef.current) {
|
||||||
audioCtxRef.current.resume().catch(console.error);
|
audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioCtxRef.current.state === 'suspended') {
|
||||||
|
try {
|
||||||
|
await audioCtxRef.current.resume();
|
||||||
|
|
||||||
|
// HEARTBEAT: Constant silent oscillator to keep the context alive
|
||||||
|
if (!heartbeatOscRef.current) {
|
||||||
|
const osc = audioCtxRef.current.createOscillator();
|
||||||
|
const gain = audioCtxRef.current.createGain();
|
||||||
|
gain.gain.value = 0.00001; // Inaudible
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(audioCtxRef.current.destination);
|
||||||
|
osc.start();
|
||||||
|
heartbeatOscRef.current = osc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject a silent buffer to "prime" the browser's audio engine
|
||||||
|
const buffer = audioCtxRef.current.createBuffer(1, 1, 22050);
|
||||||
|
const source = audioCtxRef.current.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.connect(audioCtxRef.current.destination);
|
||||||
|
source.start(0);
|
||||||
|
} catch (err) {
|
||||||
|
// Silent catch to avoid console noise on failed gestures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proactive HTML5 element unlock
|
||||||
|
if (karaokeAudioRef.current && karaokeAudioRef.current.paused && isKaraokeActive && !isPaused) {
|
||||||
|
karaokeAudioRef.current.play().catch(() => { /* Silent */ });
|
||||||
|
}
|
||||||
|
if (crowdAudioRef.current && crowdAudioRef.current.paused && isSimActive) {
|
||||||
|
crowdAudioRef.current.play().catch(() => { /* Silent */ });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('click', handleInteraction);
|
|
||||||
window.addEventListener('touchstart', handleInteraction);
|
const interactions = ['click', 'touchstart', 'mousedown', 'keydown', 'pointerdown'];
|
||||||
|
interactions.forEach(event => window.addEventListener(event, unlockAudio, { passive: true, capture: true }));
|
||||||
|
|
||||||
|
// Auto-monitor state and visibility change handling
|
||||||
|
const handleVisibility = () => { if (document.visibilityState === 'visible') unlockAudio(); };
|
||||||
|
window.addEventListener('visibilitychange', handleVisibility);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (audioCtxRef.current?.state === 'suspended' && (isKaraokeActive || isCameraActive)) {
|
||||||
|
unlockAudio();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('click', handleInteraction);
|
interactions.forEach(event => window.removeEventListener(event, unlockAudio));
|
||||||
window.removeEventListener('touchstart', handleInteraction);
|
window.removeEventListener('visibilitychange', handleVisibility);
|
||||||
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isKaraokeActive, isPaused, isSimActive, isCameraActive]);
|
||||||
|
|
||||||
// Persistent karaoke audio setup
|
// Persistent karaoke audio setup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -213,13 +264,13 @@ const ObservationPage = () => {
|
|||||||
];
|
];
|
||||||
const index = Math.floor(audio.currentTime / 5) % texts.length;
|
const index = Math.floor(audio.currentTime / 5) % texts.length;
|
||||||
setCurrentLyrics(texts[index]);
|
setCurrentLyrics(texts[index]);
|
||||||
setAudioStatus('ACTIVE');
|
if (audioStatus !== 'SYNCING') setAudioStatus('ACTIVE');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
audio.addEventListener('timeupdate', updateLyrics);
|
audio.addEventListener('timeupdate', updateLyrics);
|
||||||
audio.addEventListener('play', () => { setAudioStatus('ACTIVE'); setAudioErrorMsg(''); });
|
audio.addEventListener('play', () => { if (audioStatus !== 'SYNCING') setAudioStatus('ACTIVE'); setAudioErrorMsg(''); });
|
||||||
audio.addEventListener('waiting', () => setAudioStatus('LOADING'));
|
audio.addEventListener('waiting', () => { if (audioStatus !== 'SYNCING') setAudioStatus('LOADING'); });
|
||||||
audio.addEventListener('error', (e) => {
|
audio.addEventListener('error', (e) => {
|
||||||
console.error("Audio Element Error:", audio.error);
|
console.error("Audio Element Error:", audio.error);
|
||||||
setAudioStatus('ERROR');
|
setAudioStatus('ERROR');
|
||||||
@ -305,6 +356,46 @@ const ObservationPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const syncAll = async () => {
|
||||||
|
setAudioStatus('SYNCING');
|
||||||
|
setSyncProgress(0);
|
||||||
|
setCurrentLyrics("SINCRONIZANDO COM O NAVEGADOR...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!audioCtxRef.current) await initAudioContext();
|
||||||
|
if (audioCtxRef.current) await audioCtxRef.current.resume();
|
||||||
|
|
||||||
|
const localIds = Object.keys(customAudioMap);
|
||||||
|
for (let i = 0; i < localIds.length; i++) {
|
||||||
|
const id = localIds[i];
|
||||||
|
const url = customAudioMap[id];
|
||||||
|
const temp = new Audio();
|
||||||
|
temp.muted = true;
|
||||||
|
temp.src = url;
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
temp.oncanplaythrough = resolve;
|
||||||
|
temp.onerror = resolve;
|
||||||
|
temp.load();
|
||||||
|
setTimeout(resolve, 300);
|
||||||
|
});
|
||||||
|
setSyncProgress(((i + 1) / (localIds.length || 1)) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (karaokeAudioRef.current) {
|
||||||
|
karaokeAudioRef.current.muted = false;
|
||||||
|
karaokeAudioRef.current.volume = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAudioStatus('IDLE');
|
||||||
|
setCurrentLyrics("SISTEMA SINCRONIZADO E ATIVO!");
|
||||||
|
setTimeout(() => setCurrentLyrics(""), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Sync Error:", err);
|
||||||
|
setAudioStatus('ERROR');
|
||||||
|
setAudioErrorMsg("FALHA NA SINCRONIZAÇÃO GLOBAL");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startCamera = async () => {
|
const startCamera = async () => {
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
@ -317,6 +408,7 @@ const ObservationPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await initAudioContext(stream);
|
await initAudioContext(stream);
|
||||||
|
await syncAll();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Access denied:", err);
|
console.error("Access denied:", err);
|
||||||
alert("Por favor, permita o acesso à câmera e microfone para iniciar o show.");
|
alert("Por favor, permita o acesso à câmera e microfone para iniciar o show.");
|
||||||
@ -345,12 +437,14 @@ const ObservationPage = () => {
|
|||||||
const audio = karaokeAudioRef.current;
|
const audio = karaokeAudioRef.current;
|
||||||
if (!audio) return;
|
if (!audio) return;
|
||||||
|
|
||||||
|
// IMMEDIATE: Ensure context is resumed synchronously in the event handler stack
|
||||||
if (!audioCtxRef.current) {
|
if (!audioCtxRef.current) {
|
||||||
await initAudioContext();
|
audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioCtxRef.current?.state === 'suspended') {
|
if (audioCtxRef.current?.state === 'suspended') {
|
||||||
await audioCtxRef.current.resume();
|
audioCtxRef.current.resume().catch(() => {
|
||||||
|
// Silent recovery as per Nuclear engine mandate
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsKaraokeActive(true);
|
setIsKaraokeActive(true);
|
||||||
@ -367,10 +461,7 @@ const ObservationPage = () => {
|
|||||||
audio.muted = false;
|
audio.muted = false;
|
||||||
audio.volume = 1.0;
|
audio.volume = 1.0;
|
||||||
|
|
||||||
// Prioritize user blob URL for this specific song ID
|
|
||||||
const targetUrl = customAudioMap[song.id] || song.url;
|
const targetUrl = customAudioMap[song.id] || song.url;
|
||||||
|
|
||||||
// CRITICAL: Handle CORS for Cloud vs Blob URLs
|
|
||||||
if (targetUrl.startsWith('blob:')) {
|
if (targetUrl.startsWith('blob:')) {
|
||||||
audio.removeAttribute('crossorigin');
|
audio.removeAttribute('crossorigin');
|
||||||
} else {
|
} else {
|
||||||
@ -380,18 +471,20 @@ const ObservationPage = () => {
|
|||||||
audio.src = targetUrl;
|
audio.src = targetUrl;
|
||||||
audio.load();
|
audio.load();
|
||||||
|
|
||||||
setTimeout(async () => {
|
// NO TIMEOUT: Call play() immediately to preserve user gesture chain
|
||||||
try {
|
try {
|
||||||
await audio.play();
|
const playPromise = audio.play();
|
||||||
|
if (playPromise !== undefined) {
|
||||||
|
await playPromise;
|
||||||
setAudioStatus('ACTIVE');
|
setAudioStatus('ACTIVE');
|
||||||
} catch (err) {
|
|
||||||
console.error("Playback error:", err);
|
|
||||||
setCurrentLyrics("ERRO NO ÁUDIO - CLIQUE NO ÍCONE DE TUNE");
|
|
||||||
setAudioStatus('ERROR');
|
|
||||||
setAudioErrorMsg('O navegador bloqueou o áudio. Clique em "ATIVAR ÁUDIO" ou interaja com a página.');
|
|
||||||
setIsPaused(true);
|
|
||||||
}
|
}
|
||||||
}, 200);
|
} catch (err) {
|
||||||
|
console.error("Playback error:", err);
|
||||||
|
setCurrentLyrics("ERRO NO ÁUDIO - ATIVE NO BOTÃO ABAIXO");
|
||||||
|
setAudioStatus('ERROR');
|
||||||
|
setAudioErrorMsg('Bloqueio do Navegador. Clique em "REINICIAR ÁUDIO".');
|
||||||
|
setIsPaused(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePlayback = () => {
|
const togglePlayback = () => {
|
||||||
@ -432,13 +525,9 @@ const ObservationPage = () => {
|
|||||||
return { ...prev, [songToUpload.id]: url };
|
return { ...prev, [songToUpload.id]: url };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Immediate switch if playing
|
|
||||||
if (currentSong?.id === songToUpload.id && karaokeAudioRef.current) {
|
if (currentSong?.id === songToUpload.id && karaokeAudioRef.current) {
|
||||||
const wasPaused = karaokeAudioRef.current.paused;
|
const wasPaused = karaokeAudioRef.current.paused;
|
||||||
|
|
||||||
// CRITICAL: Update CORS mode for the new blob URL
|
|
||||||
karaokeAudioRef.current.removeAttribute('crossorigin');
|
karaokeAudioRef.current.removeAttribute('crossorigin');
|
||||||
|
|
||||||
karaokeAudioRef.current.src = url;
|
karaokeAudioRef.current.src = url;
|
||||||
karaokeAudioRef.current.load();
|
karaokeAudioRef.current.load();
|
||||||
if (!wasPaused) karaokeAudioRef.current.play().catch(e => console.error(e));
|
if (!wasPaused) karaokeAudioRef.current.play().catch(e => console.error(e));
|
||||||
@ -457,7 +546,6 @@ const ObservationPage = () => {
|
|||||||
delete newMap[songId];
|
delete newMap[songId];
|
||||||
return newMap;
|
return newMap;
|
||||||
});
|
});
|
||||||
// Restore if playing
|
|
||||||
if (currentSong?.id === songId && karaokeAudioRef.current) {
|
if (currentSong?.id === songId && karaokeAudioRef.current) {
|
||||||
const original = INITIAL_SONG_DATABASE.find(s => s.id === songId);
|
const original = INITIAL_SONG_DATABASE.find(s => s.id === songId);
|
||||||
if (original) {
|
if (original) {
|
||||||
@ -533,14 +621,14 @@ const ObservationPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isKaraokeActive && currentSong) {
|
if ((isKaraokeActive && currentSong) || audioStatus === 'SYNCING') {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
||||||
ctx.fillRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180);
|
ctx.fillRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180);
|
||||||
ctx.strokeStyle = audioStatus === 'ERROR' ? '#FF0000' : '#E3B341'; ctx.lineWidth = 4;
|
ctx.strokeStyle = audioStatus === 'ERROR' ? '#FF0000' : audioStatus === 'SYNCING' ? '#00F2FF' : '#E3B341'; ctx.lineWidth = 4;
|
||||||
ctx.strokeRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180);
|
ctx.strokeRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180);
|
||||||
ctx.fillStyle = '#E3B341'; ctx.font = 'bold 20px monospace'; ctx.textAlign = 'center';
|
ctx.fillStyle = audioStatus === 'SYNCING' ? '#00F2FF' : '#E3B341'; ctx.font = 'bold 20px monospace'; ctx.textAlign = 'center';
|
||||||
ctx.fillText(`🎤 ${currentSong.title.toUpperCase()} - ${currentSong.artist.toUpperCase()} (${currentSong.genre})`, canvas.width/2, canvas.height - 210);
|
ctx.fillText(audioStatus === 'SYNCING' ? "🔄 ATIVANDO TODOS OS ÁUDIOS DO DISPOSITIVO" : `🎤 ${currentSong?.title?.toUpperCase()} - ${currentSong?.artist?.toUpperCase()} (${currentSong?.genre})`, canvas.width/2, canvas.height - 210);
|
||||||
|
|
||||||
if (audioStatus === 'ERROR') {
|
if (audioStatus === 'ERROR') {
|
||||||
ctx.fillStyle = '#FF5555'; ctx.font = 'bold 32px sans-serif';
|
ctx.fillStyle = '#FF5555'; ctx.font = 'bold 32px sans-serif';
|
||||||
@ -551,7 +639,8 @@ const ObservationPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, 800, 10);
|
ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, 800, 10);
|
||||||
ctx.fillStyle = audioStatus === 'ACTIVE' ? '#E3B341' : '#666666'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, (playbackProgress / (karaokeAudioRef.current?.duration || 1)) * 800, 10);
|
const progress = audioStatus === 'SYNCING' ? syncProgress / 100 : (playbackProgress / (karaokeAudioRef.current?.duration || 1));
|
||||||
|
ctx.fillStyle = audioStatus === 'SYNCING' ? '#00F2FF' : audioStatus === 'ACTIVE' ? '#E3B341' : '#666666'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, progress * 800, 10);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -563,7 +652,7 @@ const ObservationPage = () => {
|
|||||||
};
|
};
|
||||||
render();
|
render();
|
||||||
return () => cancelAnimationFrame(animationFrame);
|
return () => cancelAnimationFrame(animationFrame);
|
||||||
}, [isCameraActive, isSimActive, activeViewers, featuredViewer, isKaraokeActive, currentLyrics, currentSong, getCachedImage, audienceCount, playbackProgress, audioStatus, audioErrorMsg]);
|
}, [isCameraActive, isSimActive, activeViewers, featuredViewer, isKaraokeActive, currentLyrics, currentSong, getCachedImage, audienceCount, playbackProgress, audioStatus, audioErrorMsg, syncProgress]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: any;
|
let interval: any;
|
||||||
@ -595,7 +684,6 @@ const ObservationPage = () => {
|
|||||||
<div className="relative h-screen w-full bg-[#1a1a1a] overflow-hidden flex flex-col font-sans">
|
<div className="relative h-screen w-full bg-[#1a1a1a] overflow-hidden flex flex-col font-sans">
|
||||||
<Head><title>KARAOKE GLOBAL 10K+ | AO VIVO</title></Head>
|
<Head><title>KARAOKE GLOBAL 10K+ | AO VIVO</title></Head>
|
||||||
|
|
||||||
{/* Hidden File Input for Custom Audio */}
|
|
||||||
<input type="file" ref={fileInputRef} onChange={handleFileChange} accept="audio/*" className="hidden" />
|
<input type="file" ref={fileInputRef} onChange={handleFileChange} accept="audio/*" className="hidden" />
|
||||||
|
|
||||||
<div className="absolute inset-0 z-0 bg-black overflow-hidden">
|
<div className="absolute inset-0 z-0 bg-black overflow-hidden">
|
||||||
@ -609,7 +697,7 @@ const ObservationPage = () => {
|
|||||||
<br /><span className="text-xl font-light tracking-widest text-white/60">BRASIL & MUNDO</span>
|
<br /><span className="text-xl font-light tracking-widest text-white/60">BRASIL & MUNDO</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-white/80 font-bold uppercase tracking-widest max-w-lg text-center text-xs">
|
<p className="text-white/80 font-bold uppercase tracking-widest max-w-lg text-center text-xs">
|
||||||
OS AUDIOS ESTÃO ATIVOS - CONFIGURE CADA MÚSICA COM SEU ÁUDIO LOCAL OU USE NOSSA BIBLIOTECA CLOUD
|
NUCLEAR AUDIO ENGINE ACTIVE - SINCRONIZAÇÃO TOTAL COM O NAVEGADOR
|
||||||
</p>
|
</p>
|
||||||
<button onClick={startCamera} className="bg-[#E3B341] text-black px-16 py-6 rounded-full font-black text-2xl uppercase tracking-tighter hover:scale-110 transition-all shadow-2xl">Entrar no Palco</button>
|
<button onClick={startCamera} className="bg-[#E3B341] text-black px-16 py-6 rounded-full font-black text-2xl uppercase tracking-tighter hover:scale-110 transition-all shadow-2xl">Entrar no Palco</button>
|
||||||
</div>
|
</div>
|
||||||
@ -655,7 +743,7 @@ const ObservationPage = () => {
|
|||||||
<span className="text-[#E3B341] text-lg font-black uppercase tracking-widest">{currentSong.title} - {currentSong.artist}</span>
|
<span className="text-[#E3B341] text-lg font-black uppercase tracking-widest">{currentSong.title} - {currentSong.artist}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className={`w-3 h-3 rounded-full animate-pulse ${audioStatus === 'ACTIVE' ? 'bg-green-500' : audioStatus === 'LOADING' ? 'bg-yellow-500' : 'bg-red-500'}`}></div>
|
<div className={`w-3 h-3 rounded-full animate-pulse ${audioStatus === 'ACTIVE' ? 'bg-green-500' : audioStatus === 'LOADING' ? 'bg-yellow-500' : audioStatus === 'SYNCING' ? 'bg-blue-500' : 'bg-red-500'}`}></div>
|
||||||
<span className="text-[10px] text-white/60 font-black uppercase">{audioStatus}</span>
|
<span className="text-[10px] text-white/60 font-black uppercase">{audioStatus}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -721,19 +809,11 @@ const ObservationPage = () => {
|
|||||||
<input type="range" min="0" max="1" step="0.1" value={volumeLevel} onChange={(e) => setVolumeLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#00F2FF] rounded-lg appearance-none cursor-pointer" />
|
<input type="range" min="0" max="1" step="0.1" value={volumeLevel} onChange={(e) => setVolumeLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#00F2FF] rounded-lg appearance-none cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<button onClick={async () => {
|
<button onClick={syncAll} className={`p-4 rounded-full transition-all ${audioStatus === 'SYNCING' ? 'bg-[#00F2FF] animate-spin text-black' : audioStatus === 'ERROR' ? 'bg-red-600 animate-pulse text-white' : 'bg-white/5 text-[#E3B341] hover:bg-white/10'}`}>
|
||||||
if(!audioCtxRef.current) await initAudioContext();
|
<BaseIcon path={audioStatus === 'SYNCING' ? mdiSync : audioStatus === 'ERROR' ? mdiVolumeVariantOff : mdiTune} size={32} />
|
||||||
if(audioCtxRef.current) await audioCtxRef.current.resume();
|
|
||||||
if(karaokeAudioRef.current) {
|
|
||||||
karaokeAudioRef.current.muted = false;
|
|
||||||
karaokeAudioRef.current.volume = 1.0;
|
|
||||||
if (isKaraokeActive && !isPaused) karaokeAudioRef.current.play().catch(console.error);
|
|
||||||
}
|
|
||||||
}} className={`p-4 rounded-full transition-all ${audioStatus === 'ERROR' ? 'bg-red-600 animate-pulse text-white' : 'bg-white/5 text-[#E3B341] hover:bg-white/10'}`}>
|
|
||||||
<BaseIcon path={audioStatus === 'ERROR' ? mdiVolumeVariantOff : mdiTune} size={32} />
|
|
||||||
</button>
|
</button>
|
||||||
<span className={`text-[10px] font-black mt-2 text-center uppercase ${audioStatus === 'ERROR' ? 'text-red-500' : 'text-white'}`}>
|
<span className={`text-[10px] font-black mt-2 text-center uppercase ${audioStatus === 'ERROR' ? 'text-red-500' : 'text-white'}`}>
|
||||||
{audioStatus === 'ERROR' ? 'Reativar' : 'Ativar'}<br/>Audio
|
REATIVAR<br/>ÁUDIO
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -771,7 +851,7 @@ const ObservationPage = () => {
|
|||||||
<h3 className="text-[#E3B341] font-black text-2xl italic uppercase tracking-tighter">Estúdio 10.000+</h3>
|
<h3 className="text-[#E3B341] font-black text-2xl italic uppercase tracking-tighter">Estúdio 10.000+</h3>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className={`w-2 h-2 rounded-full ${audioCtxRef.current?.state === 'running' ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
<div className={`w-2 h-2 rounded-full ${audioCtxRef.current?.state === 'running' ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">SISTEMA DE ÁUDIO: {audioCtxRef.current?.state}</p>
|
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">NAVIGATOR SYNC: {audioCtxRef.current?.state === 'running' ? 'ATIVO' : 'AGUARDANDO'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setShowPlaylist(false)} className="bg-white/5 p-2 rounded-full text-white/40 hover:text-white transition-all"><BaseIcon path={mdiClose} size={28} /></button>
|
<button onClick={() => setShowPlaylist(false)} className="bg-white/5 p-2 rounded-full text-white/40 hover:text-white transition-all"><BaseIcon path={mdiClose} size={28} /></button>
|
||||||
@ -800,7 +880,7 @@ const ObservationPage = () => {
|
|||||||
}}>
|
}}>
|
||||||
<BaseIcon path={mdiUpload} size={40} className="text-[#E3B341] mx-auto mb-3" />
|
<BaseIcon path={mdiUpload} size={40} className="text-[#E3B341] mx-auto mb-3" />
|
||||||
<h4 className="text-white font-black uppercase text-sm italic tracking-widest">Adicionar Nova Música do Dispositivo</h4>
|
<h4 className="text-white font-black uppercase text-sm italic tracking-widest">Adicionar Nova Música do Dispositivo</h4>
|
||||||
<p className="text-[9px] text-white/40 font-bold mt-1">CARREGUE SEUS PRÓPRIOS PLAYBACKS MP3/WAV</p>
|
<p className="text-[9px] text-white/40 font-bold mt-1">SINCROZINA AUTOMATICAMENTE APÓS O CARREGAMENTO</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredSongs.length > 0 ? filteredSongs.map(song => (
|
{filteredSongs.length > 0 ? filteredSongs.map(song => (
|
||||||
@ -808,7 +888,7 @@ const ObservationPage = () => {
|
|||||||
<div className={`w-full flex items-center justify-between p-4 rounded-2xl border-2 transition-all group cursor-pointer ${currentSong?.id === song.id ? 'bg-[#E3B341] border-[#E3B341] text-black' : 'bg-white/5 border-white/10 text-white hover:border-[#E3B341] hover:bg-white/10'}`} onClick={() => selectSong(song)}>
|
<div className={`w-full flex items-center justify-between p-4 rounded-2xl border-2 transition-all group cursor-pointer ${currentSong?.id === song.id ? 'bg-[#E3B341] border-[#E3B341] text-black' : 'bg-white/5 border-white/10 text-white hover:border-[#E3B341] hover:bg-white/10'}`} onClick={() => selectSong(song)}>
|
||||||
<div className="flex flex-col items-start overflow-hidden flex-grow">
|
<div className="flex flex-col items-start overflow-hidden flex-grow">
|
||||||
<span className="font-black text-lg truncate w-full italic leading-tight flex items-center space-x-2">
|
<span className="font-black text-lg truncate w-full italic leading-tight flex items-center space-x-2">
|
||||||
{customAudioMap[song.id] && <BaseIcon path={mdiCheckCircle} size={16} className="text-green-500" />}
|
{customAudioMap[song.id] && <BaseIcon path={mdiCheckCircle} size={16} className="text-green-500 animate-pulse" />}
|
||||||
<span>{song.title}</span>
|
<span>{song.title}</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center space-x-2 mt-1">
|
<div className="flex items-center space-x-2 mt-1">
|
||||||
@ -870,11 +950,10 @@ const ObservationPage = () => {
|
|||||||
<span className="text-[#E3B341] text-xs font-black">PLAYBACKS: {fullSongDatabase.length} DISPONÍVEIS</span>
|
<span className="text-[#E3B341] text-xs font-black">PLAYBACKS: {fullSongDatabase.length} DISPONÍVEIS</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{Object.keys(customAudioMap).length > 0 && (
|
<button onClick={syncAll} className="flex items-center space-x-2 bg-[#E3B341]/10 text-[#E3B341] px-4 py-2 rounded-full border border-[#E3B341]/20 hover:bg-[#E3B341]/20 transition-all">
|
||||||
<span className="text-[10px] font-black text-green-500 bg-green-500/10 px-3 py-1 rounded-full border border-green-500/20">
|
<BaseIcon path={mdiSync} size={16} className={audioStatus === 'SYNCING' ? 'animate-spin' : ''} />
|
||||||
{Object.keys(customAudioMap).length} ÁUDIOS PERSONALIZADOS
|
<span className="text-[10px] font-black uppercase">SINCRONIZAR TUDO</span>
|
||||||
</span>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user