WebRTC: Real-Time Communication in the Browser

Hardβ€’

WebRTC enables real-time peer-to-peer communication in browsers. Understanding the architecture, signaling flow, and media handling is crucial for building video conferencing, voice calls, and real-time collaboration features.

Quick Decision Guide

Quick Guide:

Architecture: Peer-to-peer connection with signaling server for offer/answer exchange Media Streams: getUserMedia() for camera/mic, getDisplayMedia() for screen share Signaling: WebSocket or HTTP for exchanging SDP offers/answers and ICE candidates STUN/TURN: STUN for NAT traversal, TURN for relay when direct connection fails Data Channels: Bidirectional data transfer for file sharing, gaming, chat

Result: Direct browser-to-browser communication without plugins or servers in the media path.

WebRTC Architecture

WebRTC Architecture Overview

WebRTC enables direct peer-to-peer communication between browsers. The architecture consists of three main components:

1. Media Capture: Access camera, microphone, screen

2. Signaling: Exchange connection information (not part of WebRTC spec)

3. Peer Connection: Establish and manage peer-to-peer connection

The WebRTC Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Browser   β”‚                    β”‚   Browser   β”‚
β”‚      A      β”‚                    β”‚      B      β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚                                   β”‚
       β”‚ 1. Create offer                   β”‚
       │──────────────────────────────────▢│
       β”‚                                   β”‚
       β”‚ 2. Set local description          β”‚
       β”‚                                   β”‚
       β”‚ 3. Exchange ICE candidates       β”‚
       │◀─────────────────────────────────│
       β”‚                                   β”‚
       β”‚ 4. Create answer                 β”‚
       β”‚                                   β”‚
       β”‚ 5. Set remote description        β”‚
       β”‚                                   β”‚
       β”‚ 6. Connection established        β”‚
       │◀─────────────────────────────────▢│
       β”‚                                   β”‚
       β”‚ 7. Media streams flow             β”‚
       │◀─────────────────────────────────▢│
       β”‚                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜

Key Components

1. RTCPeerConnection

The main API for establishing peer-to-peer connections.

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'turn:turnserver.com', username: 'user', credential: 'pass' }
  ]
});

Configuration:

β€’iceServers: STUN/TURN servers for NAT traversal
β€’iceCandidatePoolSize: Pre-gather ICE candidates
β€’bundlePolicy: Bundle multiple media streams

2. Media Streams

Capture audio/video from user's device.

// Get user media (camera + microphone)
const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
});

// Add stream to peer connection
stream.getTracks().forEach(track => {
  pc.addTrack(track, stream);
});

3. Signaling

Exchange connection information (not part of WebRTC - you implement this).

// Send offer to signaling server
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signalingServer.send({ type: 'offer', offer });

// Receive answer from signaling server
signalingServer.on('answer', async (answer) => {
  await pc.setRemoteDescription(answer);
});

Why Signaling is Needed

WebRTC doesn't specify how peers find each other. You need a signaling server (WebSocket, HTTP, etc.) to:

1. Exchange SDP offers/answers: Connection parameters

2. Exchange ICE candidates: Network addresses

3. Coordinate connection: Who initiates, who responds

Important: Signaling server is only for coordination - media flows directly between peers (peer-to-peer).

Signaling: Offer/Answer Exchange

Signaling Flow

Signaling is the process of exchanging connection information between peers. WebRTC doesn't specify how to do this - you implement it using WebSocket, HTTP, or any other method.

The Offer/Answer Pattern

Step 1: Create Offer (Initiator)

// Peer A creates offer
const pc = new RTCPeerConnection(config);

// Add local media stream
const stream = await getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));

// Create and set local description
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

// Send offer to signaling server
signalingServer.send({
  type: 'offer',
  offer: offer,
  from: 'peerA',
  to: 'peerB'
});

Step 2: Receive Offer and Create Answer (Responder)

// Peer B receives offer
signalingServer.on('offer', async ({ offer, from }) => {
  const pc = new RTCPeerConnection(config);
  
  // Set remote description (offer from peer A)
  await pc.setRemoteDescription(offer);
  
  // Add local media stream
  const stream = await getUserMedia({ video: true, audio: true });
  stream.getTracks().forEach(track => pc.addTrack(track, stream));
  
  // Create and set local description (answer)
  const answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);
  
  // Send answer to signaling server
  signalingServer.send({
    type: 'answer',
    answer: answer,
    from: 'peerB',
    to: 'peerA'
  });
});

