diff --git a/frontend/src/pages/observation.tsx b/frontend/src/pages/observation.tsx index 910931b..d79ccc1 100644 --- a/frontend/src/pages/observation.tsx +++ b/frontend/src/pages/observation.tsx @@ -30,6 +30,8 @@ import { mdiCog, mdiRefresh, mdiTrashCan, + mdiAlertCircle, + mdiTune, } from '@mdi/js'; import BaseIcon from '../components/BaseIcon'; import { useAppDispatch } from '../stores/hooks'; @@ -129,6 +131,7 @@ const ObservationPage = () => { const dryGainRef = useRef(null); const wetGainRef = useRef(null); const monitorGainRef = useRef(null); + const masterGainRef = useRef(null); const [isCameraActive, setIsCameraActive] = useState(false); const [isKaraokeActive, setIsKaraokeActive] = useState(false); @@ -139,6 +142,8 @@ 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 [volumeLevel, setVolumeLevel] = useState(1.0); const [audienceCount, setAudienceCount] = useState(0); const [isSimActive, setIsSimActive] = useState(false); @@ -167,11 +172,27 @@ const ObservationPage = () => { return [...userSongs, ...INITIAL_SONG_DATABASE]; }, [userSongs]); + // Global Audio Enabler + useEffect(() => { + const handleInteraction = () => { + if (audioCtxRef.current?.state === 'suspended') { + audioCtxRef.current.resume().catch(console.error); + } + }; + window.addEventListener('click', handleInteraction); + window.addEventListener('touchstart', handleInteraction); + return () => { + window.removeEventListener('click', handleInteraction); + window.removeEventListener('touchstart', handleInteraction); + }; + }, []); + // Persistent karaoke audio setup useEffect(() => { const audio = new Audio(); audio.crossOrigin = "anonymous"; audio.preload = "auto"; + audio.volume = 1.0; karaokeAudioRef.current = audio; const updateLyrics = () => { @@ -191,13 +212,18 @@ const ObservationPage = () => { ]; const index = Math.floor(audio.currentTime / 5) % texts.length; setCurrentLyrics(texts[index]); + setAudioStatus('ACTIVE'); } }; audio.addEventListener('timeupdate', updateLyrics); + audio.addEventListener('play', () => setAudioStatus('ACTIVE')); + audio.addEventListener('waiting', () => setAudioStatus('LOADING')); + audio.addEventListener('error', () => setAudioStatus('ERROR')); audio.addEventListener('ended', () => { setIsKaraokeActive(false); setCurrentSong(null); + setAudioStatus('IDLE'); }); return () => { @@ -217,49 +243,57 @@ const ObservationPage = () => { }, [fullSongDatabase, searchQuery, selectedGenre]); const initAudioContext = async (stream: MediaStream) => { - if (!audioCtxRef.current) { - audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); - } + try { + if (!audioCtxRef.current) { + audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); + } - if (audioCtxRef.current.state === 'suspended') { - await audioCtxRef.current.resume(); - } + if (audioCtxRef.current.state === 'suspended') { + await audioCtxRef.current.resume(); + } - if (!audioDestRef.current) { - audioDestRef.current = audioCtxRef.current.createMediaStreamDestination(); - - 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 (!audioDestRef.current) { + audioDestRef.current = audioCtxRef.current.createMediaStreamDestination(); + 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); + } + } + reverbNodeRef.current.buffer = impulse; + + 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); + masterGainRef.current.connect(audioCtxRef.current.destination); + + if (karaokeAudioRef.current && !karaokeNodeRef.current) { + karaokeNodeRef.current = audioCtxRef.current.createMediaElementSource(karaokeAudioRef.current); + karaokeNodeRef.current.connect(audioDestRef.current); + karaokeNodeRef.current.connect(masterGainRef.current); } } - reverbNodeRef.current.buffer = impulse; - - micNodeRef.current.connect(dryGainRef.current); - micNodeRef.current.connect(reverbNodeRef.current); - micNodeRef.current.connect(monitorGainRef.current); - micNodeRef.current.connect(audioDestRef.current); - - reverbNodeRef.current.connect(wetGainRef.current); - wetGainRef.current.connect(audioDestRef.current); - - monitorGainRef.current.connect(audioCtxRef.current.destination); - - if (karaokeAudioRef.current && !karaokeNodeRef.current) { - karaokeNodeRef.current = audioCtxRef.current.createMediaElementSource(karaokeAudioRef.current); - karaokeNodeRef.current.connect(audioDestRef.current); - karaokeNodeRef.current.connect(audioCtxRef.current.destination); - } + } catch (err) { + console.error("Audio Context Init Error:", err); + setAudioStatus('ERROR'); } }; @@ -287,7 +321,10 @@ const ObservationPage = () => { wetGainRef.current.gain.value = isMicOn ? reverbLevel : 0; monitorGainRef.current.gain.value = isMonitorOn ? 1.0 : 0; } - }, [isMicOn, reverbLevel, isMonitorOn]); + if (masterGainRef.current) { + masterGainRef.current.gain.value = volumeLevel; + } + }, [isMicOn, reverbLevel, isMonitorOn, volumeLevel]); const getCachedImage = useCallback((url: string) => { if (imageCache.current.has(url)) return imageCache.current.get(url); @@ -307,30 +344,42 @@ const ObservationPage = () => { setIsKaraokeActive(true); setCurrentSong(song); setPlaybackProgress(0); - setCurrentLyrics("INICIANDO PLAYBACK..."); + setAudioStatus('LOADING'); + setCurrentLyrics("SINCRONIZANDO PLAYBACK..."); setIsPaused(false); setIsSimActive(true); setShowPlaylist(false); audio.pause(); + audio.muted = false; + audio.volume = 1.0; + // Prioritize user blob URL for this specific song ID - audio.src = customAudioMap[song.id] || song.url; + const targetUrl = customAudioMap[song.id] || song.url; + audio.src = targetUrl; audio.load(); - try { - await audio.play(); - } catch (err) { - console.error("Playback error:", err); - setCurrentLyrics("CLIQUE NO PLAY PARA INICIAR"); - setIsPaused(true); - } + setTimeout(async () => { + try { + await audio.play(); + setAudioStatus('ACTIVE'); + } catch (err) { + console.error("Playback error:", err); + setCurrentLyrics("ERRO NO ÁUDIO - CLIQUE PARA REENTRAR"); + setAudioStatus('ERROR'); + setIsPaused(true); + } + }, 200); }; const togglePlayback = () => { const audio = karaokeAudioRef.current; if (!audio) return; if (audio.paused) { - audio.play().catch(e => console.error(e)); + audio.play().catch(e => { + console.error(e); + setAudioStatus('ERROR'); + }); setIsPaused(false); } else { audio.pause(); @@ -461,14 +510,14 @@ const ObservationPage = () => { ctx.save(); ctx.fillStyle = 'rgba(0,0,0,0.85)'; ctx.fillRect(canvas.width/2 - 500, canvas.height - 250, 1000, 180); - ctx.strokeStyle = '#E3B341'; ctx.lineWidth = 4; + ctx.strokeStyle = audioStatus === 'ERROR' ? '#FF0000' : '#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 = '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 = '#E3B341'; ctx.fillRect(canvas.width/2 - 400, canvas.height - 100, (playbackProgress / (karaokeAudioRef.current?.duration || 1)) * 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(); } @@ -480,7 +529,7 @@ const ObservationPage = () => { }; render(); return () => cancelAnimationFrame(animationFrame); - }, [isCameraActive, isSimActive, activeViewers, featuredViewer, isKaraokeActive, currentLyrics, currentSong, getCachedImage, audienceCount, playbackProgress]); + }, [isCameraActive, isSimActive, activeViewers, featuredViewer, isKaraokeActive, currentLyrics, currentSong, getCachedImage, audienceCount, playbackProgress, audioStatus]); useEffect(() => { let interval: any; @@ -526,7 +575,7 @@ const ObservationPage = () => {
BRASIL & MUNDO

