import { toast } from 'react-toastify'; class AudioContextManager { context = new AudioContext(); destination = this.context.createMediaStreamDestination(); getAllTracks() { return this.destination.stream.getAudioTracks() || []; } mergeAudioStreams(stream: MediaStream) { const source = this.context.createMediaStreamSource(stream); const gain = this.context.createGain(); gain.gain.value = 0.7; return source.connect(gain).connect(this.destination); } clear() { // when everything is removed, tracks will be stopped automatically (hopefully) this.context = new AudioContext(); this.destination = this.context.createMediaStreamDestination(); } } export const audioContextManager = new AudioContextManager(); const FILE_TYPE = 'video/webm'; const FRAME_RATE = 30; function createFileRecorder( stream: MediaStream, mimeType: string, recName: string, sessionId: string, saveCb: (saveObj: { name: string; duration: number }, blob: Blob) => void, onStop: () => void, ) { let ended = false; const start = new Date().getTime(); let recordedChunks: BlobPart[] = []; const SAVE_INTERVAL_MS = 200; const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm; codecs=vp8,opus', }); mediaRecorder.ondataavailable = function (e) { if (e.data.size > 0) { recordedChunks.push(e.data); } }; function onEnd() { if (ended) return; ended = true; saveFile(recordedChunks, mimeType, start, recName, sessionId, saveCb); onStop(); recordedChunks = []; } // sometimes we get race condition or the onstop won't trigger at all, // this is why we have to make it twice to make sure that stream is saved // plus we want to be able to handle both, native and custom button clicks mediaRecorder.stream.getTracks().forEach((track) => (track.onended = onEnd)); mediaRecorder.onstop = () => { onEnd(); mediaRecorder.stream.getTracks().forEach((track) => track.stop()); }; mediaRecorder.start(SAVE_INTERVAL_MS); return mediaRecorder; } function saveFile( recordedChunks: BlobPart[], mimeType: string, startDate: number, recName: string, sessionId: string, saveCb: (saveObj: { name: string; duration: number }, blob: Blob) => void, ) { const saveObject = { name: recName, duration: new Date().getTime() - startDate, sessionId, }; const blob = new Blob(recordedChunks, { type: mimeType, }); saveCb(saveObject, blob); const filename = `${recName}.${mimeType.split('/')[1]}`; const downloadLink = document.createElement('a'); downloadLink.href = URL.createObjectURL(blob); downloadLink.download = filename; document.body.appendChild(downloadLink); downloadLink.click(); URL.revokeObjectURL(downloadLink.href); // clear from memory document.body.removeChild(downloadLink); } async function recordScreen() { const desktopStreams = await navigator.mediaDevices.getDisplayMedia({ audio: { // @ts-ignore restrictOwnAudio: false, echoCancellation: true, noiseSuppression: true, sampleRate: 44100, }, video: { frameRate: FRAME_RATE }, // potential chrome hack // @ts-ignore preferCurrentTab: true, }); audioContextManager.mergeAudioStreams(desktopStreams); return new MediaStream([ ...desktopStreams.getVideoTracks(), ...audioContextManager.getAllTracks(), ]); } /** * Creates a screen recorder that sends the media stream to MediaRecorder * which then saves the stream to a file. * * Supported Browsers: * * Windows: Chrome v91+, Edge v90+ - FULL SUPPORT; * *Nix: Chrome v91+, Edge v90+ - LIMITED SUPPORT - (audio only captured from current tab) * * @returns a promise that resolves to a function that stops the recording */ export async function screenRecorder( recName: string, sessionId: string, saveCb: ( saveObj: { name: string; duration: number; }, blob: Blob, ) => void, onStop: () => void, ) { try { const stream = await recordScreen(); const mediaRecorder = createFileRecorder( stream, FILE_TYPE, recName, sessionId, saveCb, onStop, ); return () => { if (mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); onStop(); } }; } catch (e) { toast.error( 'Screen recording is not permitted by your system and/or browser. Make sure to enable it in your browser as well as in your system settings.', ); throw new Error(`OpenReplay recording: ${e}`); } } // NOT SUPPORTED: // macOS: chrome and edge only support capturing current tab's audio // windows: chrome and edge supports all audio // other: not supported