import React, { useRef, useEffect, useState, useMemo } 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 { waveSurferStyles } from "../../styles/recorder"; import useMp3 from "./useMp3"; 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; mp3Blob?: Blob | 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); // 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 "^": 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); }; }; // Setup Shortcuts useEffect(() => { if (!record) return; return setupProjectorKeys(); }, [record, deviceId]); // Waveform setup useEffect(() => { if (waveformRef.current) { const _wavesurfer = WaveSurfer.create({ container: waveformRef.current, peaks: props.waveform?.data, hideScrollbar: true, autoCenter: true, barWidth: 2, height: "auto", ...waveSurferStyles.player, }); if (!props.transcriptId) { const _wshack: any = _wavesurfer; _wshack.renderer.renderSingleCanvas = () => {}; } // 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); setRecord(_wavesurfer.registerPlugin(RecordPlugin.create())); setWaveRegions(_wavesurfer.registerPlugin(CustomRegionsPlugin.create())); if (props.isPastMeeting) _wavesurfer.toggleInteraction(true); if (props.mp3Blob) { _wavesurfer.loadBlob(props.mp3Blob); } setWavesurfer(_wavesurfer); return () => { _wavesurfer.destroy(); setIsRecording(false); setIsPlaying(false); setCurrentTime(0); }; } }, []); useEffect(() => { if (!wavesurfer) return; if (!props.mp3Blob) return; wavesurfer.loadBlob(props.mp3Blob); }, [props.mp3Blob, wavesurfer]); 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", 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 (!record) return; return record.on("stopRecording", () => { 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 [screenMediaStream, setScreenMediaStream] = useState(null); const handleRecordTabClick = async () => { if (!record) return console.log("no record"); const stream: MediaStream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: "window", }, audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 44100, suppressLocalAudioPlayback: true, }, surfaceSwitching: "include", selfBrowserSurface: "exclude", systemAudio: "include", }); console.log("stream", stream); console.log(stream.getAudioTracks()); return; const videoElem = document.getElementById( "video-recording", ) as HTMLVideoElement; videoElem.onloadedmetadata = () => { setScreenMediaStream(stream); }; videoElem.srcObject = stream; videoElem.play(); }; const audioContext = useMemo(() => { return new AudioContext(); }, []); const systemAudioGainNode = useMemo(() => { return audioContext.createGain(); }, []); const microphoneGainNode = useMemo(() => { return audioContext.createGain(); }, []); const audioDestNode = useMemo(() => { const dest = audioContext.createMediaStreamDestination(); systemAudioGainNode.connect(dest); microphoneGainNode.connect(dest); return dest; }, []); useEffect(() => { console.log("useEffect screenMediaStream", screenMediaStream); console.log("useEffect record", record); if (!screenMediaStream) return; if (!record) return console.log("no record"); const videoElem = document.getElementById( "video-recording", ) as HTMLVideoElement; const videoMS = videoElem.captureStream() as MediaStream; console.log(videoMS); console.log(videoMS.getAudioTracks()); if (videoMS.getAudioTracks().length == 0) { console.log("no audio track"); return; } // connect system audio (tab audio) const systemAudioSrc = audioContext.createMediaStreamSource(videoMS); systemAudioSrc.connect(systemAudioGainNode); // create a media stream having both system audio and microphone audio // const ms = new MediaStream(); // videoMS.getVideoTracks().forEach((track) => ms.addTrack(track)); // audioDestNode.stream // .getAudioTracks() // .forEach((track) => ms.addTrack(track)); const stream = audioDestNode.stream; if (props.setStream) props.setStream(stream); waveRegions?.clearRegions(); if (stream) { record.startRecording(stream); setIsRecording(true); } }, [record, screenMediaStream]); 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 && ( )} )} {!hasRecorded && ( <> {props.audioDevices && props.audioDevices?.length > 0 && deviceId && ( <>
setShowDevices(false)} deviceId={deviceId} />
)} )}
); }