«Сделайте видеозвонки на сайте, как в Zoom». Звучит просто, но за этой фразой — несколько недель инженерной работы и постоянная боль с TURN-серверами, потерями пакетов и Safari, который ведёт себя не как все. Разбираем, что реально требуется и где готовые решения.
Из чего состоят видеозвонки
| Компонент | Зачем |
|---|---|
| WebRTC | браузерный API для медиапотоков |
| Signaling server | обмен SDP между участниками |
| STUN-сервер | помощь с обходом NAT |
| TURN-сервер | релей трафика, если P2P не получился |
| SFU | сервер-роутер медиа для 3+ участников |
| MCU | сервер-микшер (тяжёлый, редко) |
Для звонка 1-на-1 нужны WebRTC + Signaling + STUN + TURN. Для конференции 3+ человек добавляется SFU.
Базовый WebRTC: 1-на-1
// получить камеру и микрофон
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localVideo.srcObject = stream;
// создать peer connection
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "turn:turn.example.ru:3478", username: "u", credential: "p" },
],
});
stream.getTracks().forEach((t) => pc.addTrack(t, stream));
pc.ontrack = (e) => { remoteVideo.srcObject = e.streams[0]; };
pc.onicecandidate = (e) => {
if (e.candidate) signaling.send("ice", e.candidate);
};
// инициатор
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send("offer", offer);
// получатель
signaling.on("offer", async (offer) => {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signaling.send("answer", answer);
});
Signaling — обычно WebSocket. Передаёт SDP-описания и ICE-кандидаты. Без него участники не найдут друг друга.
STUN и TURN
WebRTC старается соединить пиров напрямую. Если у обоих есть публичный IP — без проблем. Если за NAT (большинство пользователей) — STUN помогает определить внешние адреса. Если NAT симметричный или CGNAT (мобильный интернет) — нужен TURN, который ретранслирует трафик через себя.
В реальности 20-40% звонков идут через TURN. Без него ваше «P2P» работает у половины пользователей.
Бесплатных публичных TURN нет (Google STUN — только STUN). Варианты:
- Свой coturn: открытый софт, ставится на VPS. Трафик: 1 ГБ ≈ 6-7 минут видео качества HD. Один пользователь за час забирает 200-500 МБ.
- Twilio Network Traversal Service: $0.40-0.80 за ГБ, оплата с зарубежной карты.
- Xirsys: $33/мес стартер.
- Metered.ca: $0.40/ГБ, бесплатно до 50 ГБ.
Для российских проектов в 2026 — обычно coturn на собственном VPS в РФ. Каналы у российских хостингов дешёвые ($5-20 за TB).
# coturn config /etc/turnserver.conf
listening-port=3478
fingerprint
lt-cred-mech
realm=example.ru
user=clientuser:strongpass
external-ip=YOUR_VPS_IP
Конференции 3+ — нужен SFU
В P2P сетке каждый шлёт каждому. Для 3 человек — 6 потоков, для 5 — 20. Аплинк забивается у первого же участника с обычной DSL-линией.
SFU (Selective Forwarding Unit) — сервер, к которому подключаются все участники. Каждый шлёт один поток, сервер раздаёт его остальным. Нагрузка на участника фиксированная: один аплинк, N-1 даунлинков.
Готовые SFU:
| SFU | Особенность | Лицензия |
|---|---|---|
| LiveKit | современный, удобный SDK, Cloud + self-hosted | Apache 2.0 |
| mediasoup | low-level, для сложных сценариев | ISC |
| Janus | старый и проверенный | GPL |
| Jitsi Videobridge | open source, у Jitsi-проекта | Apache 2.0 |
| Pion (Go) | low-level, для своих SFU | MIT |
LiveKit — самый удобный
В 2026 — стандартный выбор для SaaS-видеозвонков. Open source, на Go, есть managed cloud.
import { Room } from "livekit-client";
const room = new Room();
await room.connect("wss://livekit.example.ru", token);
// получить камеру/мик
await room.localParticipant.enableCameraAndMicrophone();
// слушать удалённых
room.on("trackSubscribed", (track, pub, participant) => {
if (track.kind === "video") {
document.getElementById("videos")!.appendChild(track.attach());
}
});
Серверная часть генерирует JWT-токен с правами на комнату:
import { AccessToken } from "livekit-server-sdk";
const at = new AccessToken(API_KEY, API_SECRET, { identity: userId });
at.addGrant({ roomJoin: true, room: "meeting-42" });
const token = at.toJwt();
Self-hosted: один Docker-контейнер + Redis для координации, держит сотни одновременных участников на средней машине. Cloud — от $30/мес.
Запись звонков
Запись в браузере (MediaRecorder) работает только локально и только пока вкладка открыта. Для серверной записи нужен SFU с recording-функцией:
- LiveKit Egress — выгружает в S3/MinIO как MP4, HLS или серию изображений.
- Janus — recordplay плагин.
- Jibri — компонент Jitsi для записи.
Сервер тяжёлый: запись 1080p — это ~30% CPU отдельного процесса на каждый звонок. Закладывайте мощную инфраструктуру.
Шумоподавление и фон
Современные браузеры уже умеют:
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});
Размытие фона — через MediaPipe (Google) или Krisp (платный SDK). На мобильных может тормозить, делайте кнопкой «вкл/выкл», а не по умолчанию.
Safari особенности
Главные грабли:
getUserMediaв Safari требует пользовательского жеста (клика). Нельзя вызвать на page load.- Видео не играет без
playsInline. - Background audio в фоне — дополнительные ограничения.
- iOS Safari — только один live-видеопоток в момент времени до 16.4.
Тестируйте обязательно в реальном Safari на iPhone, а не только в Chrome DevTools.
Сетевые проблемы
WebRTC адаптивно подстраивается под канал, но:
- Пакетные потери > 5% — каша из квадратов.
- Jitter > 50 мс — рассинхрон.
- Длительный пик loss — сессия может разорваться.
Метрики на клиенте — pc.getStats(). На сервере — Prometheus-экспортер LiveKit.
В UI показывайте индикатор качества связи (зелёный/жёлтый/красный) и автоматически снижайте разрешение при потерях.
Сценарии и стоимость
| Сценарий | Архитектура | Месячный бюджет |
|---|---|---|
| Звонок 1-на-1 в виджете консультации | WebRTC + coturn | 2-5 тыс. ₽ |
| Видеоприём в телемед-сервисе, 50 параллельных | LiveKit self-hosted | 10-20 тыс. ₽ |
| Конференции 10-50 человек, корп.SaaS | LiveKit Cloud или Jitsi | $200-1000 |
| Стримы 1-к-100 (вебинар) | LiveKit или собственный HLS | зависит от трафика |
Альтернативы
Если не хочется возиться:
- Daily.co — managed, $9-99/мес, простой API, но платежи проблематичны.
- Twilio Video — корп-уровень, дорого, тоже платежи.
- Voximplant — российский, можно оплатить рублями.
- VK Звонок API — российский, бесплатно с лимитами.
Итого
Если у вас 1-на-1 виджет «связаться с менеджером» — голый WebRTC + свой coturn. Если конференции — LiveKit (cloud или self-hosted). Если корпоративный продукт с регуляторкой — Voximplant или сразу свой mediasoup. Никогда не обещайте «как Zoom» — за полтора месяца сделаете 30% от Zoom, остальные 70% — это годы команды и десятки миллионов долларов.
Частые вопросы
Сколько пользователей выдержит свой LiveKit на одной машине?
На VPS с 8 vCPU и 16 ГБ RAM — 200-500 одновременных участников при HD-видео. С 4K или большим количеством опубликованных треков на участника — меньше. Главный ботлнек обычно сеть: 100 участников × 2 Мбит/с = 200 Мбит/с трафика на сервер. Для масштабирования — несколько SFU за региональным распределителем.
Можно ли сделать видеочат без TURN-сервера?
Можно, но 20-40% пользователей не смогут установить соединение. Это пользователи за NAT провайдеров, корпоративных файрволлов, мобильного 4G. Для серьёзного продукта без TURN — нельзя. Для внутреннего инструмента в офисе с одной локальной сетью — иногда хватает голого STUN.
LiveKit или mediasoup?
LiveKit — высокоуровневый, есть SDK для всех платформ, готовые UI-компоненты, простой деплой. mediasoup — low-level, гибче, лучше для сложных сценариев (свой формат сигналинга, миксинг с не-WebRTC потоками). 95% проектов хватает LiveKit. mediasoup — когда у вас команда WebRTC-инженеров и нестандартные требования.
Сколько стоит трафик TURN для 100 пользователей в день по 30 минут?
100 × 30 мин × ~5 Мбит/с (релейный двусторонний) ≈ 110 ГБ/день, 3.3 ТБ/месяц. На своём coturn в РФ-хостинге трафик дешёвый: $20-50/мес. На Twilio — 3300 × $0.40 = $1320/мес. Разница огромная. Свой coturn окупается мгновенно.
Как сделать запись звонка с двух камер одновременно?
Только серверной записью: LiveKit Egress / Jibri пишут все потоки в комнате в один MP4 в layout (grid или speaker view), либо отдельными файлами. Клиентский MediaRecorder может писать только локальный микс — не для прода. Закладывайте отдельный сервер под запись, сильно нагружает CPU.
Какой размер видеопотока выбрать?
Адаптивно: simulcast (LiveKit умеет) — публикуете три разрешения (180p/360p/720p), сервер раздаёт каждому участнику то, что он может принять. Без simulcast — 480p как baseline для мобильных, 720p для десктопа. 1080p в большинстве сценариев избыточен и съедает трафик.
WebRTC через корпоративный прокси работает?
Часто нет. Многие корп-прокси режут UDP, на котором работает WebRTC. Решение — TURN over TCP/443 (как HTTPS) и TURN over TLS — coturn умеет оба. Если у вашей аудитории много корп-юзеров (B2B-инструменты), обязательно настройте оба варианта, иначе процент жалоб «не работает на работе» будет высокий.