Реализация WebRTC для видеозвонков на сайте
WebRTC — это не просто «добавить видеозвонки». Это комплекс протоколов: ICE, STUN, TURN, SDP-negotiation, DTLS-SRTP и медиапайплайн через getUserMedia. Без понимания каждого из этих компонентов получаются звонки, которые работают только в локальной сети или падают за NAT.
Из чего состоит WebRTC-стек
Браузер предоставляет RTCPeerConnection — центральный объект, который управляет всем: ICE-кандидатами, медиапотоками, шифрованием. Поверх него нужен сигнальный сервер — WebRTC сам по себе не определяет, как два клиента обмениваются SDP-офферами. Это отдельная задача.
Типичный стек для продакшена:
| Компонент | Варианты |
|---|---|
| Сигнальный сервер | Socket.IO, WebSocket (Go/Node), Phoenix Channels |
| ICE/STUN | coturn, Twilio STUN, Google STUN |
| TURN-сервер | coturn на выделенном VPS, Twilio TURN, Xirsys |
| Медиасервер (SFU) | mediasoup, Janus, LiveKit, Jitsi Videobridge |
| Клиентская библиотека | нативный RTCPeerConnection или simple-peer, mediasoup-client |
Для P2P-звонков (до 4 участников) SFU не нужен. При 5+ участниках mesh-топология создаёт n(n-1)/2 соединений на каждого — это неприемлемо. Нужен SFU.
Архитектура P2P-звонка
Alice Signal Server Bob
|------ offer SDP -------->| |
| |------ offer SDP ->|
|<----- answer SDP --------| |
| |<---- answer SDP --|
|<========= ICE candidates exchange =========>|
|<============== DTLS handshake ==============>|
|<======= encrypted RTP/RTCP media stream ====>|
Каждый браузер собирает ICE-кандидатов: host (локальный IP), srflx (публичный IP через STUN), relay (через TURN). TURN-ретрансляция нужна примерно в 15–20% соединений — корпоративные файрволы, симметричный NAT.
ICE и TURN: почему без этого ничего не работает
STUN-сервер отвечает на вопрос «какой у меня внешний IP?». TURN-сервер проксирует медиатрафик, когда прямое соединение невозможно. coturn — стандартный выбор для self-hosted:
# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
realm=yourdomain.com
server-name=yourdomain.com
lt-cred-mech
use-auth-secret
static-auth-secret=YOUR_SECRET
total-quota=100
bps-capacity=0
stale-nonce=600
cert=/etc/letsencrypt/live/yourdomain.com/fullchain.pem
pkey=/etc/letsencrypt/live/yourdomain.com/privkey.pem
TURN через TLS на 443 порту обходит большинство корпоративных ограничений.
Сигнальный сервер на Node.js + Socket.IO
// server.js
io.on('connection', (socket) => {
socket.on('join-room', (roomId, userId) => {
socket.join(roomId);
socket.to(roomId).emit('user-connected', userId);
socket.on('offer', (offer, targetId) => {
io.to(targetId).emit('offer', offer, socket.id);
});
socket.on('answer', (answer, targetId) => {
io.to(targetId).emit('answer', answer, socket.id);
});
socket.on('ice-candidate', (candidate, targetId) => {
io.to(targetId).emit('ice-candidate', candidate, socket.id);
});
socket.on('disconnect', () => {
socket.to(roomId).emit('user-disconnected', userId);
});
});
});
RTCPeerConnection на клиенте
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.yourdomain.com:3478' },
{
urls: 'turn:turn.yourdomain.com:3478',
username: generateTurnUsername(ttl),
credential: generateTurnCredential(username, secret),
},
],
iceTransportPolicy: 'all', // 'relay' для принудительного TURN
});
// Добавляем медиа
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } },
audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 48000 },
});
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Negotiation
pc.onicecandidate = ({ candidate }) => {
if (candidate) socket.emit('ice-candidate', candidate, targetId);
};
pc.onnegotiationneeded = async () => {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', offer, targetId);
};
Кодеки и качество
По умолчанию браузеры договариваются о кодеках через SDP. Для видео это VP8, VP9 или H.264; для аудио — Opus. Принудительная установка предпочтительного кодека через манипуляцию SDP:
function preferCodec(sdp, codecName) {
const lines = sdp.split('\n');
// Находим PT кодека и переставляем в начало m= секции
// ...
return lines.join('\n');
}
const offer = await pc.createOffer();
offer.sdp = preferCodec(offer.sdp, 'VP9'); // VP9 — лучшее качество при той же пропускной способности
await pc.setLocalDescription(offer);
Адаптивный битрейт через RTCRtpSender.setParameters:
const sender = pc.getSenders().find(s => s.track.kind === 'video');
const params = sender.getParameters();
params.encodings[0].maxBitrate = 800000; // 800 kbps
await sender.setParameters(params);
Simulcast для масштабируемых конференций
При работе с SFU (mediasoup, LiveKit) используется Simulcast — клиент отправляет несколько потоков с разным разрешением:
pc.addTransceiver(videoTrack, {
direction: 'sendonly',
sendEncodings: [
{ rid: 'low', maxBitrate: 150000, scaleResolutionDownBy: 4 },
{ rid: 'mid', maxBitrate: 500000, scaleResolutionDownBy: 2 },
{ rid: 'high', maxBitrate: 1500000 },
],
});
SFU выбирает нужный слой для каждого получателя в зависимости от его пропускной способности.
Запись звонков
Серверная запись через SFU предпочтительнее клиентской. Если нужна клиентская:
const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9,opus',
videoBitsPerSecond: 2500000,
});
const chunks = [];
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
uploadToServer(blob);
};
recorder.start(1000); // chunk каждую секунду для надёжности
Диагностика и мониторинг
getStats() — основной инструмент отладки:
setInterval(async () => {
const stats = await pc.getStats();
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'video') {
console.log({
packetsLost: report.packetsLost,
jitter: report.jitter,
framesDecoded: report.framesDecoded,
framesPerSecond: report.framesPerSecond,
});
}
});
}, 2000);
Для продакшен-мониторинга — интеграция с Datadog WebRTC или открытый webrtc-internals (chrome://webrtc-internals) в период отладки.
Сроки и трудозатраты
- P2P видеозвонок с сигнальным сервером — 3–5 дней (два участника, базовые контролы)
- Групповые звонки через SFU (mediasoup/LiveKit) — 2–3 недели (настройка сервера, масштабирование, комнаты)
- Запись + постобработка — плюс 1 неделя
- Полноценная конференц-платформа (комнаты, чат, screenshare, запись) — 6–10 недель
Совместимость
WebRTC поддерживается во всех современных браузерах. Safari поддерживает с версии 11, но имеет ограничения: нет поддержки Insertable Streams в старых версиях, проблемы с renegotiation. iOS требует нативного приложения или PWA — браузерный WebRTC на iOS Safari работает с версии 14.5.