Step 3: Receive Answer (Initiator)

// Peer A receives answer
signalingServer.on('answer', async ({ answer }) => {
  await pc.setRemoteDescription(answer);
  // Connection established!
});

ICE Candidate Exchange

ICE (Interactive Connectivity Establishment) candidates are network addresses that peers can use to connect.

Gathering ICE Candidates

// Listen for ICE candidates
pc.onicecandidate = (event) => {
  if (event.candidate) {
    // Send candidate to peer via signaling server
    signalingServer.send({
      type: 'ice-candidate',
      candidate: event.candidate,
      from: 'peerA',
      to: 'peerB'
    });
  } else {
    // All candidates gathered
    console.log('ICE gathering complete');
  }
};

Receiving ICE Candidates

// Receive ICE candidate from peer
signalingServer.on('ice-candidate', async ({ candidate }) => {
  await pc.addIceCandidate(candidate);
});

Connection State Monitoring

// Monitor connection state
pc.onconnectionstatechange = () => {
  console.log('Connection state:', pc.connectionState);
  // States: 'new', 'connecting', 'connected', 'disconnected', 'failed', 'closed'
};

// Monitor ICE connection state
pc.oniceconnectionstatechange = () => {
  console.log('ICE connection state:', pc.iceConnectionState);
  // States: 'new', 'checking', 'connected', 'completed', 'failed', 'disconnected', 'closed'
};

Complete Signaling Example

// Signaling server (WebSocket example)
class SignalingClient {
  constructor(wsUrl) {
    this.ws = new WebSocket(wsUrl);
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };
  }
  
  send(message) {
    this.ws.send(JSON.stringify(message));
  }
  
  on(event, handler) {
    this.handlers = this.handlers || {};
    this.handlers[event] = handler;
  }
  
  handleMessage(message) {
    const handler = this.handlers[message.type];
    if (handler) handler(message);
  }
}

// Usage
const signaling = new SignalingClient('ws://signaling-server.com');

// Send offer
signaling.send({ type: 'offer', offer, from: 'peerA', to: 'peerB' });

// Listen for answer
signaling.on('answer', async ({ answer }) => {
  await pc.setRemoteDescription(answer);
});

Media Streams: Camera, Microphone, Screen

Getting User Media

Camera and Microphone

// Get user media with constraints
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    facingMode: 'user' // or 'environment' for back camera
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  }
});

// Display in video element
const videoElement = document.getElementById('localVideo');
videoElement.srcObject = stream;

Constraints:

β€’width, height: Video resolution
β€’facingMode: Front or back camera
β€’frameRate: Frames per second
β€’echoCancellation, noiseSuppression: Audio quality

Screen Sharing

// Get display media (screen share)
const screenStream = await navigator.mediaDevices.getDisplayMedia({
  video: {
    displaySurface: 'monitor', // or 'window', 'browser'
    cursor: 'always' // or 'never', 'motion'
  },
  audio: true // Share system audio
});

// Display in video element
const screenElement = document.getElementById('screenShare');
screenElement.srcObject = screenStream;

Display Surface Options:

β€’monitor: Entire screen
β€’window: Application window
β€’browser: Browser tab

Adding Media to Peer Connection

// Add tracks to peer connection
stream.getTracks().forEach(track => {
  pc.addTrack(track, stream);
});

// Or add transceiver (more control)
const transceiver = pc.addTransceiver('video', {
  direction: 'sendrecv', // or 'sendonly', 'recvonly', 'inactive'
  streams: [stream]
});

Receiving Remote Media

// Listen for remote tracks
pc.ontrack = (event) => {
  const [remoteStream] = event.streams;
  const remoteVideo = document.getElementById('remoteVideo');
  remoteVideo.srcObject = remoteStream;
};

Controlling Media Tracks

// Mute/unmute audio
const audioTrack = stream.getAudioTracks()[0];
audioTrack.enabled = false; // Mute

// Enable/disable video
const videoTrack = stream.getVideoTracks()[0];
videoTrack.enabled = false; // Disable video

// Stop track (release camera/mic)
videoTrack.stop();

Switching Cameras

// Get available devices
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');

// Switch to specific camera
const newStream = await navigator.mediaDevices.getUserMedia({
  video: { deviceId: { exact: videoDevices[1].deviceId } }
});

// Replace old track
const oldTrack = stream.getVideoTracks()[0];
const newTrack = newStream.getVideoTracks()[0];
oldTrack.stop();
stream.removeTrack(oldTrack);
stream.addTrack(newTrack);

