diff --git a/frontend/src/pages/observation.tsx b/frontend/src/pages/observation.tsx index d79ccc1..deab3b7 100644 --- a/frontend/src/pages/observation.tsx +++ b/frontend/src/pages/observation.tsx @@ -32,6 +32,7 @@ import { mdiTrashCan, mdiAlertCircle, mdiTune, + mdiVolumeVariantOff, } from '@mdi/js'; import BaseIcon from '../components/BaseIcon'; import { useAppDispatch } from '../stores/hooks'; @@ -144,6 +145,7 @@ const ObservationPage = () => { const [playbackProgress, setPlaybackProgress] = useState(0); const [audioStatus, setAudioStatus] = useState<'IDLE' | 'LOADING' | 'ACTIVE' | 'ERROR'>('IDLE'); const [volumeLevel, setVolumeLevel] = useState(1.0); + const [audioErrorMsg, setAudioErrorMsg] = useState(''); const [audienceCount, setAudienceCount] = useState(0); const [isSimActive, setIsSimActive] = useState(false); @@ -190,7 +192,6 @@ const ObservationPage = () => { // Persistent karaoke audio setup useEffect(() => { const audio = new Audio(); - audio.crossOrigin = "anonymous"; audio.preload = "auto"; audio.volume = 1.0; karaokeAudioRef.current = audio; @@ -217,9 +218,13 @@ const ObservationPage = () => { }; audio.addEventListener('timeupdate', updateLyrics); - audio.addEventListener('play', () => setAudioStatus('ACTIVE')); + audio.addEventListener('play', () => { setAudioStatus('ACTIVE'); setAudioErrorMsg(''); }); audio.addEventListener('waiting', () => setAudioStatus('LOADING')); - audio.addEventListener('error', () => setAudioStatus('ERROR')); + audio.addEventListener('error', (e) => { + console.error("Audio Element Error:", audio.error); + setAudioStatus('ERROR'); + setAudioErrorMsg(audio.error?.message || 'Erro ao carregar o áudio. Tente outro arquivo.'); + }); audio.addEventListener('ended', () => { setIsKaraokeActive(false); setCurrentSong(null); @@ -242,7 +247,7 @@ const ObservationPage = () => { }); }, [fullSongDatabase, searchQuery, selectedGenre]); - const initAudioContext = async (stream: MediaStream) => { + const initAudioContext = async (stream?: MediaStream) => { try { if (!audioCtxRef.current) { audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); @@ -257,32 +262,35 @@ const ObservationPage = () => { masterGainRef.current = audioCtxRef.current.createGain(); masterGainRef.current.gain.value = 1.0; - micNodeRef.current = audioCtxRef.current.createMediaStreamSource(stream); - - dryGainRef.current = audioCtxRef.current.createGain(); - wetGainRef.current = audioCtxRef.current.createGain(); - monitorGainRef.current = audioCtxRef.current.createGain(); - reverbNodeRef.current = audioCtxRef.current.createConvolver(); - - const length = 2 * audioCtxRef.current.sampleRate; - const impulse = audioCtxRef.current.createBuffer(2, length, audioCtxRef.current.sampleRate); - for (let i = 0; i < 2; i++) { - const channel = impulse.getChannelData(i); - for (let j = 0; j < length; j++) { - channel[j] = (Math.random() * 2 - 1) * Math.pow(1 - j / length, 2); + if (stream) { + micNodeRef.current = audioCtxRef.current.createMediaStreamSource(stream); + + dryGainRef.current = audioCtxRef.current.createGain(); + wetGainRef.current = audioCtxRef.current.createGain(); + monitorGainRef.current = audioCtxRef.current.createGain(); + reverbNodeRef.current = audioCtxRef.current.createConvolver(); + + const length = 2 * audioCtxRef.current.sampleRate; + const impulse = audioCtxRef.current.createBuffer(2, length, audioCtxRef.current.sampleRate); + for (let i = 0; i < 2; i++) { + const channel = impulse.getChannelData(i); + for (let j = 0; j < length; j++) { + channel[j] = (Math.random() * 2 - 1) * Math.pow(1 - j / length, 2); + } } - } - reverbNodeRef.current.buffer = impulse; + reverbNodeRef.current.buffer = impulse; - micNodeRef.current.connect(dryGainRef.current); - micNodeRef.current.connect(reverbNodeRef.current); - micNodeRef.current.connect(monitorGainRef.current); + micNodeRef.current.connect(dryGainRef.current); + micNodeRef.current.connect(reverbNodeRef.current); + micNodeRef.current.connect(monitorGainRef.current); + + dryGainRef.current.connect(audioDestRef.current); + reverbNodeRef.current.connect(wetGainRef.current); + wetGainRef.current.connect(audioDestRef.current); + + monitorGainRef.current.connect(audioCtxRef.current.destination); + } - dryGainRef.current.connect(audioDestRef.current); - reverbNodeRef.current.connect(wetGainRef.current); - wetGainRef.current.connect(audioDestRef.current); - - monitorGainRef.current.connect(audioCtxRef.current.destination); masterGainRef.current.connect(audioCtxRef.current.destination); if (karaokeAudioRef.current && !karaokeNodeRef.current) { @@ -337,6 +345,10 @@ const ObservationPage = () => { const audio = karaokeAudioRef.current; if (!audio) return; + if (!audioCtxRef.current) { + await initAudioContext(); + } + if (audioCtxRef.current?.state === 'suspended') { await audioCtxRef.current.resume(); } @@ -345,6 +357,7 @@ const ObservationPage = () => { setCurrentSong(song); setPlaybackProgress(0); setAudioStatus('LOADING'); + setAudioErrorMsg(''); setCurrentLyrics("SINCRONIZANDO PLAYBACK..."); setIsPaused(false); setIsSimActive(true); @@ -356,6 +369,14 @@ const ObservationPage = () => { // 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 { + audio.crossOrigin = "anonymous"; + } + audio.src = targetUrl; audio.load(); @@ -365,8 +386,9 @@ const ObservationPage = () => { setAudioStatus('ACTIVE'); } catch (err) { console.error("Playback error:", err); - setCurrentLyrics("ERRO NO ÁUDIO - CLIQUE PARA REENTRAR"); + 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); @@ -413,6 +435,10 @@ const ObservationPage = () => { // 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)); @@ -435,6 +461,7 @@ const ObservationPage = () => { if (currentSong?.id === songId && karaokeAudioRef.current) { const original = INITIAL_SONG_DATABASE.find(s => s.id === songId); if (original) { + karaokeAudioRef.current.crossOrigin = "anonymous"; karaokeAudioRef.current.src = original.url; karaokeAudioRef.current.load(); karaokeAudioRef.current.play().catch(e => console.error(e)); @@ -514,8 +541,15 @@ const ObservationPage = () => { 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 = 'white'; ctx.font = 'bold 48px sans-serif'; - ctx.fillText(currentLyrics || "SOLTA O SOM!", canvas.width/2, canvas.height - 140); + + if (audioStatus === 'ERROR') { + ctx.fillStyle = '#FF5555'; ctx.font = 'bold 32px sans-serif'; + ctx.fillText(audioErrorMsg || "ERRO CRÍTICO NO ÁUDIO", canvas.width/2, canvas.height - 140); + } else { + ctx.fillStyle = 'white'; ctx.font = 'bold 48px sans-serif'; + ctx.fillText(currentLyrics || "SOLTA O SOM!", canvas.width/2, canvas.height - 140); + } + 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); ctx.restore(); @@ -529,7 +563,7 @@ const ObservationPage = () => { }; render(); return () => cancelAnimationFrame(animationFrame); - }, [isCameraActive, isSimActive, activeViewers, featuredViewer, isKaraokeActive, currentLyrics, currentSong, getCachedImage, audienceCount, playbackProgress, audioStatus]); + }, [isCameraActive, isSimActive, activeViewers, featuredViewer, isKaraokeActive, currentLyrics, currentSong, getCachedImage, audienceCount, playbackProgress, audioStatus, audioErrorMsg]); useEffect(() => { let interval: any; @@ -625,9 +659,11 @@ const ObservationPage = () => { {audioStatus} -
- {currentLyrics || "VAI COMEÇAR..."} + +
+ {audioStatus === 'ERROR' ? audioErrorMsg : (currentLyrics || "VAI COMEÇAR...")}
+
@@ -641,11 +677,9 @@ const ObservationPage = () => { OCULTAR
- {audioStatus === 'ERROR' && ( - - )} + @@ -687,10 +721,20 @@ const ObservationPage = () => { setVolumeLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#00F2FF] rounded-lg appearance-none cursor-pointer" />
- - Ativar
Audio
+ + {audioStatus === 'ERROR' ? 'Reativar' : 'Ativar'}
Audio +