diff --git a/src/components/playlist/PlaylistComponent.tsx b/src/components/playlist/PlaylistComponent.tsx index b962cdbe..c1059c0e 100644 --- a/src/components/playlist/PlaylistComponent.tsx +++ b/src/components/playlist/PlaylistComponent.tsx @@ -111,19 +111,29 @@ interface PlaylistWaveformProps { isActive: boolean; } +// ─── PlaylistWaveform ─────────────────────────────────────────────────────── +// FIXED: inactive tracks must NEVER touch audio src or the MediaElement backend. +// Active track re-initializes only when the track id actually changes. function PlaylistWaveform({ track, isActive }: PlaylistWaveformProps) { const containerRef = useRef(null); const wsRef = useRef(null); const timeRef = useRef(null); const durRef = useRef(null); + const lastTrackIdRef = useRef(null); useEffect(() => { - let ws: WaveSurfer | null = null; let isMounted = true; const initWaveform = async () => { if (!containerRef.current) return; - if (wsRef.current) { try { wsRef.current.destroy(); } catch { /* ok */ } wsRef.current = null; } + + // Destroy previous instance only if track actually changed + if (wsRef.current) { + try { wsRef.current.destroy(); } catch { /* ok */ } + wsRef.current = null; + } + containerRef.current.innerHTML = ""; + lastTrackIdRef.current = track.id; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); @@ -134,88 +144,77 @@ function PlaylistWaveform({ track, isActive }: PlaylistWaveformProps) { track.waveformData?.length > 0 ? track.waveformData : await getTrackWaveform(track.id); - if (!isMounted) return; - - const hasPeaks = peaks && peaks.length > 0; + if (!isMounted || !containerRef.current) return; - let parsedDur = 0; - if (track.duration && typeof track.duration === "string") { - const parts = track.duration.split(":"); - if (parts.length === 2) { - parsedDur = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); - } - } + const parsedDur = parseDur(track.duration ?? ""); if (isActive) { - ws = WaveSurfer.create({ - container: containerRef.current!, + // Active: share the global audio element — DO NOT provide url + const ws = WaveSurfer.create({ + container: containerRef.current, waveColor: g, progressColor: pg, barWidth: 2, barGap: 0.5, barRadius: 2, height: 80, - backend: "MediaElement", - media: audio, - peaks: hasPeaks ? [peaks] : undefined, + backend: "MediaElement" as const, + media: audio, // ← share, don't load new src + peaks: peaks.length > 0 ? [peaks] : undefined, duration: parsedDur > 0 ? parsedDur : undefined, }); + wsRef.current = ws; setGlobalWaveSurfer(ws, track.id); - setTrackLoadedLocally(track.id); + setTrackLoadedLocally(track.id); // tell audioService: src already set - ws.on("timeupdate", (currentTime: number) => { - if (timeRef.current) timeRef.current.textContent = fmt(currentTime); + ws.on("timeupdate", (t: number) => { + if (timeRef.current) timeRef.current.textContent = fmt(t); }); - - ws.on("interaction", (newTime: number) => { - seekAudio(newTime); - }); - - ws.on("decode", (dur: number) => { - if (durRef.current) durRef.current.textContent = fmt(dur); + ws.on("interaction", (t: number) => seekAudio(t)); + ws.on("decode", (d: number) => { + if (durRef.current) durRef.current.textContent = fmt(d); }); + ws.on("error", () => {}); } else { - // FIX 1: always set height (was missing in inactive branch) - // FIX 2: always provide url so WaveSurfer has an audio source to decode - // peaks alone give shape but no duration/interaction without a url - ws = WaveSurfer.create({ - container: containerRef.current!, + // Inactive: peaks-only, NO url, NO MediaElement — never touch global audio + const ws = WaveSurfer.create({ + container: containerRef.current, waveColor: g, progressColor: pg, barWidth: 2, barGap: 0.5, barRadius: 2, - height: 80, // ← FIX 1: was missing, caused 0px render + height: 80, interact: false, - peaks: hasPeaks ? [peaks] : undefined, + peaks: peaks.length > 0 ? [peaks] : undefined, duration: parsedDur > 0 ? parsedDur : undefined, - url: track.audioUrl, // ← FIX 2: was `!hasPeaks ? track.audioUrl : undefined` + // ← NO url here: loading audio for display-only tracks hijacks the + // global