Media Constraints Best Practices

// Adaptive constraints based on network
const getAdaptiveConstraints = (networkQuality) => {
  if (networkQuality === 'poor') {
    return {
      video: { width: 640, height: 480, frameRate: 15 },
      audio: { echoCancellation: true }
    };
  } else if (networkQuality === 'good') {
    return {
      video: { width: 1280, height: 720, frameRate: 30 },
      audio: { echoCancellation: true, noiseSuppression: true }
    };
  }
};

Data Channels: Peer-to-Peer Data Transfer

Data Channels Overview

Data channels enable bidirectional peer-to-peer data transfer. Unlike media streams, data channels are for arbitrary data (files, messages, game state, etc.).

Creating Data Channels

Reliable Data Channel

// Create reliable data channel (ordered, guaranteed delivery)
const dataChannel = pc.createDataChannel('messages', {
  ordered: true, // Messages arrive in order
  maxRetransmits: 3 // Max retransmission attempts
});

// Listen for messages
dataChannel.onmessage = (event) => {
  console.log('Received:', event.data);
};

// Send message
dataChannel.send('Hello, peer!');

Unreliable Data Channel

// Create unreliable data channel (low latency, may drop packets)
const gameChannel = pc.createDataChannel('game-state', {
  ordered: false, // Messages may arrive out of order
  maxRetransmits: 0 // No retransmission (low latency)
});

Receiving Data Channels

// Listen for incoming data channels
pc.ondatachannel = (event) => {
  const dataChannel = event.channel;
  
  dataChannel.onopen = () => {
    console.log('Data channel opened');
  };
  
  dataChannel.onmessage = (event) => {
    console.log('Received:', event.data);
  };
  
  dataChannel.onclose = () => {
    console.log('Data channel closed');
  };
};

Data Channel States

// Monitor data channel state
const states = ['connecting', 'open', 'closing', 'closed'];

dataChannel.onopen = () => {
  console.log('Channel state: open');
  // Channel is ready to send/receive
};

dataChannel.onclose = () => {
  console.log('Channel state: closed');
};

dataChannel.onerror = (error) => {
  console.error('Channel error:', error);
};

File Transfer Example

// Sender: Send file
async function sendFile(file) {
  const reader = new FileReader();
  reader.onload = (event) => {
    const arrayBuffer = event.target.result;
    const chunkSize = 16 * 1024; // 16KB chunks
    
    // Send file metadata first
    dataChannel.send(JSON.stringify({
      type: 'file-start',
      name: file.name,
      size: file.size,
      type: file.type
    }));
    
    // Send file in chunks
    for (let offset = 0; offset < arrayBuffer.byteLength; offset += chunkSize) {
      const chunk = arrayBuffer.slice(offset, offset + chunkSize);
      dataChannel.send(chunk);
    }
    
    // Send file end marker
    dataChannel.send(JSON.stringify({ type: 'file-end' }));
  };
  reader.readAsArrayBuffer(file);
}

// Receiver: Receive file
let receivedChunks = [];
let fileMetadata = null;

dataChannel.onmessage = (event) => {
  if (typeof event.data === 'string') {
    const message = JSON.parse(event.data);
    if (message.type === 'file-start') {
      fileMetadata = message;
      receivedChunks = [];
    } else if (message.type === 'file-end') {
      // Reconstruct file
      const blob = new Blob(receivedChunks, { type: fileMetadata.type });
      const url = URL.createObjectURL(blob);
      // Download file
      const a = document.createElement('a');
      a.href = url;
      a.download = fileMetadata.name;
      a.click();
    }
  } else {
    // Binary chunk
    receivedChunks.push(event.data);
  }
};

Use Cases for Data Channels

1. File Transfer: Send files directly between peers

2. Chat Messages: Real-time text communication

3. Game State: Synchronize game state in multiplayer games

4. Collaborative Editing: Real-time document collaboration

5. Control Signals: Send control commands (mute, video toggle)

Best Practices

β€’Use reliable channels for important data (files, messages)
β€’Use unreliable channels for time-sensitive data (game state, real-time updates)
β€’Chunk large files for efficient transfer
β€’Handle channel state changes gracefully
β€’Implement reconnection logic for dropped connections

STUN and TURN Servers

Why STUN/TURN Servers?

Most devices are behind NAT (Network Address Translation) or firewalls, which makes direct peer-to-peer connections difficult. STUN and TURN servers help establish connections.

STUN Servers

