Сегодня разберёмся, как сделать видеозвонки — ту самую фичу, без которой сложно представить современное общение в 2025 году, на примере реализации мессенджера.
Для этого мы познакомимся с WebRTC — технологией, которая позволяет приложениям устанавливать прямое соединение друг с другом для обмена аудио, видео и другими данными. Это мощный, но местами капризный инструмент, который требует понимания архитектуры, сигналинга и сетевых нюансов вроде NAT и ICE.
В этой части мы:
разберёмся, как работает WebRTC под капотом;
напишем сигнальный сервер для обмена данными между участниками звонка;
научим клиентов подключаться друг к другу и передавать медиапотоки;
настроим поддержку STUN и TURN — чтобы звонки работали даже за NAT или в мобильных сетях;
Готовы? Поехали 🚀
Архитектура видео-звонков: как работает WebRTC
Прежде чем писать код, давайте разберёмся, что происходит "под капотом", когда вы запускаете видеозвонок.
WebRTC (Web Real-Time Communication) — это технология, которая позволяет двум клиентам (чаще всего браузерам или мобильным приложениям) обмениваться аудио, видео, а также текстовыми сообщениями. Такое соединение называется P2P (peer-to-peer) — в идеале медиа-трафик передаётся напрямую между клиентами, что снижает задержку и нагрузку на сервер. А если прямое соединение невозможно, в дело вступает TURN-сервер, который ретранслирует трафик между участниками.
В отличие от WebSocket, который работает поверх TCP и используется, например, для чатов, WebRTC использует в основном UDP. Это позволяет добиться минимальной задержки при передаче аудио и видео, что критично для звонков и стриминга.
WebRTC — это не только про видео и аудио. Он также поддерживает передачу любых данных между клиентами через RTCDataChannel. С его помощью можно, например, отправлять текстовые сообщения напрямую, реализовать файлообмен или синхронизацию действий между пользователями без участия сервера.
Вот из чего состоит типичный WebRTC-звонок:
1. Обмен техническими параметрами (SDP)
Когда один клиент хочет позвонить другому, он формирует специальное описание соединения — SDP (Session Description Protocol). Это обычный текст, в котором описано:
какие медиа (аудио/видео) будут передаваться,
какие кодеки поддерживаются,
какие порты слушать и т.д.
Этот SDP называется offer. Второй клиент отвечает своим SDP (answer), где говорит: «Окей, я тоже умею вот это». Но WebRTC не умеет сам доставлять SDP от одного клиента к другому — для этого нужен сигнальный сервер.
2. Поиск пути (ICE, STUN и TURN)
Теперь, когда стороны договорились «что» передавать, им нужно решить «как» соединиться. Ведь за NAT'ами у клиентов могут быть скрытые IP и порты.
Тут включается процесс ICE (Interactive Connectivity Establishment):
каждый клиент собирает список возможных адресов (называются ICE-кандидаты),
обменивается ими через сигнальный сервер,
WebRTC пробует разные пути, пока не найдёт рабочий.
Чтобы найти свой внешний IP и порт, клиент использует STUN-сервер. Если напрямую соединиться не удалось — используется TURN-сервер, который ретранслирует весь трафик.
Подробнее про STUN и TURN мы поговорим чуть ниже.
3. Прямое соединение и передача медиа
Когда соединение установлено, начинается передача аудио и видео — напрямую между клиентами или через TURN. Браузеры (или, в нашем случае, React Native) обрабатывают всё это автоматически: нам нужно лишь правильно сконфигурировать RTCPeerConnection
и передать нужные данные через сигналинг.
Что такое SDP и ICE — простыми словами
Когда два клиента хотят начать звонок, им нужно договориться как это сделать: какие кодеки использовать, какие порты слушать, как передавать видео и т.д. Для этого используется SDP (Session Description Protocol) — текстовый формат описания соединения.
SDP передаётся между клиентами через сигнальный сервер. Один клиент создаёт "offer", второй отвечает "answer". Внутри — всё: от типа медиапотока до информации о сети.
Пример SDP offer
может выглядеть так (сильно упрощённо):
v=0
o=- 123456 2 IN IP4 192.0.2.1
s=-
t=0 0
m=audio 49170 RTP/AVP 0
c=IN IP4 192.0.2.1
a=rtpmap:0 PCMU/8000
ICE (Interactive Connectivity Establishment) — это процесс поиска пути для соединения между клиентами. Так как оба могут находиться за NAT, им нужно "вычислить", как достучаться друг до друга: напрямую, через промежуточные адреса или через TURN-сервер.
Пример ICE-кандидата:
{
"candidate": "candidate:842163049 1 udp 1677729535 192.0.2.3 54874 typ srflx raddr 10.0.0.2 rport 54874",
"sdpMid": "0",
"sdpMLineIndex": 0
}
candidate
: описание маршрута (IP, порт, тип —host
,srflx
,relay
);sdpMid
: ID медиалинии (аудио/видео);sdpMLineIndex
: индекс медиалинии (0 — обычно аудио, 1 — видео и т.п.).
Каждый клиент собирает список возможных адресов (ICE-кандидатов) и отправляет их другому. Затем WebRTC пробует соединиться по ним в порядке приоритета — и выбирает лучший.
Компоненты, которые нам нужны
Чтобы всё заработало, нам понадобятся:
Сигнальный сервер — для первоначального обмена информацией между клиентами. WebRTC не включает механизм сигналинга, поэтому мы сами организуем обмен:
описанием соединения (SDP),
и сетевыми адресами (ICE-кандидатами).
Обычно используют WebSocket или любую другую двустороннюю связь.
STUN-сервер — помогает клиентам определить свои внешние IP-адреса и порты за NAT. Это бесплатно и работает в большинстве случаев.
TURN-сервер — если прямое соединение не получается (жёсткий NAT, корпоративные сети), трафик идёт через промежуточный сервер. Это дороже, но надёжнее.
Клиенты — инициализируют соединение, получают доступ к камере/микрофону, обмениваются данными и отображают видео.
Поток действий
Клиент A инициирует звонок → отправляет SDP на сервер.
Сервер пересылает SDP Клиенту B.
Клиент B отвечает своим SDP.
Оба клиента обмениваются ICE-кандидатами — это информация о возможных путях соединения.
WebRTC выбирает лучший путь (напрямую, через STUN или через TURN).
Поток видео/аудио начинает передаваться напрямую (если повезёт) или через TURN.

