feat(audio): migrate from ScriptProcessorNode to AudioWorkletNode for low-latency broadcasting

- Implemented `BroadcasterProcessor` for audio processing in a separate audio thread.
- Replaced deprecated `ScriptProcessorNode` with `AudioWorkletNode` in `startBroadcast`.
- Enhanced audio performance by reducing main thread interference and improving scalability.
- Added `broadcaster-processor.js` to handle custom audio processing logic.

This change ensures compatibility with modern browsers and improves broadcast audio quality.
This commit is contained in:
Raven Scott 2024-11-23 03:23:47 -05:00
parent f07b3fac56
commit d3e3e92f9b
3 changed files with 49 additions and 11 deletions

26
app.js
View File

@ -86,32 +86,38 @@ async function startBroadcast() {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Load and register the audio worklet processor
await audioContext.audioWorklet.addModule('broadcaster-processor.js');
micStream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: currentDeviceId ? { exact: currentDeviceId } : undefined },
});
const source = audioContext.createMediaStreamSource(micStream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
source.connect(processor);
processor.connect(audioContext.destination);
// Create AudioWorkletNode
const broadcasterNode = new AudioWorkletNode(audioContext, 'broadcaster-processor');
source.connect(broadcasterNode);
processor.onaudioprocess = (event) => {
const audioData = event.inputBuffer.getChannelData(0);
const buffer = b4a.from(new Float32Array(audioData).buffer);
// Send audio data to all connections
// Handle audio data
broadcasterNode.port.onmessage = (event) => {
const buffer = event.data;
for (const conn of conns) {
conn.write(buffer);
}
};
broadcasterNode.connect(audioContext.destination); // Optional monitoring
isBroadcasting = true;
console.log("Broadcasting started.");
console.log("Broadcasting started with AudioWorklet.");
} catch (err) {
console.error("Error accessing microphone:", err);
console.error("Error accessing microphone or setting up broadcast:", err);
}
}
// Function to stop broadcasting and clean up resources
function stopBroadcast() {
if (!isBroadcasting) return;

19
broadcaster-processor.js Normal file
View File

@ -0,0 +1,19 @@
class BroadcasterProcessor extends AudioWorkletProcessor {
process(inputs, outputs) {
const input = inputs[0];
const output = outputs[0];
if (input && output) {
for (let channel = 0; channel < input.length; ++channel) {
const inputChannel = input[channel];
const outputChannel = output[channel];
for (let i = 0; i < inputChannel.length; ++i) {
outputChannel[i] = inputChannel[i];
}
}
}
return true; // Keep the processor alive
}
}
registerProcessor('broadcaster-processor', BroadcasterProcessor);

View File

@ -24,6 +24,18 @@
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
// Handle audio stream here
console.log("Microphone access granted:", stream);
})
.catch(error => {
console.error("Microphone access denied:", error);
});
});
</script>
</head>
<body class="bg-dark text-light">
<div id="titlebar">
@ -31,6 +43,7 @@
</div>
<div class="container mt-5 text-center">
<h1>pearCast</h1>
<div id="retry-message-bar" class="alert alert-warning d-none" role="alert"></div>
<div id="setup" class="btn-group mt-4">
<button id="create-station" class="btn btn-primary">Create Station</button>
<button id="open-join-modal" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#joinModal">Join Station</button>