WebRTC: real-time коммуникации в браузере
Создаем видеозвонки, скринкаст и P2P обмен данными
WebRTC позволяет создавать real-time аудио, видео и data коммуникации прямо в браузере без плагинов. Zoom, Discord, Google Meet — все используют WebRTC. Разберем как это работает и как создать свое решение. Что такое WebRTC WebRTC (Web Real-Time Communication) — набор API для P2P коммуникаций между браузерами. Основные компоненты: MediaStream API — доступ к камере/микрофону RTCPeerConnection — P2P соединение RTCDataChannel — обмен данными Получение доступа к камере/микрофону async function startCamera() { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' }, audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); const videoElement = document.getElementById('localVideo'); videoElement.srcObject = stream; return stream; } catch (error) { console.error('Camera access denied:', error); } } Screen Sharing: async function shareScreen() { const stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always', displaySurface: 'monitor' }, audio: true // включить системный звук }); return stream; } Создание P2P соединения Основной процесс: Создать RTCPeerConnection Добавить медиа треки Создать offer (инициатор) Обменяться SDP через signaling server Обменяться ICE candidates Соединение установлено! Код инициатора (caller): const configuration = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'turn:turn.example.com', username: 'user', credential: 'pass' } ] }; const pc = new RTCPeerConnection(configuration); // Добавляем локальные треки localStream.getTracks().forEach(track => { pc.addTrack(track, localStream); }); // Получаем удаленные треки pc.ontrack = (event) => { remoteVideo.srcObject = event.streams[0]; }; // Собираем ICE candidates pc.onicecandidate = (event) => { if (event.candidate) { sendToSignalingServer({ type: 'candidate', candidate: event.candidate }); } }; // Создаем offer const offer = await pc.createOffer(); await pc.setLocalDescription(offer); // Отправляем offer через signaling server sendToSignalingServer({ type: 'offer', sdp: pc.localDescription }); Код получателя (callee): // Получаем offer onOfferReceived(async (offer) => { await pc.setRemoteDescription(offer); // Добавляем свои треки localStream.getTracks().forEach(track => { pc.addTrack(track, localStream); }); // Создаем answer const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); // Отправляем answer sendToSignalingServer({ type: 'answer', sdp: pc.localDescription }); }); // Получаем answer (на стороне caller) onAnswerReceived(async (answer) => { await pc.setRemoteDescription(answer); }); // Получаем ICE candidates onCandidateReceived(async (candidate) => { await pc.addIceCandidate(candidate); }); Signaling Server WebRTC требует signaling server для обмена SDP и ICE candidates. WebSocket server (Node.js): import { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); const rooms = new Map(); wss.on('connection', (ws) => { ws.on('message', (data) => { const message = JSON.parse(data); switch (message.type) { case 'join': const { roomId, userId } = message; if (!rooms.has(roomId)) { rooms.set(roomId, new Set()); } rooms.get(roomId).add(ws); ws.roomId = roomId; ws.userId = userId; break; case 'offer': case 'answer': case 'candidate': // Пересылаем другим участникам комнаты const room = rooms.get(ws.roomId); room.forEach(client => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); } }); break; } }); ws.on('close', () => { if (ws.roomId) { rooms.get(ws.roomId).delete(ws); } }); }); RTCDataChannel: обмен данными Для передачи файлов, чата, игровых данных: // Создание канала (caller) const dataChannel = pc.createDataChannel('chat', { ordered: true, // гарантия порядка maxRetransmits: 3 }); dataChannel.onopen = () => { console.log('Data channel opened'); dataChannel.send('Hello!'); }; dataChannel.onmessage = (event) => { console.log('Received:', event.data); }; // Получение канала (callee) pc.ondatachannel = (event) => { const dataChannel = event.channel; dataChannel.onmessage = (event) => { console.log('Received:', event.data); }; }; Передача файлов: async function sendFile(file, dataChannel) { const chunkSize = 16384; // 16KB chunks const fileReader = new FileReader(); let offset = 0; // Отправляем метаданные dataChannel.send(JSON.stringify({ type: 'file-meta', name: file.name, size: file.size, type: file.type })); const readChunk = () => { const slice = file.slice(offset, offset + chunkSize); fileReader.readAsArrayBuffer(slice); }; fileReader.onload = (e) => { dataChannel.send(e.target.result); offset += e.target.result.byteLength; if (offset { if (typeof event.data === 'string') { const message = JSON.parse(event.data); if (message.type === 'file-meta') { fileInfo = message; chunks.length = 0; } else if (message.type === 'file-end') { const blob = new Blob(chunks, { type: fileInfo.type }); const url = URL.createObjectURL(blob); // Скачивание файла const a = document.createElement('a'); a.href = url; a.download = fileInfo.name; a.click(); } } else { chunks.push(event.data); } }; Quality of Service Адаптивный битрейт: pc.getSenders().forEach(sender => { if (sender.track?.kind === 'video') { const parameters = sender.getParameters(); if (!parameters.encodings) { parameters.encodings = [{}]; } // Установка максимального битрейта parameters.encodings[0].maxBitrate = 500000; // 500 kbps sender.setParameters(parameters); } }); Мониторинг качества: 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, framesPerSecond: report.framesPerSecond, bytesReceived: report.bytesReceived }); } }); }, 1000); Многопользовательские звонки Mesh topology (P2P все со всеми): Плюсы: низкая latency, нет сервера Минусы: не масштабируется (3-4 участника максимум) SFU (Selective Forwarding Unit): Каждый отправляет один stream на сервер Сервер пересылает streams участникам Используется в: Zoom, Meet, Teams MCU (Multipoint Control Unit): Сервер микширует все streams в один Каждый получает один combined stream Высокая нагрузка на сервер STUN и TURN серверы STUN (Session Traversal Utilities for NAT): Помогает узнать публичный IP адрес Бесплатные: Google STUN TURN (Traversal Using Relays around NAT): Relay сервер для сложных NAT Используется в 5-10% случаев Платный (трафик идет через сервер) Self-hosted TURN (coturn): # turnserver.conf listening-port=3478 external-ip=YOUR_SERVER_IP user=username:password realm=example.com # Start sudo turnserver -c /etc/turnserver.conf Security HTTPS обязателен — getUserMedia работает только на HTTPS Permissions — пользователь должен разрешить доступ E2E Encryption — WebRTC шифрует по умолчанию (DTLS-SRTP) Signaling security — используйте WSS, authentication Best Practices Graceful degradation — отключайте видео при плохом интернете Echo cancellation — включайте в constraints Bandwidth optimization — адаптивный битрейт Reconnection logic — автоматический реконнект UI feedback — показывайте статус соединения Готовые решения Simple-peer — упрощенный WebRTC PeerJS — P2P data connections Jitsi — open-source видеоконференции Mediasoup — SFU сервер Agora — managed WebRTC platform Заключение: WebRTC открывает возможности для real-time коммуникаций в браузере. Начните с простого P2P соединения, затем масштабируйте через SFU для многопользовательских звонков. Используйте DataChannel для обмена данными. Не забывайте про TURN сервер для сложных NAT. WebRTC — мощная технология, но требует понимания networking. Практикуйтесь, экспериментируйте, создавайте свои решения.