STUN и TURN: помощники в мире NAT'ов
В идеальном мире два клиента соединяются напрямую. Но в реальности оба часто находятся за NAT'ами — роутерами, которые скрывают их настоящие IP-адреса. И тут на помощь приходят STUN и TURN.
STUN (Session Traversal Utilities for NAT) — это лёгкий сервер, к которому клиент обращается, чтобы узнать свой публичный IP и порт. Получив эту информацию, клиент может передать её другому участнику звонка как один из ICE-кандидатов. STUN работает быстро и бесплатно, и этого часто достаточно, если хотя бы один клиент не за жёстким NAT.
TURN (Traversal Using Relays around NAT) — резервный план. Если напрямую соединиться не получается (например, оба клиента сидят за симметричными NAT'ами или в корпоративных сетях), TURN-сервер становится посредником: он принимает трафик от одного клиента и пересылает другому. Это медленнее и дороже (требуется мощный сервер с хорошим каналом), но обеспечивает соединение в 100% случаев.
📌 Поэтому хорошая конфигурация WebRTC всегда включает оба сервера:

Переходим к реализации
Мы уже разобрались, как устроен WebRTC: что такое SDP, ICE, STUN и TURN, зачем нужен сигнальный сервер и как строится соединение. Теперь давайте перейдём к самому интересному — реализации видеозвонков в нашем приложении на React Native. Мы будем использовать библиотеку react-native-webrtc
— это популярная обёртка над WebRTC API для React Native. Она позволяет управлять медиапотоками, создавать соединения, слушать события ICE-кандидатов и отображать видео прямо в приложении.
В качестве сигнальной системы мы будет использовать graphql бекенд реализованный в прошлой части. Добавим в него следующее возможности пересылки сигналов между клиентами:
type CallSignal {
type: CallSignalType! # Тип сигнала: OFFER, ANSWER, ICE_CANDIDATE и т.п.
payload: String! # Содержимое сигнала — например, SDP или ICE-кандидат (в виде строки)
chatID: String! # ID чата (используется как "комната" для звонка)
}
input CallSignalInput {
type: CallSignalType! # Тип сигнала
payload: String! # Сам сигнал (JSON-строка)
chatID: String! # К какому чату/разговору относится
}
type Mutation {
sendCallSignal(input: CallSignalInput!): Boolean!
endCall(chatID: String!): Boolean!
}
type Subscription {
callSignalReceived(chatID: String!): CallSignal!
callEnded(chatID: String!): Boolean!
}
Все необходимые инструменты для работы с WebRTC в react-native содержатся в пакете react-native-webrtc.
Для инициализации WebRTC соединения нужно создать объект типа RTCPeerConnection и передать ему наши STUN и TURN сервера в качестве параметров.
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your.turn.server:3478',
username: 'user',
credential: 'password'
}
]
});
STUN пробует первым. Если не сработало — подключается TURN. Всё это происходит прозрачно для разработчика, но критично для стабильности видеозвонков.
После создания объекта RTCPeerConnection, мы можем начинать установку соединения между двумя клиентами. Этот процесс включает несколько шагов:
Где взять STUN и как настроить свой TURN
Бесплатные STUN-серверы
Для разработки и прототипов можно смело использовать публичные STUN-серверы:
stun:stun.l.google.com:19302
stun:stun1.l.google.com:19302
stun:stun.stunprotocol.org:3478
Они работают стабильно, и почти всегда позволяют установить прямое соединение.
TURN-сервер
Если оба клиента находятся за жёсткими NAT'ами, или в корпоративной сети, или мобильной сети с CGNAT — P2P просто не получится. И вот тут на сцену выходит TURN-сервер: он ретранслирует весь трафик между клиентами.
Чтобы обеспечить 100% успешные соединения, особенно в продакшене, нужен свой TURN-сервер. Один из самых популярных и бесплатных — coturn.
Или можно воспользоваться одним из облачных TURN-сервисов, цены зависят от трафика.
1. Добавляем медиапотоки (камера и микрофон)
Сначала получим доступ к камере и микрофону пользователя:
// Импортируем API для работы с медиа-устройствами
import {
mediaDevices
} from 'react-native-webrtc';
// Запрашиваем доступ к аудио и видео (фронтальная камера, 30 fps)
const stream = await mediaDevices.getUserMedia({
audio: true,
video: {
frameRate: 30,
facingMode: 'user',
},
});
// Добавляем все треки (аудио и видео) в соединение
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
2. Создание SDP offer (для инициатора звонка)
Если вы инициируете звонок:
// Создаём offer — описание параметров соединения
const offer = await pc.createOffer({
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true,
VoiceActivityDetection: true
}
});
await pc.setLocalDescription(offer);
// Отправляем OFFER SDP через сигналинг собеседнику
sendCallSignal('OFFER', JSON.stringify(offer));
3. Получение offer и создание answer (для принимающей стороны)
Когда второй клиент получает offer через сигнальный канал:
await pc.setRemoteDescription(
new RTCSessionDescription(offer),
);
// Создаём answer — ответ с нашими параметрами
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// Отправляем ANSWER SDP собеседнику
sendCallSignal('ANSWER', JSON.stringify(answer));
4. ICE-кандидаты (сетевые адреса)
WebRTC сам начинает собирать ICE-кандидаты. Каждый нужно отправлять другой стороне через сигналинг:
// Обработчик события: найден новый ICE-кандидат
pc.addEventListener('icecandidate', event => {
if (event.candidate) {
// Отправляем ICE кандидата собеседнику через сигналинг
sendCallSignal('ICE_CANDIDATE', JSON.stringify(event.candidate));
}
});
ICE-кандидаты приходят не все сразу — а постепенно, в течение нескольких секунд. Поэтому их нужно пересылать каждый раз, когда приходит новый.
И при получении:
// При получении ICE-кандидата от собеседника:
const iceCandidate = new RTCIceCandidate(candidate);
await pc.addIceCandidate(iceCandidate)
💡 Совет: добавьте iceConnectionStateChange
обработчик, чтобы отслеживать состояние соединения и показывать пользователю статус (подключается / отключено / подключено).
5. Отображение видео удалённого пользователя
Когда другой клиент начнёт передавать медиапоток:
// Обработчик события: получен удалённый медиапоток
pc.addEventListener('track', event => {
// Сохраняем поток для отображения на экране
if (event.streams && event.streams[0]) {
setRemoteStream(event.streams[0]);
}
});
После этих шагов два клиента будут видеть и слышать друг друга, если ICE-соединение удалось — напрямую (P2P) или через TURN.
Входящие звонки и push-уведомления
Чтобы пользователь мог получить звонок, даже если приложение закрыто — нужен push-механизм.
Идеальный вариант — push уведомление с интерактивным UI, как у Telegram или WhatsApp:
«Вам звонит Иван Иванов. Принять / Отклонить».
Групповые звонки: больше двух участников (SFU)
WebRTC из коробки отлично работает в модели "один на один" (P2P), но как только вы хотите созвониться втроём, начинаются проблемы. Каждый участник должен установить отдельное соединение с каждым другим: это mesh-схема.
Такой подход быстро упирается в потолок:
при 4 участниках каждый стримит 3 видео потока = ×4 нагрузки;
при 6 — уже 5 потоков на каждого;
и всё это по uplink’у телефона 😅
Решение — использовать SFU
SFU (Selective Forwarding Unit) — это сервер, который принимает потоки от участников и раздаёт их другим. Клиенты отправляют один исходящий поток (а не 5), а SFU уже «перекладывает» их между участниками.
Популярные SFU-решения:
mediasoup — мощный SFU на Node.js + C++;
Janus — лёгкий и гибкий;
Jitsi Videobridge — используется в Jitsi Meet;
LiveKit — современный open-source стек с mobile SDK.
Подключение SFU немного меняет логику:
звонок больше не P2P, а через сервер;
вам нужно управлять подписками на потоки (кто кого видит);
сигналинг становится сложнее — нужно координировать участников, управление layout'ом, mute/unmute и т.д.
Но в обмен вы получаете масштабируемость и контроль, что делает SFU незаменимым для групповых звонков.

