JavaScript APIsFeatured

JavaScript WebRTC: Real-Time Communication in the Browser

Master WebRTC for building real-time communication applications. Learn peer-to-peer connections, media streaming, data channels, and signaling implementation.

By JavaScript Document Team
webrtcweb-apisreal-timevideopeer-to-peer

WebRTC (Web Real-Time Communication) enables peer-to-peer communication of audio, video, and data directly between browsers without plugins. It's the foundation for video conferencing, screen sharing, and real-time collaboration applications.

Understanding WebRTC

WebRTC consists of three main APIs: MediaStream (getUserMedia), RTCPeerConnection, and RTCDataChannel, working together to enable real-time communication.

Basic WebRTC Setup

// Get user media
async function getUserMedia() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true,
    });

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

    return stream;
  } catch (error) {
    console.error('Error accessing media devices:', error);
  }
}

// Create peer connection
function createPeerConnection() {
  const configuration = {
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' },
      { urls: 'stun:stun1.l.google.com:19302' },
    ],
  };

  const pc = new RTCPeerConnection(configuration);

  // Handle ICE candidates
  pc.onicecandidate = (event) => {
    if (event.candidate) {
      // Send candidate to remote peer
      sendSignal({
        type: 'ice-candidate',
        candidate: event.candidate,
      });
    }
  };

  // Handle remote stream
  pc.ontrack = (event) => {
    const remoteVideo = document.getElementById('remoteVideo');
    remoteVideo.srcObject = event.streams[0];
  };

  return pc;
}

// Basic connection flow
async function initiateCall() {
  const localStream = await getUserMedia();
  const pc = createPeerConnection();

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

  // Create offer
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);

  // Send offer to remote peer
  sendSignal({
    type: 'offer',
    offer: offer,
  });
}

// Handle incoming offer
async function handleOffer(offer) {
  const localStream = await getUserMedia();
  const pc = createPeerConnection();

  // Add local stream
  localStream.getTracks().forEach((track) => {
    pc.addTrack(track, localStream);
  });

  // Set remote description
  await pc.setRemoteDescription(offer);

  // Create answer
  const answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);

  // Send answer to remote peer
  sendSignal({
    type: 'answer',
    answer: answer,
  });
}

Advanced Media Constraints

// Detailed media constraints
const mediaConstraints = {
  video: {
    width: { min: 640, ideal: 1280, max: 1920 },
    height: { min: 480, ideal: 720, max: 1080 },
    frameRate: { min: 15, ideal: 30, max: 60 },
    facingMode: 'user', // 'user' for front camera, 'environment' for back
    aspectRatio: 16 / 9,
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    sampleRate: 44100,
    sampleSize: 16,
    channelCount: 2,
    autoGainControl: true,
  },
};

// Get display media for screen sharing
async function getScreenShare() {
  try {
    const displayStream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: 'always',
        displaySurface: 'monitor', // 'monitor', 'window', 'application', 'browser'
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        sampleRate: 44100,
      },
    });

    return displayStream;
  } catch (error) {
    console.error('Error sharing screen:', error);
  }
}

// Device enumeration
async function getAvailableDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();

  const videoDevices = devices.filter((device) => device.kind === 'videoinput');
  const audioDevices = devices.filter((device) => device.kind === 'audioinput');
  const audioOutputs = devices.filter(
    (device) => device.kind === 'audiooutput'
  );

  return { videoDevices, audioDevices, audioOutputs };
}

// Switch camera
async function switchCamera(currentStream, deviceId) {
  // Stop current video track
  currentStream.getVideoTracks().forEach((track) => track.stop());

  // Get new stream with specific device
  const newStream = await navigator.mediaDevices.getUserMedia({
    video: { deviceId: { exact: deviceId } },
    audio: false,
  });

  return newStream;
}

Practical Applications

Video Chat Application

class VideoChat {
  constructor() {
    this.localStream = null;
    this.remoteStream = null;
    this.peerConnection = null;
    this.dataChannel = null;
    this.signalingServer = new WebSocket('wss://signaling.example.com');

    this.init();
  }

  async init() {
    // Set up signaling
    this.signalingServer.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleSignalingMessage(message);
    };

    this.signalingServer.onopen = () => {
      console.log('Connected to signaling server');
    };

