Trong thế giới ngày nay, việc truyền tải âm thanh và hình ảnh qua Internet đang trở nên ngày càng phổ biến. Để đáp ứng nhu cầu này, WebRTC (Web Real-Time Communication) đã trở thành một công nghệ quan trọng. Trong bài viết này, chúng ta sẽ tìm hiểu về cách WebRTC hoạt động, đặc biệt là trong hai mô hình quan trọng: P2P (Peer-to-Peer) và SFU (Selective Forwarding Unit).

1. WebRTC là Gì?

WebRTC là một dự án mã nguồn mở được phát triển để hỗ trợ truyền tải đa phương tiện trực tuyến giữa các trình duyệt web và ứng dụng di động. Nó cung cấp một giao thức thống nhất cho việc truyền tải âm thanh, video và dữ liệu thời gian thực mà không cần cài đặt các plugin bổ sung.

2. Mô Hình P2P (Peer-to-Peer)

Trong mô hình P2P, các đối tác trực tiếp kết nối với nhau mà không thông qua một máy chủ trung gian. WebRTC P2P cho phép trực tiếp trao đổi dữ liệu giữa hai máy tính mà không thông qua bất kỳ máy chủ trung gian nào. Điều này giúp giảm độ trễ và tăng trải nghiệm người dùng.

Các bước cơ bản của quá trình này bao gồm:

  • Xác định đối tác kết nối: Mỗi máy tính sẽ xác định đối tác kết nối với nó thông qua một "signaling server".
  • Thiết lập kết nối: Dựa trên thông tin từ signaling server, máy tính sẽ thiết lập kết nối trực tiếp với đối tác của mình.
  • Truyền tải dữ liệu: Sau khi kết nối được thiết lập, các máy tính có thể trực tiếp truyền tải dữ liệu cho nhau.

3. Mô Hình SFU (Selective Forwarding Unit)

Ngược lại với P2P, mô hình SFU sử dụng một máy chủ trung gian để tối ưu hóa truyền tải đa phương tiện. SFU nhận dữ liệu từ một nguồn và chọn lọc (forward) nó đến các đối tác khác.

Các bước cơ bản của quá trình này bao gồm:

  • Gửi dữ liệu đến SFU: Các máy tính kết nối với SFU và gửi dữ liệu đến nó.
  • Chọn lọc và chuyển tiếp: SFU chọn lọc dữ liệu từ một nguồn và chuyển tiếp (forward) nó đến các máy tính khác.
  • Đồng bộ hóa dữ liệu: SFU đảm bảo đồng bộ hóa dữ liệu để tạo ra một trải nghiệm đồng bộ cho tất cả các đối tác.

4. Ưu nhược điểm của các mô hình

4.1. Mô hình p2p

Lợi ích:

Vì tải máy chủ thấp, nguồn tài nguyên máy chủ giảm.

Thời gian thực được đảm bảo vì dữ liệu được truyền và nhận thông qua kết nối trực tiếp giữa các đối tác.

Nhược điểm:

Trong kết nối 1:N hoặc N:M, tải của máy khách tăng lên nhanh chóng. Ví dụ, giả sử có 5 người kết nối với WebRTC, có 4 Uplink (số lượng gửi dữ liệu đến người dùng khác) và 4 Downlink (số lượng dữ liệu của người dùng khác kết nối đến). Nó duy trì 8 liên kết và truyền nhận dữ liệu.

4.2. Mô hình SFU

Ưu điểm:

  • Mặc dù dữ liệu trải qua máy chủ và chậm hơn so với việc sử dụng một máy chủ signaling (P2P/Mesh), nhưng nó vẫn có thể duy trì một mức độ thời gian thực tương đối.
  • Tải trên máy khách giảm đi so với việc sử dụng máy chủ signaling.

Nhược điểm:

  • Chi phí máy chủ tăng lên so với máy chủ signaling.
  • Trong cấu trúc N:M quy mô lớn, máy khách vẫn phải xử lý một lượng lớn tải.

5. Cách implement webrtc SFU


Ta sẽ sử dụng một thư viện khá phổ biến đó là mediasoup.

Cấu trúc code gồm có 2 phần là server và client.

server

const express = require('express');
const http = require('http');
const mediasoup = require('mediasoup');

const app = express();
const server = http.createServer(app);

const { Worker, Router } = mediasoup;

const PORT = 3000;

// WebRTC workers
let worker;

// MediaSoup router
let router;

// Map to store connected clients
const clients = new Map();

// Set up Express server
server.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

// Set up MediaSoup worker and router
(async () => {
  worker = await Worker.create();
  router = await worker.createRouter({ mediaCodecs: mediasoup.defaultRouterOptions.mediaCodecs });
})();

// Set up Express routes
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

// Set up WebSocket for signaling
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  console.log(`New user connected: ${socket.id}`);

  // Create a new client object and store it in the clients map
  clients.set(socket.id, { socket, producerTransport: null, consumerTransport: null });

  // Handle client disconnection
  socket.on('disconnect', () => {
    console.log(`User disconnected: ${socket.id}`);
    handleDisconnect(socket.id);
  });

  // Handle WebRTC transport creation and connection
  socket.on('createWebRtcTransport', async (data, callback) => {
    await createWebRtcTransport(socket.id, data, callback);
  });

  socket.on('connectWebRtcTransport', async (data, callback) => {
    await connectWebRtcTransport(socket.id, data, callback);
  });

  // Handle producer creation and transport connection
  socket.on('createProducer', async (data, callback) => {
    await createProducer(socket.id, data, callback);
  });

  // Handle consumer creation and transport connection
  socket.on('createConsumer', async (data, callback) => {
    await createConsumer(socket.id, data, callback);
  });
});

