Примеры Коммуникации WebRTC в Реальном Времени

Комплексные примеры WebRTC для P2P аудио/видео коммуникации, каналов данных, обмена экраном и сервера сигнализации

💻 Реализация P2P Видеозвонка

🟡 intermediate ⭐⭐⭐⭐

Полная настройка видеозвонка WebRTC с доступом к камере и аудио

⏱️ 30 min 🏷️ webrtc, p2p, video, audio, peer-connection, media-stream
// WebRTC Peer-to-Peer Video Call Implementation
// Complete setup for video and audio communication

class WebRTCPeerConnection {
  constructor(config = {}) {
    this.localVideo = document.getElementById('localVideo');
    this.remoteVideo = document.getElementById('remoteVideo');

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

    this.setupPeerConnection();
    this.setupMediaStreams();
  }

  async setupMediaStreams() {
    try {
      // Get local media stream with video and audio
      const stream = await navigator.mediaDevices.getUserMedia({
        video: {
          width: { ideal: 1280 },
          height: { ideal: 720 },
          facingMode: 'user'
        },
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true
        }
      });

      this.localVideo.srcObject = stream;

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

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

  setupPeerConnection() {
    // Handle incoming tracks
    this.pc.ontrack = (event) => {
      if (event.streams && event.streams[0]) {
        this.remoteVideo.srcObject = event.streams[0];
      }
    };

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

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

      if (this.pc.connectionState === 'connected') {
        console.log('WebRTC connection established!');
      }
    };

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

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

      await this.pc.setLocalDescription(offer);

      // Send offer to remote peer
      this.sendSignalingMessage({
        type: 'offer',
        offer: offer
      });

    } catch (error) {
      console.error('Error creating offer:', error);
    }
  }

  async handleOffer(offer) {
    try {
      await this.pc.setRemoteDescription(offer);

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

      // Send answer back
      this.sendSignalingMessage({
        type: 'answer',
        answer: answer
      });

    } catch (error) {
      console.error('Error handling offer:', error);
    }
  }

  async handleAnswer(answer) {
    try {
      await this.pc.setRemoteDescription(answer);
    } catch (error) {
      console.error('Error handling answer:', error);
    }
  }

  async handleIceCandidate(candidate) {
    try {
      await this.pc.addIceCandidate(candidate);
    } catch (error) {
      console.error('Error adding ICE candidate:', error);
    }
  }

  sendSignalingMessage(message) {
    // Implement WebSocket or signaling server communication
    console.log('Sending signaling message:', message);
  }

  // Media controls
  toggleAudio() {
    const audioTrack = this.localVideo.srcObject.getAudioTracks()[0];
    if (audioTrack) {
      audioTrack.enabled = !audioTrack.enabled;
    }
  }

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

  // Hang up the call
  hangup() {
    if (this.pc) {
      this.pc.close();
      this.pc = null;
    }

    // Stop local media
    if (this.localVideo.srcObject) {
      this.localVideo.srcObject.getTracks().forEach(track => track.stop());
      this.localVideo.srcObject = null;
    }

    this.remoteVideo.srcObject = null;
  }
}

// Usage example
const webrtcConnection = new WebRTCPeerConnection();

// Start the call
webrtcConnection.createOffer();

💻 Коммуникация Каналов Данных WebRTC

🟡 intermediate ⭐⭐⭐

Реализация каналов данных для сообщений и передачи файлов в реальном времени

⏱️ 25 min 🏷️ data-channel, real-time, messaging, file-transfer, p2p
// WebRTC Data Channel Implementation
// Real-time data communication between peers

class WebRTCDataChannel {
  constructor(peerConnection) {
    this.pc = peerConnection;
    this.dataChannel = null;
    this.messageCallbacks = new Map();

    this.setupDataChannel();
  }

  setupDataChannel() {
    // Create data channel for reliable messaging
    this.dataChannel = this.pc.createDataChannel('chat', {
      ordered: true,
      maxRetransmits: 3,
      protocol: 'json'
    });

    this.setupDataChannelHandlers();
  }

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

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

    this.dataChannel.onclose = () => {
      console.log('Data channel closed');
      this.onChannelClose();
    };

