feat: room-based calls + AudioWorklet for capture and playback

Rooms:
- URL-based: open /myroom to join a room
- Two clients in same room get bridged through relay
- Input field for room name, also supports URL path and hash
- Each room creates independent relay connections

AudioWorklet (replaces deprecated ScriptProcessorNode):
- capture-processor.js: accumulates mic samples, sends 960-sample frames
- playback-processor.js: pull-based output with 200ms buffer cap
- Falls back to ScriptProcessor if AudioWorklet unavailable
- Eliminates drift: worklet runs on audio thread, not main thread

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 20:16:06 +04:00
parent 722bca0c87
commit 12b6f30f9b
4 changed files with 288 additions and 181 deletions

View File

@@ -0,0 +1,45 @@
// AudioWorklet processor for playing received audio.
// Receives PCM samples from the main thread and outputs them.
class PlaybackProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.buffer = new Float32Array(0);
this.maxBuffered = 48000 / 5; // 200ms max
this.port.onmessage = (e) => {
const incoming = new Float32Array(e.data);
// Append
const newBuf = new Float32Array(this.buffer.length + incoming.length);
newBuf.set(this.buffer);
newBuf.set(incoming, this.buffer.length);
this.buffer = newBuf;
// Cap buffer to prevent drift
if (this.buffer.length > this.maxBuffered) {
this.buffer = this.buffer.slice(this.buffer.length - this.maxBuffered);
}
};
}
process(inputs, outputs, parameters) {
const output = outputs[0];
if (!output || !output[0]) return true;
const out = output[0]; // 128 samples typically
if (this.buffer.length >= out.length) {
out.set(this.buffer.subarray(0, out.length));
this.buffer = this.buffer.slice(out.length);
} else if (this.buffer.length > 0) {
out.set(this.buffer);
for (let i = this.buffer.length; i < out.length; i++) out[i] = 0;
this.buffer = new Float32Array(0);
} else {
for (let i = 0; i < out.length; i++) out[i] = 0;
}
return true;
}
}
registerProcessor('playback-processor', PlaybackProcessor);