fix: son coupé par createMediaElementSource sur audio cross-origin

createMediaElementSource détache l'audio de sa sortie par défaut.
Pour les ressources cross-origin sans CORS, le graphe Web Audio
produit du silence — aucun son.

- Retire createMediaElementSource et tout le pipeline Web Audio
- L'audio joue directement via l'élément <audio> (fiable)
- Le visualiseur utilise une animation simulée (plus de dépendance CORS)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.7 committed 2026-05-01 23:17:46 +02:00
1 parent ee04841d9e
commit 3a90f77d65
2 files changed
+10 -84

No files matched your search

+8 -37
View File
@@ -10,17 +10,14 @@ const MAX_AMPLITUDE = 0.22;
const PADDING = 6;
export default function AudioVisualizer({ height = 80 }: AudioVisualizerProps) {
const { isPlaying, currentTime, duration, getFrequencyData } = useAudio();
const { isPlaying, currentTime, duration } = useAudio();
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef<number>(0);
const heightsRef = useRef<number[]>(Array(NUM_POINTS).fill(0));
const playingRef = useRef(isPlaying);
const getDataRef = useRef(getFrequencyData);
const timeRef = useRef({ currentTime, duration });
const hasRealDataRef = useRef(false);
useEffect(() => { playingRef.current = isPlaying; }, [isPlaying]);
useEffect(() => { getDataRef.current = getFrequencyData; }, [getFrequencyData]);
useEffect(() => { timeRef.current = { currentTime, duration }; }, [currentTime, duration]);
useEffect(() => {
@@ -48,47 +45,21 @@ export default function AudioVisualizer({ height = 80 }: AudioVisualizerProps) {
const animate = () => {
if (!ctx || !canvas) return;
frame++;
const raw = getDataRef.current?.();
const hts = heightsRef.current;
const step = drawW / (NUM_POINTS - 1);
const { currentTime: ct, duration: dur } = timeRef.current;
const progress = dur > 0 ? ct / dur : 0;
const playing = playingRef.current;
// Detect if real frequency data is available (CORS allows it)
if (raw && playing) {
const sum = raw.reduce((a, b) => a + b, 0);
hasRealDataRef.current = sum > 0;
}
// Update heights
// Simulated waveform animation (Web Audio API not used to avoid CORS muting)
if (playing) {
if (raw && hasRealDataRef.current) {
// Real frequency data from Web Audio API
for (let i = 0; i < NUM_POINTS; i++) {
const bucketSize = raw.length / NUM_POINTS;
const startIdx = Math.floor(i * bucketSize);
const endIdx = Math.floor((i + 1) * bucketSize);
let sum = 0;
let count = 0;
for (let j = startIdx; j < endIdx && j < raw.length; j++) {
sum += raw[j];
count++;
}
const avg = count > 0 ? sum / count : 0;
const target = (avg / 255) * MAX_AMPLITUDE;
hts[i] += (target - hts[i]) * 0.25;
}
} else {
// Simulated visualization for cross-origin audio (no CORS)
for (let i = 0; i < NUM_POINTS; i++) {
const wave = Math.sin(frame * 0.04 + i * 0.35) * 0.5 + 0.5;
const noise = Math.sin(frame * 0.07 + i * 1.7) * 0.3 + 0.7;
const target = MAX_AMPLITUDE * 0.6 * (0.5 + wave * noise);
hts[i] += (target - hts[i]) * 0.18;
}
for (let i = 0; i < NUM_POINTS; i++) {
const wave = Math.sin(frame * 0.04 + i * 0.35) * 0.5 + 0.5;
const noise = Math.sin(frame * 0.07 + i * 1.7) * 0.3 + 0.7;
const target = MAX_AMPLITUDE * 0.6 * (0.5 + wave * noise);
hts[i] += (target - hts[i]) * 0.18;
}
} else if (!playing) {
} else {
for (let i = 0; i < NUM_POINTS; i++) {
hts[i] += (0 - hts[i]) * 0.08;
}
+2 -47
View File
@@ -33,9 +33,6 @@ 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);
@@ -107,44 +104,8 @@ export function AudioProvider({ children }: { children: ReactNode }) {
}).catch(() => setIsResuming(false));
}, [currentEpisode?._id, userId]);
// Initialize Web Audio Analyser when audio element is available
useEffect(() => {
if (!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]);
const getFrequencyData = () => {
if (!analyserRef.current || !dataArrayRef.current) return null;
analyserRef.current.getByteFrequencyData(dataArrayRef.current as Uint8Array<ArrayBuffer>);
return dataArrayRef.current;
};
// getFrequencyData returns null — visualizer uses simulated data
const getFrequencyData = () => null;
// Actions
const playEpisode = (episode: Episode) => {
@@ -154,12 +115,6 @@ export function AudioProvider({ children }: { children: ReactNode }) {
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);