STUN (Session Traversal Utilities for NAT) servers help discover your public IP address.

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' }
  ]
});

What STUN does:

β€’Discovers your public IP address
β€’Determines NAT type
β€’Enables direct peer-to-peer connection when possible
β€’Free to use (public STUN servers available)

When STUN works:

β€’Both peers are on same network
β€’NAT allows direct connections
β€’No symmetric NAT (most common case)

TURN Servers

TURN (Traversal Using Relays around NAT) servers relay traffic when direct connection fails.

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { 
      urls: 'turn:turnserver.com:3478',
      username: 'user',
      credential: 'password'
    }
  ]
});

What TURN does:

β€’Relays media traffic between peers
β€’Works when direct connection fails
β€’Requires server infrastructure (costs money)
β€’Higher latency than direct connection

When TURN is needed:

β€’Symmetric NAT (strict firewall)
β€’Both peers behind restrictive firewalls
β€’Corporate networks with strict policies
β€’Direct connection fails (fallback)

ICE Candidate Types

pc.onicecandidate = (event) => {
  if (event.candidate) {
    console.log('Candidate type:', event.candidate.type);
    // Types: 'host', 'srflx', 'prflx', 'relay'
  }
};

Candidate Types:

β€’host: Local IP address (direct connection)
β€’srflx: Server reflexive (via STUN, public IP)
β€’prflx: Peer reflexive (discovered during connection)
β€’relay: Relayed (via TURN server)

Connection Priority

WebRTC tries connections in order of preference:

1. Host (direct): Fastest, lowest latency

2. Server reflexive (STUN): Good, direct connection via public IP

3. Peer reflexive: Discovered during connection

4. Relay (TURN): Fallback, higher latency

TURN Server Setup

Using a TURN Service

// Example: Using Twilio TURN service
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:global.turn.twilio.com:3478?transport=udp',
      username: 'your-username',
      credential: 'your-credential'
    },
    {
      urls: 'turn:global.turn.twilio.com:3478?transport=tcp',
      username: 'your-username',
      credential: 'your-credential'
    }
  ]
});

Self-Hosted TURN Server

// Using coturn (open-source TURN server)
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:your-turn-server.com:3478',
      username: 'turn-user',
      credential: 'turn-password'
    }
  ]
});

Best Practices

1. Always include STUN servers: Helps with most connections

2. Have TURN fallback: Essential for production (20-30% of connections need it)

3. Use multiple TURN servers: Redundancy and geographic distribution

4. Monitor connection quality: Track which connection type is used

5. Optimize TURN usage: Only use when necessary (costs money)

Cost Considerations

β€’STUN: Free (public servers available)
β€’TURN: Costs money (bandwidth usage)
β€’Strategy: Try STUN first, fallback to TURN only when needed

Complete WebRTC Example

Complete Peer-to-Peer Video Call

Here's a complete example of a WebRTC video call implementation.

HTML

<!DOCTYPE html>
<html>
<head>
  <title>WebRTC Video Call</title>
</head>
<body>
  <video id="localVideo" autoplay muted></video>
  <video id="remoteVideo" autoplay></video>
  <button id="startCall">Start Call</button>
  <button id="endCall">End Call</button>
</body>
</html>

JavaScript

class WebRTCCall {
  constructor() {
    this.pc = null;
    this.localStream = null;
    this.signaling = new SignalingClient('ws://signaling-server.com');
    this.setupSignaling();
  }
  
  async startCall() {
    // 1. Get user media
    this.localStream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    
    // Display local video
    document.getElementById('localVideo').srcObject = this.localStream;
    
    // 2. Create peer connection
    this.pc = new RTCPeerConnection({
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        {
          urls: 'turn:turnserver.com:3478',
          username: 'user',
          credential: 'pass'
        }
      ]
    });
    
    // 3. Add local tracks
    this.localStream.getTracks().forEach(track => {
      this.pc.addTrack(track, this.localStream);
    });
    
    // 4. Handle remote tracks
    this.pc.ontrack = (event) => {
      const [remoteStream] = event.streams;
      document.getElementById('remoteVideo').srcObject = remoteStream;
    };
    
    // 5. Handle ICE candidates
    this.pc.onicecandidate = (event) => {
      if (event.candidate) {
        this.signaling.send({
          type: 'ice-candidate',
          candidate: event.candidate
        });
      }
    };
    