- Configure cada música com seu áudio local ou use nossa biblioteca cloud de alta fidelidade + OS AUDIOS ESTÃO ATIVOS - CONFIGURE CADA MÚSICA COM SEU ÁUDIO LOCAL OU USE NOSSA BIBLIOTECA CLOUD

@@ -566,10 +615,15 @@ const ObservationPage = () => { {isKaraokeActive && currentSong && isInterfaceVisible && (
-
- - {currentSong.title} - {currentSong.artist} - {customAudioMap[currentSong.id] && ÁUDIO LOCAL ATIVO} +
+
+ + {currentSong.title} - {currentSong.artist} +
+
+
+ {audioStatus} +
{currentLyrics || "VAI COMEÇAR..."} @@ -581,9 +635,17 @@ const ObservationPage = () => { - +
+ + OCULTAR +
+ {audioStatus === 'ERROR' && ( + + )}
@@ -607,7 +669,7 @@ const ObservationPage = () => { - MICROFONE + MIC
-
-
EFEITO (REVERB){(reverbLevel * 100).toFixed(0)}%
+
+
EFEITO{(reverbLevel * 100).toFixed(0)}%
setReverbLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#E3B341] rounded-lg appearance-none cursor-pointer" />
+
+
VOLUME{(volumeLevel * 100).toFixed(0)}%
+ setVolumeLevel(parseFloat(e.target.value))} className="w-full h-1 bg-white/10 accent-[#00F2FF] rounded-lg appearance-none cursor-pointer" /> +
+
+ + Ativar
Audio
+
@@ -653,7 +725,10 @@ const ObservationPage = () => {

Estúdio 10.000+

-

Configuração Avançada de Playbacks

+
+
+

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

+
@@ -704,7 +779,7 @@ const ObservationPage = () => {
- +