Интеграция WebRTC для звонков в мобильном приложении
WebRTC — открытый стандарт P2P-коммуникации в реальном времени. Google поддерживает нативные SDK для Android и iOS, есть Flutter-обёртки. Выбор WebRTC вместо Twilio/Vonage означает больший контроль над инфраструктурой и меньшие операционные расходы при масштабировании, но требует самостоятельной реализации сигнализации, управления ICE и развёртывания TURN/STUN серверов.
Как устроен WebRTC-звонок
Установка соединения — многошаговый процесс через ICE (Interactive Connectivity Establishment):
- Вызывающий создаёт
PeerConnection, генерируетoffer(SDP — Session Description Protocol) -
offerпередаётся собеседнику через сигнальный канал (WebSocket) - Собеседник создаёт
PeerConnection, применяетoffer, генерируетanswer -
answerвозвращается через сигнальный канал - Оба клиента обмениваются ICE candidates — потенциальными сетевыми путями
- ICE агент выбирает наилучший путь и устанавливает P2P соединение
ICE candidates бывают трёх типов: host (локальный IP), srflx (через STUN — публичный IP), relay (через TURN). Прямое P2P (host/srflx) работает в 70–80% случаев. В корпоративных сетях за симметричным NAT нужен relay через TURN.
Нативная реализация на Android
Google поддерживает WebRTC Android SDK — io.getstream:stream-webrtc-android или напрямую бинарники с webrtc.org.
// Инициализация
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(context)
.createInitializationOptions()
)
val factory = PeerConnectionFactory.builder()
.setAudioDeviceModule(JavaAudioDeviceModule.builder(context).createAudioDeviceModule())
.createPeerConnectionFactory()
// ICE конфигурация
val config = PeerConnection.RTCConfiguration(
listOf(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
PeerConnection.IceServer.builder("turn:your-turn.example.com:3478")
.setUsername("user").setPassword("pass").createIceServer()
)
)
val peerConnection = factory.createPeerConnection(config, peerConnectionObserver)
// Аудио трек
val audioSource = factory.createAudioSource(MediaConstraints())
val audioTrack = factory.createAudioTrack("audio0", audioSource)
val localStream = factory.createLocalMediaStream("stream0")
localStream.addTrack(audioTrack)
peerConnection?.addStream(localStream)
Видео добавляется аналогично через VideoCapturer — Camera2Capturer для нативной камеры.
iOS: RTCPeerConnection
На iOS используем тот же Google WebRTC SDK через CocoaPods (pod 'GoogleWebRTC') или Swift Package (google/webrtc).
let config = RTCConfiguration()
config.iceServers = [
RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"]),
RTCIceServer(urlStrings: ["turn:your-turn.example.com:3478"],
username: "user", credential: "pass")
]
config.sdpSemantics = .unifiedPlan
let constraints = RTCMediaConstraints(
mandatoryConstraints: nil,
optionalConstraints: ["DtlsSrtpKeyAgreement": "true"]
)
let peerConnection = factory.peerConnection(
with: config, constraints: constraints, delegate: self
)
Интеграция с CallKit обязательна для iOS — без неё звонок не получит приоритет аудиосессии. Подробности — в статье про VoIP-звонки.
TURN-сервер: почему это не опционально
Без TURN-сервера WebRTC не работает за корпоративными firewall и симметричным NAT. Это ~20–30% реальных пользователей. Развернуть coturn:
# /etc/turnserver.conf
listening-port=3478
listening-ip=0.0.0.0
relay-ip=YOUR_PUBLIC_IP
external-ip=YOUR_PUBLIC_IP
realm=your-domain.com
user=webrtc:strongpassword
lt-cred-mech
Использование публичного STUN от Google (stun.l.google.com) бесплатно, но не предоставляет TURN. Нужен собственный или платный сервис (Twilio Network Traversal Service, Xirsys).
Сигнальный протокол
WebRTC не определяет сигнализацию — это ответственность разработчика. Минимум: WebSocket-канал для передачи SDP offer/answer и ICE candidates.
// Отправка offer через WebSocket
fun createOffer() {
val constraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
}
peerConnection?.createOffer(object : SdpObserver {
override fun onCreateSuccess(sdp: SessionDescription) {
peerConnection?.setLocalDescription(this, sdp)
signalingChannel.send(json { "type" to "offer"; "sdp" to sdp.description })
}
// ...
}, constraints)
}
Состояние сессии: new → connecting → connected → disconnected → failed. Обработка failed — попытка restartIce() или переустановка соединения. Без обработчика переходов пользователь видит зависший звонок без обратной связи.
Качество звука
WebRTC включает Opus кодек, echo cancellation (AEC), noise suppression (NS) и automatic gain control (AGC) по умолчанию. Для мониторинга качества в реальном времени — WebRTC stats API:
peerConnection?.getStats { report ->
val inboundAudio = report.statsMap.values
.filterIsInstance<RTCInboundRtpStreamStats>()
.firstOrNull { it.kind == "audio" }
val packetsLost = inboundAudio?.packetsLost ?: 0
val jitter = inboundAudio?.jitter ?: 0.0
}
Jitter > 30 мс и loss > 5% — порог заметного ухудшения качества голоса.
Flutter
flutter_webrtc пакет обёртывает нативные WebRTC SDK. API схож с нативным, но с дополнительной прослойкой. Production-опыт: пакет работает стабильно, но обновляется с задержкой относительно нативных SDK — при критических уязвимостях в WebRTC ждать обновления пакета иногда приходится несколько недель.
Что входит в работу
Развёртываем TURN/STUN инфраструктуру, реализуем сигнальный сервер (WebSocket), интегрируем WebRTC SDK на мобильных платформах, подключаем CallKit (iOS) / ConnectionService (Android), настраиваем мониторинг качества соединения.
Срок: 3–6 недель для аудио/видеозвонков с учётом инфраструктуры и интеграции с системными звонковыми API.