    this.dataChannel.onerror = (error) => {
      console.error('Data channel error:', error);
      this.onChannelError(error);
    };
  }

  handleMessage(data) {
    try {
      const message = JSON.parse(data);

      switch (message.type) {
        case 'chat':
          this.onChatMessage(message);
          break;
        case 'file':
          this.onFileTransfer(message);
          break;
        case 'typing':
          this.onTypingIndicator(message);
          break;
        case 'cursor':
          this.onCursorUpdate(message);
          break;
        default:
          console.log('Unknown message type:', message.type);
      }

      // Call custom callbacks
      if (this.messageCallbacks.has(message.type)) {
        this.messageCallbacks.get(message.type)(message);
      }

    } catch (error) {
      console.error('Error parsing message:', error);
    }
  }

  // Send different types of messages
  sendChatMessage(text, sender = 'Anonymous') {
    const message = {
      type: 'chat',
      text: text,
      sender: sender,
      timestamp: Date.now()
    };

    this.sendMessage(message);
  }

  sendFile(file, onProgress) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = (event) => {
        const message = {
          type: 'file',
          name: file.name,
          size: file.size,
          type: file.type,
          data: event.target.result
        };

        this.sendMessage(message);
        resolve(message);
      };

      reader.onerror = (error) => reject(error);
      reader.readAsArrayBuffer(file);
    });
  }

  sendTypingIndicator(isTyping) {
    const message = {
      type: 'typing',
      isTyping: isTyping,
      timestamp: Date.now()
    };

    this.sendMessage(message);
  }

  sendCursorUpdate(x, y) {
    const message = {
      type: 'cursor',
      x: x,
      y: y,
      timestamp: Date.now()
    };

    this.sendMessage(message);
  }

  sendCustomMessage(type, data) {
    const message = {
      type: type,
      data: data,
      timestamp: Date.now()
    };

    this.sendMessage(message);
  }

  sendMessage(message) {
    if (this.dataChannel && this.dataChannel.readyState === 'open') {
      this.dataChannel.send(JSON.stringify(message));
    } else {
      console.error('Data channel is not open');
    }
  }

  // Event handlers (can be overridden)
  onChannelOpen() {
    console.log('Data channel ready for communication');
  }

  onChannelClose() {
    console.log('Data channel communication ended');
  }

  onChannelError(error) {
    console.error('Data channel communication error:', error);
  }

  onChatMessage(message) {
    console.log('Chat message received:', message);
  }

  onFileTransfer(message) {
    console.log('File transfer received:', message);
  }

  onTypingIndicator(message) {
    console.log('Typing indicator:', message);
  }

  onCursorUpdate(message) {
    console.log('Cursor update:', message);
  }

  // Register custom message handlers
  onMessageType(type, callback) {
    this.messageCallbacks.set(type, callback);
  }

  // Data channel status
  isOpen() {
    return this.dataChannel && this.dataChannel.readyState === 'open';
  }

  close() {
    if (this.dataChannel) {
      this.dataChannel.close();
      this.dataChannel = null;
    }
  }
}

// File transfer helper class
class FileTransferHandler {
  constructor(dataChannel) {
    this.dataChannel = dataChannel;
    this.receivingFiles = new Map();
  }

  async receiveFile(fileInfo, onProgress) {
    const chunks = [];
    let receivedBytes = 0;

    return new Promise((resolve, reject) => {
      const handleChunk = (chunk) => {
        chunks.push(chunk);
        receivedBytes += chunk.byteLength;

        if (onProgress) {
          onProgress(receivedBytes, fileInfo.size);
        }

        if (receivedBytes >= fileInfo.size) {
          const blob = new Blob(chunks, { type: fileInfo.type });
          const file = new File([blob], fileInfo.name, { type: fileInfo.type });
          resolve(file);
        }
      };

      this.dataChannel.onMessageType('file-chunk', (message) => {
        if (message.fileId === fileInfo.id) {
          handleChunk(message.data);
        }
      });
    });
  }

  async sendFile(file, onProgress) {
    const chunkSize = 16384; // 16KB chunks
    const fileId = Date.now().toString();

    // Send file metadata
    this.dataChannel.sendCustomMessage('file-info', {
      id: fileId,
      name: file.name,
      size: file.size,
      type: file.type
    });

    // Send file in chunks
    const reader = new FileReader();
    let offset = 0;

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

    reader.onload = (event) => {
      this.dataChannel.sendCustomMessage('file-chunk', {
        fileId: fileId,
        data: event.target.result
      });

      offset += chunkSize;

      if (onProgress) {
        onProgress(offset, file.size);
      }

      if (offset < file.size) {
        setTimeout(sendNextChunk, 10); // Small delay to prevent blocking
      }
    };

    sendNextChunk();
  }
}

// Usage example
const webrtcDataChannel = new WebRTCDataChannel(peerConnection);

// Set up message handlers
webrtcDataChannel.onMessageType('game-state', (message) => {
  updateGameDisplay(message.data);
});

// Send messages
webrtcDataChannel.sendChatMessage('Hello from peer!');
webrtcDataChannel.sendTypingIndicator(true);

💻 Реализация Обмена Экраном

🟡 intermediate ⭐⭐⭐

Полная настройка обмена экраном с поддержкой вкладок и аудио

⏱️ 20 min 🏷️ screen-share, desktop-capture, tab-sharing, picture-in-picture, recording
// WebRTC Screen Sharing Implementation
// Screen capture, tab sharing, and audio support

