diff --git a/frontend/src/pages/observation.tsx b/frontend/src/pages/observation.tsx index deab3b7..e5e6ca3 100644 --- a/frontend/src/pages/observation.tsx +++ b/frontend/src/pages/observation.tsx @@ -33,6 +33,7 @@ import { mdiAlertCircle, mdiTune, mdiVolumeVariantOff, + mdiSync, } from '@mdi/js'; import BaseIcon from '../components/BaseIcon'; import { useAppDispatch } from '../stores/hooks'; @@ -133,6 +134,7 @@ const ObservationPage = () => { const wetGainRef = useRef(null); const monitorGainRef = useRef(null); const masterGainRef = useRef(null); + const heartbeatOscRef = useRef(null); const [isCameraActive, setIsCameraActive] = useState(false); const [isKaraokeActive, setIsKaraokeActive] = useState(false); @@ -143,7 +145,7 @@ const ObservationPage = () => { const [isMonitorOn, setIsMonitorOn] = useState(false); const [reverbLevel, setReverbLevel] = useState(0.5); 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 [audioErrorMsg, setAudioErrorMsg] = useState(''); @@ -166,6 +168,7 @@ const ObservationPage = () => { const [customAudioMap, setCustomAudioMap] = useState>({}); const [songToUpload, setSongToUpload] = useState(null); const [editingSongId, setEditingSongId] = useState(null); + const [syncProgress, setSyncProgress] = useState(0); const imageCache = useRef>(new Map()); @@ -174,20 +177,68 @@ const ObservationPage = () => { return [...userSongs, ...INITIAL_SONG_DATABASE]; }, [userSongs]); - // Global Audio Enabler + // Global Audio Master Unlock Engine + // This ensures that ANY interaction with the screen resumes the audio engine useEffect(() => { - const handleInteraction = () => { - if (audioCtxRef.current?.state === 'suspended') { - audioCtxRef.current.resume().catch(console.error); + const unlockAudio = async () => { + if (!audioCtxRef.current) { + 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 () => { - window.removeEventListener('click', handleInteraction); - window.removeEventListener('touchstart', handleInteraction); + interactions.forEach(event => window.removeEventListener(event, unlockAudio)); + window.removeEventListener('visibilitychange', handleVisibility); + clearInterval(interval); }; - }, []); + }, [isKaraokeActive, isPaused, isSimActive, isCameraActive]); // Persistent karaoke audio setup useEffect(() => { @@ -213,13 +264,13 @@ const ObservationPage = () => { ]; const index = Math.floor(audio.currentTime / 5) % texts.length; setCurrentLyrics(texts[index]); - setAudioStatus('ACTIVE'); + if (audioStatus !== 'SYNCING') setAudioStatus('ACTIVE'); } }; audio.addEventListener('timeupdate', updateLyrics); - audio.addEventListener('play', () => { setAudioStatus('ACTIVE'); setAudioErrorMsg(''); }); - audio.addEventListener('waiting', () => setAudioStatus('LOADING')); + audio.addEventListener('play', () => { if (audioStatus !== 'SYNCING') setAudioStatus('ACTIVE'); setAudioErrorMsg(''); }); + audio.addEventListener('waiting', () => { if (audioStatus !== 'SYNCING') setAudioStatus('LOADING'); }); audio.addEventListener('error', (e) => { console.error("Audio Element Error:", audio.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 () => { try { const stream = await navigator.mediaDevices.getUserMedia({ @@ -317,6 +408,7 @@ const ObservationPage = () => { } await initAudioContext(stream); + await syncAll(); } catch (err) { console.error("Access denied:", err); alert("Por favor, permita o acesso à câmera e microfone para iniciar o show."); @@ -345,12 +437,14 @@ const ObservationPage = () => { const audio = karaokeAudioRef.current; if (!audio) return; + // IMMEDIATE: Ensure context is resumed synchronously in the event handler stack if (!audioCtxRef.current) { - await initAudioContext(); + audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); } - if (audioCtxRef.current?.state === 'suspended') { - await audioCtxRef.current.resume(); + audioCtxRef.current.resume().catch(() => { + // Silent recovery as per Nuclear engine mandate + }); } setIsKaraokeActive(true); @@ -367,10 +461,7 @@ const ObservationPage = () => { audio.muted = false; audio.volume = 1.0; - // Prioritize user blob URL for this specific song ID const targetUrl = customAudioMap[song.id] || song.url; - - // CRITICAL: Handle CORS for Cloud vs Blob URLs if (targetUrl.startsWith('blob:')) { audio.removeAttribute('crossorigin'); } else { @@ -380,18 +471,20 @@ const ObservationPage = () => { audio.src = targetUrl; audio.load(); - setTimeout(async () => { - try { - await audio.play(); + // NO TIMEOUT: Call play() immediately to preserve user gesture chain + try { + const playPromise = audio.play(); + if (playPromise !== undefined) { + await playPromise; 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 = () => { @@ -432,13 +525,9 @@ const ObservationPage = () => { return { ...prev, [songToUpload.id]: url }; }); - // Immediate switch if playing if (currentSong?.id === songToUpload.id && karaokeAudioRef.current) { const wasPaused = karaokeAudioRef.current.paused; - - // CRITICAL: Update CORS mode for the new blob URL karaokeAudioRef.current.removeAttribute('crossorigin'); - karaokeAudioRef.current.src = url; karaokeAudioRef.current.load(); if (!wasPaused) karaokeAudioRef.current.play().catch(e => console.error(e)); @@ -457,7 +546,6 @@ const ObservationPage = () => { delete newMap[songId]; return newMap; }); - // Restore if playing if (currentSong?.id === songId && karaokeAudioRef.current) { const original = INITIAL_SONG_DATABASE.find(s => s.id === songId); if (original) { @@ -533,14 +621,14 @@ const ObservationPage = () => { } } - if (isKaraokeActive && currentSong) { + if ((isKaraokeActive && currentSong) || audioStatus === 'SYNCING') { ctx.save(); ctx.fillStyle = 'rgba(0,0,0,0.85)'; 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.fillStyle = '#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.fillStyle = audioStatus === 'SYNCING' ? '#00F2FF' : '#E3B341'; ctx.font = 'bold 20px monospace'; ctx.textAlign = 'center'; + 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') { 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 = 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(); } @@ -563,7 +652,7 @@ const ObservationPage = () => { }; render(); 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(() => { let interval: any; @@ -595,7 +684,6 @@ const ObservationPage = () => {
KARAOKE GLOBAL 10K+ | AO VIVO - {/* Hidden File Input for Custom Audio */}
@@ -609,7 +697,7 @@ const ObservationPage = () => {
BRASIL & MUNDO

- 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

@@ -655,7 +743,7 @@ const ObservationPage = () => { {currentSong.title} - {currentSong.artist}
-
+
{audioStatus}
@@ -721,19 +809,11 @@ const ObservationPage = () => { setVolumeLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#00F2FF] rounded-lg appearance-none cursor-pointer" />
- - {audioStatus === 'ERROR' ? 'Reativar' : 'Ativar'}
Audio + REATIVAR
ÁUDIO
@@ -771,7 +851,7 @@ const ObservationPage = () => {

Estúdio 10.000+

-

SISTEMA DE ÁUDIO: {audioCtxRef.current?.state}

+

NAVIGATOR SYNC: {audioCtxRef.current?.state === 'running' ? 'ATIVO' : 'AGUARDANDO'}

@@ -800,7 +880,7 @@ const ObservationPage = () => { }}>

Adicionar Nova Música do Dispositivo

-

CARREGUE SEUS PRÓPRIOS PLAYBACKS MP3/WAV

+

SINCROZINA AUTOMATICAMENTE APÓS O CARREGAMENTO

{filteredSongs.length > 0 ? filteredSongs.map(song => ( @@ -808,7 +888,7 @@ const ObservationPage = () => {
selectSong(song)}>
- {customAudioMap[song.id] && } + {customAudioMap[song.id] && } {song.title}
@@ -870,11 +950,10 @@ const ObservationPage = () => { PLAYBACKS: {fullSongDatabase.length} DISPONÍVEIS
- {Object.keys(customAudioMap).length > 0 && ( - - {Object.keys(customAudioMap).length} ÁUDIOS PERSONALIZADOS - - )} +