    // Get user media
    await this.setupLocalStream();
  }

  async setupLocalStream() {
    try {
      this.localStream = await navigator.mediaDevices.getUserMedia({
        video: {
          width: { ideal: 1280 },
          height: { ideal: 720 },
        },
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
        },
      });

      const localVideo = document.getElementById('localVideo');
      localVideo.srcObject = this.localStream;
    } catch (error) {
      console.error('Failed to get user media:', error);
    }
  }

  createPeerConnection() {
    const config = {
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        {
          urls: 'turn:turnserver.example.com:3478',
          username: 'user',
          credential: 'password',
        },
      ],
      iceCandidatePoolSize: 10,
    };

    this.peerConnection = new RTCPeerConnection(config);

    // Add local stream
    this.localStream.getTracks().forEach((track) => {
      this.peerConnection.addTrack(track, this.localStream);
    });

    // Handle remote stream
    this.peerConnection.ontrack = (event) => {
      console.log('Received remote track');
      const remoteVideo = document.getElementById('remoteVideo');
      remoteVideo.srcObject = event.streams[0];
      this.remoteStream = event.streams[0];
    };

    // Handle ICE candidates
    this.peerConnection.onicecandidate = (event) => {
      if (event.candidate) {
        this.sendSignal({
          type: 'ice-candidate',
          candidate: event.candidate,
        });
      }
    };

    // Handle connection state
    this.peerConnection.onconnectionstatechange = () => {
      console.log('Connection state:', this.peerConnection.connectionState);
      this.updateConnectionStatus(this.peerConnection.connectionState);
    };

    // Set up data channel
    this.setupDataChannel();
  }

  setupDataChannel() {
    // Create data channel
    this.dataChannel = this.peerConnection.createDataChannel('chat', {
      ordered: true,
    });

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

    this.dataChannel.onmessage = (event) => {
      this.displayMessage(event.data, 'remote');
    };

    // Handle incoming data channel
    this.peerConnection.ondatachannel = (event) => {
      const channel = event.channel;
      channel.onopen = () => console.log('Received data channel');
      channel.onmessage = (e) => this.displayMessage(e.data, 'remote');
    };
  }

  async startCall() {
    this.createPeerConnection();

    try {
      const offer = await this.peerConnection.createOffer({
        offerToReceiveVideo: true,
        offerToReceiveAudio: true,
      });

      await this.peerConnection.setLocalDescription(offer);

      this.sendSignal({
        type: 'offer',
        offer: offer,
      });
    } catch (error) {
      console.error('Failed to create offer:', error);
    }
  }

  async handleSignalingMessage(message) {
    switch (message.type) {
      case 'offer':
        await this.handleOffer(message.offer);
        break;
      case 'answer':
        await this.handleAnswer(message.answer);
        break;
      case 'ice-candidate':
        await this.handleIceCandidate(message.candidate);
        break;
    }
  }

  async handleOffer(offer) {
    if (!this.peerConnection) {
      this.createPeerConnection();
    }

    try {
      await this.peerConnection.setRemoteDescription(offer);

      const answer = await this.peerConnection.createAnswer();
      await this.peerConnection.setLocalDescription(answer);

      this.sendSignal({
        type: 'answer',
        answer: answer,
      });
    } catch (error) {
      console.error('Failed to handle offer:', error);
    }
  }

  async handleAnswer(answer) {
    try {
      await this.peerConnection.setRemoteDescription(answer);
    } catch (error) {
      console.error('Failed to handle answer:', error);
    }
  }

  async handleIceCandidate(candidate) {
    try {
      await this.peerConnection.addIceCandidate(candidate);
    } catch (error) {
      console.error('Failed to add ICE candidate:', error);
    }
  }

  sendSignal(message) {
    if (this.signalingServer.readyState === WebSocket.OPEN) {
      this.signalingServer.send(JSON.stringify(message));
    }
  }

  sendChatMessage(message) {
    if (this.dataChannel && this.dataChannel.readyState === 'open') {
      this.dataChannel.send(message);
      this.displayMessage(message, 'local');
    }
  }

  displayMessage(message, sender) {
    const chatContainer = document.getElementById('chat');
    const messageElement = document.createElement('div');
    messageElement.className = `message ${sender}`;
    messageElement.textContent = message;
    chatContainer.appendChild(messageElement);
  }

  updateConnectionStatus(state) {
    const statusElement = document.getElementById('connectionStatus');
    statusElement.textContent = state;
    statusElement.className = `status ${state}`;
  }

  enableChat() {
    const chatInput = document.getElementById('chatInput');
    const sendButton = document.getElementById('sendButton');

    chatInput.disabled = false;
    sendButton.disabled = false;

    sendButton.onclick = () => {
      const message = chatInput.value.trim();
      if (message) {
        this.sendChatMessage(message);
        chatInput.value = '';
      }
    };
  }

  toggleAudio() {
    const audioTrack = this.localStream.getAudioTracks()[0];
    if (audioTrack) {
      audioTrack.enabled = !audioTrack.enabled;
      return audioTrack.enabled;
    }
  }

  toggleVideo() {
    const videoTrack = this.localStream.getVideoTracks()[0];
    if (videoTrack) {
      videoTrack.enabled = !videoTrack.enabled;
      return videoTrack.enabled;
    }
  }

  async shareScreen() {
    try {
      const screenStream = await navigator.mediaDevices.getDisplayMedia({
        video: true,
        audio: false,
      });

      const screenTrack = screenStream.getVideoTracks()[0];
      const sender = this.peerConnection
        .getSenders()
        .find((s) => s.track && s.track.kind === 'video');

      if (sender) {
        sender.replaceTrack(screenTrack);
      }

      screenTrack.onended = () => {
        // Replace with camera when screen share ends
        const cameraTrack = this.localStream.getVideoTracks()[0];
        sender.replaceTrack(cameraTrack);
      };
    } catch (error) {
      console.error('Failed to share screen:', error);
    }
  }

  endCall() {
    if (this.peerConnection) {
      this.peerConnection.close();
      this.peerConnection = null;
    }

    if (this.localStream) {
      this.localStream.getTracks().forEach((track) => track.stop());
    }

    if (this.remoteStream) {
      this.remoteStream.getTracks().forEach((track) => track.stop());
    }

    // Clear video elements
    document.getElementById('localVideo').srcObject = null;
    document.getElementById('remoteVideo').srcObject = null;
  }
}