// Function to handle client disconnection
function handleDisconnect(socketId) {
  const client = clients.get(socketId);

  if (client) {
    // Close producer and consumer transports
    closeTransport(client.producerTransport);
    closeTransport(client.consumerTransport);

    // Remove the client from the map
    clients.delete(socketId);
  }
}

// Function to create a WebRTC transport
async function createWebRtcTransport(socketId, data, callback) {
  const client = clients.get(socketId);

  if (!client) {
    return callback({ error: 'Client not found' });
  }

  const { transport, params } = await createTransport();
  client[data.direction + 'Transport'] = transport;
  callback(params);
}

// Function to connect a WebRTC transport
async function connectWebRtcTransport(socketId, data, callback) {
  const client = clients.get(socketId);

  if (!client) {
    return callback({ error: 'Client not found' });
  }

  await client[data.direction + 'Transport'].connect({ dtlsParameters: data.dtlsParameters });
  callback();
}

// Function to create a producer
async function createProducer(socketId, data, callback) {
  const client = clients.get(socketId);

  if (!client || !client.producerTransport) {
    return callback({ error: 'Client or transport not found' });
  }

  const producer = await client.producerTransport.produce({ kind: data.kind, rtpParameters: data.rtpParameters });
  client.producer = producer;

  // Notify other clients about the new producer
  for (const otherClient of clients.values()) {
    if (otherClient.socket.id !== socketId) {
      otherClient.socket.emit('newProducer', { producerId: producer.id, kind: data.kind });
    }
  }

  callback({ producerId: producer.id });
}

// Function to create a consumer
async function createConsumer(socketId, data, callback) {
  const client = clients.get(socketId);

  if (!client || !client.consumerTransport) {
    return callback({ error: 'Client or transport not found' });
  }

  const producer = getProducerById(data.producerId);
  if (!producer) {
    return callback({ error: 'Producer not found' });
  }

  const consumer = await client.consumerTransport.consume({
    producerId: producer.id,
    rtpCapabilities: data.rtpCapabilities,
    paused: false, // Change to true if you want to pause the consumer initially
  });

  callback({
    producerId: producer.id,
    id: consumer.id,
    kind: consumer.kind,
    rtpParameters: consumer.rtpParameters,
    type: consumer.type,
  });

  // Notify the producer about the new consumer
  producer.transport.socket.emit('newConsumer', { consumerId: consumer.id, socketId });
}

// Helper function to create a WebRTC transport
async function createTransport() {
  const transport = await router.createWebRtcTransport({
    listenIps: [{ ip: '127.0.0.1', announcedIp: null }],
    enableUdp: true,
    enableTcp: true,
    preferUdp: true,
  });

  const params = {
    id: transport.id,
    iceParameters: transport.iceParameters,
    iceCandidates: transport.iceCandidates,
    dtlsParameters: transport.dtlsParameters,
  };

  return { transport, params };
}

// Helper function to close a WebRTC transport
function closeTransport(transport) {
  if (transport) {
    transport.close();
  }
}

// Helper function to get a producer by ID
function getProducerById(producerId) {
  for (const client of clients.values()) {
    if (client.producer && client.producer.id === producerId) {
      return client.producer;
    }
  }
  return null;
}

client

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebRTC SFU Example</title>
</head>
<body>
  <script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
  <script>
    const socket = io();

    socket.on('connect', () => {
      console.log('Connected to server');
    });

    socket.on('disconnect', () => {
      console.log('Disconnected from server');
    });

    let producerTransport;
    let consumerTransport;
    let producer;

    // Create WebRTC transports
    socket.emit('createWebRtcTransport', { direction: 'producer' }, (params) => {
      createWebRtcTransport('producer', params);
    });

    socket.emit('createWebRtcTransport', { direction: 'consumer' }, (params) => {
      createWebRtcTransport('consumer', params);
    });

    // Function to create a WebRTC transport
    function createWebRtcTransport(direction, params) {
      const transport = new RTCPeerConnection({ iceServers: params.ice });
      const kind = direction === 'producer' ? 'video' : 'audio';

      transport.onicecandidate = (event) => {
        if (event.candidate) {
          socket.emit('iceCandidate', { direction, candidate: event.candidate });
        }
      };

      transport.oniceconnectionstatechange = () => {
        console.log(`ICE connection state changed: ${transport.iceConnectionState}`);
      };

      if (direction === 'producer') {
        producerTransport = transport;
        // Implement logic to get media stream (e.g., getUserMedia) and create producer
        // ...

      } else {
        consumerTransport = transport;
        // Implement logic to create consumer
        // ...

      }
    }

    // Handle new producer event
    socket.on('newProducer', (data) => {
      console.log(`New producer available: ${data.producerId}`);
      // Implement logic to create a consumer for the new producer
      // ...
    });

    // Handle new consumer event
    socket.on('newConsumer', (data) => {
      console.log(`New consumer available: ${data.consumerId}`);
      // Implement logic to handle the new consumer
      // ...
    });

    // Function to handle iceCandidate events
    function handleIceCandidate(direction, candidate) {
      const transport = direction === 'producer' ? producerTransport : consumerTransport;
      transport.addIceCandidate(new RTCIceCandidate(candidate))
        .catch((error) => console.error(`Error adding ICE candidate: ${error}`));
    }

    // Listen for iceCandidate events from the server
    socket.on('iceCandidate', (data) => {
      handleIceCandidate(data.direction, data.candidate);
    });

  </script>
</body>
</html>

Tham khảo

About WebRTC | millo’s tech blog
Let’s learn about ICE, SDP, STUN, TURN, and NAT in WebRTC.