WebRTC peer-to-peer browser audio/video/data. Three scenarios: P2P call setup (signaling SDP offer/answer + ICE/STUN + DTLS + SRTP media), TURN relay when symmetric NAT blocks direct connection, and SFU multi-party conference with simulcast. Includes 4 ADRs covering WebRTC vs WebSocket vs HLS, Trickle ICE, geo-distributed TURN, SFU vs MCU vs Mesh.
Когда нужны видеозвонки, голос, экранный шеринг или peer-to-peer обмен данными прямо из браузера — без плагинов, без Flash, без нативных приложений — WebRTC единственный стандартизованный путь. Это набор JavaScript API (getUserMedia, RTCPeerConnection, RTCDataChannel) плюс целая стопка сетевых протоколов (ICE, STUN, TURN, DTLS, SRTP, SCTP), упакованных так, чтобы два браузера (или браузер с мобильным app, или браузер с медиа-сервером) могли обменяться зашифрованными audio/video/data потоками напрямую, минуя ваш сервер для самих стримов.
WebRTC лежит в основе Google Meet, Zoom Web, Discord voice, Microsoft Teams, Whereby, Jitsi, Telegram Calls, Cloudflare Stream Live. Если строишь что-то realtime в браузере и latency должен быть меньше 100мс — альтернативы нет.
Главная задача протокола — спрятать от приложения три неприятные реальности: NAT/firewall между большинством пользователей, гетерогенные сети (от fiber до 3G), и требование шифрования всего трафика по умолчанию. Всё это работает в одном API, который выглядит обманчиво простым: pc.addTrack(stream), pc.createOffer(), и где-то под капотом 15 RFC.
«WebRTC — это шифрованный P2P UDP-туннель между двумя браузерами, установленный через signaling-сервер, NAT-обходимый через STUN/TURN, с media-кодеками для audio/video и DataChannel для произвольных данных.»
Ключевая абстракция: WebRTC не определяет signaling. Как два браузера узнают друг о друге — ваша проблема (WebSocket, HTTP-polling, что угодно). WebRTC берёт на себя только то, что начинается после обмена SDP: ICE, шифрование, media. Это специально — авторы стандарта не захотели навязывать вертикальный протокол, и любая чат-платформа может прикрутить звонки поверх своего готового канала.
Вторая важная мысль: после рукопожатия signaling-сервер выключается. Media летит peer↔peer (или через TURN), сервер не видит контента и не платит за bandwidth стримов. Это принципиальное отличие от HLS/WebSocket — там сервер всегда в середине.
Три независимых сценария, каждый в своей цветовой группе:
RTCPeerConnection. По центру оранжевая группа Signaling — WebSocket-сервер для обмена SDP и ICE-кандидатами. Справа фиолетовая STUN на :19302. Edges: оба pc-* ходят к WS signaling (TCP), оба к STUN (UDP), и прямой UDP-edge pc-a ↔ pc-b для media после ICE-checks.:3478. Когда P2P не пробивается (symmetric NAT, корпоративный firewall), оба пира открывают UDP-аллокации на TURN и весь media идёт через relay. Edges: pc-a → turn, pc-b → turn.mediasoup). Каждый клиент шлёт 1 upload-стрим в SFU и получает обратно N-1 downloads. Edges подписаны upload SRTP.ADR-001 (на pc-a) — про выбор WebRTC vs WebSocket vs HLS. ADR-002 (тоже на pc-a) — про trickle ICE. ADR-003 (на turn) — geo-распределение TURN. ADR-004 (на sfu) — SFU vs MCU vs Mesh.
Цвет групп специально несхожий, чтобы три сценария читались как отдельные подсистемы — в проде у тебя обычно работает то одно, то другое, в зависимости от того что сделал NAT.
P2P call setup — happy path. Alice нажимает Call, браузер вызывает getUserMedia и захватывает камеру с микрофоном. pc-a.createOffer() генерирует SDP — текстовый блок с описанием media (Opus для audio, VP8/H264 для video), ICE-ufrag, DTLS fingerprint. Offer летит через WSS на signaling-сервер, оттуда — Bob-у. Bob делает setRemoteDescription, createAnswer, отправляет назад. SDP-handshake занимает ~100мс.
Параллельно (trickle ICE — это критично, без него setup был бы вдвое медленнее) оба пира бомбят STUN запросами «какой у меня публичный адрес?». STUN отвечает: «вижу тебя как 203.0.113.5
». Этоsrflx candidate. Каждый кандидат немедленно летит через signaling другому пиру — не ждём пока соберутся все.
Затем ICE connectivity checks: пиры пингуют друг друга по всем парам candidates (host↔host для LAN, srflx↔srflx для интернета). Первая ответившая пара становится nominated. Поверх неё DTLS-handshake — обмен X.509 сертификатами, derive SRTP keys из DTLS-secret. Дальше SRTP пакеты летят зашифрованными напрямую. Total setup time: ~500мс в нормальной сети. End-to-end latency: 30мс по cable, 80мс по WiFi.
Symmetric NAT — TURN включается. Тот же setup, но Alice в корпоративной сети с Cisco ASA, которая делает symmetric NAT — каждому destination даёт разный outbound port. STUN-адрес валиден только для STUN-сервера. ICE checks между Alice и Bob проваливаются: пакеты не доходят ни в одну сторону.
Тогда оба пира делают Allocate request на TURN, получают relay-адреса (192.0.2.10:50001 и :50002). Новый relay candidate летит через trickle ICE, и через несколько секунд media идёт pc-a → turn → pc-b. Latency растёт на 30-100мс (зависит от того где TURN), bandwidth идёт через вашу инфру. В проде 15-20% звонков ходят через TURN — это норма, и без него корпоративные пользователи просто не дозвонятся.
SFU для группового звонка. Четыре участника в комнате. Каждый шлёт 1 upload в SFU с simulcast (три quality layers: 1080p, 720p, 360p). SFU не декодирует и не транскодирует — просто форвардит. User 2 на fiber получает 1080p всех остальных, User 1 на 4G получает 360p — SFU выбирает layer per-receiver на основе bandwidth estimation.
User 3 теряет 5% пакетов — SFU видит NACK, делает RTX-retransmit, добавляет FEC. Если loss растёт — BWE автоматически даунгрейдит до 720p. Bandwidth math: 4 user × 3 stream × 2 Mbps = 24 Mbps server downstream на одну комнату. Сто комнат — 2.4 Gbps egress, влезает в один c5.4xlarge.
ADR-001: WebRTC vs WebSocket vs HLS — когда какой.
Real-time коммуникация в браузере имеет три mainstream-варианта, и каждый занимает свою клетку:
Решающий фактор — пересечение трёх осей: latency budget, число участников, нужен ли media (audio/video) или только data. Hybrid-паттерны нормальны: signaling для WebRTC всегда поверх WebSocket; Discord использует WS gateway + WebRTC media; collaborative editing — CRDT по WebSocket плюс presence по WebRTC DataChannel.
Anti-pattern: использовать DataChannel вместо WebSocket для обычного чата. NAT-traversal сложность, signaling-сервер всё равно нужен, на выходе тот же 200мс latency, зато debugging стал кошмаром.
ADR-002: Trickle ICE vs vanilla ICE.
В vanilla режиме peer ждёт окончания сбора всех candidates, потом формирует один большой SDP-offer со всеми кандидатами внутри. В trickle (RFC 8838) peer шлёт каждый candidate сразу как только нашёл.
Решение очевидное: всегда trickle. Setup time падает с 2-4 секунд до 200-500мс, потому что connectivity checks начинаются параллельно сбору candidates. Цена — больше мелких messages через signaling (10-20 за сессию вместо одного), но на WS это копейки. Vanilla ICE остался только в legacy SIP gateways.
ADR-003: Geo-distributed TURN vs single-region.
TURN ретранслирует media когда P2P невозможен. Один TURN на весь мир — добавляет 100-300мс latency. Размещать TURN в каждой стране пользователя нереально (он не платит за инфру).
Компромисс: 3-5 PoP по миру (us-east, us-west, eu-west, ap-southeast, sa-east), клиент выбирает ближайший через geo-DNS или client-side latency probe. Стоимость bandwidth серьёзная: Twilio берёт ~$0.0007/MB, AWS — $0.09/GB egress. Звонок 30 минут 1080p ≈ 1.5GB. На 1М звонков в день это $100-150K в месяц чистого bandwidth. Managed-сервисы (Twilio, Cloudflare Calls) дешевле на старте, но при масштабе own coturn-кластер выходит в 3-5x экономнее.
ADR-004: SFU vs MCU vs Mesh.
Mesh: каждый с каждым через P2P. N=4 = 3 upload + 3 download на клиента. N=8 = 7+7, мёртвая сеть (15 Mbps только upload). Жизнеспособно до 3-4 пиров.
MCU: сервер микширует все streams в один композитный. Низкий клиентский bandwidth, но огромная CPU-нагрузка (декодировать N + перекодировать 1). Дорого скейлится. Legacy SIP-телефония, embedded devices без CPU.
SFU: сервер форвардит без трансформации. Клиент шлёт 1, принимает N-1. Server: ~1-2% CPU per stream, горизонтальное масштабирование тривиально. С simulcast SFU подбирает quality layer per-receiver. Defacto стандарт на 2026: mediasoup, LiveKit, Janus, Pion. MCU держится только в legacy. Anti-pattern — выбирать MCU «чтобы сэкономить bandwidth»: современные сети дают 50-100 Mbps download у 90% юзеров, реальное узкое место — CPU mixing на сервере.
RTCPeerConnection.getStats(): jitter, packet loss, RTT, bandwidth estimation. Без дашборда из этих метрик невозможно отличить «у пользователя плохой WiFi» от «наш SFU перегружен».