File Transfer

class FileTransfer {
  constructor(peerConnection) {
    this.peerConnection = peerConnection;
    this.dataChannel = null;
    this.receivedBuffers = [];
    this.receivedSize = 0;
    this.fileMetadata = null;

    this.setupDataChannel();
  }

  setupDataChannel() {
    // Create data channel with specific configuration
    this.dataChannel = this.peerConnection.createDataChannel('fileTransfer', {
      ordered: true,
      maxPacketLifeTime: 3000,
    });

    this.dataChannel.binaryType = 'arraybuffer';

    this.dataChannel.onopen = () => {
      console.log('File transfer channel opened');
    };

    this.dataChannel.onmessage = (event) => {
      this.handleIncomingData(event.data);
    };

    // Handle incoming data channel
    this.peerConnection.ondatachannel = (event) => {
      if (event.channel.label === 'fileTransfer') {
        this.handleIncomingChannel(event.channel);
      }
    };
  }

  handleIncomingChannel(channel) {
    channel.binaryType = 'arraybuffer';

    channel.onmessage = (event) => {
      this.handleIncomingData(event.data);
    };
  }

  async sendFile(file) {
    const chunkSize = 16384; // 16KB chunks
    const metadata = {
      type: 'metadata',
      fileName: file.name,
      fileSize: file.size,
      fileType: file.type,
    };

    // Send metadata first
    this.dataChannel.send(JSON.stringify(metadata));

    // Read and send file
    const reader = new FileReader();
    let offset = 0;

    const readSlice = () => {
      const slice = file.slice(offset, offset + chunkSize);
      reader.readAsArrayBuffer(slice);
    };

    reader.onload = (e) => {
      this.dataChannel.send(e.target.result);
      offset += e.target.result.byteLength;

      // Update progress
      const progress = (offset / file.size) * 100;
      this.updateProgress('send', progress);

      if (offset < file.size) {
        // Queue next chunk
        if (this.dataChannel.bufferedAmount < chunkSize * 10) {
          readSlice();
        } else {
          // Wait for buffer to clear
          setTimeout(readSlice, 50);
        }
      } else {
        // Send completion signal
        this.dataChannel.send(JSON.stringify({ type: 'complete' }));
      }
    };

    readSlice();
  }

  handleIncomingData(data) {
    if (typeof data === 'string') {
      const message = JSON.parse(data);

      if (message.type === 'metadata') {
        this.fileMetadata = message;
        this.receivedBuffers = [];
        this.receivedSize = 0;
        this.prepareDownload(message);
      } else if (message.type === 'complete') {
        this.completeDownload();
      }
    } else {
      // Binary data
      this.receivedBuffers.push(data);
      this.receivedSize += data.byteLength;

      // Update progress
      const progress = (this.receivedSize / this.fileMetadata.fileSize) * 100;
      this.updateProgress('receive', progress);
    }
  }