class ScreenShareManager {
  constructor(peerConnection) {
    this.pc = peerConnection;
    this.screenStream = null;
    this.localVideo = document.getElementById('localScreen');
    this.isSharing = false;
  }

  async startScreenShare(options = {}) {
    try {
      // Get screen share stream
      this.screenStream = await navigator.mediaDevices.getDisplayMedia({
        video: {
          cursor: options.cursor || 'always',
          displaySurface: options.displaySurface || 'monitor'
        },
        audio: options.audio || false,
        preferCurrentTab: options.preferCurrentTab || false
      });

      // Replace local video with screen share
      this.localVideo.srcObject = this.screenStream;

      // Add or replace video tracks in peer connection
      const videoTracks = this.screenStream.getVideoTracks();
      const sender = this.pc.getSenders().find(s =>
        s.track && s.track.kind === 'video'
      );

      if (sender && videoTracks.length > 0) {
        // Replace existing video track
        await sender.replaceTrack(videoTracks[0]);
      } else if (videoTracks.length > 0) {
        // Add new video track
        this.pc.addTrack(videoTracks[0], this.screenStream);
      }

      // Add audio tracks if available
      const audioTracks = this.screenStream.getAudioTracks();
      audioTracks.forEach(track => {
        const audioSender = this.pc.getSenders().find(s =>
          s.track && s.track.kind === 'audio'
        );

        if (audioSender) {
          audioSender.replaceTrack(track);
        } else {
          this.pc.addTrack(track, this.screenStream);
        }
      });

      this.isSharing = true;

      // Listen for stream end
      this.screenStream.getVideoTracks()[0].onended = () => {
        this.stopScreenShare();
      };

      return this.screenStream;

    } catch (error) {
      console.error('Error starting screen share:', error);
      throw error;
    }
  }

  async startTabShare() {
    try {
      // Use Chrome extension API for tab sharing (if available)
      if (chrome && chrome.desktopCapture) {
        return new Promise((resolve, reject) => {
          chrome.desktopCapture.chooseDesktopMedia(
            ['screen', 'window', 'tab'],
            (streamId) => {
              if (streamId) {
                navigator.mediaDevices.getUserMedia({
                  video: {
                    mandatory: {
                      chromeMediaSource: 'desktop',
                      chromeMediaSourceId: streamId
                    }
                  }
                }).then(resolve).catch(reject);
              } else {
                reject(new Error('No stream selected'));
              }
            }
          );
        });
      } else {
        // Fallback to standard screen capture with tab preference
        return this.startScreenShare({
          preferCurrentTab: true,
          displaySurface: 'browser'
        });
      }
    } catch (error) {
      console.error('Error starting tab share:', error);
      throw error;
    }
  }

  async stopScreenShare() {
    try {
      // Stop all tracks in screen stream
      if (this.screenStream) {
        this.screenStream.getTracks().forEach(track => track.stop());
        this.screenStream = null;
      }

      // Clear local video
      this.localVideo.srcObject = null;

      // Try to restore camera stream if available
      await this.restoreCameraStream();

      this.isSharing = false;

    } catch (error) {
      console.error('Error stopping screen share:', error);
    }
  }

  async restoreCameraStream() {
    try {
      const cameraStream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true
      });

      this.localVideo.srcObject = cameraStream;

      // Replace tracks in peer connection
      const videoTracks = cameraStream.getVideoTracks();
      const sender = this.pc.getSenders().find(s =>
        s.track && s.track.kind === 'video'
      );

      if (sender && videoTracks.length > 0) {
        await sender.replaceTrack(videoTracks[0]);
      }

