import React, { useRef, useEffect, useState } from "react"; import WaveSurfer from "wavesurfer.js"; import RegionsPlugin from "wavesurfer.js/dist/plugins/regions.esm.js"; import { formatTime } from "../../lib/time"; import { Topic } from "./webSocketTypes"; import { AudioWaveform } from "../../api"; import { waveSurferStyles } from "../../styles/recorder"; type PlayerProps = { topics: Topic[]; useActiveTopic: [ Topic | null, React.Dispatch>, ]; waveform: AudioWaveform; media: HTMLMediaElement; mediaDuration: number; }; export default function Player(props: PlayerProps) { const waveformRef = useRef(null); const [wavesurfer, setWavesurfer] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [waveRegions, setWaveRegions] = useState(null); const [activeTopic, setActiveTopic] = props.useActiveTopic; const topicsRef = useRef(props.topics); // Waveform setup useEffect(() => { if (waveformRef.current) { // XXX duration is required to prevent recomputing peaks from audio // However, the current waveform returns only the peaks, and no duration // And the backend does not save duration properly. // So at the moment, we deduct the duration from the topics. // This is not ideal, but it works for now. const _wavesurfer = WaveSurfer.create({ container: waveformRef.current, peaks: props.waveform.data, hideScrollbar: true, autoCenter: true, barWidth: 2, height: "auto", duration: Math.floor(props.mediaDuration / 1000), media: props.media, ...waveSurferStyles.player, }); // styling const wsWrapper = _wavesurfer.getWrapper(); wsWrapper.style.cursor = waveSurferStyles.playerStyle.cursor; wsWrapper.style.backgroundColor = waveSurferStyles.playerStyle.backgroundColor; wsWrapper.style.borderRadius = waveSurferStyles.playerStyle.borderRadius; _wavesurfer.on("play", () => { setIsPlaying(true); }); _wavesurfer.on("pause", () => { setIsPlaying(false); }); _wavesurfer.on("timeupdate", setCurrentTime); setWaveRegions(_wavesurfer.registerPlugin(RegionsPlugin.create())); _wavesurfer.toggleInteraction(true); setWavesurfer(_wavesurfer); return () => { _wavesurfer.destroy(); setIsPlaying(false); setCurrentTime(0); }; } }, []); useEffect(() => { if (!wavesurfer) return; if (!props.media) return; wavesurfer.setMediaElement(props.media); }, [props.media, wavesurfer]); useEffect(() => { topicsRef.current = props.topics; renderMarkers(); }, [props.topics, waveRegions]); const renderMarkers = () => { if (!waveRegions) return; waveRegions.clearRegions(); for (let topic of topicsRef.current) { const content = document.createElement("div"); content.setAttribute("style", waveSurferStyles.marker); content.onmouseover = () => { content.style.backgroundColor = waveSurferStyles.markerHover.backgroundColor; content.style.zIndex = "999"; content.style.width = "300px"; }; content.onmouseout = () => { content.setAttribute("style", waveSurferStyles.marker); }; content.textContent = topic.title; const region = waveRegions.addRegion({ start: topic.timestamp, content, color: "f00", drag: false, }); region.on("click", (e) => { e.stopPropagation(); setActiveTopic(topic); wavesurfer?.setTime(region.start); }); } }; useEffect(() => { if (activeTopic) { wavesurfer?.setTime(activeTopic.timestamp); } }, [activeTopic]); const handlePlayClick = () => { wavesurfer?.playPause(); }; const timeLabel = () => { if (props.mediaDuration) return `${formatTime(currentTime)}/${formatTime(props.mediaDuration)}`; return ""; }; return (
{timeLabel()}
); }