Skip to main content
โšก Calmops

WebRTC: Real-Time Communication for the Web

WebRTC (Web Real-Time Communication) is a browser API that enables real-time peer-to-peer communication including video, audio, and data sharing. This comprehensive guide covers everything you need to know.

What is WebRTC?

WebRTC is a browser API for building real-time communication applications without plugins or external software.

// Basic WebRTC - peer-to-peer video
const pc = new RTCPeerConnection(iceServers);

pc.ontrack = (event) => {
  document.getElementById('video').srcObject = event.streams[0];
};

navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => stream.getTracks().forEach(track => pc.addTrack(track, stream)));

Key Features

  • Video/Audio - Real-time media streaming
    • P2P - Direct peer-to-peer connections
    • Data Channels - Arbitrary data transfer
    • NAT Traversal - ICE, STUN, TURN
    • Encryption - DTLS/SRTP built-in
    • Standards-based - W3C/IETF standard

Core Components

RTCPeerConnection

const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { 
      urls: 'turn:turn.example.com:3478',
      username: 'user',
      credential: 'pass'
    }
  ]
};

const pc = new RTCPeerConnection(config);

// Handle remote tracks
pc.ontrack = (event) => {
  console.log('Received remote track:', event.track.kind);
  const stream = event.streams[0];
  // Add stream to video element
};

// Handle ICE candidates
pc.onicecandidate = (event) => {
  if (event.candidate) {
    // Send candidate to peer via signaling
    sendSignalingMessage({ type: 'candidate', candidate: event.candidate });
  }
};

// Connection state
pc.onconnectionstatechange = () => {
  console.log('Connection state:', pc.connectionState);
};

// ICE connection state
pc.oniceconnectionstatechange = () => {
  console.log('ICE state:', pc.iceConnectionState);
};

Media Streams

// Get user media
async function getUserMedia() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        frameRate: { ideal: 30 }
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true
      }
    });
    
    return stream;
  } catch (error) {
    console.error('Error accessing media devices:', error);
  }
}

// Display local video
const localVideo = document.getElementById('localVideo');
localVideo.srcObject = localStream;
localVideo.muted = true; // Mute local playback

RTCSessionDescription

// Create offer
async function createOffer(pc) {
  const offer = await pc.createOffer({
    offerToReceiveAudio: true,
    offerToReceiveVideo: true
  });
  
  await pc.setLocalDescription(offer);
  return offer;
}

// Create answer
async function createAnswer(pc) {
  const answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);
  return answer;
}

// Set remote description
async function setRemoteDescription(pc, description) {
  await pc.setRemoteDescription(new RTCSessionDescription(description));
}

Signaling

Server Setup (Node.js)

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

const rooms = new Map();

wss.on('connection', (ws) => {
  let roomId = null;
  let clientId = null;
  
  ws.on('message', (message) => {
    const data = JSON.parse(message);
    
    switch (data.type) {
      case 'join':
        roomId = data.roomId;
        clientId = Date.now().toString();
        
        if (!rooms.has(roomId)) {
          rooms.set(roomId, new Set());
        }
        
        rooms.get(roomId).add({ ws, clientId });
        
        // Notify others in room
        broadcastToRoom(roomId, {
          type: 'user-joined',
          clientId
        }, clientId);
        
        // Send existing users to new client
        const existingClients = Array.from(rooms.get(roomId))
          .filter(c => c.clientId !== clientId)
          .map(c => c.clientId);
        
        ws.send(JSON.stringify({
          type: 'room-users',
          users: existingClients
        }));
        break;
        
      case 'offer':
      case 'answer':
      case 'candidate':
        // Forward to specific client
        sendToClient(roomId, data.targetClientId, data);
        break;
        
      case 'leave':
        removeFromRoom(roomId, clientId);
        break;
    }
  });
  
  ws.on('close', () => {
    if (roomId && clientId) {
      removeFromRoom(roomId, clientId);
    }
  });
});

function broadcastToRoom(roomId, message, excludeClientId) {
  if (!rooms.has(roomId)) return;
  
  const data = JSON.stringify(message);
  rooms.get(roomId).forEach(client => {
    if (client.clientId !== excludeClientId && client.ws.readyState === WebSocket.OPEN) {
      client.ws.send(data);
    }
  });
}

function sendToClient(roomId, targetClientId, message) {
  if (!rooms.has(roomId)) return;
  
  const data = JSON.stringify(message);
  rooms.get(roomId).forEach(client => {
    if (client.clientId === targetClientId && client.ws.readyState === WebSocket.OPEN) {
      client.ws.send(data);
    }
  });
}

function removeFromRoom(roomId, clientId) {
  if (!rooms.has(roomId)) return;
  
  rooms.get(roomId).forEach(client => {
    if (client.clientId === clientId) {
      client.ws.send(JSON.stringify({ type: 'user-left', clientId }));
      rooms.get(roomId).delete(client);
    }
  });
}

Client Implementation

class SignalingClient {
  constructor(url, roomId) {
    this.ws = new WebSocket(url);
    this.roomId = roomId;
    this.pc = null;
    this.clientId = null;
    
    this.setupWebSocket();
  }
  
  setupWebSocket() {
    this.ws.onopen = () => {
      this.send({ type: 'join', roomId: this.roomId });
    };
    
    this.ws.onmessage = async (event) => {
      const data = JSON.parse(event.data);
      await this.handleSignalingMessage(data);
    };
  }
  
