* feat spot: init commit for extension * nvmrc * fix login flow * Spots Gridview Updates (#2422) * feat ui: login flow for spot extension * spot list, store and service created * some fixing for header * start work on single spot * spot player start * header for player, comments, icons, etc * split stuff into compoennts, create player state manager * player controls, activity panel etc etc * comments, empty page, rename and stuff * interval buttons etc * access modal * pubkey support * fix tooltip * limit 10 -> 9 * hls lib instead of videojs * some warnings * fix date display for exp * change public links * display more client data * fix cleaning, init comment * map network to replay player network ev * stream support, console panel, close panels on X * fixing streaming, destroy on leave * fix autoplay * show notification on spot login * fix spot login * backup player added, fix audio issue * show thumbnail when no video, add spot roles * add poster thumbnail * some fixes to video check * fix events jump * fix play btn * try catch over pubkey * feat ui: login flow for spot extension * spot list, store and service created * some fixing for header * start work on single spot * spot player start * header for player, comments, icons, etc * split stuff into compoennts, create player state manager * player controls, activity panel etc etc * comments, empty page, rename and stuff * interval buttons etc * access modal * pubkey support * fix tooltip * limit 10 -> 9 * hls lib instead of videojs * some warnings * fix date display for exp * change public links * display more client data * fix cleaning, init comment * map network to replay player network ev * stream support, console panel, close panels on X * fixing streaming, destroy on leave * fix autoplay * show notification on spot login * fix spot login * backup player added, fix audio issue * show thumbnail when no video, add spot roles * add poster thumbnail * some fixes to video check * fix events jump * fix play btn * try catch over pubkey * icons * Various updates * Update SVG.tsx * Update SideMenu.tsx * SpotList & Menu updates * feat ui: login flow for spot extension * spot list, store and service created * some fixing for header * start work on single spot * spot player start * header for player, comments, icons, etc * split stuff into compoennts, create player state manager * player controls, activity panel etc etc * comments, empty page, rename and stuff * interval buttons etc * access modal * pubkey support * fix tooltip * limit 10 -> 9 * hls lib instead of videojs * some warnings * fix date display for exp * change public links * display more client data * fix cleaning, init comment * map network to replay player network ev * stream support, console panel, close panels on X * fixing streaming, destroy on leave * fix autoplay * show notification on spot login * fix spot login * backup player added, fix audio issue * show thumbnail when no video, add spot roles * add poster thumbnail * some fixes to video check * fix events jump * fix play btn * try catch over pubkey * icons * spot login pinging * Spot List & Player Updates * move spot login flow to login comp, use separate spot login path for unique jwt * invalidate spot jwt on logout * add visual data on page load event * typo fix * Spot Listing improvements post review. * Update SpotListItem.tsx * Improved Spot List and Item Details * Minor improvements * More improvements * Public player header improvements * Moved formatExpirationTime to utils * fixes after merge --------- Co-authored-by: nick-delirium <nikita@openreplay.com> * set sso link to <a>? * some small perf fixes * login duck reformat... * Update frontend.yaml * add observer to spot list header * split list header * update spotjwt param in router * fix toast in router * fix async fetch, move ctx * capture space btn ev * fix header link * public sharing error msg * fix err msg for unsuccessful rec start * fix list alignment * Caching assets. Finally!!! * fix typing in comment field * add pubkey to comments, fix console jump btn * no content comp * change refresh token logic * move thumbnail ts * move thumbnail ts * fix tab change * switch up toggler * early exit if no jwt present * regenerate icons * fix location str * fix ctx * change thumnail res, return autoplay for video player * parse links in console rows, fix injected method parse? * remove ts from js * fix console parsing order? * fixes for autoplay * xray for spot player * move to spot list after login; esc to cancel; fix signup link; move ux commit * kb sc for skipping; xray for spot ext * track aborted requests * tooltip for readability * fixing empty state * New blank state + various minor improvements (#2471) * New blank state + various minor improvements * apres merge --------- Co-authored-by: nick-delirium <nikita@openreplay.com> * rm temp v * init or card * empty state debug * empty state debug * empty state debug * fix initor img * spotonly scope support * Improved Spot dead-end pages (#2475) * Improved Spot dead-end pages * Initiate OpenReplay Setup and some more * get scope changes * fix crash * scope upgrade/downgrade * scope setup flow * ping for backend * upgrade wxt deps * cancel ping int on expiration * check rec status * fix ping * check video processing state * check video processing state * fix xray close, network highlight, fcp rounding * update wxt, move open spot stuff to settings * fix some history issues * fix spot login flow * fix spot login again * fix spot login again * don't send two requests * limit messages for logged users * limit messages for logged users * fix public ignore * microphone stuff * microphone stuff * Various improvements (#2509) * Various improvements - Updated icons in mic settings - Included prefix in Spot title - Save recording notification has been updated - Other minor UI improvements * Inline declaration of spot name field, and settings UI * str f --------- Co-authored-by: nick-delirium <nikita@openreplay.com> * UI changes in player header, spot list (#2510) * Added UI elements in player page - Badge with counts for comments - Download and Delete dropdown in player - Spot selection -- UI improvement * Minor copy updates * completing changes --------- Co-authored-by: nick-delirium <nikita@openreplay.com> * rm cmt * fix cellmeasurer * thumbnail dur * fix download * Minor fixes (#2512) - Spot delete confirmation - Spot comments UI update - Minor copy updates * limit number of notif messages * add spot title to doc title, add cache groups for webpack * drop mic controls from recording popup view * fix for webpack compress * fix for auto mic pickup * change status banners * move svgs around, remove undefined check * refactor svgs * fix timetable scaling * fix error popup * self contain css * pre-select spot on spot onboarding --------- Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com> Co-authored-by: Rajesh Rajendran <rjshrjndrn@users.noreply.github.com>
398 lines
10 KiB
JavaScript
398 lines
10 KiB
JavaScript
const hardLimit = 24 * 1024 * 1024; // 24 MB
|
|
|
|
function getRecordingSettings(qualityValue) {
|
|
const settingsMap = {
|
|
"4k": { audioBitsPerSecond: 192000, videoBitsPerSecond: 40000000, width: 4096, height: 2160 },
|
|
"1080p": { audioBitsPerSecond: 192000, videoBitsPerSecond: 8000000, width: 1920, height: 1080 },
|
|
"720p": { audioBitsPerSecond: 96000, videoBitsPerSecond: 2500000, width: 1280, height: 720 },
|
|
"480p": { audioBitsPerSecond: 96000, videoBitsPerSecond: 2500000, width: 854, height: 480 },
|
|
"360p": { audioBitsPerSecond: 96000, videoBitsPerSecond: 1000000, width: 640, height: 360 },
|
|
"240p": { audioBitsPerSecond: 64000, videoBitsPerSecond: 500000, width: 426, height: 240 },
|
|
};
|
|
|
|
const defaultSettings = { audioBitsPerSecond: 128000, videoBitsPerSecond: 5000000, width: 1920, height: 1080 };
|
|
const { audioBitsPerSecond, videoBitsPerSecond, width, height } = settingsMap[qualityValue] || defaultSettings;
|
|
const duration = 3 * 60 * 1000; // 3 minutes
|
|
|
|
const mimeTypes = [
|
|
"video/webm;codecs=h264",
|
|
"video/webm;codecs=avc1",
|
|
"video/webm;codecs=av1",
|
|
"video/mp4;codecs=avc1",
|
|
"video/webm;codecs=vp8,opus",
|
|
"video/webm;codecs=vp9,opus",
|
|
];
|
|
|
|
let mimeType = mimeTypes[0];
|
|
|
|
const constrains = {
|
|
frameRate: {
|
|
min: 20,
|
|
max: 30,
|
|
},
|
|
audio: true,
|
|
video: {
|
|
width: {
|
|
exact: width,
|
|
},
|
|
height: {
|
|
exact: height,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = {
|
|
constrains,
|
|
mimeType,
|
|
audioBitsPerSecond,
|
|
videoBitsPerSecond,
|
|
duration,
|
|
};
|
|
return result;
|
|
}
|
|
|
|
class ScreenRecorder {
|
|
isRecording = false;
|
|
videoBlob = null;
|
|
videoUrl = null;
|
|
duration = 0;
|
|
mRecorder;
|
|
videoStream;
|
|
|
|
constructor() {
|
|
this.isRecording = false;
|
|
this.mRecorder = null;
|
|
this.chunks = [];
|
|
this.stream = null;
|
|
this.settings = null;
|
|
this.audioTrack = null;
|
|
}
|
|
|
|
clearAll() {
|
|
this.isRecording = false;
|
|
this.videoBlob = null;
|
|
this.videoUrl = null;
|
|
this.duration = 0;
|
|
this.mRecorder = null;
|
|
this.chunks = [];
|
|
this.stream = null;
|
|
this.audioTrack = null;
|
|
}
|
|
|
|
init(settings) {
|
|
this.settings = settings;
|
|
}
|
|
|
|
durationInt = null;
|
|
trackDuration = () => {
|
|
this.durationInt = setInterval(() => {
|
|
this.duration += 1000;
|
|
}, 1000);
|
|
};
|
|
|
|
clearDurationInterval = () => {
|
|
clearInterval(this.durationInt);
|
|
this.durationInt = null;
|
|
};
|
|
|
|
async startRecording(type, streamId, microphone = false, audioId) {
|
|
this.duration = 0;
|
|
if (this.isRecording) {
|
|
throw new Error("Called startRecording while recording is in progress.");
|
|
}
|
|
const combinedStream = await this._getStream(
|
|
type,
|
|
streamId,
|
|
microphone,
|
|
audioId,
|
|
);
|
|
|
|
this.stream = combinedStream;
|
|
this.mRecorder = new MediaRecorder(combinedStream, {
|
|
mimeType: this.settings.mimeType,
|
|
audioBitsPerSecond: this.settings.audioBitsPerSecond,
|
|
videoBitsPerSecond: this.settings.videoBitsPerSecond,
|
|
});
|
|
|
|
this.mRecorder.ondataavailable = this._handleDataAvailable;
|
|
this.mRecorder.onstop = this._handleStop;
|
|
|
|
this.mRecorder.start();
|
|
this.isRecording = true;
|
|
this.trackDuration();
|
|
}
|
|
|
|
stop() {
|
|
if (this.mRecorder) {
|
|
this.mRecorder.stop();
|
|
this.mRecorder = null;
|
|
}
|
|
|
|
const clearAll = () => {
|
|
if (this.stream) {
|
|
this.stream.getTracks().forEach((track) => track.stop());
|
|
this.stream = null;
|
|
}
|
|
if (this.audioTrack) {
|
|
this.audioTrack.stop();
|
|
this.audioTrack = null;
|
|
}
|
|
this.isRecording = false;
|
|
this.clearDurationInterval();
|
|
};
|
|
|
|
if (!this.recorded) {
|
|
let tries = 0;
|
|
const int = setInterval(() => {
|
|
if (this.recorded || tries > 20) {
|
|
clearInterval(int);
|
|
clearAll();
|
|
this.recorded = false;
|
|
if (tries > 20) {
|
|
console.error("Failed to stop recording properly");
|
|
}
|
|
}
|
|
tries++;
|
|
}, 250);
|
|
} else {
|
|
clearAll();
|
|
}
|
|
}
|
|
|
|
pause() {
|
|
if (this.mRecorder) {
|
|
this.mRecorder.pause();
|
|
this.clearDurationInterval();
|
|
}
|
|
}
|
|
|
|
resume() {
|
|
if (this.mRecorder) {
|
|
this.mRecorder.resume();
|
|
this.trackDuration();
|
|
}
|
|
}
|
|
|
|
muteMicrophone() {
|
|
if (this.audioTrack) {
|
|
this.audioTrack.enabled = false;
|
|
}
|
|
}
|
|
|
|
async unmuteMicrophone() {
|
|
if (this.audioTrack) {
|
|
this.audioTrack.enabled = true;
|
|
}
|
|
}
|
|
|
|
async _getStream(type, streamId, useMicrophone = false, audioId) {
|
|
const constraints = this.settings;
|
|
this.videoStream = null;
|
|
let microphoneStream = null;
|
|
try {
|
|
this.videoStream = await (type === "tab"
|
|
? navigator.mediaDevices.getUserMedia({
|
|
audio: {
|
|
mandatory: {
|
|
chromeMediaSource: "tab",
|
|
chromeMediaSourceId: streamId,
|
|
},
|
|
},
|
|
video: {
|
|
mandatory: {
|
|
chromeMediaSource: "tab",
|
|
chromeMediaSourceId: streamId,
|
|
},
|
|
},
|
|
})
|
|
: navigator.mediaDevices.getDisplayMedia({
|
|
...constraints,
|
|
video: {
|
|
...constraints.video,
|
|
displaySurface: "monitor",
|
|
},
|
|
}));
|
|
} catch (e) {
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
try {
|
|
microphoneStream = await navigator.mediaDevices.getUserMedia({
|
|
audio: { echoCancellation: false, deviceId: audioId },
|
|
});
|
|
this.audioTrack = microphoneStream.getAudioTracks()[0];
|
|
if (!useMicrophone) {
|
|
this.audioTrack.enabled = false;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
this.audioTrack = this.createPlaceholderAudioTrack();
|
|
}
|
|
|
|
return new MediaStream([
|
|
...this.videoStream.getVideoTracks(),
|
|
this.audioTrack,
|
|
]);
|
|
}
|
|
|
|
createPlaceholderAudioTrack = () => {
|
|
const audioContext = new (window.AudioContext ||
|
|
window.webkitAudioContext)();
|
|
const oscillator = audioContext.createOscillator();
|
|
const gainNode = audioContext.createGain();
|
|
const destination = audioContext.createMediaStreamDestination();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(destination);
|
|
gainNode.gain.value = 0; // Set volume to 0
|
|
oscillator.start();
|
|
|
|
return destination.stream.getAudioTracks()[0];
|
|
};
|
|
|
|
_handleDataAvailable = (event) => {
|
|
if (event.data.size > 0) {
|
|
this.chunks.push(event.data);
|
|
}
|
|
};
|
|
|
|
recorded = false;
|
|
_handleStop = () => {
|
|
const blob = new Blob(this.chunks, { type: this.settings.mimeType });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
this.videoBlob = blob;
|
|
this.videoUrl = url;
|
|
this.videoStream.getTracks().forEach((track) => track.stop());
|
|
if (this.audioTrack) {
|
|
this.audioTrack.stop();
|
|
}
|
|
this.recorded = true;
|
|
};
|
|
|
|
getVideoData = (iteration = 0) => {
|
|
return new Promise((resolve) => {
|
|
if (this.recorded) {
|
|
resolve({
|
|
blob: this.videoBlob,
|
|
mtype: this.settings.mimeType,
|
|
});
|
|
} else {
|
|
if (iteration > 10 * 1000) {
|
|
return resolve({ blob: null, mtype: null });
|
|
}
|
|
setTimeout(() => {
|
|
resolve(this.getVideoData(iteration++));
|
|
}, 100);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
let recorder = new ScreenRecorder();
|
|
recorder.init(getRecordingSettings("720p"));
|
|
|
|
browser.runtime.onMessage.addListener((message, _, respond) => {
|
|
if (message.target === "offscreen") {
|
|
if (message.type === "offscr:start-recording") {
|
|
recorder.clearAll();
|
|
recorder
|
|
.startRecording(
|
|
message.area,
|
|
message.data,
|
|
message.microphone,
|
|
message.audioId,
|
|
)
|
|
.then(() => {
|
|
respond({ success: true, time: Date.now() });
|
|
})
|
|
.catch(e => {
|
|
console.error(e);
|
|
respond({ success: false, time: Date.now() });
|
|
})
|
|
return true;
|
|
}
|
|
if (message.type === "offscr:get-ts") {
|
|
respond({ time: recorder.duration });
|
|
return true;
|
|
}
|
|
if (message.type === "offscr:check-status") {
|
|
respond({ status: recorder.isRecording, time: recorder.duration });
|
|
return true;
|
|
}
|
|
if (message.type === "offscr:stop-discard") {
|
|
recorder.stop();
|
|
recorder.clearAll();
|
|
}
|
|
if (message.type === "offscr:stop-recording") {
|
|
recorder.stop();
|
|
const duration = recorder.duration;
|
|
recorder.getVideoData().then((data) => {
|
|
if (!data.blob) {
|
|
respond({ status: "empty" });
|
|
}
|
|
convertBlobToBase64(data.blob).then(({ result, size }) => {
|
|
if (size > hardLimit) {
|
|
respond({ status: "parts" });
|
|
result.forEach((chunk, i) => {
|
|
void browser.runtime.sendMessage({
|
|
type: "offscr:video-data-chunk",
|
|
data: chunk,
|
|
index: i,
|
|
total: result.length,
|
|
duration,
|
|
});
|
|
});
|
|
} else {
|
|
respond({ status: "full", base64data: result, duration });
|
|
}
|
|
recorder.clearAll();
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
if (message.type === "offscr:pause-recording") {
|
|
recorder.pause();
|
|
return "paused";
|
|
}
|
|
if (message.type === "offscr:resume-recording") {
|
|
recorder.resume();
|
|
return "resumed";
|
|
}
|
|
if (message.type === "offscr:mute-microphone") {
|
|
recorder.muteMicrophone();
|
|
return "muted";
|
|
}
|
|
if (message.type === "offscr:unmute-microphone") {
|
|
void recorder.unmuteMicrophone();
|
|
return "unmuted";
|
|
}
|
|
return "miss";
|
|
}
|
|
});
|
|
|
|
const convertBlobToBase64 = (blob) =>
|
|
new Promise((resolve) => {
|
|
const reader = new FileReader();
|
|
try {
|
|
reader.readAsDataURL(blob);
|
|
reader.onloadend = () => {
|
|
const parts = [];
|
|
const base64data = reader.result;
|
|
if (base64data.length > hardLimit) {
|
|
const chunkSize = hardLimit;
|
|
for (let i = 0; i < base64data.length; i += chunkSize) {
|
|
parts.push(base64data.slice(i, i + chunkSize));
|
|
}
|
|
} else {
|
|
parts.push(base64data);
|
|
}
|
|
resolve({
|
|
result: parts,
|
|
size: base64data.length,
|
|
});
|
|
};
|
|
} catch (e) {
|
|
console.error(e, blob, reader);
|
|
}
|
|
});
|