    // 6. Create and send offer
    const offer = await this.pc.createOffer();
    await this.pc.setLocalDescription(offer);
    this.signaling.send({ type: 'offer', offer });
  }
  
  setupSignaling() {
    // Handle incoming offer
    this.signaling.on('offer', async ({ offer }) => {
      if (!this.pc) {
        await this.initializeAsAnswerer();
      }
      await this.pc.setRemoteDescription(offer);
      const answer = await this.pc.createAnswer();
      await this.pc.setLocalDescription(answer);
      this.signaling.send({ type: 'answer', answer });
    });
    
    // Handle incoming answer
    this.signaling.on('answer', async ({ answer }) => {
      await this.pc.setRemoteDescription(answer);
    });
    
    // Handle ICE candidates
    this.signaling.on('ice-candidate', async ({ candidate }) => {
      await this.pc.addIceCandidate(candidate);
    });
  }
  
  async initializeAsAnswerer() {
    // Get user media
    this.localStream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    
    document.getElementById('localVideo').srcObject = this.localStream;
    
    // Create peer connection
    this.pc = new RTCPeerConnection({
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }
      ]
    });
    
    // Add local tracks
    this.localStream.getTracks().forEach(track => {
      this.pc.addTrack(track, this.localStream);
    });
    
    // Handle remote tracks
    this.pc.ontrack = (event) => {
      const [remoteStream] = event.streams;
      document.getElementById('remoteVideo').srcObject = remoteStream;
    };
    
    // Handle ICE candidates
    this.pc.onicecandidate = (event) => {
      if (event.candidate) {
        this.signaling.send({
          type: 'ice-candidate',
          candidate: event.candidate
        });
      }
    };
  }
  
  endCall() {
    // Stop all tracks
    if (this.localStream) {
      this.localStream.getTracks().forEach(track => track.stop());
    }
    
    // Close peer connection
    if (this.pc) {
      this.pc.close();
      this.pc = null;
    }
    
    // Clear video elements
    document.getElementById('localVideo').srcObject = null;
    document.getElementById('remoteVideo').srcObject = null;
  }
}

// Usage
const call = new WebRTCCall();

document.getElementById('startCall').onclick = () => call.startCall();
document.getElementById('endCall').onclick = () => call.endCall();

Error Handling

// Handle connection errors
pc.onconnectionstatechange = () => {
  if (pc.connectionState === 'failed') {
    // Connection failed, try to reconnect
    console.error('Connection failed');
  } else if (pc.connectionState === 'disconnected') {
    // Connection lost, attempt to reconnect
    console.warn('Connection disconnected');
  }
};

// Handle ICE connection errors
pc.oniceconnectionstatechange = () => {
  if (pc.iceConnectionState === 'failed') {
    // ICE failed, restart ICE
    pc.restartIce();
  }
};

// Handle media stream errors
pc.ontrack = (event) => {
  event.track.onended = () => {
    console.log('Track ended');
  };
  
  event.track.onerror = (error) => {
    console.error('Track error:', error);
  };
};

Best Practices

1. Always handle errors: Connection can fail for many reasons

2. Implement reconnection: Network issues are common

3. Monitor connection quality: Track latency, packet loss

4. Clean up resources: Stop tracks, close connections

5. Test with different networks: WiFi, cellular, VPN

6. Use TURN servers in production: Essential for reliability

WebRTC vs Alternatives

FeatureWebRTCWebSocket
Mediaβœ“ Native audio/videoβœ— Text/binary only
Latencyβœ“ Very low (P2P)⚠ Higher (via server)
Bandwidthβœ“ Efficient (P2P)βœ— Server relays all traffic
Complexityβœ— More complexβœ“ Simpler
Use CaseVideo/audio callsReal-time data (chat, games)
FeatureWebRTC (P2P)SFU/MCU
-------------------------------
Scalabilityβœ— Limited (2-10 peers)βœ“ Scales to 100+ participants
Bandwidthβœ“ Efficient (direct)βœ— Server relays all traffic
Costβœ“ Lower (no server)βœ— Higher (server infrastructure)
Quality Controlβœ— Limitedβœ“ Server can adjust quality
Recordingβœ— Client-side onlyβœ“ Server-side recording
FeatureWebRTCPlugins
-------------------------
Browser Supportβœ“ Native (no plugin)βœ— Requires plugin installation
Securityβœ“ Better (sandboxed)βœ— Plugin vulnerabilities
Mobileβœ“ Works on mobileβœ— Limited mobile support
Performanceβœ“ Good⚠ Varies

Best Practices & Common Pitfalls

Best Practices

1. Always Include STUN/TURN Servers