  prepareDownload(metadata) {
    // Show download UI
    const downloadInfo = document.getElementById('downloadInfo');
    downloadInfo.innerHTML = `
      <p>Receiving: ${metadata.fileName}</p>
      <p>Size: ${this.formatBytes(metadata.fileSize)}</p>
      <progress id="downloadProgress" max="100" value="0"></progress>
    `;
  }

  completeDownload() {
    // Combine received buffers
    const received = new Blob(this.receivedBuffers, {
      type: this.fileMetadata.fileType,
    });

    // Create download link
    const downloadUrl = URL.createObjectURL(received);
    const a = document.createElement('a');
    a.href = downloadUrl;
    a.download = this.fileMetadata.fileName;
    a.textContent = 'Download ' + this.fileMetadata.fileName;

    document.getElementById('downloads').appendChild(a);

    // Auto-download
    a.click();

    // Clean up
    this.receivedBuffers = [];
    this.receivedSize = 0;
    this.fileMetadata = null;
  }

  updateProgress(type, progress) {
    const progressBar = document.getElementById(
      type === 'send' ? 'uploadProgress' : 'downloadProgress'
    );

    if (progressBar) {
      progressBar.value = progress;
    }
  }

  formatBytes(bytes) {
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    if (bytes === 0) return '0 Bytes';
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i];
  }
}

Multi-party Conference

class ConferenceRoom {
  constructor(roomId) {
    this.roomId = roomId;
    this.participants = new Map();
    this.localStream = null;
    this.signalingServer = new WebSocket(
      `wss://signaling.example.com/room/${roomId}`
    );

    this.init();
  }

  async init() {
    // Get local stream
    this.localStream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true,
    });

    // Display local video
    this.displayVideo('local', this.localStream);

    // Set up signaling
    this.signalingServer.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleSignalingMessage(message);
    };

    // Join room
    this.signalingServer.onopen = () => {
      this.sendSignal({ type: 'join', roomId: this.roomId });
    };
  }

  handleSignalingMessage(message) {
    switch (message.type) {
      case 'participant-joined':
        this.handleParticipantJoined(message.participantId);
        break;
      case 'participant-left':
        this.handleParticipantLeft(message.participantId);
        break;
      case 'offer':
        this.handleOffer(message.from, message.offer);
        break;
      case 'answer':
        this.handleAnswer(message.from, message.answer);
        break;
      case 'ice-candidate':
        this.handleIceCandidate(message.from, message.candidate);
        break;
    }
  }

  async handleParticipantJoined(participantId) {
    // Create peer connection for new participant
    const pc = this.createPeerConnection(participantId);
    this.participants.set(participantId, { pc });

    // Create offer
    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);

    this.sendSignal({
      type: 'offer',
      to: participantId,
      offer: offer,
    });
  }

  handleParticipantLeft(participantId) {
    const participant = this.participants.get(participantId);
    if (participant) {
      participant.pc.close();
      this.participants.delete(participantId);
      this.removeVideo(participantId);
    }
  }

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

    // Add local stream
    this.localStream.getTracks().forEach((track) => {
      pc.addTrack(track, this.localStream);
    });

    // Handle remote stream
    pc.ontrack = (event) => {
      this.displayVideo(participantId, event.streams[0]);
    };

    // Handle ICE candidates
    pc.onicecandidate = (event) => {
      if (event.candidate) {
        this.sendSignal({
          type: 'ice-candidate',
          to: participantId,
          candidate: event.candidate,
        });
      }
    };

    return pc;
  }

  async handleOffer(from, offer) {
    const pc = this.createPeerConnection(from);
    this.participants.set(from, { pc });

    await pc.setRemoteDescription(offer);
    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);

    this.sendSignal({
      type: 'answer',
      to: from,
      answer: answer,
    });
  }

  async handleAnswer(from, answer) {
    const participant = this.participants.get(from);
    if (participant) {
      await participant.pc.setRemoteDescription(answer);
    }
  }

  async handleIceCandidate(from, candidate) {
    const participant = this.participants.get(from);
    if (participant) {
      await participant.pc.addIceCandidate(candidate);
    }
  }

  displayVideo(participantId, stream) {
    const container = document.getElementById('videos');

    let videoWrapper = document.getElementById(`video-${participantId}`);
    if (!videoWrapper) {
      videoWrapper = document.createElement('div');
      videoWrapper.id = `video-${participantId}`;
      videoWrapper.className = 'video-wrapper';

      const video = document.createElement('video');
      video.autoplay = true;
      video.playsInline = true;
      if (participantId === 'local') {
        video.muted = true;
      }

      const label = document.createElement('div');
      label.className = 'video-label';
      label.textContent =
        participantId === 'local' ? 'You' : `Participant ${participantId}`;

      videoWrapper.appendChild(video);
      videoWrapper.appendChild(label);
      container.appendChild(videoWrapper);
    }

    const video = videoWrapper.querySelector('video');
    video.srcObject = stream;
  }

  removeVideo(participantId) {
    const videoWrapper = document.getElementById(`video-${participantId}`);
    if (videoWrapper) {
      videoWrapper.remove();
    }
  }

  sendSignal(message) {
    if (this.signalingServer.readyState === WebSocket.OPEN) {
      this.signalingServer.send(JSON.stringify(message));
    }
  }

  toggleAudio() {
    const audioTrack = this.localStream.getAudioTracks()[0];
    if (audioTrack) {
      audioTrack.enabled = !audioTrack.enabled;

      // Notify other participants
      this.sendSignal({
        type: 'audio-status',
        enabled: audioTrack.enabled,
      });

      return audioTrack.enabled;
    }
  }

  toggleVideo() {
    const videoTrack = this.localStream.getVideoTracks()[0];
    if (videoTrack) {
      videoTrack.enabled = !videoTrack.enabled;

      // Notify other participants
      this.sendSignal({
        type: 'video-status',
        enabled: videoTrack.enabled,
      });

      return videoTrack.enabled;
    }
  }

  leave() {
    // Close all peer connections
    this.participants.forEach((participant) => {
      participant.pc.close();
    });

    // Stop local stream
    this.localStream.getTracks().forEach((track) => track.stop());

    // Notify server
    this.sendSignal({ type: 'leave' });

    // Close websocket
    this.signalingServer.close();
  }
}

