Waveform.tsx (3622B)
1 import { FC, memo, useEffect, useRef, useState } from "react"; 2 import { Pause, Play } from 'lucide-react'; 3 import WaveSurfer from 'wavesurfer.js'; 4 5 type WaveformProps = { 6 /** 7 * The media url to render 8 */ 9 src_url: string; 10 }; 11 12 const Waveform: FC<WaveformProps> = memo(({ src_url }) => { 13 const [playing, setPlaying] = useState(false); 14 const [duration, setDuration] = useState(0); 15 const [waveform, setWaveform] = useState<WaveSurfer | undefined>(undefined); 16 const waveformRef = useRef<HTMLDivElement>(null); 17 18 useEffect(() => { 19 if (waveformRef.current) { 20 if (waveformRef.current.children.length > 0) { 21 if (waveform) { 22 setWaveform(prev => { 23 prev?.destroy(); 24 if (waveformRef.current) { 25 for (const child of waveformRef.current.children) { 26 child.remove(); 27 } 28 } 29 return undefined; 30 }); 31 } 32 } 33 if (waveformRef.current.children.length === 0) { 34 setWaveform(WaveSurfer.create({ 35 barWidth: 5, 36 cursorWidth: 1, 37 container: waveformRef.current, 38 backend: 'WebAudio', 39 height: 45, 40 progressColor: '#f59e0b', 41 responsive: true, 42 waveColor: '#cbd5e1', 43 cursorColor: 'transparent', 44 barRadius: 5, 45 hideScrollbar: true, 46 normalize: true, 47 })); 48 } 49 } 50 }, []); 51 52 53 useEffect(() => { 54 if (waveform) { 55 waveform.load(src_url); 56 waveform.on('ready', () => { 57 setDuration(waveform.getDuration()); 58 }); 59 } 60 }, [waveform]) 61 62 const handlePlay = () => { 63 if (waveform) { 64 setPlaying((prev) => !prev); 65 waveform.playPause(); 66 } 67 }; 68 69 return ( 70 <div className="flex w-[min-content] flex-row h-full items-start justify-start gap-3 bg-slate-200 rounded border-2 border-slate-500 p-1"> 71 <button aria-label={!playing ? "Play" : "Pause"} className="text-center flex justify-center items-center w-12 h-12 min-w-12 min-h-12 rounded-full outline-none cursor-pointer pb-1 bg-orange-500 border-orange-600 border hover:bg-orange-600 group" onClick={handlePlay}> 72 {!playing ? <Play viewBox="0 0 21 24" className="stroke-slate-50 group-hover:stroke-slate-100" /> : <Pause size={24} className="stroke-slate-700 group-hover:stroke-slate-600" />} 73 </button> 74 <div className="flex flex-col w-96 gap-2 justify-between"> 75 <div className="w-full h-12" id="waveform" ref={waveformRef}></div> 76 <span className="w-full text-sm font-medium text-slate-600 text-right">{secondsToHms(duration)}</span> 77 </div> 78 </div> 79 ); 80 }) 81 82 function secondsToHms(d?: number) { 83 if (!d) { 84 return "00:00"; 85 } 86 const h = Math.floor(d / 3600); 87 const m = Math.floor(d % 3600 / 60); 88 const s = Math.floor(d % 3600 % 60); 89 90 if (h === 0 && m === 0 && s === 0) { 91 return "00:00"; 92 } else if (h === 0 && m === 0) { 93 return `00:${s.toString().padStart(2, "0")}`; 94 } else if (h === 0) { 95 return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; 96 } 97 return `${h}:${m}:${s}`; 98 } 99 100 export default Waveform;