Lesson 10-Combining WebSocket and WebRTC

WebSocket and WebRTC are two cornerstone technologies for modern real-time communication, each with distinct advantages and use cases. Their integration enables the creation of more robust and versatile real-time communication systems. This document delves into the methods of combining WebSocket and WebRTC, their application scenarios, and detailed implementation approaches.

Comparison of WebSocket and WebRTC Technologies

Technical Characteristics Comparison

FeatureWebSocketWebRTC
Protocol TypeApplication-layer protocol (TCP-based)Application-layer protocol (UDP-based)
Primary UseBidirectional full-duplex communicationPeer-to-peer real-time audio/video/data transfer
Connection EstablishmentVia HTTP upgradeComplex ICE/STUN/TURN negotiation
Data TransmissionReliable transmission (TCP)Best-effort transmission (UDP)
NAT TraversalRelies on application-layer implementationBuilt-in ICE framework support
Applicable ScenariosChat, real-time notifications, game commandsAudio/video calls, screen sharing, P2P data transfer

Complementary Analysis

WebSocket’s Strengths:

  • Scenarios requiring reliable transmission (e.g., chat messages)
  • Scenarios needing simple NAT traversal
  • Scenarios requiring integration with existing HTTP infrastructure
  • Scenarios needing server-side broadcast/multicast

WebRTC’s Strengths:

  • Low-latency audio/video transmission
  • Peer-to-peer data transfer
  • Scenarios bypassing intermediate servers
  • High-bandwidth data transfer (e.g., screen sharing)

Value of Combined Use:

  1. Signaling Channel: WebSocket serves as the signaling channel for WebRTC
  2. Hybrid Communication: WebSocket handles control signaling, while WebRTC manages media streams
  3. NAT Traversal Assistance: WebSocket aids WebRTC’s ICE candidate collection
  4. Server Fallback: WebSocket acts as a fallback when WebRTC P2P connections fail

WebSocket as WebRTC Signaling Channel

Typical Architecture Design

[Client A] ←WebSocket→ [Signaling Server] ←WebSocket→ [Client B]
       ↑ WebRTC               ↑ WebRTC
       ↓                       ↓
[Media Stream/Data Channel]      [Media Stream/Data Channel]

Signaling Message Design

Basic Signaling Message Types:

interface SignalingMessage {
  type: 'offer' | 'answer' | 'ice-candidate' | 'join' | 'leave' | 'error';
  sender: string;       // Sender ID
  target?: string;      // Target ID (optional)
  sdp?: string;         // SDP description (used for offer/answer)
  candidate?: RTCIceCandidate; // ICE candidate (used for ice-candidate)
  roomId?: string;      // Room ID
}

Complete Signaling Flow Example:

  1. Client A connects to the WebSocket server and joins a room
  2. Client B connects to the WebSocket server and joins the same room
  3. The server notifies Client A and B of each other’s presence
  4. Client A creates an offer and sends it to the server
  5. The server forwards the offer to Client B
  6. Client B creates an answer and sends it to the server
  7. The server forwards the answer to Client A
  8. Both parties exchange ICE candidate information
  9. WebRTC connection is established

Implementation Example (Node.js)

Signaling Server Implementation:

const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');

const wss = new WebSocket.Server({ port: 8080 });
const rooms = new Map(); // roomId -> Set<clientId>
const clients = new Map(); // clientId -> WebSocket

wss.on('connection', (ws) => {
  const clientId = uuidv4();
  clients.set(clientId, ws);

  ws.on('message', (message) => {
    try {
      const msg = JSON.parse(message);
      handleSignalingMessage(msg, clientId);
    } catch (error) {
      console.error('Signaling message parsing error:', error);
    }
  });

  ws.on('close', () => {
    // Clean up client resources
    cleanupClient(clientId);
  });
});

function handleSignalingMessage(msg, senderId) {
  switch (msg.type) {
    case 'join':
      handleJoin(msg.roomId, senderId);
      break;
    case 'offer':
    case 'answer':
    case 'ice-candidate':
      forwardMessage(msg);
      break;
    default:
      console.warn('Unknown signaling type:', msg.type);
  }
}