      // Add audio tracks
      const audioTracks = cameraStream.getAudioTracks();
      audioTracks.forEach(track => {
        const audioSender = this.pc.getSenders().find(s =>
          s.track && s.track.kind === 'audio'
        );

        if (audioSender) {
          audioSender.replaceTrack(track);
        }
      });

    } catch (error) {
      console.error('Error restoring camera stream:', error);
    }
  }

  async toggleScreenShare() {
    if (this.isSharing) {
      await this.stopScreenShare();
    } else {
      await this.startScreenShare();
    }
  }

  // Get available screen sources
  async getScreenSources() {
    try {
      if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
        const devices = await navigator.mediaDevices.enumerateDevices();
        return devices.filter(device => device.kind === 'videoinput');
      }
      return [];
    } catch (error) {
      console.error('Error getting screen sources:', error);
      return [];
    }
  }

  // Check screen sharing capabilities
  getCapabilities() {
    return {
      screenCapture: !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia),
      tabCapture: !!(chrome && chrome.desktopCapture),
      audioCapture: !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia),
      cursorSupport: true,
      multipleSources: true
    };
  }

  // Add picture-in-picture support
  async enablePictureInPicture() {
    try {
      if (this.localVideo && 'requestPictureInPicture' in this.localVideo) {
        await this.localVideo.requestPictureInPicture();
      }
    } catch (error) {
      console.error('Error enabling picture-in-picture:', error);
    }
  }

  disablePictureInPicture() {
    if (this.localVideo && 'exitPictureInPicture' in this.localVideo) {
      this.localVideo.exitPictureInPicture();
    }
  }

  // Recording functionality
  startRecording() {
    if (!this.screenStream) return null;

    const chunks = [];
    const mediaRecorder = new MediaRecorder(this.screenStream, {
      mimeType: MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
        ? 'video/webm;codecs=vp9'
        : 'video/webm'
    });

    mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) {
        chunks.push(event.data);
      }
    };

    mediaRecorder.onstop = () => {
      const blob = new Blob(chunks, { type: 'video/webm' });
      this.onRecordingComplete(blob);
    };

    mediaRecorder.start();
    return mediaRecorder;
  }

  onRecordingComplete(blob) {
    // Handle recording completion
    console.log('Screen share recording completed:', blob);

    // Create download link
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `screen-share-${Date.now()}.webm`;
    a.click();
    URL.revokeObjectURL(url);
  }

  // Get current status
  getStatus() {
    return {
      isSharing: this.isSharing,
      hasVideo: !!(this.screenStream && this.screenStream.getVideoTracks().length),
      hasAudio: !!(this.screenStream && this.screenStream.getAudioTracks().length),
      trackCount: this.screenStream ? this.screenStream.getTracks().length : 0
    };
  }
}

// UI Integration Example
class ScreenShareUI {
  constructor(screenShareManager) {
    this.screenShare = screenShareManager;
    this.setupUI();
  }

  setupUI() {
    // Create control buttons
    this.createShareButton();
    this.createTabShareButton();
    this.createStopButton();
    this.createRecordButton();

    // Create status display
    this.createStatusDisplay();
  }

  createShareButton() {
    const button = document.createElement('button');
    button.textContent = 'Share Screen';
    button.onclick = async () => {
      try {
        await this.screenShare.startScreenShare();
        this.updateStatus();
      } catch (error) {
        this.showError('Failed to start screen share: ' + error.message);
      }
    };
    document.body.appendChild(button);
  }

  createTabShareButton() {
    const button = document.createElement('button');
    button.textContent = 'Share Tab';
    button.onclick = async () => {
      try {
        await this.screenShare.startTabShare();
        this.updateStatus();
      } catch (error) {
        this.showError('Failed to start tab share: ' + error.message);
      }
    };
    document.body.appendChild(button);
  }

  createStopButton() {
    const button = document.createElement('button');
    button.textContent = 'Stop Sharing';
    button.onclick = async () => {
      await this.screenShare.stopScreenShare();
      this.updateStatus();
    };
    document.body.appendChild(button);
  }

  createRecordButton() {
    const button = document.createElement('button');
    button.textContent = 'Start Recording';

    button.onclick = () => {
      if (!this.recording) {
        this.recording = this.screenShare.startRecording();
        button.textContent = 'Stop Recording';
      } else {
        this.recording.stop();
        this.recording = null;
        button.textContent = 'Start Recording';
      }
    };

    document.body.appendChild(button);
  }

  createStatusDisplay() {
    this.statusDiv = document.createElement('div');
    this.statusDiv.style.marginTop = '10px';
    this.updateStatus();
    document.body.appendChild(this.statusDiv);
  }

  updateStatus() {
    const status = this.screenShare.getStatus();
    this.statusDiv.innerHTML = `
      <div>Sharing: ${status.isSharing ? 'Yes' : 'No'}</div>
      <div>Video: ${status.hasVideo ? 'Yes' : 'No'}</div>
      <div>Audio: ${status.hasAudio ? 'Yes' : 'No'}</div>
      <div>Tracks: ${status.trackCount}</div>
    `;
  }

  showError(message) {
    alert(message);
  }
}

// Usage example
const screenShareManager = new ScreenShareManager(peerConnection);
const screenShareUI = new ScreenShareUI(screenShareManager);

💻 Сервер Сигнализации WebRTC

🔴 complex ⭐⭐⭐⭐

WebSocket сервер для установления P2P соединений и медиа-неготивации

⏱️ 35 min 🏷️ signaling, websocket, peer-connection, room-management, server
// WebRTC Signaling Server Implementation
// Node.js WebSocket server for peer connection coordination

const WebSocket = require('ws');
const http = require('http');
const express = require('express');
const cors = require('cors');

class WebRTCSignalingServer {
  constructor(port = 8080) {
    this.port = port;
    this.app = express();
    this.server = http.createServer(this.app);
    this.wss = new WebSocket.Server({ server: this.server });

    this.rooms = new Map(); // room_id -> Set of peers
    this.peers = new Map(); // peer_id -> { ws, room_id, metadata }

    this.setupExpress();
    this.setupWebSocket();
  }

