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.