Connection Quality Monitor

class ConnectionQualityMonitor {
  constructor(peerConnection) {
    this.pc = peerConnection;
    this.stats = {
      video: { bitrate: 0, packetsLost: 0, jitter: 0 },
      audio: { bitrate: 0, packetsLost: 0, jitter: 0 },
      connection: { rtt: 0, state: 'new' },
    };

    this.startMonitoring();
  }

  async startMonitoring() {
    setInterval(() => this.collectStats(), 1000);
  }

  async collectStats() {
    const stats = await this.pc.getStats();

    stats.forEach((report) => {
      if (report.type === 'inbound-rtp') {
        this.processInboundStats(report);
      } else if (report.type === 'outbound-rtp') {
        this.processOutboundStats(report);
      } else if (
        report.type === 'candidate-pair' &&
        report.state === 'succeeded'
      ) {
        this.processConnectionStats(report);
      }
    });

    this.updateUI();
    this.checkQuality();
  }

  processInboundStats(report) {
    const mediaType = report.mediaType;
    if (!this.stats[mediaType]) return;

    // Calculate bitrate
    if (this.lastStats && this.lastStats[report.id]) {
      const timeDiff =
        (report.timestamp - this.lastStats[report.id].timestamp) / 1000;
      const bytesDiff =
        report.bytesReceived - this.lastStats[report.id].bytesReceived;
      this.stats[mediaType].bitrate = Math.round((bytesDiff * 8) / timeDiff);
    }

    // Packet loss
    this.stats[mediaType].packetsLost = report.packetsLost || 0;

    // Jitter
    this.stats[mediaType].jitter = report.jitter || 0;

    // Store for next calculation
    if (!this.lastStats) this.lastStats = {};
    this.lastStats[report.id] = report;
  }

  processOutboundStats(report) {
    // Similar processing for outbound stats
  }

  processConnectionStats(report) {
    this.stats.connection.rtt = report.currentRoundTripTime * 1000; // Convert to ms
    this.stats.connection.state = this.pc.connectionState;
  }

  checkQuality() {
    let quality = 'good';

    // Check video quality
    if (this.stats.video.bitrate < 500000) {
      // Less than 500 kbps
      quality = 'poor';
    } else if (this.stats.video.bitrate < 1000000) {
      // Less than 1 Mbps
      quality = 'fair';
    }

    // Check packet loss
    if (this.stats.video.packetsLost > 10) {
      quality = 'poor';
    }

    // Check RTT
    if (this.stats.connection.rtt > 300) {
      quality = 'poor';
    } else if (this.stats.connection.rtt > 150) {
      quality = quality === 'good' ? 'fair' : quality;
    }

    this.notifyQualityChange(quality);
  }

