I am trying to record audio from a speaker only using TypeScript or JavaScript.
Expected: it is recording audio from a speaker only, not a microphone.
Actual: it is recording audio from a microphone
What is the problem in my code?
let audioChunks: any = [];
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
audioChunks = [];
let rec = new MediaRecorder(stream);
rec.ondataavailable = e => {
audioChunks.push(e.data);
if (rec.state == "inactive") {
let blob = new Blob(audioChunks, { type: 'audio/x-mpeg-3' });
downloadFile(blob, "filename.wav");
}
}
rec.start();
setTimeout(() => {
rec.stop();
}, 10000);
})
.catch(e => console.log(e));
Circa 2025 you can use getDisplayMedia(). See below.
Capturing speakers has been possible for years on Firefox. Not on Chromium based browsers on Linux, by conventional means, for 5 years.
Previously, at least on Linux on Chromium-based browsers, it was necessary to set an input device to an output device. See Chromium does not support capture of monitor devices by default #17 for some history and workarounds.
In brief, on Linux, with Pulse Audio, something like this
pactl load-module module-remap-source \
master=@DEFAULT_MONITOR@ \
source_name=speakers source_properties=device.description=Speakers \
&& pactl set-default-source speakers
the you can do something like this
navigator.mediaDevices.getUserMedia({audio: true})
.then(async stream => {
const [track] = stream.getAudioTracks();
const devices = await navigator.mediaDevices.enumerateDevices();
const device = devices.find(({label}) => label === "speakers");
if (track.getSettings().deviceId === device.deviceId) {
return stream;
} else {
track.stop();
console.log(devices, device);
return navigator.mediaDevices.getUserMedia({audio: {deviceId: {exact: device.deviceId}}});
}
})
.then(stream => {
const recorder = new MediaRecorder(stream);
// Do stuff with output of speakers
taking that a step further we can capture the audio output of specific devices,
pactl load-module module-combine-sink \
sink_name=Web_Speech_Sink slaves=$(pacmd list-sinks | grep -A1 "* index" | grep -oP "<\K[^ >]+") \
sink_properties=device.description="Web_Speech_Stream" \
format=s16le \
channels=1 \
rate=22050
pactl load-module module-remap-source \
master=Web_Speech_Sink.monitor \
source_name=Web_Speech_Monitor \
source_properties=device.description=Web_Speech_Output
pactl move-sink-input $(pacmd list-sink-inputs | tac | perl -E'undef$/;$_=<>;/speech-dispatcher-espeak-ng.*?index: (\d+)\n/s;say $1') Web_Speech_Sink
navigator.mediaDevices.getUserMedia({audio: true})
.then(async stream => {
const [track] = stream.getAudioTracks();
const devices = await navigator.mediaDevices.enumerateDevices();
const device = devices.find(({label}) => label === 'Web_Speech_Output');
if (track.getSettings().deviceId === device.deviceId) {
return stream;
} else {
track.stop();
console.log(devices, device);
return navigator.mediaDevices.getUserMedia({audio: {deviceId: {exact: device.deviceId}}});
}
})
.then(stream => {
const recorder = new MediaRecorder(stream);
recorder.ondataavailable = e => console.log(URL.createObjectURL(e.data));
const synth = speechSynthesis;
const u = new SpeechSynthesisUtterance('test');
u.onstart = e => {
recorder.start();
console.log(e);
}
u.onend = e => {
recorder.stop();
recorder.stream.getTracks().forEach(track => track.stop());
console.log(e);
}
synth.speak(u);
});
This repository was created by myself to create and demonstrate different ways to capture system audio in the brower captureSystemAudio.
It's possible to finally use getDisplayMedia() to capture speakers, without using extensions or system audio settings for workarounds; see the gist I wrote here Finally possible to capture monitor devices in Chromium and Chrome on Linux. Something like this
// It is finally possible to capture speechSynthesis.speak() on Chromium and Chrome
// Enable Speech Dispatcher, PulseAudio loopback for screen capture, disable default WebRTC input volume adjustment from 100% to 8%
// chrome --enable-speech-dispatcher --enable-features=PulseaudioLoopbackForScreenShare --disable-features=WebRtcAllowInputVolumeAdjustment
// Still have to manually select share system audio in picker with systemAudio set to "include"
// https://issues.chromium.org/issues/40155218
let stream = await navigator.mediaDevices.getDisplayMedia({
// We're not going to be using the video track
video: {
width: 0,
height: 0,
frameRate: 0,
displaySurface: "monitor"
},
audio: {
suppressLocalAudioPlayback: false,
// Speech synthesis audio output is generally 1 channel
channelCount: 2,
noiseSuppression: false,
autoGainControl: false,
echoCancellation: false
},
systemAudio: "include",
// Doesn't work for Tab capture
// preferCurrentTab: true
});
function log(e, ...args) {
if (e?.target) {
console.log(e.target.constructor.name, e.type);
} else {
console.log(...args);
}
};
let [videoTrack] = stream.getVideoTracks();
videoTrack.stop();
let [audioTrack] = stream.getAudioTracks();
log(null, audioTrack.constructor.name, audioTrack.kind, audioTrack.getSettings().deviceId);
let recorder = new MediaRecorder(stream);
recorder.onstart = log;
recorder.onstop = (e) => {
recorder.stream.getTracks().forEach((track) => track.stop());
log(e);
};
recorder.ondataavailable = (e) => {
console.log(URL.createObjectURL(e.data));
log(e);
};
let utterance = new SpeechSynthesisUtterance(`von Braun believed in testing. I cannot
emphasize that term enough – test, test,
test. Test to the point it breaks.
- Ed Buckbee, NASA Public Affairs Officer, Chasing the Moon`);
utterance.onstart = (e) => {
recorder.start();
log(e);
};
utterance.onend = (e) => {
recorder.stop();
log(e);
};
globalThis.speechSynthesis.speak(utterance);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With