* applied eslint * add locales and lint the project * removed error boundary * updated locales * fix min files * fix locales
179 lines
4.6 KiB
TypeScript
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
|