  setupExpress() {
    this.app.use(cors());
    this.app.use(express.json());

    // Static files for client
    this.app.use(express.static('public'));

    // API endpoints
    this.app.get('/api/rooms', (req, res) => {
      const rooms = Array.from(this.rooms.keys()).map(roomId => ({
        id: roomId,
        peerCount: this.rooms.get(roomId).size
      }));
      res.json(rooms);
    });

    this.app.post('/api/rooms', (req, res) => {
      const roomId = this.generateRoomId();
      this.rooms.set(roomId, new Set());
      res.json({ roomId });
    });

    this.app.get('/api/rooms/:roomId/peers', (req, res) => {
      const roomId = req.params.roomId;
      const roomPeers = this.rooms.get(roomId);

      if (!roomPeers) {
        return res.status(404).json({ error: 'Room not found' });
      }

      const peers = Array.from(roomPeers).map(peerId => ({
        id: peerId,
        metadata: this.peers.get(peerId)?.metadata
      }));

      res.json(peers);
    });
  }

  setupWebSocket() {
    this.wss.on('connection', (ws, req) => {
      const peerId = this.generatePeerId();

      console.log(`Peer connected: ${peerId}`);

      // Store peer connection
      this.peers.set(peerId, {
        ws: ws,
        room_id: null,
        metadata: {
          userAgent: req.headers['user-agent'],
          ip: req.socket.remoteAddress,
          connectedAt: Date.now()
        }
      });

      // Send peer ID to client
      this.sendToPeer(peerId, {
        type: 'peer-id',
        peerId: peerId
      });

      // Handle messages
      ws.on('message', (message) => {
        try {
          const data = JSON.parse(message);
          this.handleMessage(peerId, data);
        } catch (error) {
          console.error(`Invalid message from peer ${peerId}:`, error);
          this.sendError(peerId, 'Invalid message format');
        }
      });

      // Handle disconnection
      ws.on('close', () => {
        console.log(`Peer disconnected: ${peerId}`);
        this.handlePeerDisconnect(peerId);
      });

      // Handle errors
      ws.on('error', (error) => {
        console.error(`WebSocket error for peer ${peerId}:`, error);
      });
    });
  }