  async handleSignalingMessage(data) {
    switch (data.type) {
      case 'room-users':
        // Handle existing users
        data.users.forEach(userId => this.createPeerConnection(userId, true));
        break;
        
      case 'user-joined':
        this.createPeerConnection(data.clientId, false);
        break;
        
      case 'offer':
        await this.handleOffer(data);
        break;
        
      case 'answer':
        await this.handleAnswer(data);
        break;
        
      case 'candidate':
        await this.handleCandidate(data);
        break;
        
      case 'user-left':
        this.removePeerConnection(data.clientId);
        break;
    }
  }
  
  async createPeerConnection(peerId, isInitiator) {
    this.pc = new RTCPeerConnection(iceServers);
    
    this.pc.ontrack = (event) => {
      // Display remote stream
      const remoteVideo = document.getElementById('remoteVideo');
      remoteVideo.srcObject = event.streams[0];
    };
    
    // Add local tracks
    const localStream = await getUserMedia();
    localStream.getTracks().forEach(track => {
      this.pc.addTrack(track, localStream);
    });
    
    if (isInitiator) {
      const offer = await this.pc.createOffer();
      await this.pc.setLocalDescription(offer);
      this.send({ type: 'offer', offer, targetClientId: peerId });
    }
  }
  
  async handleOffer(data) {
    await this.pc.setRemoteDescription(data.offer);
    const answer = await this.pc.createAnswer();
    await this.pc.setLocalDescription(answer);
    this.send({ type: 'answer', answer, targetClientId: data.clientId });
  }
  
  async handleAnswer(data) {
    await this.pc.setRemoteDescription(data.answer);
  }
  
  async handleCandidate(data) {
    await this.pc.addIceCandidate(new RTCIceCandidate(data.candidate));
  }
  
  send(message) {
    this.ws.send(JSON.stringify(message));
  }
}

Data Channels

Sending Data

const pc = new RTCPeerConnection(config);

// Create data channel
const dataChannel = pc.createDataChannel('chat', {
  ordered: true, // Guarantee order
  maxRetransmits: 30
});

dataChannel.onopen = () => {
  console.log('Data channel opened');
};

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

// Send message
dataChannel.send(JSON.stringify({
  type: 'message',
  text: 'Hello!',
  timestamp: Date.now()
}));

Receiving Data

pc.ondatachannel = (event) => {
  const receiveChannel = event.channel;
  
  receiveChannel.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('Received:', data);
  };
  
  receiveChannel.onopen = () => {
    console.log('Receive channel opened');
  };
};

Advanced Features

Screen Sharing

async function shareScreen() {
  try {
    const screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: 'always'
      },
      audio: false
    });
    
    // Replace video track
    const videoTrack = screenStream.getVideoTracks()[0];
    const sender = pc.getSenders().find(s => s.track?.kind === 'video');
    
    sender.replaceTrack(videoTrack);
    
    // Handle stop sharing
    videoTrack.onended = () => {
      // Switch back to camera
      navigator.mediaDevices.getUserMedia({ video: true })
        .then(stream => {
          const cameraTrack = stream.getVideoTracks()[0];
          sender.replaceTrack(cameraTrack);
        });
    };
    
  } catch (error) {
    console.error('Error sharing screen:', error);
  }
}

Connection Statistics

async function getStats(pc) {
  const stats = await pc.getStats();
  
  stats.forEach(report => {
    if (report.type === 'inbound-rtp' && report.kind === 'video') {
      console.log('Video bytes received:', report.bytesReceived);
      console.log('Packets lost:', report.packetsLost);
      console.log('Jitter:', report.jitter);
    }
    
    if (report.type === 'outbound-rtp' && report.kind === 'video') {
      console.log('Video bytes sent:', report.bytesSent);
      console.log('Round trip time:', report.roundTripTime);
    }
  });
}

// Monitor every 2 seconds
setInterval(() => getStats(pc), 2000);

Audio/Video Controls

// Mute/unmute
function toggleAudio(enabled) {
  const audioTrack = localStream.getAudioTracks()[0];
  audioTrack.enabled = enabled;
}

function toggleVideo(enabled) {
  const videoTrack = localStream.getVideoTracks()[0];
  videoTrack.enabled = enabled;
}

// Adjust bitrate
async function setVideoBitrate(pc, bitrate) {
  const sender = pc.getSenders().find(s => s.track?.kind === 'video');
  
  const params = sender.getParameters();
  if (!params.encodings) {
    params.encodings = [{}];
  }
  
  params.encodings[0].maxBitrate = bitrate;
  await sender.setParameters(params);
}

Error Handling

pc.oniceconnectionstatechange = () => {
  switch (pc.iceConnectionState) {
    case 'failed':
      console.error('ICE connection failed');
      // Try restarting ICE
      pc.restartIce();
      break;
      
    case 'disconnected':
      console.log('ICE disconnected');
      // Attempt to reconnect
      break;
      
    case 'closed':
      console.log('ICE closed');
      break;
  }
};

pc.oniceerror = (event) => {
  console.error('ICE error:', event);
};

pc.ontrackerror = (event) => {
  console.error('Track error:', event);
};

Browser Support

WebRTC is supported in all modern browsers:

  • Chrome 56+
  • Firefox 22+
  • Safari 11+
  • Edge 12+
  • Opera 43+

External Resources

Conclusion

WebRTC enables powerful real-time communication applications directly in the browser. Key points:

  • Use RTCPeerConnection for media and data channels
  • Implement signaling server for connection setup
  • Handle ICE candidates for NAT traversal
  • Use STUN/TURN servers for production
  • Monitor connection stats for quality

WebRTC powers video chat apps, file sharing, gaming, and more - all without plugins or external software.

Comments