Безопасность в WebRTC-звонках
Когда речь заходит о звонках, особенно видео, безопасность — не опция, а необходимость. К счастью, WebRTC уже из коробки делает многое за нас, но есть и подводные камни, о которых стоит знать.
Шифрование
WebRTC всегда использует сквозное шифрование:
SRTP (Secure RTP) — для аудио/видео трафика;
DTLS (Datagram TLS) — для ключевого обмена и сигналов управления.
Это значит, что:
даже если кто-то перехватит трафик (например, в Wi-Fi-сети), он не сможет его расшифровать;
даже TURN-сервер, через который проходит весь поток, видит только зашифрованные данные.
А как насчёт сигналинга? Вот тут уже всё зависит от вас.
WebRTC не диктует, как реализовать сигналинг — вы можете использовать WebSocket, GraphQL, REST, почтовых голубей… Но именно через этот канал передаётся SDP и ICE-кандидаты — то есть информация о соединении.
Итого
В первой части мы начали создавать свой мессенджер с нуля: реализовали сервер на WebSocket, организовали обмен сообщениями между клиентами, и даже добавили хранение истории чатов.
Теперь наш мессенджер умеет делать видеозвонки. Мы разобрались, как работает WebRTC, настроили сигналинг через GraphQL, подключили камеру и микрофон, обменялись SDP и ICE-кандидатами, и наконец — получили рабочее P2P-соединение между клиентами.
Ссылка на пример