fix: onde réelle quand CORS dispo, son + simulation sinon

Approche hybride CORS retry:
- Tentative 1: crossOrigin="anonymous" + createMediaElementSource
  → données de fréquence réelles si le serveur le permet
- Tentative 2 (si CORS bloque): sans crossOrigin, audio direct
  → son fonctionne, visualiseur simulé

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.7 committed 2026-05-01 23:22:11 +02:00
1 parent 3a90f77d65
commit 3d73ed1e62
2 files changed
+103 -12

No files matched your search

+67 -5
View File
@@ -33,6 +33,9 @@ const AudioContext = createContext<AudioContextType | undefined>(undefined);
export function AudioProvider({ children }: { children: ReactNode }) {
const audioRef = useRef<HTMLAudioElement>(null);
const webAudioRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const dataArrayRef = useRef<Uint8Array | null>(null);
const [currentEpisode, setCurrentEpisode] = useState<Episode | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
@@ -44,6 +47,9 @@ export function AudioProvider({ children }: { children: ReactNode }) {
const [error, setError] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);
// CORS retry: start with CORS for real frequency data, fall back if blocked
const [corsMode, setCorsMode] = useState<'cors' | 'no-cors'>('cors');
// Refs for interval-based progress saving
const currentTimeRef = useRef(0);
const durationRef = useRef(0);
@@ -104,21 +110,66 @@ export function AudioProvider({ children }: { children: ReactNode }) {
}).catch(() => setIsResuming(false));
}, [currentEpisode?._id, userId]);
// getFrequencyData returns null — visualizer uses simulated data
const getFrequencyData = () => null;
// Initialize Web Audio Analyser (only in CORS mode)
useEffect(() => {
if (corsMode !== 'cors' || !audioRef.current || !currentEpisode) return;
try {
const AudioCtx = (window as any).AudioContext || (window as any).webkitAudioContext;
if (!AudioCtx) return;
const ctx = new AudioCtx();
const source = ctx.createMediaElementSource(audioRef.current);
const analyser = ctx.createAnalyser();
analyser.fftSize = 128;
analyser.smoothingTimeConstant = 0.8;
source.connect(analyser);
analyser.connect(ctx.destination);
webAudioRef.current = ctx;
analyserRef.current = analyser;
dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
} catch {
// Fallback: audio plays without visualizer
}
return () => {
if (webAudioRef.current && webAudioRef.current.state !== 'closed') {
webAudioRef.current.close().catch(() => {});
}
analyserRef.current = null;
dataArrayRef.current = null;
webAudioRef.current = null;
};
}, [currentEpisode?._id, corsMode]);
const getFrequencyData = () => {
if (!analyserRef.current || !dataArrayRef.current) return null;
analyserRef.current.getByteFrequencyData(dataArrayRef.current as Uint8Array<ArrayBuffer>);
return dataArrayRef.current;
};
// Actions
const playEpisode = (episode: Episode) => {
setCorsMode('cors');
setError(null);
setCurrentEpisode(episode);
setIsPlaying(true);
};
const closePlayer = () => {
if (audioRef.current) audioRef.current.pause();
if (webAudioRef.current && webAudioRef.current.state !== 'closed') {
webAudioRef.current.close().catch(() => {});
}
analyserRef.current = null;
dataArrayRef.current = null;
webAudioRef.current = null;
setCurrentEpisode(null);
setIsPlaying(false);
setCurrentTime(0);
setDuration(0);
setCorsMode('cors');
};
const togglePlay = () => {
@@ -151,6 +202,16 @@ export function AudioProvider({ children }: { children: ReactNode }) {
setPlaybackSpeedState(prev => speeds[(speeds.indexOf(prev) + 1) % speeds.length]);
};
const handleAudioError = () => {
if (corsMode === 'cors') {
// CORS blocked the audio — retry without CORS (sound works, no real frequency data)
setCorsMode('no-cors');
setError(null);
} else {
setError("Impossible de charger ce fichier audio. Le lien est peut-être expiré ou protégé.");
}
};
return (
<AudioContext.Provider value={{
currentEpisode, isPlaying, currentTime, duration, volume, playbackSpeed, isMuted, isResuming, error,
@@ -160,15 +221,16 @@ export function AudioProvider({ children }: { children: ReactNode }) {
{/* Persistent audio element — lives at provider level so it survives page navigation */}
{currentEpisode && (
<audio
key={currentEpisode._id}
key={`${currentEpisode._id}-${corsMode}`}
ref={audioRef}
src={currentEpisode.audioUrl}
crossOrigin={corsMode === 'cors' ? 'anonymous' : undefined}
onTimeUpdate={() => audioRef.current && setCurrentTime(audioRef.current.currentTime)}
onLoadedMetadata={() => audioRef.current && setDuration(audioRef.current.duration)}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
onEnded={() => setIsPlaying(false)}
onError={() => setError("Impossible de charger ce fichier audio. Le lien est peut-être expiré ou protégé.")}
onError={handleAudioError}
autoPlay
/>
)}
@@ -183,4 +245,4 @@ export function useAudio() {
throw new Error('useAudio must be used within an AudioProvider');
}
return context;
}
}