PROBLEM : I am trying to capture the audio for my chrome extension but it is muting the audio for the user.
WHAT I WANT : I want the audio should be available for both the user(speaker output) and for my extension for processing.
I have tried most of the methods none working for me you may also try I might be missing something. I would really appreciate your help.
NOTE: You can reproduce the same below is the complete code.
//manifest.json
{
"name": "Audio Capture Test",
"description": "Records tab audio with transcription.",
"version": "1",
"manifest_version": 3,
"minimum_chrome_version": "116",
"background": {
"service_worker": "service-worker.js",
"type": "module"
},
"action": {
"default_popup": "popup.html",
"default_title": "Capture Audio"
},
"permissions": [
"tabCapture",
"activeTab",
"scripting"
],
"host_permissions": [
"http://*/*",
"https://*/*"
]
}
//popup.js
document.getElementById("startCapture").addEventListener("click", async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) {
alert("start recording called")
chrome.runtime.sendMessage({ type: "start-recording", tabId: tab.id });
}
});
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if (message.type === 'message') {
alert(message.mes)
}
})
//service-worker.js
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if (message.type === "start-recording") {
try {
console.log("Requesting tab media stream...");
const streamId = await chrome.tabCapture.getMediaStreamId({
consumerTabId: message.tabId
});
console.log("Stream ID received:", streamId);
// Inject content script
await chrome.scripting.executeScript({
target: { tabId: message.tabId },
files: ["content.js"]
});
// Send stream ID to content script
await chrome.tabs.sendMessage(message.tabId, {
type: "process-stream",
streamId: streamId
});
} catch (error) {
console.error("Error starting tab capture:", error);
}
}
});
//content.js
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if (message.type === 'process-stream') {
try {
const audioContext = new AudioContext({
sampleRate: 48000,
latencyHint: 'interactive'
});
// Get tab audio stream
let tabStream = await navigator.mediaDevices.getUserMedia({
audio: {
mandatory: {
chromeMediaSource: 'tab',
chromeMediaSourceId: message.streamId
}
},
video: false
});
chrome.runtime.sendMessage({ type: "message", mes: "starting hte capture" });
// Load the AudioWorklet Processor
await audioContext.audioWorklet.addModule("processor.js");
const audioProcessor = new AudioWorkletNode(audioContext, "audio-processor");
const source = audioContext.createMediaStreamSource(tabStream);
source.connect(audioProcessor).connect(audioContext.destination);
} catch (error) {
console.error("Error capturing audio:", error.message || error);
}
}
return true;
});
//processor.js
class AudioProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
if (input.length > 0) {
for (let channel = 0; channel < input.length; channel++) {
output[channel].set(input[channel]); // Forward audio to keep it audible
}
}
return true; // Keep the processor running
}
}
registerProcessor('audio-processor', AudioProcessor);
// popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio Capture</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<button id="startCapture">Start Capture</button>
<script src="popup.js"></script>
</body>
</html>