163 lines
4.3 KiB
TypeScript
163 lines
4.3 KiB
TypeScript
import React, { useEffect, useRef, useState } from "react";
|
|
import WaveSurfer from "wavesurfer.js";
|
|
|
|
interface AudioWaveformProps {
|
|
audioUrl: string;
|
|
height?: number;
|
|
className?: string;
|
|
}
|
|
|
|
const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|
audioUrl,
|
|
height = 32,
|
|
className = "",
|
|
}) => {
|
|
const waveformRef = useRef<HTMLDivElement>(null);
|
|
const wavesurfer = useRef<WaveSurfer | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
let ws = wavesurfer.current;
|
|
|
|
const initWaveSurfer = async () => {
|
|
if (!waveformRef.current || !audioUrl) return;
|
|
|
|
try {
|
|
// Clear any existing instance safely
|
|
if (ws) {
|
|
// Instead of immediately destroying, just set to null
|
|
// We'll destroy it outside this function
|
|
wavesurfer.current = null;
|
|
}
|
|
|
|
// Create a fresh instance
|
|
const newWaveSurfer = WaveSurfer.create({
|
|
container: waveformRef.current,
|
|
waveColor: "rgba(255, 255, 255, 0.6)",
|
|
progressColor: "rgba(255, 255, 255, 0.9)",
|
|
cursorColor: "transparent",
|
|
barWidth: 2,
|
|
barGap: 1,
|
|
height,
|
|
normalize: true,
|
|
interact: false,
|
|
});
|
|
|
|
// Assign to ref only if component is still mounted
|
|
if (mounted) {
|
|
wavesurfer.current = newWaveSurfer;
|
|
} else {
|
|
// Component unmounted during initialization, clean up
|
|
try {
|
|
newWaveSurfer.destroy();
|
|
} catch (e) {
|
|
// Ignore destroy errors
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Event listeners
|
|
newWaveSurfer.on("ready", () => {
|
|
if (mounted) {
|
|
setIsLoading(false);
|
|
setError(false);
|
|
}
|
|
});
|
|
|
|
newWaveSurfer.on("error", (err) => {
|
|
console.error("WaveSurfer error:", err);
|
|
if (mounted) {
|
|
setError(true);
|
|
setIsLoading(false);
|
|
}
|
|
});
|
|
|
|
await newWaveSurfer.load(audioUrl);
|
|
} catch (err) {
|
|
console.error("Failed to initialize WaveSurfer:", err);
|
|
if (mounted) {
|
|
setError(true);
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
// First safely destroy previous instance if it exists
|
|
if (ws) {
|
|
// Use this pattern to safely destroy the previous instance
|
|
const wsToDestroy = ws;
|
|
// Detach from ref immediately
|
|
wavesurfer.current = null;
|
|
|
|
// Wait a tick to destroy so any pending operations can complete
|
|
requestAnimationFrame(() => {
|
|
try {
|
|
wsToDestroy.destroy();
|
|
} catch (e) {
|
|
// Ignore errors during destroy
|
|
}
|
|
// Only initialize new instance after destroying the old one
|
|
if (mounted) {
|
|
initWaveSurfer();
|
|
}
|
|
});
|
|
} else {
|
|
// No previous instance to clean up, initialize directly
|
|
initWaveSurfer();
|
|
}
|
|
|
|
return () => {
|
|
// Mark component as unmounted
|
|
mounted = false;
|
|
|
|
// Store reference to current wavesurfer instance
|
|
const wsToDestroy = wavesurfer.current;
|
|
|
|
// Immediately clear the ref to prevent accessing it after unmount
|
|
wavesurfer.current = null;
|
|
|
|
// If we have an instance to clean up, do it safely
|
|
if (wsToDestroy) {
|
|
// Delay destruction to avoid race conditions
|
|
requestAnimationFrame(() => {
|
|
try {
|
|
wsToDestroy.destroy();
|
|
} catch (e) {
|
|
// Ignore destroy errors - they're expected
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}, [audioUrl, height]);
|
|
|
|
if (error) {
|
|
return (
|
|
<div
|
|
className={`flex items-center justify-center ${className}`}
|
|
style={{ height }}
|
|
>
|
|
<span className="text-xs text-foreground/60">Audio unavailable</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
{isLoading && (
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<span className="text-xs text-foreground/60">Loading...</span>
|
|
</div>
|
|
)}
|
|
<div
|
|
ref={waveformRef}
|
|
className={`w-full transition-opacity duration-200 ${isLoading ? "opacity-0" : "opacity-100"}`}
|
|
style={{ height }}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AudioWaveform;
|