  notifyQualityChange(quality) {
    const event = new CustomEvent('connectionQuality', {
      detail: { quality, stats: this.stats },
    });

    window.dispatchEvent(event);
  }

  updateUI() {
    const statsElement = document.getElementById('connectionStats');
    if (statsElement) {
      statsElement.innerHTML = `
        <div>Video: ${this.formatBitrate(this.stats.video.bitrate)}</div>
        <div>Audio: ${this.formatBitrate(this.stats.audio.bitrate)}</div>
        <div>RTT: ${Math.round(this.stats.connection.rtt)}ms</div>
        <div>State: ${this.stats.connection.state}</div>
      `;
    }
  }

  formatBitrate(bitrate) {
    if (bitrate > 1000000) {
      return `${(bitrate / 1000000).toFixed(2)} Mbps`;
    } else if (bitrate > 1000) {
      return `${(bitrate / 1000).toFixed(0)} kbps`;
    }
    return `${bitrate} bps`;
  }
}

Best Practices

  1. Always handle errors gracefully
async function safeGetUserMedia(constraints) {
  try {
    return await navigator.mediaDevices.getUserMedia(constraints);
  } catch (error) {
    if (error.name === 'NotFoundError') {
      throw new Error('No camera or microphone found');
    } else if (error.name === 'NotAllowedError') {
      throw new Error(
        'Permission denied. Please allow camera/microphone access'
      );
    } else if (error.name === 'NotReadableError') {
      throw new Error('Device is already in use');
    } else {
      throw error;
    }
  }
}
  1. Clean up resources properly
function cleanup() {
  // Stop all tracks
  if (localStream) {
    localStream.getTracks().forEach((track) => {
      track.stop();
    });
  }

  // Close peer connections
  if (peerConnection) {
    peerConnection.close();
  }

  // Close data channels
  if (dataChannel) {
    dataChannel.close();
  }

  // Clear video elements
  document.querySelectorAll('video').forEach((video) => {
    video.srcObject = null;
  });
}

window.addEventListener('beforeunload', cleanup);
  1. Implement connection retry logic
class ResilientConnection {
  constructor() {
    this.maxRetries = 3;
    this.retryDelay = 1000;
  }

  async connectWithRetry() {
    for (let i = 0; i < this.maxRetries; i++) {
      try {
        await this.connect();
        return;
      } catch (error) {
        console.error(`Connection attempt ${i + 1} failed:`, error);

        if (i < this.maxRetries - 1) {
          await new Promise((resolve) =>
            setTimeout(resolve, this.retryDelay * Math.pow(2, i))
          );
        }
      }
    }

    throw new Error('Failed to establish connection after retries');
  }
}
  1. Optimize media quality based on bandwidth
async function adaptVideoQuality(peerConnection) {
  const stats = await peerConnection.getStats();
  let availableBandwidth = Infinity;

  stats.forEach((report) => {
    if (report.type === 'candidate-pair' && report.availableOutgoingBitrate) {
      availableBandwidth = report.availableOutgoingBitrate;
    }
  });

  const sender = peerConnection
    .getSenders()
    .find((s) => s.track && s.track.kind === 'video');

  if (sender) {
    const params = sender.getParameters();

    if (availableBandwidth < 500000) {
      // Low bandwidth: reduce quality
      params.encodings[0].maxBitrate = 300000;
      params.encodings[0].scaleResolutionDownBy = 2;
    } else if (availableBandwidth < 1000000) {
      // Medium bandwidth
      params.encodings[0].maxBitrate = 800000;
      params.encodings[0].scaleResolutionDownBy = 1.5;
    } else {
      // Good bandwidth
      params.encodings[0].maxBitrate = 2000000;
      params.encodings[0].scaleResolutionDownBy = 1;
    }

    await sender.setParameters(params);
  }
}

Conclusion

WebRTC provides powerful capabilities for real-time communication directly in the browser. By understanding peer connections, media streams, and data channels, you can build sophisticated applications for video conferencing, file sharing, and real-time collaboration. The key to successful WebRTC applications lies in proper signaling implementation, error handling, and connection quality management. As WebRTC continues to evolve, it remains the cornerstone technology for building real-time communication features on the web.