Перейти к содержимому

11. WebRTC: основы

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

Обычная архитектура:
Клиент A → Сервер → Клиент B
WebRTC (после установки):
Клиент A ←————→ Клиент B (прямое соединение!)
↕ ↕
Сервер (только для signaling)

Помогает двум клиентам найти друг друга и обменяться конфигурацией. После этого — не нужен:

// Signaling через Socket.io
io.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 });
});
});

Помогают клиентам за 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',
},
],
};

Основной API браузера для WebRTC:

const pc = new RTCPeerConnection(ICE_SERVERS);
Клиент 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;
}
}

WebRTC можно использовать не только для медиа, но и для произвольных данных:

// Отправитель создаёт DataChannel
const 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, 'байт');
}
};
// Получатель принимает DataChannel
pc.ondatachannel = (event) => {
const channel = event.channel;
channel.onmessage = (e) => console.log('Получено:', e.data);
};
// Получить поток экрана
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();
};
}
// 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);
}
});
  1. Создай простой видеочат: два браузера на одной машине через WebRTC
  2. Реализуй P2P передачу файла через DataChannel (без сервера!)
  3. Добавь кнопку демонстрации экрана в видеочат
  4. Используй getStats() для показа качества соединения (потери пакетов, bitrate)

WebRTC = P2P видео/аудио/данные через браузер. Нужен signaling-сервер для установки соединения, затем связь идёт напрямую. Идеален для видеозвонков и P2P передачи данных. В следующем уроке — безопасность real-time систем.