  handleMessage(peerId, message) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    switch (message.type) {
      case 'join-room':
        this.handleJoinRoom(peerId, message.roomId);
        break;

      case 'leave-room':
        this.handleLeaveRoom(peerId);
        break;

      case 'offer':
        this.handleRelayMessage(peerId, message, 'offer');
        break;

      case 'answer':
        this.handleRelayMessage(peerId, message, 'answer');
        break;

      case 'ice-candidate':
        this.handleRelayMessage(peerId, message, 'ice-candidate');
        break;

      case 'room-info':
        this.sendRoomInfo(peerId);
        break;

      case 'peer-list':
        this.sendPeerList(peerId);
        break;

      case 'direct-message':
        this.handleDirectMessage(peerId, message);
        break;

      default:
        console.log(`Unknown message type: ${message.type}`);
    }
  }

  handleJoinRoom(peerId, roomId) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    // Leave current room if in one
    if (peer.room_id) {
      this.handleLeaveRoom(peerId);
    }

    // Join new room
    if (!this.rooms.has(roomId)) {
      this.rooms.set(roomId, new Set());
    }

    this.rooms.get(roomId).add(peerId);
    peer.room_id = roomId;

    console.log(`Peer ${peerId} joined room ${roomId}`);

    // Notify room about new peer
    this.broadcastToRoom(roomId, {
      type: 'peer-joined',
      peerId: peerId,
      metadata: peer.metadata
    }, peerId);

    // Send room info to new peer
    this.sendRoomInfo(peerId);
  }

  handleLeaveRoom(peerId) {
    const peer = this.peers.get(peerId);
    if (!peer || !peer.room_id) return;

    const roomId = peer.room_id;
    this.rooms.get(roomId).delete(peerId);

    // Clean up empty rooms
    if (this.rooms.get(roomId).size === 0) {
      this.rooms.delete(roomId);
    }

    console.log(`Peer ${peerId} left room ${roomId}`);

    // Notify room about peer leaving
    this.broadcastToRoom(roomId, {
      type: 'peer-left',
      peerId: peerId
    });

    peer.room_id = null;
  }

  handleRelayMessage(senderId, message, messageType) {
    const peer = this.peers.get(senderId);
    if (!peer || !peer.room_id) return;

    // Add sender info to message
    message.senderId = senderId;

    // Relay to all peers in room except sender
    this.broadcastToRoom(peer.room_id, message, senderId);
  }

  handleDirectMessage(senderId, message) {
    const recipientId = message.recipientId;
    const recipient = this.peers.get(recipientId);

    if (recipient) {
      message.senderId = senderId;
      this.sendToPeer(recipientId, message);
    } else {
      this.sendError(senderId, `Recipient ${recipientId} not found`);
    }
  }

  handlePeerDisconnect(peerId) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    // Leave room if in one
    if (peer.room_id) {
      this.handleLeaveRoom(peerId);
    }

    // Clean up peer data
    this.peers.delete(peerId);
  }

  sendToPeer(peerId, message) {
    const peer = this.peers.get(peerId);
    if (peer && peer.ws.readyState === WebSocket.OPEN) {
      peer.ws.send(JSON.stringify(message));
    }
  }

  sendError(peerId, error) {
    this.sendToPeer(peerId, {
      type: 'error',
      error: error
    });
  }

  broadcastToRoom(roomId, message, excludePeerId = null) {
    const room = this.rooms.get(roomId);
    if (!room) return;

    room.forEach(peerId => {
      if (peerId !== excludePeerId) {
        this.sendToPeer(peerId, message);
      }
    });
  }

  sendRoomInfo(peerId) {
    const peer = this.peers.get(peerId);
    if (!peer || !peer.room_id) return;

    const room = this.rooms.get(peer.room_id);
    const peers = Array.from(room || []).filter(id => id !== peerId);

    this.sendToPeer(peerId, {
      type: 'room-info',
      roomId: peer.room_id,
      peers: peers,
      peerCount: peers.length
    });
  }

  sendPeerList(peerId) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    const allPeers = Array.from(this.peers.keys())
      .filter(id => id !== peerId)
      .map(id => ({
        id: id,
        metadata: this.peers.get(id)?.metadata
      }));

    this.sendToPeer(peerId, {
      type: 'peer-list',
      peers: allPeers
    });
  }

  generatePeerId() {
    return `peer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  generateRoomId() {
    return Math.random().toString(36).substr(2, 8).toUpperCase();
  }

  start() {
    this.server.listen(this.port, () => {
      console.log(`Signaling server running on port ${this.port}`);
    });
  }

  stop() {
    this.wss.close();
    this.server.close();
  }

  // Server statistics
  getStats() {
    return {
      totalPeers: this.peers.size,
      totalRooms: this.rooms.size,
      rooms: Array.from(this.rooms.entries()).map(([id, peers]) => ({
        id,
        peerCount: peers.size
      }))
    };
  }
}

// Room management extension
class RoomManager {
  constructor(signalingServer) {
    this.server = signalingServer;

    // Add room management endpoints
    this.server.app.delete('/api/rooms/:roomId', (req, res) => {
      const roomId = req.params.roomId;
      const room = this.server.rooms.get(roomId);

      if (!room) {
        return res.status(404).json({ error: 'Room not found' });
      }

      // Disconnect all peers in room
      room.forEach(peerId => {
        this.server.sendToPeer(peerId, {
          type: 'room-closed',
          roomId: roomId,
          reason: 'Room deleted by admin'
        });

        const peer = this.server.peers.get(peerId);
        if (peer) {
          peer.room_id = null;
        }
      });

      this.server.rooms.delete(roomId);
      res.json({ message: 'Room deleted' });
    });

    // Add room settings
    this.server.app.patch('/api/rooms/:roomId/settings', (req, res) => {
      const roomId = req.params.roomId;
      const settings = req.body;

      // Store room settings (implement room settings storage)
      console.log(`Room ${roomId} settings updated:`, settings);

      // Broadcast settings update to room
      this.server.broadcastToRoom(roomId, {
        type: 'room-settings-updated',
        roomId: roomId,
        settings: settings
      });

      res.json({ message: 'Settings updated' });
    });
  }
}

// Usage example
const signalingServer = new WebRTCSignalingServer(8080);
const roomManager = new RoomManager(signalingServer);

// Start server
signalingServer.start();

// Graceful shutdown
process.on('SIGINT', () => {
  console.log('Shutting down signaling server...');
  signalingServer.stop();
  process.exit(0);
});

// Health check endpoint
signalingServer.app.get('/health', (req, res) => {
  const stats = signalingServer.getStats();
  res.json({
    status: 'healthy',
    ...stats
  });
});

💻 Реализация SFU Медиа Сервера

🔴 complex ⭐⭐⭐⭐⭐

Архитектура SFU для масштабируемых видеоконференций с множеством участников

⏱️ 45 min 🏷️ sfu, media-server, scalable, video-conference, simulcast, quality-control
// WebRTC SFU (Selective Forwarding Unit) Implementation
// Scalable media server for multi-participant conferences

const WebSocket = require('ws');
const { RTCPeerConnection, RTCSessionDescription } = require('wrtc');
const uuid = require('uuid');

class SFUMediaServer {
  constructor() {
    this.rooms = new Map(); // roomId -> Room instance
    this.peers = new Map(); // peerId -> Peer instance
    this.wss = new WebSocket.Server({ port: 3000 });

    this.setupWebSocket();
  }

  setupWebSocket() {
    this.wss.on('connection', (ws, req) => {
      console.log('New SFU client connected');

      ws.on('message', async (data) => {
        try {
          const message = JSON.parse(data);
          await this.handleMessage(ws, message);
        } catch (error) {
          console.error('Error handling message:', error);
        }
      });

      ws.on('close', () => {
        console.log('SFU client disconnected');
        this.handleDisconnect(ws);
      });
    });
  }

  async handleMessage(ws, message) {
    const { type, roomId, peerId, data } = message;

    switch (type) {
      case 'join-room':
        await this.handleJoinRoom(ws, roomId, peerId, data);
        break;

      case 'leave-room':
        this.handleLeaveRoom(peerId);
        break;

      case 'offer':
        await this.handleOffer(peerId, data);
        break;

      case 'ice-candidate':
        await this.handleIceCandidate(peerId, data);
        break;

      case 'request-tracks':
        await this.handleTrackRequest(peerId, data);
        break;

      case 'update-layout':
        this.handleLayoutUpdate(roomId, data);
        break;

      default:
        console.log(`Unknown message type: ${type}`);
    }
  }

  async handleJoinRoom(ws, roomId, peerId, metadata = {}) {
    try {
      // Create room if it doesn't exist
      if (!this.rooms.has(roomId)) {
        this.rooms.set(roomId, new SFURoom(roomId));
      }

      const room = this.rooms.get(roomId);

      // Create peer instance
      const peer = new SFUPeer(peerId, ws, room, metadata);
      this.peers.set(peerId, peer);

      // Add peer to room
      await room.addPeer(peer);

      console.log(`Peer ${peerId} joined room ${roomId}`);

    } catch (error) {
      console.error('Error joining room:', error);
      ws.send(JSON.stringify({
        type: 'error',
        error: error.message
      }));
    }
  }

  handleLeaveRoom(peerId) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    peer.leave();
    this.peers.delete(peerId);
  }

  async handleOffer(peerId, offerData) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    await peer.handleOffer(offerData);
  }

  async handleIceCandidate(peerId, candidateData) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    await peer.handleIceCandidate(candidateData);
  }

  async handleTrackRequest(peerId, request) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    await peer.handleTrackRequest(request);
  }

  handleLayoutUpdate(roomId, layout) {
    const room = this.rooms.get(roomId);
    if (!room) return;

    room.updateLayout(layout);
  }

  handleDisconnect(ws) {
    // Find and disconnect peer
    for (const [peerId, peer] of this.peers.entries()) {
      if (peer.ws === ws) {
        this.handleLeaveRoom(peerId);
        break;
      }
    }
  }
}

class SFURoom {
  constructor(roomId) {
    this.roomId = roomId;
    this.peers = new Map(); // peerId -> Peer instance
    this.tracks = new Map(); // trackId -> Track instance
    this.layout = 'grid'; // grid, speaker, custom
    this.maxVideoQuality = '1080p';
  }

  async addPeer(peer) {
    this.peers.set(peer.id, peer);

    // Send existing tracks to new peer
    for (const [trackId, track] of this.tracks.entries()) {
      await this.sendTrackToPeer(peer, track);
    }

    // Notify other peers about new peer
    this.broadcastToPeers({
      type: 'peer-joined',
      peerId: peer.id,
      metadata: peer.metadata
    }, peer.id);
  }

  removePeer(peerId) {
    const peer = this.peers.get(peerId);
    if (!peer) return;

    // Clean up peer's tracks
    for (const trackId of peer.trackIds) {
      this.tracks.delete(trackId);
    }

    // Remove peer from room
    this.peers.delete(peerId);

    // Notify other peers
    this.broadcastToPeers({
      type: 'peer-left',
      peerId: peerId
    });

    console.log(`Peer ${peerId} left room ${this.roomId}`);
  }

  async sendTrackToPeer(peer, track) {
    try {
      // Create transceiver for the track
      const transceiver = peer.pc.addTransceiver(track.track, {
        direction: 'sendonly'
      });

      // Store track mapping
      peer.trackMappings.set(track.id, {
        transceiver: transceiver,
        trackId: track.id
      });

      console.log(`Sent track ${track.id} to peer ${peer.id}`);

    } catch (error) {
      console.error(`Error sending track to peer ${peer.id}:`, error);
    }
  }

  addTrack(peerId, track) {
    const trackId = uuid.v4();
    const sfuTrack = new SFUTrack(trackId, peerId, track);

    this.tracks.set(trackId, sfuTrack);

    // Send track to all other peers
    this.broadcastToPeers({
      type: 'track-added',
      trackId: trackId,
      peerId: peerId,
      metadata: sfuTrack.metadata
    }, peerId);

    return sfuTrack;
  }

  removeTrack(trackId) {
    const track = this.tracks.get(trackId);
    if (!track) return;

    this.tracks.delete(trackId);

    // Notify all peers
    this.broadcastToPeers({
      type: 'track-removed',
      trackId: trackId
    });
  }

  updateLayout(layout) {
    this.layout = layout;

    // Recalculate track quality and simulcast layers
    this.optimizeTracks();

    // Notify peers about layout change
    this.broadcastToPeers({
      type: 'layout-updated',
      layout: layout
    });
  }

  optimizeTracks() {
    // Implement track optimization based on layout
    // Adjust quality levels, enable/disable simulcast layers
    console.log(`Optimizing tracks for layout: ${this.layout}`);
  }

  broadcastToPeers(message, excludePeerId = null) {
    for (const [peerId, peer] of this.peers.entries()) {
      if (peerId !== excludePeerId) {
        peer.send(message);
      }
    }
  }
}

class SFUPeer {
  constructor(id, ws, room, metadata = {}) {
    this.id = id;
    this.ws = ws;
    this.room = room;
    this.metadata = metadata;

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

    this.trackIds = new Set();
    this.trackMappings = new Map();
    this.requestedTracks = new Set();

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

  setupPeerConnection() {
    // Handle incoming tracks
    this.pc.ontrack = (event) => {
      console.log(`Received track from peer ${this.id}:`, event.track);

      const trackId = uuid.v4();
      this.trackIds.add(trackId);

      // Add track to room
      this.room.addTrack(this.id, event.track);
    };

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

    // Handle connection state
    this.pc.onconnectionstatechange = () => {
      console.log(`Peer ${this.id} connection state: ${this.pc.connectionState}`);
    };
  }

  setupWebSocket() {
    this.ws.on('close', () => {
      this.leave();
    });

    this.ws.on('error', (error) => {
      console.error(`WebSocket error for peer ${this.id}:`, error);
    });
  }

  async handleOffer(offerData) {
    try {
      const offer = new RTCSessionDescription(offerData);
      await this.pc.setRemoteDescription(offer);

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

      // Send answer back
      this.send({
        type: 'answer',
        answer: answer
      });

    } catch (error) {
      console.error(`Error handling offer for peer ${this.id}:`, error);
    }
  }

  async handleIceCandidate(candidateData) {
    try {
      const candidate = new RTCIceCandidate(candidateData);
      await this.pc.addIceCandidate(candidate);
    } catch (error) {
      console.error(`Error adding ICE candidate for peer ${this.id}:`, error);
    }
  }

  async handleTrackRequest(request) {
    const { trackId, quality } = request;

    if (!this.room.tracks.has(trackId)) {
      console.warn(`Track ${trackId} not found`);
      return;
    }

    // Adjust track quality based on request
    await this.adjustTrackQuality(trackId, quality);

    console.log(`Peer ${this.id} requested track ${trackId} with quality ${quality}`);
  }

  async adjustTrackQuality(trackId, quality) {
    const track = this.room.tracks.get(trackId);
    if (!track) return;

    // Implement quality adjustment logic
    // This could involve simulcast layer selection or bitrate adjustment
    console.log(`Adjusting track ${trackId} quality to ${quality}`);
  }

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

  leave() {
    console.log(`Peer ${this.id} leaving`);

    // Clean up peer connection
    if (this.pc) {
      this.pc.close();
    }

    // Remove from room
    this.room.removePeer(this.id);
  }
}

class SFUTrack {
  constructor(id, peerId, track) {
    this.id = id;
    this.peerId = peerId;
    this.track = track;
    this.metadata = {
      kind: track.kind,
      enabled: track.enabled,
      readyState: track.readyState
    };
    this.quality = 'auto'; // auto, high, medium, low
    this.simulcastLayers = [];
  }

  setQuality(quality) {
    this.quality = quality;
    // Implement actual quality adjustment
  }

  enable() {
    this.track.enabled = true;
    this.metadata.enabled = true;
  }

  disable() {
    this.track.enabled = false;
    this.metadata.enabled = false;
  }
}

// Usage example
const sfuServer = new SFUMediaServer();

console.log('SFU Media Server running on ws://localhost:3000');

// Add graceful shutdown
process.on('SIGINT', () => {
  console.log('Shutting down SFU server...');
  process.exit(0);
});

// Monitoring and statistics
class SFUMonitor {
  constructor(sfuServer) {
    this.sfu = sfuServer;

    // Add monitoring endpoint
    setInterval(() => {
      this.logStats();
    }, 30000); // Log stats every 30 seconds
  }

  logStats() {
    const stats = this.getStats();
    console.log('SFU Statistics:', stats);
  }

  getStats() {
    let totalPeers = 0;
    let totalTracks = 0;

    for (const room of this.sfu.rooms.values()) {
      totalPeers += room.peers.size;
      totalTracks += room.tracks.size;
    }

    return {
      rooms: this.sfu.rooms.size,
      peers: totalPeers,
      tracks: totalTracks,
      timestamp: new Date().toISOString()
    };
  }
}

const monitor = new SFUMonitor(sfuServer);