7
This commit is contained in:
parent
f9592521da
commit
66c1c5f233
@ -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 = () => {
|
||||
<span className="text-[10px] text-white/60 font-black uppercase">{audioStatus}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white text-5xl font-black leading-tight drop-shadow-[0_4px_4px_rgba(0,0,0,1)] transition-all duration-300">
|
||||
{currentLyrics || "VAI COMEÇAR..."}
|
||||
|
||||
<div className={`text-5xl font-black leading-tight drop-shadow-[0_4px_4px_rgba(0,0,0,1)] transition-all duration-300 ${audioStatus === 'ERROR' ? 'text-red-500' : 'text-white'}`}>
|
||||
{audioStatus === 'ERROR' ? audioErrorMsg : (currentLyrics || "VAI COMEÇAR...")}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 h-2 bg-white/10 rounded-full overflow-hidden relative">
|
||||
<div className="h-full bg-gradient-to-r from-[#E3B341] to-[#ffda85] shadow-[0_0_15px_rgba(227,179,65,0.6)]" style={{ width: `${(playbackProgress / (karaokeAudioRef.current?.duration || 1)) * 100}%` }}></div>
|
||||
</div>
|
||||
@ -641,11 +677,9 @@ const ObservationPage = () => {
|
||||
</button>
|
||||
<span className="text-[8px] text-white/40 font-black">OCULTAR</span>
|
||||
</div>
|
||||
{audioStatus === 'ERROR' && (
|
||||
<button onClick={() => selectSong(currentSong)} className="bg-red-600 text-white p-4 rounded-full animate-bounce">
|
||||
<BaseIcon path={mdiRefresh} size={24} />
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => selectSong(currentSong)} className={`p-4 rounded-full transition-all ${audioStatus === 'ERROR' ? 'bg-red-600 text-white animate-bounce shadow-lg' : 'bg-white/10 text-white hover:bg-white/20'}`}>
|
||||
<BaseIcon path={mdiRefresh} size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -687,10 +721,20 @@ 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" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<button onClick={() => { if(audioCtxRef.current) audioCtxRef.current.resume(); }} className="p-4 bg-white/5 rounded-full text-[#E3B341] hover:bg-white/10 transition-all">
|
||||
<BaseIcon path={mdiTune} size={32} />
|
||||
<button onClick={async () => {
|
||||
if(!audioCtxRef.current) await initAudioContext();
|
||||
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>
|
||||
<span className="text-[10px] font-black text-white mt-2 text-center uppercase">Ativar<br/>Audio</span>
|
||||
<span className={`text-[10px] font-black mt-2 text-center uppercase ${audioStatus === 'ERROR' ? 'text-red-500' : 'text-white'}`}>
|
||||
{audioStatus === 'ERROR' ? 'Reativar' : 'Ativar'}<br/>Audio
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user