import React, { useRef, useEffect, useState } from "react"; import WaveSurfer from "wavesurfer.js"; import RecordPlugin from "../lib/custom-plugins/record"; import CustomRegionsPlugin from "../lib/custom-plugins/regions"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faMicrophone } from "@fortawesome/free-solid-svg-icons"; import { faDownload } from "@fortawesome/free-solid-svg-icons"; import { formatTime } from "../lib/time"; import { Topic } from "./webSocketTypes"; import { AudioWaveform } from "../api"; import AudioInputsDropdown from "./audioInputsDropdown"; import { Option } from "react-dropdown"; import { useError } from "../(errors)/errorContext"; type RecorderProps = { setStream?: React.Dispatch>; onStop?: () => void; topics: Topic[]; getAudioStream?: (deviceId) => Promise; audioDevices?: Option[]; useActiveTopic: [ Topic | null, React.Dispatch>, ]; waveform?: AudioWaveform | null; isPastMeeting: boolean; transcriptId?: string | null; }; export default function Recorder(props: RecorderProps) { const waveformRef = useRef(null); const [wavesurfer, setWavesurfer] = useState(null); const [record, setRecord] = useState(null); const [isRecording, setIsRecording] = useState(false); const [hasRecorded, setHasRecorded] = useState(props.isPastMeeting); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [timeInterval, setTimeInterval] = useState(null); const [duration, setDuration] = useState(0); const [waveRegions, setWaveRegions] = useState( null, ); const [deviceId, setDeviceId] = useState(null); const [recordStarted, setRecordStarted] = useState(false); const [activeTopic, setActiveTopic] = props.useActiveTopic; const topicsRef = useRef(props.topics); const [showDevices, setShowDevices] = useState(false); const { setError } = useError(); // Function used to setup keyboard shortcuts for the streamdeck const setupProjectorKeys = (): (() => void) => { if (!record) return () => {}; const handleKeyPress = (event: KeyboardEvent) => { switch (event.key) { case "~": location.href = ""; break; case ",": location.href = "/transcripts/new"; break; case "!": if (record.isRecording()) return; handleRecClick(); break; case "@": if (!record.isRecording()) return; handleRecClick(); break; case "%": setError(new Error("Error triggered by '%' shortcut")); break; case "^": throw new Error("Unhandled Exception thrown by '^' shortcut"); case "(": location.href = "/login"; break; case ")": location.href = "/logout"; break; default: break; } }; document.addEventListener("keydown", handleKeyPress); // Return the cleanup function return () => { document.removeEventListener("keydown", handleKeyPress); }; }; useEffect(() => { if (waveformRef.current) { const _wavesurfer = WaveSurfer.create({ container: waveformRef.current, waveColor: "#777", progressColor: "#222", cursorColor: "OrangeRed", hideScrollbar: true, autoCenter: true, barWidth: 2, height: "auto", url: props.transcriptId ? `${process.env.NEXT_PUBLIC_API_URL}/v1/transcripts/${props.transcriptId}/audio/mp3` : undefined, }); const wsWrapper = _wavesurfer.getWrapper(); wsWrapper.style.cursor = "pointer"; wsWrapper.style.backgroundColor = "RGB(240 240 240)"; wsWrapper.style.borderRadius = "15px"; _wavesurfer.on("play", () => { setIsPlaying(true); }); _wavesurfer.on("pause", () => { setIsPlaying(false); }); _wavesurfer.on("timeupdate", setCurrentTime); setRecord(_wavesurfer.registerPlugin(RecordPlugin.create())); setWaveRegions(_wavesurfer.registerPlugin(CustomRegionsPlugin.create())); if (props.transcriptId) _wavesurfer.toggleInteraction(true); setWavesurfer(_wavesurfer); return () => { _wavesurfer.destroy(); setIsRecording(false); setIsPlaying(false); setCurrentTime(0); }; } }, []); useEffect(() => { topicsRef.current = props.topics; if (!isRecording) 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", ` position: absolute; border-left: solid 1px orange; padding: 0 2px 0 5px; font-size: 0.7rem; width: 100px; max-width: fit-content; cursor: pointer; background-color: white; border-radius: 0 3px 3px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: width 100ms linear; `, ); content.onmouseover = () => { content.style.backgroundColor = "orange"; content.style.zIndex = "999"; content.style.width = "300px"; }; content.onmouseout = () => { content.style.backgroundColor = "white"; content.style.zIndex = "0"; content.style.width = "100px"; }; 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 (!record) return; return setupProjectorKeys(); }, [record, deviceId]); useEffect(() => { if (!record) return; return record.on("stopRecording", () => { const link = document.getElementById("download-recording"); if (!link) return; link.setAttribute("href", record.getRecordedUrl()); link.setAttribute("download", "reflector-recording.webm"); link.style.visibility = "visible"; renderMarkers(); }); }, [record]); useEffect(() => { if (isRecording) { const interval = window.setInterval(() => { setCurrentTime((prev) => prev + 1); }, 1000); setTimeInterval(interval); return () => clearInterval(interval); } else { clearInterval(timeInterval as number); setCurrentTime((prev) => { setDuration(prev); return 0; }); } }, [isRecording]); useEffect(() => { if (activeTopic) { wavesurfer?.setTime(activeTopic.timestamp); } }, [activeTopic]); const handleRecClick = async () => { if (!record) return console.log("no record"); if (record.isRecording()) { if (props.onStop) props.onStop(); record.stopRecording(); setIsRecording(false); setHasRecorded(true); } else { const stream = await getCurrentStream(); if (props.setStream) props.setStream(stream); waveRegions?.clearRegions(); if (stream) { await record.startRecording(stream); setIsRecording(true); } } }; const handlePlayClick = () => { wavesurfer?.playPause(); }; const timeLabel = () => { if (isRecording) return formatTime(currentTime); if (duration) return `${formatTime(currentTime)}/${formatTime(duration)}`; return ""; }; const getCurrentStream = async () => { setRecordStarted(true); return deviceId && props.getAudioStream ? await props.getAudioStream(deviceId) : null; }; useEffect(() => { if (props.audioDevices && props.audioDevices.length > 0) { setDeviceId[props.audioDevices[0].value]; } }, [props.audioDevices]); return (
{isRecording && (
)} {timeLabel()}
{hasRecorded && ( <> {props.transcriptId && ( )} {!props.transcriptId && ( )} )} {!hasRecorded && ( <> {props.audioDevices && props.audioDevices?.length > 0 && ( <>
setShowDevices(false)} />
)} )}
); }