Three.js From Zero · Article s9-05
S9-05 WebRTC Voice
Season 9 · Article 05
WebRTC Voice Chat
Peer-to-peer audio streams between browsers. getUserMedia + RTCPeerConnection + signaling. In-game voice chat without a server relay.
1. getUserMedia
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// stream.getAudioTracks()[0] — local mic
Prompts user. Returns MediaStream. Use as WebRTC input or as Web Audio source.
2. Mic level meter
const ctx = new AudioContext();
const source = ctx.createMediaStreamSource(stream);
const analyser = ctx.createAnalyser();
source.connect(analyser);
const data = new Uint8Array(analyser.fftSize);
function tick() {
analyser.getByteTimeDomainData(data);
let sum = 0;
for (const v of data) sum += Math.abs(v - 128);
const level = sum / data.length / 128; // 0..1
meterEl.style.width = (level * 200) + '%'; // amplified
requestAnimationFrame(tick);
}
3. Live demo — mic level
Click Request mic to test
4. RTCPeerConnection setup
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
// Add local audio track
stream.getAudioTracks().forEach(t => pc.addTrack(t, stream));
// Receive remote tracks
pc.ontrack = (e) => {
remoteAudio.srcObject = e.streams[0];
};
// ICE candidates must cross to other peer
pc.onicecandidate = (e) => {
if (e.candidate) signaling.send('ice', e.candidate);
};
5. Signaling (offer/answer)
// Caller
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send('offer', offer);
// Callee
signaling.on('offer', async (offer) => {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signaling.send('answer', answer);
});
// Caller receives answer
signaling.on('answer', async (answer) => {
await pc.setRemoteDescription(answer);
});
Signaling channel is anything: WebSocket, your own HTTP polling, Firebase Realtime DB. Only needed to bootstrap.
6. STUN / TURN
- STUN: free. Tells your peer your public IP. Works behind most NAT.
- TURN: relay. Needed when STUN fails (symmetric NAT, strict firewall). Paid service (Twilio, Xirsys) or self-host (coturn).
7. In-game voice
- Connect peers in the same "room" (server-decided).
- Pipe each remote stream through a PannerNode positioned at their avatar.
- Mute indicator + push-to-talk.
- Range limit — sound only audible within N meters.
8. Noise suppression
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
noiseSuppression: true,
echoCancellation: true,
autoGainControl: true,
}
});
Browser does the DSP for you. Free.
9. Libraries
- simple-peer: WebRTC API made tolerable.
- Daily.co / Agora: SaaS, handle signaling + TURN.
- LiveKit: open-source SFU (scales past 2 peers).
- PeerJS: dead simple WebRTC.
10. Takeaways
- getUserMedia for mic.
- RTCPeerConnection for peer-to-peer.
- Signaling via any channel (WebSocket easiest).
- STUN free, TURN paid relay.
- Pipe remote streams through Panner for spatial in-game voice.
- Enable browser noise suppression automatically.