Files
Plataforma-de-desarrolladores/SIG.Web/Scripts/RecordTabNavigation.js
Jorge Luis caudillo esparza 6685ef63fb Initial commit
2026-07-14 11:15:57 -07:00

628 lines
15 KiB
JavaScript

const TabRecorder = (() => {
const qualityProfiles = {
low: {
width: 854,
height: 480,
frameRate: 15,
videoBitsPerSecond: 1000000
},
medium: {
width: 1280,
height: 720,
frameRate: 30,
videoBitsPerSecond: 2500000
},
high: {
width: 1920,
height: 1080,
frameRate: 30,
videoBitsPerSecond: 5000000
},
veryHigh: {
width: 1920,
height: 1080,
frameRate: 60,
videoBitsPerSecond: 8000000
}
};
let displayStream = null;
let microphoneStream = null;
let recordingStream = null;
let mediaRecorder = null;
let audioContext = null;
let audioDestination = null;
let tabAudioSource = null;
let microphoneAudioSource = null;
let tabGainNode = null;
let microphoneGainNode = null;
let tabVolume = 1;
let microphoneVolume = 1;
let recordedChunks = [];
let stopPromiseResolver = null;
function streamIsActive(stream) {
return stream?.getTracks().some(
track => track.readyState === "live"
);
}
function getSupportedMimeType() {
const mimeTypes = [
"video/webm;codecs=vp9,opus",
"video/webm;codecs=vp8,opus",
"video/webm",
"video/mp4"
];
return mimeTypes.find(type =>
MediaRecorder.isTypeSupported(type)
) || "";
}
function getFileExtension(mimeType) {
return mimeType.includes("mp4") ? "mp4" : "webm";
}
function downloadRecording(blob, mimeType) {
const extension = getFileExtension(mimeType);
const date = new Date()
.toISOString()
.replace(/[:.]/g, "-");
const fileName =
`tab-recording-${date}.${extension}`;
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
link.style.display = "none";
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 30000);
}
async function selectTab() {
if (!navigator.mediaDevices?.getDisplayMedia) {
throw new Error(
"Your browser does not support tab recording."
);
}
if (streamIsActive(displayStream)) {
return displayStream;
}
displayStream =
await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: "browser",
width: {
ideal: 1920
},
height: {
ideal: 1080
},
frameRate: {
ideal: 30,
max: 60
}
},
// The user must enable "Share tab audio"
// in the browser window.
audio: true,
preferCurrentTab: true,
selfBrowserSurface: "include",
surfaceSwitching: "exclude",
systemAudio: "include"
});
const videoTrack =
displayStream.getVideoTracks()[0];
videoTrack.addEventListener("ended", async () => {
if (
mediaRecorder &&
mediaRecorder.state !== "inactive"
) {
await stopAndSave();
}
await stopMicrophone();
await closeAudioMixer();
displayStream = null;
});
return displayStream;
}
async function selectMicrophone() {
if (!navigator.mediaDevices?.getUserMedia) {
throw new Error(
"Your browser does not support microphone recording."
);
}
if (streamIsActive(microphoneStream)) {
return microphoneStream;
}
microphoneStream =
await navigator.mediaDevices.getUserMedia({
video: false,
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
return microphoneStream;
}
async function prepareSources() {
// The tab-selection permission appears first.
await selectTab();
try {
// The microphone permission appears after that.
await selectMicrophone();
} catch (error) {
displayStream
?.getTracks()
.forEach(track => track.stop());
displayStream = null;
throw new Error(
`The microphone could not be opened: ${error.message}`
);
}
}
async function createAudioMixer() {
await closeAudioMixer();
const AudioContextClass =
window.AudioContext ||
window.webkitAudioContext;
if (!AudioContextClass) {
throw new Error(
"Your browser does not support audio mixing."
);
}
audioContext = new AudioContextClass();
if (audioContext.state === "suspended") {
await audioContext.resume();
}
audioDestination =
audioContext.createMediaStreamDestination();
/*
* Add the tab audio to the final audio track.
*/
const tabAudioTracks =
displayStream.getAudioTracks();
if (tabAudioTracks.length > 0) {
const tabAudioStream =
new MediaStream(tabAudioTracks);
tabAudioSource =
audioContext.createMediaStreamSource(
tabAudioStream
);
tabGainNode = audioContext.createGain();
tabGainNode.gain.value = tabVolume;
tabAudioSource.connect(tabGainNode);
tabGainNode.connect(audioDestination);
} else {
console.warn(
"The selected tab does not contain an audio track. " +
'Make sure "Share tab audio" is enabled.'
);
}
/*
* Add the microphone audio to the final audio track.
*/
const microphoneAudioTracks =
microphoneStream.getAudioTracks();
if (microphoneAudioTracks.length > 0) {
const microphoneAudioStream =
new MediaStream(microphoneAudioTracks);
microphoneAudioSource =
audioContext.createMediaStreamSource(
microphoneAudioStream
);
microphoneGainNode =
audioContext.createGain();
microphoneGainNode.gain.value =
microphoneVolume;
microphoneAudioSource.connect(
microphoneGainNode
);
microphoneGainNode.connect(
audioDestination
);
}
/*
* Do not connect the microphone to
* audioContext.destination.
*
* Doing that could make your voice play through the
* speakers and cause echo or feedback.
*/
}
async function start(quality = "medium") {
if (
mediaRecorder &&
mediaRecorder.state !== "inactive"
) {
throw new Error(
"A recording is already running."
);
}
const profile = qualityProfiles[quality];
if (!profile) {
throw new Error(
`Invalid video quality: ${quality}`
);
}
await prepareSources();
const videoTrack =
displayStream.getVideoTracks()[0];
try {
await videoTrack.applyConstraints({
width: {
ideal: profile.width
},
height: {
ideal: profile.height
},
frameRate: {
ideal: profile.frameRate,
max: profile.frameRate
}
});
} catch (error) {
console.warn(
"The browser could not apply all video settings:",
error
);
}
await createAudioMixer();
const mixedAudioTracks =
audioDestination.stream.getAudioTracks();
recordingStream = new MediaStream([
...displayStream.getVideoTracks(),
...mixedAudioTracks
]);
const mimeType = getSupportedMimeType();
const options = {
videoBitsPerSecond:
profile.videoBitsPerSecond,
audioBitsPerSecond: 192000
};
if (mimeType) {
options.mimeType = mimeType;
}
recordedChunks = [];
mediaRecorder = new MediaRecorder(
recordingStream,
options
);
mediaRecorder.addEventListener(
"dataavailable",
event => {
if (event.data && event.data.size > 0) {
recordedChunks.push(event.data);
}
}
);
mediaRecorder.addEventListener(
"stop",
async () => {
const finalMimeType =
mediaRecorder.mimeType ||
mimeType ||
"video/webm";
const blob = new Blob(
recordedChunks,
{
type: finalMimeType
}
);
downloadRecording(
blob,
finalMimeType
);
recordedChunks = [];
recordingStream = null;
await closeAudioMixer();
if (stopPromiseResolver) {
stopPromiseResolver(blob);
stopPromiseResolver = null;
}
}
);
mediaRecorder.addEventListener(
"error",
event => {
console.error(
"Recording error:",
event.error
);
}
);
// Generate a recording chunk every second.
mediaRecorder.start(1000);
return {
state: mediaRecorder.state,
quality,
mimeType: mediaRecorder.mimeType,
hasTabAudio:
displayStream.getAudioTracks().length > 0,
hasMicrophoneAudio:
microphoneStream.getAudioTracks().length > 0
};
}
function pause() {
if (mediaRecorder?.state !== "recording") {
throw new Error(
"There is no active recording to pause."
);
}
mediaRecorder.pause();
}
function resume() {
if (mediaRecorder?.state !== "paused") {
throw new Error(
"There is no paused recording to resume."
);
}
mediaRecorder.resume();
}
function stopAndSave() {
if (
!mediaRecorder ||
mediaRecorder.state === "inactive"
) {
return Promise.resolve(null);
}
return new Promise(resolve => {
stopPromiseResolver = resolve;
mediaRecorder.stop();
});
}
function setMicrophoneVolume(volume) {
microphoneVolume = Math.max(
0,
Math.min(Number(volume), 2)
);
if (microphoneGainNode) {
microphoneGainNode.gain.value =
microphoneVolume;
}
}
function setTabVolume(volume) {
tabVolume = Math.max(
0,
Math.min(Number(volume), 2)
);
if (tabGainNode) {
tabGainNode.gain.value = tabVolume;
}
}
function muteMicrophone() {
setMicrophoneVolume(0);
}
function unmuteMicrophone() {
setMicrophoneVolume(1);
}
function muteTab() {
setTabVolume(0);
}
function unmuteTab() {
setTabVolume(1);
}
async function closeAudioMixer() {
try {
tabAudioSource?.disconnect();
microphoneAudioSource?.disconnect();
tabGainNode?.disconnect();
microphoneGainNode?.disconnect();
} catch (error) {
console.warn(
"An audio node could not be disconnected:",
error
);
}
if (
audioContext &&
audioContext.state !== "closed"
) {
await audioContext.close();
}
audioContext = null;
audioDestination = null;
tabAudioSource = null;
microphoneAudioSource = null;
tabGainNode = null;
microphoneGainNode = null;
}
async function stopMicrophone() {
microphoneStream
?.getTracks()
.forEach(track => track.stop());
microphoneStream = null;
}
async function finishCapture() {
if (
mediaRecorder &&
mediaRecorder.state !== "inactive"
) {
await stopAndSave();
}
displayStream
?.getTracks()
.forEach(track => track.stop());
microphoneStream
?.getTracks()
.forEach(track => track.stop());
displayStream = null;
microphoneStream = null;
recordingStream = null;
mediaRecorder = null;
await closeAudioMixer();
}
function getState() {
return {
recordingState:
mediaRecorder?.state || "inactive",
sharingTab:
streamIsActive(displayStream),
usingMicrophone:
streamIsActive(microphoneStream),
hasTabAudio:
displayStream
?.getAudioTracks()
.some(
track =>
track.readyState === "live"
) || false,
microphoneVolume,
tabVolume
};
}
return {
start,
pause,
resume,
stopAndSave,
finishCapture,
setMicrophoneVolume,
setTabVolume,
muteMicrophone,
unmuteMicrophone,
muteTab,
unmuteTab,
getState
};
})();
/*
// Start recording in high quality.
// The browser requests permission for the tab and microphone.
await TabRecorder.start("high");
// Reduce the tab audio to 70%.
TabRecorder.setTabVolume(0.7);
// Increase the microphone volume slightly.
TabRecorder.setMicrophoneVolume(1.2);
// Temporarily mute the microphone.
TabRecorder.muteMicrophone();
// Enable the microphone again.
TabRecorder.unmuteMicrophone();
// Pause the video.
TabRecorder.pause();
// Continue the same video.
TabRecorder.resume();
// Stop and automatically download the recording.
await TabRecorder.stopAndSave();
// Start another recording with a different quality.
await TabRecorder.start("medium");
// Stop the recording and completely stop sharing.
await TabRecorder.finishCapture();
*/