I have fully working code which starts, stop recording audio from a tab. It also plays and download the most recent recorded audio. I tested it both Youtube and Google Meet. It captures the speaker output but when I try to capture mic audio from Google Meet, it fails. What should I be modifying in the code so that it captures both mic as well speaker audio?
Manifest.json
{
"name": "Audio Recorder",
"description": "Records audio (speaker + Mic) of current tab in an offscreen document.",
"version": "1",
"manifest_version": 3,
"minimum_chrome_version": "116",
"action": {
"default_popup": "popup.html",
"default_icon": "not-recording.png"
},
"permissions": ["offscreen", "contextMenus", "activeTab", "tabCapture", "tabs", "storage", "cookies"]
}
offscreen.js
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if (message.target === 'offscreen') {
switch (message.type) {
case 'start-recording':
startRecording(message.data);
break;
case 'stop-recording':
stopRecording();
break;
case 'play-audio':
playAudio();
break;
case 'download-audio':
downloadAudio();
break;
default:
throw new Error('Unrecognized message:', message.type);
}
} else if (message.type === 'check-audio') {
sendResponse({ audioAvailable: audioBlobs.length > 0 });
return true;
}
});
let recorder;
let data = [];
let audioBlobs = []; // Array to store audio blobs
async function startRecording(streamId) {
if (recorder?.state === 'recording') {
throw new Error('Called startRecording while recording is in progress.');
}
// Request tab audio
const tabAudio = await navigator.mediaDevices.getUserMedia({
audio: {
mandatory: {
chromeMediaSource: 'tab',
chromeMediaSourceId: streamId
}
}
});
let micAudio;
try {
// Attempt to request microphone audio
micAudio = await navigator.mediaDevices.getUserMedia({
audio: true // Request microphone audio
});
} catch (error) {
console.warn('Microphone not available or permission denied:', error);
micAudio = null; // Set micAudio to null if not available
}
// Combine the audio streams if microphone is available
const combinedStream = micAudio
? new MediaStream([...tabAudio.getAudioTracks(), ...micAudio.getAudioTracks()])
: tabAudio; // Use only tab audio if mic is not available
const output = new AudioContext();
const source = output.createMediaStreamSource(combinedStream);
source.connect(output.destination);
recorder = new MediaRecorder(combinedStream, { mimeType: 'audio/webm' });
recorder.ondataavailable = (event) => data.push(event.data);
recorder.onstop = () => {
const blob = new Blob(data, { type: 'audio/webm' });
audioBlobs.push(blob); // Store the blob in the array
data = [];
};
recorder.start();
window.location.hash = 'recording';
}
async function stopRecording() {
recorder.stop();
recorder.stream.getTracks().forEach((t) => t.stop());
window.location.hash = '';
}
function playAudio() {
if (audioBlobs.length > 0) {
const lastBlob = audioBlobs[audioBlobs.length - 1];
const audioUrl = URL.createObjectURL(lastBlob);
const audio = new Audio(audioUrl);
audio.play();
} else {
console.error('No audio available to play.');
}
}
function downloadAudio() {
if (audioBlobs.length > 0) {
const lastBlob = audioBlobs[audioBlobs.length - 1];
const audioUrl = URL.createObjectURL(lastBlob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = audioUrl;
a.download = 'recording.webm';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} else {
console.error('No audio available to download.');
}
}
popup.js
document.addEventListener('DOMContentLoaded', () => {
const isRecording = localStorage.getItem('isRecording') === 'true';
document.getElementById('startButton').disabled = isRecording;
document.getElementById('stopButton').disabled = !isRecording;
// Check if there are any audio recordings stored
chrome.runtime.sendMessage({ type: 'check-audio' }, (response) => {
const audioAvailable = response.audioAvailable;
document.getElementById('playButton').disabled = !audioAvailable;
document.getElementById('downloadButton').disabled = !audioAvailable;
});
});
document.getElementById('startButton').addEventListener('click', async () => {
document.getElementById('startButton').disabled = true;
document.getElementById('stopButton').disabled = false;
document.getElementById('playButton').disabled = true;
document.getElementById('downloadButton').disabled = true;
localStorage.setItem('isRecording', 'true');
const existingContexts = await chrome.runtime.getContexts({});
let offscreenDocument = existingContexts.find(
(c) => c.contextType === 'OFFSCREEN_DOCUMENT'
);
if (!offscreenDocument) {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['USER_MEDIA'],
justification: 'Recording from chrome.tabCapture API'
});
}
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const streamId = await chrome.tabCapture.getMediaStreamId({ targetTabId: tab.id });
chrome.runtime.sendMessage({
type: 'start-recording',
target: 'offscreen',
data: streamId
});
});
document.getElementById('stopButton').addEventListener('click', () => {
document.getElementById('startButton').disabled = false;
document.getElementById('stopButton').disabled = true;
chrome.runtime.sendMessage({
type: 'stop-recording',
target: 'offscreen'
}, () => {
chrome.runtime.sendMessage({ type: 'check-audio' }, (response) => {
const audioAvailable = response.audioAvailable;
document.getElementById('playButton').disabled = !audioAvailable;
document.getElementById('downloadButton').disabled = !audioAvailable;
});
});
localStorage.setItem('isRecording', 'false');
});
document.getElementById('playButton').addEventListener('click', () => {
chrome.runtime.sendMessage({
type: 'play-audio',
target: 'offscreen'
});
});
document.getElementById('downloadButton').addEventListener('click', () => {
chrome.runtime.sendMessage({
type: 'download-audio',
target: 'offscreen'
});
});