function handleJoin(roomId, clientId) {
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }

  const room = rooms.get(roomId);
  room.add(clientId);

  // Notify other users in the room of the new user
  const otherClients = Array.from(room).filter(id => id !== clientId);
  otherClients.forEach(id => {
    const ws = clients.get(id);
    if (ws) {
      ws.send(JSON.stringify({
        type: 'user-joined',
        userId: clientId,
        roomId
      }));
    }
  });

  // Notify the new user of existing users in the room
  room.forEach(id => {
    if (id !== clientId) {
      const ws = clients.get(id);
      if (ws) {
        clients.get(senderId).send(JSON.stringify({
          type: 'existing-user',
          userId: id,
          roomId
        }));
      }
    }
  });
}

function forwardMessage(msg) {
  // Determine the target client based on message content
  // Simplified handling; real-world scenarios may require more complex routing
  if (msg.target) {
    const ws = clients.get(msg.target);
    if (ws) {
      ws.send(JSON.stringify(msg));
    }
  } else if (msg.roomId) {
    // Broadcast within the room (real-world applications may need more precise routing)
    const room = rooms.get(msg.roomId);
    if (room) {
      room.forEach(id => {
        if (id !== msg.sender) {
          const ws = clients.get(id);
          if (ws) {
            ws.send(JSON.stringify(msg));
          }
        }
      });
    }
  }
}

function cleanupClient(clientId) {
  // Remove the client from all rooms
  for (const [roomId, room] of rooms.entries()) {
    if (room.has(clientId)) {
      room.delete(clientId);

      // Notify other users in the room
      const otherClients = Array.from(room);
      otherClients.forEach(id => {
        const ws = clients.get(id);
        if (ws) {
          ws.send(JSON.stringify({
            type: 'user-left',
            userId: clientId,
            roomId
          }));
        }
      });

      // Delete the room if empty
      if (room.size === 0) {
        rooms.delete(roomId);
      }
    }
  }

  clients.delete(clientId);
}

Client Implementation (Simplified):

class WebRTCClient {
  constructor() {
    this.ws = new WebSocket('ws://localhost:8080');
    this.peerConnection = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
    });

    this.setupWebSocket();
    this.setupPeerConnection();
  }

  setupWebSocket() {
    this.ws.onopen = () => {
      console.log('WebSocket connection established');
      this.ws.send(JSON.stringify({
        type: 'join',
        roomId: 'room123'
      }));
    };

    this.ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      this.handleSignalingMessage(msg);
    };

    this.ws.onclose = () => {
      console.log('WebSocket connection closed');
    };
  }

  setupPeerConnection() {
    // Handle remote SDP
    this.peerConnection.onicecandidate = (event) => {
      if (event.candidate) {
        this.ws.send(JSON.stringify({
          type: 'ice-candidate',
          candidate: event.candidate,
          roomId: 'room123'
        }));
      }
    };

    this.peerConnection.ontrack = (event) => {
      console.log('Received remote media stream');
      // Process remote media stream...
    };

    // Handle signaling messages
    this.handleSignalingMessage = (msg) => {
      switch (msg.type) {
        case 'offer':
          this.handleOffer(msg);
          break;
        case 'answer':
          this.handleAnswer(msg);
          break;
        case 'ice-candidate':
          this.handleIceCandidate(msg);
          break;
        case 'user-joined':
          // New user joined, create offer
          this.createOffer();
          break;
        case 'user-left':
          // User left, clean up resources
          console.log(`User ${msg.userId} left`);
          break;
      }
    };
  }

  createOffer() {
    this.peerConnection.createOffer()
      .then(offer => this.peerConnection.setLocalDescription(offer))
      .then(() => {
        this.ws.send(JSON.stringify({
          type: 'offer',
          sdp: this.peerConnection.localDescription,
          roomId: 'room123'
        }));
      })
      .catch(error => console.error('Failed to create offer:', error));
  }

  handleOffer(msg) {
    this.peerConnection.setRemoteDescription(new RTCSessionDescription(msg.sdp))
      .then(() => this.peerConnection.createAnswer())
      .then(answer => this.peerConnection.setLocalDescription(answer))
      .then(() => {
        this.ws.send(JSON.stringify({
          type: 'answer',
          sdp: this.peerConnection.localDescription,
          roomId: 'room123'
        }));
      })
      .catch(error => console.error('Failed to handle offer:', error));
  }

  handleAnswer(msg) {
    this.peerConnection.setRemoteDescription(new RTCSessionDescription(msg.sdp))
      .catch(error => console.error('Failed to handle answer:', error));
  }

  handleIceCandidate(msg) {
    this.peerConnection.addIceCandidate(new RTCIceCandidate(msg.candidate))
      .catch(error => console.error('Failed to add ICE candidate:', error));
  }

  // Other methods...
}

// Usage example
const client = new WebRTCClient();

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love