openreplay/frontend/app/utils/screenRecorder.ts
Andrey Babushkin fd5c0c9747
Add lokalisation (#3092)
* applied eslint

* add locales and lint the project

* removed error boundary

* updated locales

* fix min files

* fix locales
2025-03-06 17:43:15 +01:00

179 lines
4.6 KiB
TypeScript

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