WebRTC: основы

WebRTC (Web Real-Time Communication) — браузерный API для прямой peer-to-peer передачи аудио, видео и данных между клиентами без промежуточного сервера.

Зачем нужно

В отличие от WebSocket и SSE, которые проходят через сервер, WebRTC создаёт прямое соединение между браузерами. Это снижает задержку, экономит серверные ресурсы и позволяет передавать медиапотоки (видеозвонки, screensharing) с минимальным латентностью. Используется в Google Meet, Zoom Web, Discord.

Где используется

  • Видеоконференции в браузере (без плагинов)
  • Peer-to-peer передача файлов
  • Совместная игра в реальном времени
  • Screen sharing и remote desktop в браузере
  • Голосовые чаты в WebApps

Процесс установки соединения (Signaling)

Клиент A                  Сигнальный сервер          Клиент B
    |                      (WebSocket/HTTP)               |
    |--- Offer (SDP) ------>|                              |
    |                       |--- Offer (SDP) ------------>|
    |                       |<--- Answer (SDP) -----------|
    |<--- Answer (SDP) -----|                              |
    |--- ICE candidate ---->|--- ICE candidate ---------->|
    |<--- ICE candidate ----|<--- ICE candidate -----------|
    |<=======================Прямое P2P соединение======= () => |

Базовый пример

Клиент A — создаёт предложение (Offer)

// Получаем медиапоток (камера + микрофон)
const localStream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true,
});

// Отображаем локальное видео
document.getElementById('localVideo').srcObject = localStream;

// Создаём RTCPeerConnection
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }, // STUN для обнаружения внешнего IP
  ],
});

// Добавляем локальные треки
localStream.getTracks.forEach(track => pc.addTrack(track, localStream));

// Получаем удалённый поток
pc.ontrack = event => {
  document.getElementById('remoteVideo').srcObject = event.streams[0];
};

// ICE кандидаты — отправляем через сигнальный сервер
pc.onicecandidate = event => {
  if (event.candidate) {
    signalingChannel.send({ type: 'ice', candidate: event.candidate });
  }
};

// Создаём Offer
const offer = await pc.createOffer;
await pc.setLocalDescription(offer);
signalingChannel.send({ type: 'offer', sdp: offer });

Клиент B — принимает и отвечает

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

signalingChannel.on('message', async ({ type, sdp, candidate }) => {
  if (type === 'offer') {
    await pc.setRemoteDescription(sdp);
    const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
    localStream.getTracks.forEach(t => pc.addTrack(t, localStream));
    const answer = await pc.createAnswer;
    await pc.setLocalDescription(answer);
    signalingChannel.send({ type: 'answer', sdp: answer });
  }
  if (type === 'answer') {
    await pc.setRemoteDescription(sdp);
  }
  if (type === 'ice') {
    await pc.addIceCandidate(candidate);
  }
});

DataChannel — передача данных без медиа

// Создание канала данных (клиент A)
const channel = pc.createDataChannel('chat');
channel.onopen = () => channel.send('Привет!');
channel.onmessage = e => console.log('Сообщение:', e.data);

// Клиент B получает канал
pc.ondatachannel = event => {
  const channel = event.channel;
  channel.onmessage = e => console.log('Получено:', e.data);
};

Частые ошибки

  • Путают WebRTC с WebSocket — WebRTC P2P, WebSocket через сервер
  • Не учитывают NAT traversal — STUN/TURN серверы обязательны в продакшне
  • Забывают освобождать медиапотоки при завершении: stream.getTracks.forEach(t => t.stop())
  • Реализуют signaling по HTTP вместо WebSocket — задержка при обмене ICE-кандидатами

Связанные темы

Ресурсы


🎓 Источник: WebRTC для создания P2P чата (Node.js STUN, сигнальный сервер)

  • 📅 2025-12-12 · YouTube
  • Тезисы:
    • Два сервера: сигнальный (HTTP) для обмена адресами + STUN (UDP) для узнавания своего внешнего IP/порта.
    • STUN-сервер раскрывает внешний адрес за NAT через датаграммный обмен.
    • inbox (очередь сообщений) на сигнальном сервере — если пир отключится, после переподключения получит накопленные сигналы.
    • Парсинг тела HTTP через for await (chunk of req) — async iterable стрим.
    • Бинарный протокол STUN — компактный формат поверх UDP.
    • Сигнальный код на HTTP не переносится на WebRTC автоматически — нужно отвязать бизнес-логику от транспорта.
  • Цитата: «joinRoom привязан к HTTP, к кодам ответов 400/200 и readable stream — его не пересадить на WebRTC или WebSocket автоматически.»