11. WebRTC: основы
WebRTC (Web Real-Time Communication) — технология для прямой peer-to-peer связи между браузерами: видео, аудио, данные — без сервера-посредника.
Как работает WebRTC
Заголовок раздела «Как работает WebRTC»Обычная архитектура:Клиент A → Сервер → Клиент B
WebRTC (после установки):Клиент A ←————→ Клиент B (прямое соединение!) ↕ ↕ Сервер (только для signaling)Три компонента WebRTC
Заголовок раздела «Три компонента WebRTC»1. Signaling Server (ваш сервер)
Заголовок раздела «1. Signaling Server (ваш сервер)»Помогает двум клиентам найти друг друга и обменяться конфигурацией. После этого — не нужен:
// Signaling через Socket.ioio.on('connection', (socket) => { // Передать offer от A к B socket.on('offer', ({ to, offer }) => { io.to(to).emit('offer', { from: socket.id, offer }); });
// Передать answer от B к A socket.on('answer', ({ to, answer }) => { io.to(to).emit('answer', { from: socket.id, answer }); });
// ICE кандидаты (информация о маршруте) socket.on('ice-candidate', ({ to, candidate }) => { io.to(to).emit('ice-candidate', { from: socket.id, candidate }); });});2. STUN/TURN серверы
Заголовок раздела «2. STUN/TURN серверы»Помогают клиентам за NAT найти свой публичный IP:
const ICE_SERVERS = { iceServers: [ // STUN — бесплатный Google { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' },
// TURN — нужен если прямое P2P невозможно (за строгим NAT) { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'password', }, ],};3. RTCPeerConnection
Заголовок раздела «3. RTCPeerConnection»Основной API браузера для WebRTC:
const pc = new RTCPeerConnection(ICE_SERVERS);SDP Offer/Answer — установка соединения
Заголовок раздела «SDP Offer/Answer — установка соединения»Клиент A (инициатор) Клиент B (получатель) │ │ │ createOffer() │ │ setLocalDescription(offer) │ │──── offer → Signaling ──────→│ │ setRemoteDescription(offer) │ createAnswer() │ setLocalDescription(answer) │←─── answer ← Signaling ─────│ │ setRemoteDescription(answer) │ │ │ │←── ICE candidates ──────────→│ (несколько раундов) │ │ │←═══ P2P соединение ════════→│ 🎉Полный пример: видеозвонок
Заголовок раздела «Полный пример: видеозвонок»Клиент-инициатор
Заголовок раздела «Клиент-инициатор»class VideoCall { constructor(signalingSocket) { this.socket = signalingSocket; this.pc = new RTCPeerConnection(ICE_SERVERS); this.localStream = null; this.remoteStream = new MediaStream();
this._setupPeerConnection(); this._setupSignaling(); }
_setupPeerConnection() { // Отправлять ICE кандидатов через signaling this.pc.onicecandidate = ({ candidate }) => { if (candidate) { this.socket.emit('ice-candidate', { to: this.peerId, candidate }); } };
// Получить remote медиа треки this.pc.ontrack = ({ streams }) => { streams[0].getTracks().forEach((track) => { this.remoteStream.addTrack(track); }); document.getElementById('remote-video').srcObject = this.remoteStream; };
// Следить за состоянием ICE this.pc.oniceconnectionstatechange = () => { console.log('ICE state:', this.pc.iceConnectionState); }; }
_setupSignaling() { this.socket.on('answer', async ({ from, answer }) => { await this.pc.setRemoteDescription(new RTCSessionDescription(answer)); });
this.socket.on('ice-candidate', async ({ candidate }) => { try { await this.pc.addIceCandidate(new RTCIceCandidate(candidate)); } catch (err) { console.error('ICE error:', err); } });
this.socket.on('offer', async ({ from, offer }) => { this.peerId = from; await this.pc.setRemoteDescription(new RTCSessionDescription(offer));
// Добавить локальный стрим (если ещё не добавлен) if (this.localStream) { this.localStream.getTracks().forEach((track) => { this.pc.addTrack(track, this.localStream); }); }
const answer = await this.pc.createAnswer(); await this.pc.setLocalDescription(answer); this.socket.emit('answer', { to: from, answer }); }); }
// Начать звонок async call(peerId) { this.peerId = peerId;
// Получить доступ к камере и микрофону this.localStream = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720 }, audio: true, });
document.getElementById('local-video').srcObject = this.localStream;
// Добавить треки в соединение this.localStream.getTracks().forEach((track) => { this.pc.addTrack(track, this.localStream); });
// Создать и отправить offer const offer = await this.pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true, }); await this.pc.setLocalDescription(offer); this.socket.emit('offer', { to: peerId, offer }); }
// Завершить звонок hangup() { this.localStream?.getTracks().forEach((t) => t.stop()); this.pc.close(); this.socket.emit('hangup', { to: this.peerId }); }
// Выключить/включить микрофон toggleMic() { const audioTrack = this.localStream?.getAudioTracks()[0]; if (audioTrack) audioTrack.enabled = !audioTrack.enabled; }
// Выключить/включить камеру toggleCamera() { const videoTrack = this.localStream?.getVideoTracks()[0]; if (videoTrack) videoTrack.enabled = !videoTrack.enabled; }}RTCDataChannel — P2P данные
Заголовок раздела «RTCDataChannel — P2P данные»WebRTC можно использовать не только для медиа, но и для произвольных данных:
// Отправитель создаёт DataChannelconst channel = pc.createDataChannel('chat', { ordered: true, // Гарантированный порядок maxRetransmits: 3, // Максимум повторных отправок});
channel.onopen = () => { channel.send('Привет!'); channel.send(JSON.stringify({ type: 'file-info', name: 'photo.jpg', size: 1024000 }));
// Отправить бинарный файл channel.binaryType = 'arraybuffer'; channel.send(fileArrayBuffer);};
channel.onmessage = (event) => { if (typeof event.data === 'string') { console.log('Текст:', event.data); } else { console.log('Бинарные данные:', event.data.byteLength, 'байт'); }};
// Получатель принимает DataChannelpc.ondatachannel = (event) => { const channel = event.channel; channel.onmessage = (e) => console.log('Получено:', e.data);};Screen sharing — демонстрация экрана
Заголовок раздела «Screen sharing — демонстрация экрана»// Получить поток экранаasync function startScreenShare() { const stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always', // Показывать курсор displaySurface: 'monitor', // 'monitor', 'window', 'browser' }, audio: false, });
// Заменить видеотрек в существующем соединении const videoTrack = stream.getVideoTracks()[0]; const sender = pc.getSenders().find((s) => s.track?.kind === 'video'); await sender?.replaceTrack(videoTrack);
// Когда пользователь остановил шаринг videoTrack.onended = () => { stopScreenShare(); };}Отладка WebRTC
Заголовок раздела «Отладка WebRTC»// chrome://webrtc-internals — встроенный отладчик Chrome
// Статистика соединенияconst stats = await pc.getStats();stats.forEach((stat) => { if (stat.type === 'inbound-rtp' && stat.mediaType === 'video') { console.log('Видео RTP:'); console.log(' Пакетов получено:', stat.packetsReceived); console.log(' Потерь:', stat.packetsLost); console.log(' Jitter:', stat.jitter); console.log(' Байт:', stat.bytesReceived); }
if (stat.type === 'candidate-pair' && stat.state === 'succeeded') { console.log('Маршрут:', stat.localCandidateId, '→', stat.remoteCandidateId); }});Задания
Заголовок раздела «Задания»- Создай простой видеочат: два браузера на одной машине через WebRTC
- Реализуй P2P передачу файла через DataChannel (без сервера!)
- Добавь кнопку демонстрации экрана в видеочат
- Используй
getStats()для показа качества соединения (потери пакетов, bitrate)
WebRTC = P2P видео/аудио/данные через браузер. Нужен signaling-сервер для установки соединения, затем связь идёт напрямую. Идеален для видеозвонков и P2P передачи данных. В следующем уроке — безопасность real-time систем.