import Wavesurfer, { WaveSurferOptions } from "wavesurfer.js"; import { useEffect, useMemo, useRef, useState } from "react"; import RegionsPlugin, { RegionParams } from "wavesurfer.js/dist/plugins/regions"; import './styles.css'; export function ForwardIcon() { return } export function PauseIcon() { return } export function Rewind() { return } export function PlayIcon() { return } interface WaveformProps { audioUrl: string; options: WaveSurferOptions; ProgressRenderer?: React.FC<{ waveform: React.MutableRefObject }>; ControlsRenderer?: React.FC<{ waveform: React.MutableRefObject, isPlaying: boolean }>; progressRendererClassName?: string; playUsingRange?: { start: number, end: number }; controls?: boolean; WaveformWrapperClass?: string; waveformClass?: string; progress?: boolean; controlsOptions?: { buttons?: { playPause?: boolean; forward?: boolean; rewind?: boolean; }, forwardBySeconds?: number; rewindBySeconds?: number; icons?: { play?: React.ReactNode; pause?: React.ReactNode; forward?: React.ReactNode; rewind?: React.ReactNode; }, classNames?: { playPause?: string; forward?: string; rewind?: string; } }; regionsList?: RegionParams[]; skeletonLoader?: React.ReactNode; rangePlayStatus?: (isPlaying: boolean) => void; } let rangeInterval: NodeJS.Timeout; const controlsOptionsDefault = { buttons: { playPause: true, forward: true, rewind: true }, forwardBySeconds: 10, rewindBySeconds: 10, icons: { play: , pause: , forward: , rewind: }, classNames: { playPause: "control-button play-pause", forward: "control-button forward", rewind: "control-button rewind" } } function ReactWaveform({ audioUrl = "", options, ProgressRenderer, progressRendererClassName = "", playUsingRange, controls = true, WaveformWrapperClass = "", waveformClass = "", progress = false, ControlsRenderer, controlsOptions, regionsList, skeletonLoader = true, rangePlayStatus }: WaveformProps) { const waveform = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [displayProgress, setDisplayProgress] = useState(false); const [loading, setLoading] = useState(true); const controlsOptionsCombined = useMemo(() => { return { buttons: { ...controlsOptionsDefault.buttons, ...controlsOptions?.buttons }, forwardBySeconds: controlsOptions?.forwardBySeconds ?? controlsOptionsDefault.forwardBySeconds, rewindBySeconds: controlsOptions?.rewindBySeconds ?? controlsOptionsDefault.rewindBySeconds, icons: { ...controlsOptionsDefault.icons, ...controlsOptions?.icons }, classNames: { ...controlsOptionsDefault.classNames, ...controlsOptions?.classNames } } }, [controlsOptions]); const regions = useMemo(() => RegionsPlugin.create(), []); useEffect(() => { if (waveform.current) { waveform.current.load(audioUrl); } else { waveform.current = Wavesurfer.create({ ...options, plugins: [regions], }) waveform.current.load(audioUrl); waveform.current.on("ready", () => { setDisplayProgress(true); setLoading(false); }); waveform.current.on("play", () => setIsPlaying(true)); waveform.current.on("pause", () => setIsPlaying(false)); waveform.current.on("finish", () => setIsPlaying(false)); waveform.current.on("interaction", () => { if (rangeInterval) { clearInterval(rangeInterval) if (rangePlayStatus) { rangePlayStatus(false); } } }); waveform.current.on("decode", function () { if (regionsList && regionsList.length > 0) { regionsList.forEach(region => { regions.addRegion(region); }); } }); return () => { if (waveform.current) { waveform.current.destroy(); } }; } }, [audioUrl]); const playPause = () => { if (waveform.current) { waveform.current.playPause(); setIsPlaying(!isPlaying); } } const forWardBySeconds = (seconds: number = 10) => { if (waveform.current) { waveform.current.setTime(waveform.current.getCurrentTime() + seconds); } } const rewindBySeconds = (seconds: number = 10) => { if (waveform.current) { waveform.current.setTime(Math.max(0, waveform.current.getCurrentTime() - seconds)); } } useEffect(() => { const isPlayRangeValid = Boolean(playUsingRange && "start" in playUsingRange && "end" in playUsingRange && playUsingRange.start < playUsingRange.end); if (!isPlayRangeValid && playUsingRange) { console.error("Invalid play range"); } if (rangeInterval) { if (rangePlayStatus) { rangePlayStatus(false); } clearInterval(rangeInterval); } if (playUsingRange && isPlayRangeValid && waveform.current) { let { start, end } = playUsingRange; start = Math.max(0, start); end = Math.min(end, waveform.current.getDuration()); waveform.current.setTime(start); waveform.current.play(); if (rangePlayStatus) { rangePlayStatus(true); } rangeInterval = setTimeout(() => { if (waveform.current) { waveform.current.pause(); if (rangePlayStatus) { rangePlayStatus(false); } } }, (end - start) * 1000); } return () => { if (rangeInterval) { clearInterval(rangeInterval); if (rangePlayStatus) { rangePlayStatus(false); } } } }, [playUsingRange]); return (
{loading && skeletonLoader && (
)}
{controls ? (ControlsRenderer ? :
{controlsOptionsCombined.buttons.rewind &&
rewindBySeconds(controlsOptionsCombined.forwardBySeconds ?? 10)} className={controlsOptionsCombined.classNames?.rewind || "control-button rewind"}> {controlsOptionsCombined?.icons.rewind ? <> {controlsOptionsCombined.icons.rewind} : }
} {controlsOptionsCombined.buttons.playPause &&
{isPlaying ? (controlsOptionsCombined?.icons?.pause ? <> {controlsOptionsCombined.icons.pause} : ) : (controlsOptionsCombined.icons?.play ? <> {controlsOptionsCombined.icons.play} : )}
} {controlsOptionsCombined.buttons?.forward &&
forWardBySeconds(controlsOptionsCombined.forwardBySeconds ?? 10)} className={controlsOptionsCombined.classNames?.forward || "control-button forward"}> {controlsOptionsCombined?.icons?.forward ? <>{controlsOptionsCombined.icons.forward} : }
}
) : null}
{progress && displayProgress && (ProgressRenderer ? :
)}
) } export default ReactWaveform;