Реалізація WebRTC для відеозвонків на сайті
WebRTC — це не просто «додати відеозвонки». Комплекс протоколів: ICE, STUN, TURN, SDP-negotiation, DTLS-SRTP та медіапайплайн через getUserMedia. Без розуміння кожного компонента отримаєш звонки, що працюють лише у локальній мережі або падають за NAT.
Компоненти WebRTC-стеку
Браузер надає RTCPeerConnection — центральний об'єкт, який керує всім: ICE-кандидатами, медіапотоками, шифруванням. Поверх нього потрібен сигнальний сервер — WebRTC сам не визначає, як два клієнти обмінюються SDP-офферами. Це окрема задача.
Типовий production-стек:
| Компонент | Варіанти |
|---|---|
| Сигнальний сервер | 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
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);
Діагностика та моніторинг
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);
Для production-моніторингу — інтеграція з 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.