// Good: Multiple STUN servers + TURN fallback
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' },
    {
      urls: 'turn:turnserver.com:3478',
      username: 'user',
      credential: 'pass'
    }
  ]
});

2. Handle Connection State Changes

pc.onconnectionstatechange = () => {
  switch (pc.connectionState) {
    case 'connected':
      console.log('Connected!');
      break;
    case 'disconnected':
      console.log('Disconnected, attempting reconnect...');
      // Implement reconnection logic
      break;
    case 'failed':
      console.error('Connection failed');
      // Restart connection
      break;
  }
};

3. Clean Up Resources

function cleanup() {
  // Stop all media tracks
  if (localStream) {
    localStream.getTracks().forEach(track => track.stop());
  }
  
  // Close peer connection
  if (pc) {
    pc.close();
    pc = null;
  }
  
  // Clear video elements
  localVideo.srcObject = null;
  remoteVideo.srcObject = null;
}

4. Handle Permissions Gracefully

async function getUserMedia() {
  try {
    return await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      // User denied permission
      alert('Please allow camera/microphone access');
    } else if (error.name === 'NotFoundError') {
      // No camera/mic found
      alert('No camera/microphone found');
    } else {
      console.error('Error accessing media:', error);
    }
    throw error;
  }
}

5. Monitor Connection Quality

// Get connection statistics
const stats = await pc.getStats();

stats.forEach(report => {
  if (report.type === 'candidate-pair' && report.selected) {
    console.log('RTT:', report.currentRoundTripTime);
    console.log('Packets sent:', report.packetsSent);
    console.log('Packets received:', report.packetsReceived);
  }
});

Common Pitfalls

1. Not Setting Local Description Before Sending Offer

// ❌ Wrong: Send offer before setting local description
const offer = await pc.createOffer();
signaling.send({ type: 'offer', offer }); // Missing setLocalDescription!

// βœ… Correct: Set local description first
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', offer });

2. Not Handling ICE Candidates Properly

// ❌ Wrong: Only handling first candidate
pc.onicecandidate = (event) => {
  if (event.candidate) {
    signaling.send({ candidate: event.candidate });
    // Missing: Continue listening for more candidates
  }
};

// βœ… Correct: Handle all candidates
pc.onicecandidate = (event) => {
  if (event.candidate) {
    signaling.send({ candidate: event.candidate });
  } else {
    console.log('ICE gathering complete');
  }
};

3. Not Cleaning Up Tracks

// ❌ Wrong: Don't stop tracks
function endCall() {
  pc.close(); // Tracks still active!
}

// βœ… Correct: Stop tracks before closing
function endCall() {
  localStream.getTracks().forEach(track => track.stop());
  pc.close();
}

4. Not Handling Reconnection

// ❌ Wrong: No reconnection logic
pc.onconnectionstatechange = () => {
  if (pc.connectionState === 'failed') {
    console.error('Connection failed');
    // No recovery attempt
  }
};

// βœ… Correct: Implement reconnection
pc.onconnectionstatechange = () => {
  if (pc.connectionState === 'failed') {
    // Restart ICE
    pc.restartIce();
    // Or recreate connection
  }
};

5. Not Testing with Different Networks

// Always test:
// - Same network (LAN)
// - Different networks (internet)
// - Behind NAT/firewall
// - Mobile networks (3G, 4G, 5G)
// - VPN connections

Production Checklist

β€’[ ] Include STUN servers (always)
β€’[ ] Include TURN servers (for production)
β€’[ ] Handle connection state changes
β€’[ ] Implement reconnection logic
β€’[ ] Clean up resources properly
β€’[ ] Handle permissions gracefully
β€’[ ] Monitor connection quality
β€’[ ] Test with different networks
β€’[ ] Handle errors appropriately
β€’[ ] Optimize for mobile devices

Key Takeaways

1WebRTC enables peer-to-peer audio, video, and data communication directly in browsers
2Signaling (offer/answer/ICE exchange) is required but not part of WebRTC spec - you implement it
3STUN servers help discover public IP addresses for direct connections
4TURN servers relay traffic when direct connection fails (essential for production)
5Media streams use getUserMedia() for camera/mic and getDisplayMedia() for screen share
6Data channels enable bidirectional peer-to-peer data transfer (files, messages, game state)
7Always include STUN/TURN servers, handle connection state changes, and clean up resources
8WebRTC is best for 1-on-1 or small group calls; use SFU/MCU for large meetings (10+ participants)