WebRTC в iOS: как добавить видеозвонки в приложение (2026)

Главное
• Видеозвонок на iOS состоит из пяти частей. Сигнальный сервер, STUN, TURN, сам фреймворк WebRTC и iOS-обвязка (CallKit, PushKit, AVAudioSession). Если пропустить любую, выйдет демо, которое нельзя выпускать.
• P2P годится для звонков на 2–3 участника. С четырёх участников нужен SFU (mediasoup, LiveKit, Janus, Pion). MCU в 2026 году почти всегда лишний: он дорог по процессору сервера.
• Аппаратно iPhone кодирует в WebRTC только H.264. Сборка stasel/WebRTC (155.0.0 от 12 октября 2026 года) несёт ещё VP8, VP9 и AV1, но они идут программно и тратят батарею, а кодера и декодера HEVC в ней нет.
• Без VoIP-пуша и CallKit звонками никто не пользуется. По документации Apple приложение, собранное на iOS 13 SDK и новее, система завершает, если оно получило VoIP-пуш и не сообщило о звонке в CallKit.
• Для продукта на российском рынке серверы и данные пользователей держите в России. С 1 июля 2025 года 152-ФЗ запрещает первичный сбор персональных данных россиян через иностранные базы, а звонки в WhatsApp и Telegram с августа 2025 года частично ограничены.
Обновлено в ноябре 2026: статья переписана целиком — stasel/WebRTC 155.0.0, правила VoIP-пушей и CallKit, код на Swift, цены SDK, требования 152-ФЗ, сроки с ИИ-инструментами и четыре схемы.
Почему этот разбор пишет Фора Софт
Эту статью мы впервые опубликовали в 2020 году как простое объяснение WebRTC для тех, кто хочет добавить видеозвонок в своё приложение. За шесть лет поменялось почти всё, кроме самой схемы звонка: Google перестал выпускать официальные сборки WebRTC для iOS, Apple ужесточила правила VoIP-пушей, а в России появились свои требования к хранению данных и ограничения популярных мессенджеров. Поэтому в ноябре 2026 года мы переписали разбор целиком.
С 2005 года Фора Софт сдала 250+ проектов, и в большинстве из них есть видео реального времени. Мы делали HD-классы на WebRTC для платформы BrainCert с веб-, iOS- и Android-клиентами, видеоконсультации врача и пациента для CirrusMED, облачные переговорные и десятки звонков один на один в мобильных приложениях. Медиасервер, сигнализацию и мобильные клиенты таких продуктов закрывает наша команда разработки платформ видеоконференций. Ниже то, что мы рассказываем техлиду или основателю на первом созвоне: из чего состоит звонок, где ломается iOS и сколько это стоит.
Если времени мало, прочитайте разделы «Пять частей iOS-видеозвонка» и «CallKit и PushKit». В них ошибаются чаще всего.
Нужно второе мнение по iOS-звонку на WebRTC?
Пришлите логи ICE, настройки AVAudioSession и схему серверов. За 30 минут скажем, где появится следующий баг и как его закрыть.
Как добавить видеозвонок в iOS-приложение: короткий ответ
Чтобы добавить видеозвонок в iOS-приложение, подключите фреймворк WebRTC через Swift Package Manager, поднимите сигнальный сервер на WebSocket, STUN и TURN, обменяйтесь между клиентами SDP и ICE-кандидатами и выведите удалённое видео в RTCMTLVideoView. Для входящих звонков при закрытом приложении добавьте VoIP-пуши PushKit и системный экран CallKit. Для групп от четырёх человек вместо P2P нужен SFU.
Сроки зависят от пути. Облачный SDK вроде LiveKit Cloud даёт рабочий звонок за 1–3 недели. Нативный WebRTC с open-source SFU занимает 4–6 недель с ИИ-инструментами (5–8 недель у команд без них). Подробный расчёт ниже, в разделе про выбор между своим WebRTC и SDK.
Что такое WebRTC на iOS
WebRTC (Web Real-Time Communications) — открытый стандарт IETF и W3C для передачи аудио, видео и данных в реальном времени с задержкой меньше секунды. Медиа идёт по UDP, при необходимости по TCP или TLS. Главная особенность WebRTC в том, что он умеет соединять пользователей напрямую (P2P): медиапоток идёт от устройства к устройству без сервера посередине, если сеть это позволяет.
На iOS WebRTC бывает в двух видах. Первый — нативный фреймворк на Objective-C и Swift, собранный из исходников Chromium. Второй — RTCPeerConnection внутри Safari и WKWebView, где WebRTC работает с iOS 14.3. Для приложения со звонками почти всегда выигрывает нативный путь: аппаратный H.264, интеграция с CallKit, меньший расход батареи и полный контроль над аудиосессией.
Полезно помнить, что WebRTC отвечает за захват, кодирование, упаковку пакетов, контроль перегрузки и шифрование. Он не решает, кто кому звонит, не умеет сам обменяться параметрами соединения и не пробивает NAT. Эти три задачи остаются на вас, и как раз о них молчит большинство туториалов. Базовые понятия подробно разобраны в нашей статье «Что такое WebRTC: как работает, STUN, TURN и SFU» и в уроке «WebRTC без зауми» нашего курса по видеостримингу.
Пять частей iOS-видеозвонка
В каждом iOS-приложении со звонками, которое дошло до App Store, есть пять компонентов. Если в вашем плане работ нет хотя бы одного из них, в плане дыра.
- Сигнальный сервер. Постоянное соединение, через которое два устройства, ещё не знающие друг о друге, обмениваются предложением (offer), ответом (answer) и ICE-кандидатами. Обычно это WebSocket поверх TLS.
- STUN-сервер. Сообщает устройству его внешний IP-адрес и порт, чтобы оно могло описать себя собеседнику.
- TURN-сервер. Ретранслирует медиа, когда прямой путь закрыт. Для iOS он обязателен: мобильные операторы, корпоративные сети и VPN по нашей статистике блокируют прямой UDP примерно в 15–20% звонков.
- Фреймворк WebRTC. Нативная библиотека: сборка stasel/WebRTC через Swift Package Manager, SDK вендора поверх неё (LiveKit, Stream, Agora) или собственная сборка из исходников Chromium.
- iOS-обвязка. CallKit (системный экран входящего звонка), PushKit (VoIP-пуши, которые будят приложение), AVAudioSession (маршрут и режим звука) и фоновый режим
voipв Info.plist. Здесь команды чаще всего теряют незапланированные две недели.

Рисунок 1. Пять частей iOS-видеозвонка: WebRTC отвечает за медиа, остальные четыре части строите и обслуживаете вы.
Представьте WebRTC как двигатель. Кузов (сигнализацию), колёса (STUN и TURN) и приборную панель (CallKit) всё равно собираете вы. А на iOS вам ещё достаётся капризная коробка передач: AVAudioSession.
P2P, SFU или MCU для iOS
Архитектуру выбирают по числу участников в типичном звонке. Коротко: P2P для пар, SFU для групп, MCU только для особых случаев. Подробный разбор с расчётом потоков есть в статье «Архитектура видеочата: P2P, MCU или SFU».
- P2P (точка-точка). Каждое устройство отправляет N−1 потоков и столько же принимает. Выше трёх участников iPhone быстро упирается в батарею и исходящий канал: на LTE видеозвонок вчетвером в 720p редко держится дольше 15 минут. Подходит для телемедицины один на один, знакомств, консультаций.
- SFU (Selective Forwarding Unit). Устройство отправляет один поток на сервер и получает N−1 потоков обратно. Сервер пересылает пакеты, не декодируя их, поэтому нагрузка на процессор низкая. Варианты с открытым кодом: mediasoup, LiveKit Server, Janus, Pion, Jitsi Videobridge. Это правильный выбор для групп, онлайн-классов и записи.
- MCU (Multipoint Control Unit). Сервер декодирует все потоки, сводит их в одну картинку и отправляет каждому один поток. Звонок на 50 человек занимает 4–8 ядер сервера и добавляет 100–300 мс задержки на сведение. Мы берём MCU только тогда, когда клиенты сидят на очень слабом канале.
Выбирайте SFU, когда: в звонке бывает четыре участника и больше, звонки нужно записывать или транслировать, а один сервер средней мощности на ~500 одновременных медиапотоков вас устраивает по бюджету. Сравнение SFU с открытым кодом есть в уроке «Выбор SFU: mediasoup, Janus, LiveKit, Jitsi, Pion».

Рисунок 2. P2P, SFU и MCU на iOS: зелёным отмечены сильные стороны, оранжевым — цена или предел архитектуры.
Сигнальный сервер: как клиенты обмениваются SDP и ICE
Сам WebRTC не умеет передавать данные о соединении, поэтому клиентам нужен посредник. Сигнальный сервер — это просто шина сообщений: она должна надёжно и быстро доставить SDP и ICE-кандидаты от одного клиента другому. Протокол может быть любым, лишь бы стороны обменивались данными. Стандартом стал WebSocket поверх TLS: соединение постоянное, задержка низкая, а приложение уже авторизовано на вашем бэкенде.
Типичный сценарий звонка один на один: клиент подключается к wss://signal.example.ru с JWT, входит в комнату, получает список участников и обменивается с собеседником сообщениями offer, answer и ice-candidate. Используйте Trickle ICE: кандидаты начинают уходить, пока предложение ещё собирается, и соединение устанавливается на 200–800 мс быстрее.
import Foundation
// Сигнализация на URLSessionWebSocketTask: без сторонних библиотек
final class SignalingClient {
private var task: URLSessionWebSocketTask?
var onMessage: ((SignalMessage) -> Void)?
func connect(url: URL, token: String) {
var request = URLRequest(url: url)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
task = URLSession.shared.webSocketTask(with: request)
task?.resume()
receive()
}
func send(_ message: SignalMessage) {
guard let data = try? JSONEncoder().encode(message) else { return }
task?.send(.data(data)) { error in
if let error { print("signaling send error:", error) }
}
}
private func receive() {
task?.receive { [weak self] result in
if case .success(.data(let data)) = result,
let msg = try? JSONDecoder().decode(SignalMessage.self, from: data) {
self?.onMessage?(msg)
}
self?.receive() // слушаем дальше
}
}
}
На этом этапе команды обычно спотыкаются о три вещи. Во-первых, о переподключение: iPhone переключается между 5G, LTE и Wi-Fi прямо во время звонка, поэтому WebSocket нужен экспоненциальный повтор и токен возобновления сессии на сервере. Во-вторых, о сроке жизни JWT: если токен истёк посреди долгого звонка, новые ICE-кандидаты теряются. В-третьих, о порядке сообщений: если транспорт его не гарантирует, кандидат может прийти раньше SDP, к которому относится, и молча не примениться.
STUN и TURN: зачем iOS-приложению нужны оба
Сети обычно строят на частных IP-адресах: они работают внутри дома или офиса и не маршрутизируются в интернете. Чтобы устройство с частным адресом вышло наружу, роутер подменяет его на публичный с помощью NAT (Network Address Translation) и записывает соответствие в свою таблицу. Пакеты от собеседника дойдут до клиента, только если в этой таблице есть нужная запись, а создаётся она, когда клиент сам что-то отправляет наружу. Проблема в том, что ни один из двух клиентов не знает своего внешнего адреса.
STUN отвечает на вопрос «какой у меня внешний адрес». Клиент отправляет пакет на STUN-сервер, тот видит, с какого публичного адреса и порта пакет пришёл, и возвращает их клиенту. Заодно в таблице NAT появляется запись. TURN нужен, когда прямой путь закрыт: он работает как ретранслятор, а клиенты продолжают считать, что общаются напрямую. Без STUN и TURN можно соединить только два устройства в одной сети. Подробно механика ICE описана в уроке «ICE, STUN, TURN: подробный разбор».
На iOS без TURN не обойтись, и причин тому три:
- Симметричный NAT у мобильных операторов. Такой NAT запоминает не только внешний адрес, но и сервер, с которым клиент говорил, и отбрасывает пакеты от всех остальных. STUN сообщит адрес, но собеседник по нему не пробьётся.
- VPN, MDM-профили и CGNAT. Корпоративный VPN на управляемом iPhone или операторский NAT второго уровня ломают прямые пути. Выручает TURN поверх TLS на порту 443.
- Корпоративный Wi-Fi и гостиницы. Там часто закрыт UDP и всё, кроме 443. TURN поверх TLS на 443 выглядит как обычный HTTPS и проходит почти везде.
Стандарт для собственного TURN — coturn. Настройте и UDP, и TLS, а клиент пусть перебирает их по порядку:
let iceServers = [
RTCIceServer(urlStrings: ["stun:stun.example.ru:3478"]),
RTCIceServer(urlStrings: ["turn:turn.example.ru:3478?transport=udp",
"turns:turn.example.ru:443?transport=tcp"],
username: creds.username, // выдаёт бэкенд
credential: creds.password) // живёт 60-120 секунд
]
Логин и пароль TURN выдавайте с бэкенда, подписанными и с временем жизни 60–120 секунд. Статичные учётные данные внутри IPA рано или поздно вытащат, и ваш TURN начнёт бесплатно ретранслировать чужой трафик.
Звонки не соединяются в продакшене?
Пришлите пятиминутный дамп webrtc-internals или статистику RTCStatsReport. Определим тип NAT, место, где TURN теряет пакеты, и дадим исправление.
Свой WebRTC или готовый SDK
Выбор «нативный WebRTC или SDK» на деле шире. Есть четыре пути, и они отличаются сроком выхода, контролем и тем, кто отвечает за сбои в три часа ночи.
Как мы считаем сроки с ИИ-инструментами. Работу делим на четыре типа. А — шаблонный код (авторизация, формы, CI/CD): экономия часов 60–80%. Б — продуктовая логика и клиенты iOS и Android: 40–60%. В — тяжёлая инженерия (WebRTC под нагрузкой, медиасерверы, отладка звука): 15–30%. Г — то, что от скорости кода не зависит: ревью App Store, юристы, нагрузочные тесты, приёмка: 0%. Календарь сжимается меньше часов. Публичные исследования расходятся, наши коэффициенты взяты из проектов Фора Софт.
| Путь | Фора Софт с ИИ-инструментами | Рынок без ИИ | Когда подходит |
|---|---|---|---|
| Нативный WebRTC, свой SFU и сигнализация | 6–10 недель | 8–14 недель | Особые требования к медиа, полный контроль |
| Нативный WebRTC и open-source SFU (mediasoup, LiveKit Server) | 4–6 недель | 5–8 недель | Рост выше 1 млн минут в месяц, данные в России |
| Облачный SDK (LiveKit Cloud, Stream, Agora) | 1–3 недели | 1–3 недели | Быстрый MVP, объём до ~1 млн минут в месяц |
| Готовый интерфейс (Daily Prebuilt, Zoom Video SDK) | несколько дней | несколько дней | Демо для инвестора, звонок не главная функция |
Как считали: профиль «видео и real-time» (А 30% / Б 30% / В 30% / Г 10% часов) → экономия часов ≈ 43%; одна неделя типа Г (ревью App Store, нагрузочный тест) без изменений, остальные недели × (1 − 0,43 × 0,75), округление до недели вверх; калибровка Фора Софт от 7 ноября 2026 года. Ревью сторов, лицензии и инфраструктура не сокращаются.
Нашим клиентам, которые планируют рост выше миллиона минут в месяц, мы обычно предлагаем второй путь: нативный клиент и SFU с открытым кодом. На облачном SDK вы быстрее выходите, но платите за каждую минуту и зависите от вендора. LiveKit смягчает эту зависимость: его сервер открыт, и тот же код позже можно перенести на свои машины. Если пока не ясно, какой путь ваш, начните с планирования MVP: за несколько дней фиксируем объём первой версии, архитектуру и бюджет.
Отдельно закладывайте время на iOS-обвязку. CallKit, PushKit и AVAudioSession занимают около двух недель и с ИИ-инструментами, и у команд без них: это смесь продуктовой логики и тяжёлой отладки звука, и календарь здесь почти не сжимается.
Сравнение SDK для видеозвонков на iOS в 2026 году
Цены ниже мы сверили со страницами вендоров в ноябре 2026 года. Они меняются раз в квартал, поэтому перед решением проверьте их ещё раз и прогоните через свой профиль нагрузки. «Минута участника» считает каждого подключившегося. Звонок на 10 человек длиной 60 минут — это 600 оплачиваемых минут, а не 60.
| SDK | Цена | Бесплатно | Когда брать | Риски для России |
|---|---|---|---|---|
| LiveKit Cloud | Ship $50 в месяц со 150 000 минут, дальше $0,0005 за минуту участника; Scale $500 с 1,5 млн минут (livekit.com, 11.2026) | 5 000 минут в месяц | Масштаб, ИИ-агенты, возможность уйти на свой сервер | Оплата из России и серверы за рубежом; открытый сервер можно развернуть у себя |
| Agora | $0,99 за 1000 минут аудио, $3,99 за HD-видео, $8,99 за Full HD (agora.io, 11.2026) | 10 000 минут в месяц | Глобальный охват, зрелый SDK | Сильная привязка к вендору, оплата и данные за рубежом |
| Stream Video | Оплата за минуты участника, корпоративные тарифы | Есть пробный кредит | Чат и звонки в одном SDK | Серверы и оплата за рубежом |
| Daily | Оплата за минуты участника | Есть, Daily Prebuilt | Самый быстрый MVP с готовым интерфейсом | Нет варианта со своими серверами |
| Свой SFU (mediasoup, LiveKit OSS) в российском облаке | Аренда серверов и время DevOps | Открытый код | От 1–5 млн минут в месяц, 152-ФЗ, полный контроль | SLA и дежурства на вашей стороне |
Подробно цены и пути миграции мы разобрали в статьях «Альтернативы Agora.io в 2026 году» и «Разработка видеочата: white-label, SDK, свой сервер или свой WebRTC».
Видеозвонки в приложении для России: 152-ФЗ, серверы и ограничения
Если приложение работает на российском рынке, к техническим вопросам добавляются юридические и сетевые. Мы выделяем три.
- Локализация данных. С 1 июля 2025 года 152-ФЗ запрещает первичный сбор персональных данных граждан России с использованием иностранных баз («Газета.ру», 01.07.2025). Аккаунты, история звонков и записи должны жить на серверах в России. Облачный SDK с серверами за рубежом для такого продукта становится риском, поэтому чаще выбирают свой SFU в российском облаке.
- Ограничения чужих мессенджеров. В августе 2025 года Роскомнадзор подтвердил, что звонки в WhatsApp и Telegram частично ограничены (iPhones.ru, 13.08.2025). Для клиники, школы или сервиса консультаций это аргумент встроить звонок в своё приложение, а не отправлять клиентов в мессенджер.
- Доля iOS. По данным Statcounter за октябрь 2026 года, iOS занимает 27,83% мобильного рынка России, Android — 72,15%. Звонок почти всегда нужен на обеих платформах; Android-часть разобрана в статье «Как создать нативный видеочат на Android с WebRTC».
Выбирайте свой SFU в российском облаке, когда: продукт хранит аккаунты, историю звонков или записи пользователей из России, а заказчик — клиника, банк или госструктура с собственными требованиями к контуру.
Перед запуском прогоните звонки на сетях крупных операторов и в корпоративных сетях заказчика: доля звонков через TURN в России может отличаться от наших средних 15–20%.
Как подключить WebRTC к iOS-проекту в 2026 году
Самая устаревшая строка в старых туториалах — зависимость. Официальный под GoogleWebRTC давно не обновляется: Google перестал публиковать сборки для iOS. Сообщество поддерживает сборку stasel/WebRTC с готовыми XCFramework для каждой версии Chromium. На ноябрь 2026 года последний релиз — 155.0.0 от 12 октября 2026 года, новые версии выходят каждые две–четыре недели. Сборка поддерживает iOS 15+, macOS 12+ и Mac Catalyst. Версию фиксируйте, плавающую ветку не берите.
// Package.swift
dependencies: [
.package(url: "https://github.com/stasel/WebRTC.git", exact: "155.0.0")
]
// Info.plist: без этих ключей приложение упадёт при первом доступе к камере
// NSCameraUsageDescription - «Камера нужна для видеозвонков»
// NSMicrophoneUsageDescription - «Микрофон нужен для звонков»
// UIBackgroundModes - audio, voip
В Xcode добавьте возможности Push Notifications и Background Modes на вкладке Signing & Capabilities. Собирать WebRTC из исходников стоит, только если нужен патч: например, своё шифрование или модуль обработки звука. Это 2–3 рабочих дня с ИИ-инструментами (около трёх у команд без них), потому что сборка Chromium через depot_tools, gn и ninja хорошо описана, но не прощает ошибок.
Видеозвонок 1 на 1: шаги и код на Swift
Схема звонка одинакова на всех платформах, и iPhone может звонить на Android, в браузер или на компьютер. Участников двое: инициатор и тот, кто ждёт звонка. Код ниже сокращён, обработка ошибок и потоков опущена для наглядности, но структура такая же, как в продакшене.
1. Создайте фабрику соединений один раз и используйте её всё время жизни приложения.
import WebRTC
let factory: RTCPeerConnectionFactory = {
RTCInitializeSSL()
return RTCPeerConnectionFactory(
encoderFactory: RTCDefaultVideoEncoderFactory(),
decoderFactory: RTCDefaultVideoDecoderFactory())
}()
2. Получите локальный медиапоток. Медиапоток — это видео и аудио с устройства. Он состоит из треков, и треки одного потока синхронизированы между собой: звук совпадает с видео камеры. Разные потоки, например камера и демонстрация экрана, друг с другом не синхронизированы. Как добавить демонстрацию экрана, мы показали в статье «Как добавить скриншаринг в iOS-приложение».
let videoSource = factory.videoSource()
let capturer = RTCCameraVideoCapturer(delegate: videoSource)
let localVideo = factory.videoTrack(with: videoSource, trackId: "video0")
let localAudio = factory.audioTrack(with: factory.audioSource(with: nil), trackId: "audio0")
if let camera = RTCCameraVideoCapturer.captureDevices().first(where: { $0.position == .front }),
let format = camera.formats.last {
capturer.startCapture(with: camera, format: format, fps: 30)
}
3. Создайте соединение, передайте ему серверы ICE и добавьте локальные треки.
let config = RTCConfiguration() config.iceServers = iceServers config.sdpSemantics = .unifiedPlan config.continualGatheringPolicy = .gatherContinually let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) let pc = factory.peerConnection(with: config, constraints: constraints, delegate: self)! pc.add(localVideo, streamIds: ["stream0"]) pc.add(localAudio, streamIds: ["stream0"])
4. Обменяйтесь SDP. SDP (Session Description Protocol) — текстовое описание параметров устройства: какие кодеки оно поддерживает, сколько треков отправляет и как их шифровать. Инициатор формирует offer, то есть предлагает начать звонок, и отправляет его через сигнальный сервер. Собеседник отвечает answer, который строит с оглядкой на offer инициатора, поэтому стороны выбирают общий кодек. По сути offer и answer устроены одинаково, разница только в роли.
// Инициатор
pc.offer(for: constraints) { sdp, _ in
guard let sdp else { return }
pc.setLocalDescription(sdp) { _ in signaling.send(.offer(sdp.sdp)) }
}
// Собеседник: получил offer
pc.setRemoteDescription(RTCSessionDescription(type: .offer, sdp: remoteSdp)) { _ in
pc.answer(for: constraints) { sdp, _ in
guard let sdp else { return }
pc.setLocalDescription(sdp) { _ in signaling.send(.answer(sdp.sdp)) }
}
}
5. Обменяйтесь ICE-кандидатами. SDP устанавливает логическое соединение, но физически клиенты ещё не нашли друг друга. ICE-кандидаты описывают, где клиент находится в сети: локальный адрес, внешний адрес роутера, адрес на TURN-сервере. SDP формируется один раз, а кандидатов обычно несколько, и они приходят по мере сбора.
// RTCPeerConnectionDelegate
func peerConnection(_ pc: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
signaling.send(.candidate(sdp: candidate.sdp,
mLineIndex: candidate.sdpMLineIndex,
mid: candidate.sdpMid))
}
// Пришёл кандидат собеседника
pc.add(RTCIceCandidate(sdp: c.sdp, sdpMLineIndex: c.mLineIndex, sdpMid: c.mid)) { _ in }
6. Выведите удалённое видео. Берите RTCMTLVideoView на Metal. Старый RTCEAGLVideoView работает на OpenGL ES, который Apple объявила устаревшим ещё в iOS 12. В SwiftUI оборачивайте рендерер в UIViewRepresentable и отвязывайте его от трека, когда вью исчезает; подробности в статье «SwiftUI или UIKit для видеозвонков на iOS».
func peerConnection(_ pc: RTCPeerConnection, didAdd rtpReceiver: RTCRtpReceiver,
streams: [RTCMediaStream]) {
guard let track = rtpReceiver.track as? RTCVideoTrack else { return }
DispatchQueue.main.async {
let view = RTCMTLVideoView(frame: .zero)
view.videoContentMode = .scaleAspectFill
track.add(view)
self.remoteContainer.embed(view)
}
}
Если коротко: сначала клиенты обмениваются SDP и устанавливают логическое соединение, затем ICE-кандидатами и устанавливают физическое. После этого они видят и слышат друг друга. Готовый проект для старта — демо stasel/WebRTC-iOS с клиентом на Swift и небольшим сигнальным сервером: добейтесь звонка между двумя устройствами там, а потом переносите код в своё приложение.
CallKit и PushKit: входящий звонок, когда приложение закрыто
Если звонок проходит только при открытом приложении, им не будут пользоваться: оба человека должны сидеть в приложении и ждать. Решение в iOS — VoIP-пуш через PushKit, который будит приложение даже после выгрузки из памяти, и системный экран входящего звонка CallKit, тот же, что у обычных звонков. Пользователь может играть или держать телефон заблокированным, вызов всё равно появится.
Apple здесь строга. По документации, приложение, получившее VoIP-пуш, должно быстро сообщить о звонке в CallKit; приложение, собранное на iOS 13 SDK или новее, система завершает, если оно этого не сделало, а при повторных нарушениях перестаёт будить его VoIP-пушами (Apple Developer, PKPushType.voIP). Поэтому о звонке сообщаем сразу, а WebRTC поднимаем потом.
import PushKit
import CallKit
// 1. Регистрация VoIP-пушей при запуске
let registry = PKPushRegistry(queue: .main)
registry.delegate = self
registry.desiredPushTypes = [.voIP]
// 2. Пуш пришёл: сообщаем в CallKit немедленно
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType, completion: @escaping () -> Void) {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: payload.callerName)
update.hasVideo = true
provider.reportNewIncomingCall(with: UUID(), update: update) { _ in
completion() // даже если звонок уже завершён: сообщить и сразу закончить
}
}
Бэкенд отправляет пуш в APNs с топиком <bundle-id>.voip, типом voip и приоритетом 10; в полезной нагрузке — имя звонящего, идентификатор комнаты и свежий токен сигнализации. Когда пользователь нажимает «Ответить», CXProviderDelegate получает событие: тут настраиваем звук, создаём соединение и проходим обмен SDP. Делать это внутри обработчика пуша нельзя: CallKit ещё не взял аудиосессию под контроль. В iOS 26 Apple добавила в CallKit действие CXSetTranslatingCallAction для перевода звонка с субтитрами. Это ещё одна причина держать звонок в системном интерфейсе.

Рисунок 3. Входящий звонок при закрытом приложении: семь шагов и две ловушки, из-за которых iOS завершает приложение или глушит микрофон.
AVAudioSession: три ловушки со звуком
Больше багов в iOS-звонках мы находим в AVAudioSession, чем в самом WebRTC. Три правила:
- Категория
.playAndRecordи режим.voiceChat. Режим включает встроенное в iOS эхоподавление и шумоподавление. Если поверх него включить программное эхоподавление WebRTC, звук обработается дважды и станет «металлическим». - Активацию аудиосессии отдайте CallKit. Когда пользователь отвечает, CallKit сам поднимает сессию. Вызов
setActive(true)из своего кода не вовремя глушит микрофон на первые секунды или на весь звонок. На старте выставьтеRTCAudioSession.sharedInstance().useManualAudio = true, а вprovider(_:didActivate:)вызовитеaudioSessionDidActivate(_:)и включитеisAudioEnabled. - Обрабатывайте прерывания. Обычный звонок по сотовой сети, Siri или другое VoIP-приложение прерывают сессию. Подпишитесь на
AVAudioSession.interruptionNotification, глушите трек на начале прерывания и возвращайте звук в конце. Отдельно протестируйте входящий сотовый звонок: это самая частая причина бага «звонок навсегда замолчал».
Звук в звонке «роботизированный» или с эхом? Почти всегда это двойное эхоподавление. Оставьте .playAndRecord с режимом .voiceChat, выключите программное эхоподавление WebRTC, и звук станет нормальным. О звуке для видео подробно рассказывает наш курс «Аудио для видео».
Кодеки: H.264, VP8, VP9, AV1 и симулкаст на iPhone
Нативный WebRTC на iOS аппаратно ускоряет один видеокодек — H.264, через VideoToolbox. В сборке stasel/WebRTC есть ещё VP8, VP9 и AV1, но они работают программно: звонок идёт, просто дороже по процессору и батарее. С HEVC часто путаются. В описании stasel/WebRTC прямо сказано, что сигнализация и упаковка H.265 в сборку вкомпилированы, но кодера и декодера HEVC для платформ Apple в WebRTC нет, поэтому строить звонок на H.265 не стоит. Звук идёт в Opus.
Это важно по двум причинам. Первая — совместимость с браузерами: Chrome и Firefox по умолчанию предпочитают VP8, и iPhone декодирует его программно, а сетка из VP8-плиток греет телефон. Поставьте H.264 первым в настройках кодеков SFU, VP8 и VP9 оставьте запасными и проверяйте согласованный кодек в RTCStatsReport. Вторая — симулкаст: iPhone публикует H.264 с тремя слоями разрешения, каждый со своим аппаратным кодером, а вот SVC (слои VP9 или AV1) идёт только программно. Для iPhone выбирайте симулкаст, а не SVC; разницу объясняет урок «Simulcast и SVC в SFU».
На чипах A17 Pro и новее аппаратный H.264 почти бесплатен для процессора: публикация 1080p30 занимает около 3% CPU. На A12 и старше та же публикация стоит 12–18%, и через 10–15 минут телефон начнёт снижать частоту кадров из-за нагрева. На устройствах старше A14 ограничьте публикацию 720p, пока не померили сами.
Безопасность: DTLS-SRTP, SFrame и требования к данным
Любое WebRTC-соединение зашифровано по умолчанию: DTLS-SRTP обязателен по стандарту и не отключается. Это даёт конфиденциальность и целостность на каждом участке пути, но не сквозное шифрование, если в середине стоит SFU: сервер завершает DTLS-сессию. Подробнее в статье «Как работает безопасность WebRTC».
- Сквозное шифрование. Если в модели угроз есть оператор SFU, добавьте SFrame поверх SRTP. Ключа у сервера нет, он пересылает пакеты, но не видит медиа. В LiveKit и mediasoup есть готовые пути для SFrame.
- Медицина. Журнал, кто и когда подключался, согласие на запись, шифрование записей при хранении, права доступа на сигнальном сервере. В России к этому добавляются требования к медицинским информационным системам и 152-ФЗ.
- Персональные данные. Согласие до начала записи, хранение записей и логов в России, работающее удаление данных, которое доходит до записей, расшифровок и логов.
Сколько стоит медиатрафик: свой SFU, LiveKit Cloud или Agora
Ниже пример для трёх объёмов групповых HD-звонков. Облачные колонки посчитаны по публичным ценам ноября 2026 года, запись и CDN не учитываем. Для своего SFU рублёвую цену не даём: она зависит от облака и курса, поэтому указываем состав серверов, а стоимость берите из прайса выбранного российского провайдера.
| Объём в месяц | Свой SFU в российском облаке | LiveKit Cloud | Agora HD |
|---|---|---|---|
| 100 тыс. минут участника | 1 сервер SFU и 1 TURN | ~$50 (тариф Ship) | ~$360 |
| 1 млн минут участника | 2–3 сервера SFU и TURN | ~$475 | ~$3 950 |
| 10 млн минут участника | Кластер SFU, отдельные TURN, трафик | ~$3 900 плюс трафик сверх тарифа | ~$39 900 |
Agora: $3,99 за 1000 HD-минут минус 10 000 бесплатных минут. LiveKit Cloud: более дешёвый из тарифов Ship и Scale только за минуты участника; исходящий трафик сверх лимита тарифа оплачивается отдельно и на групповом видео в 720p может сравняться со счётом за минуты.
Таблица не показывает две вещи. Колонка своего SFU прячет стоимость DevOps-инженера, который следит за серверами. Облачные колонки прячут выигрыш во времени: вы выходите на рынок на два месяца раньше, и в первый год это часто дороже любой наценки облака. Полный расчёт бюджета разработки, а не только трафика, есть в статье «Стоимость разработки на WebRTC в 2026 году».
Платите за минуты больше, чем рассчитывали?
Пришлите счёт облачного SDK и схему звонков. Посчитаем, когда окупится свой SFU в российском облаке и как перейти на него без потерь для пользователей.
Мини-кейс: звонок для телемедицины на iOS за 12 недель
Ситуация. К нам пришёл американский стартап частной телемедицины: веб-версия работала, Android-клиент был в разработке, iOS-приложения не было. До демо для инвесторов оставалось 12 недель. Нужны были защищённое видео, входящие звонки через CallKit и запись консультаций в зашифрованное хранилище с согласия пациента.
План. Недели 1–3: подключили WebRTC через SPM, собрали сигнализацию на существующем шлюзе Node.js с WebSocket и подняли coturn с TURN поверх TLS на 443. Недели 4–7: CallKit, PushKit и AVAudioSession, именно эта часть съела больше всего времени. Неделя 8: SFU на mediasoup для звонка втроём (врач, пациент, переводчик). Недели 9–11: запись с шифрованием AES-256 при хранении. Неделя 12: ревью App Store и демо.
Результат. Доля успешных первых звонков составила 98,3% на первых 1 200 звонках в продакшене. Медианное время соединения — 1,4 секунды напрямую и 2,1 секунды через TURN (так шли около 14% звонков). Демо помогло закрыть следующий раунд. Тот же подход мы применяли в CirrusMED; если вы строите похожий продукт, посмотрите, как работает наша разработка платформ телемедицины.
Как выбрать стек за пять вопросов
- Сколько участников в типичном звонке? 1–3 — подходит P2P с одним TURN. 4–100 — нужен SFU. Больше 100 — SFU с записью и логикой выборочной пересылки, и для первой версии почти всегда лучше управляемый SFU.
- Сколько минут в месяц будет через год? До 1 млн — облачный SDK. 1–5 млн — облако выгоднее, если нет своей DevOps-команды. Больше 5 млн — свой mediasoup или LiveKit OSS.
- Нужны ли хранение данных в России, медицинские требования или закрытый контур? Если да, облачные SDK с серверами за рубежом отпадают, остаётся свой SFU в российском облаке или у заказчика.
- Как быстро нужен релиз? Две недели — готовый интерфейс вроде Daily Prebuilt (и с ИИ-инструментами, и без них). 5 недель с ИИ-инструментами (6 у команд без них) — нативный клиент и LiveKit Cloud. 9 недель с ИИ-инструментами (12 без ИИ) — нативный клиент и свой mediasoup.
- Какой опыт WebRTC у команды? Нулевой — управляемый SDK. Один проект — SFU с открытым кодом и нативный WebRTC. Два выпущенных продукта и больше — можно уверенно держать всё у себя.

Рисунок 4. Стек iOS-видеозвонка за четыре вопроса: идите сверху вниз, первое «да» даёт ответ.
Если ответы противоречат друг другу, закажите ревью архитектуры: посмотрим текущую схему, нагрузку и требования к данным и предложим путь без переделок через полгода.
Когда нативный WebRTC на iOS не нужен
- Трансляция на тысячи зрителей. Если сценарий «смотрим ведущего и иногда поднимаем руку», нужен HLS или LL-HLS для зрителей и WebRTC только для выступающих. Сравнение есть в статье «WebRTC или HLS в 2026 году».
- Демо через три недели. Если iOS-приложение — тонкая обёртка над готовым веб-сценарием, не переписывайте всё на нативный WebRTC. Возьмите готовый SDK, покажите демо, замените позже.
- Свой SFU на 500+ участников. Каскады SFU, географическая маршрутизация и запись — около 10 месяцев работы сеньорной команды с ИИ-инструментами (около 12 месяцев у команд без них). Пока нет такой команды, берите управляемый SFU.
Пять ошибок, которые мы находим на ревью iOS-кода с WebRTC
- Статичные учётные данные TURN внутри приложения. Любой, кто распакует IPA, получит бесплатный ретранслятор. Выдавайте временные данные со сроком жизни 60–120 секунд.
- Свой вызов
setActive(true)во время звонка через CallKit. Микрофон молчит первые секунды, а иногда весь звонок. Ждитеprovider(_:didActivate:). - Не каждый VoIP-пуш доходит до CallKit. Система завершает такое приложение и со временем перестаёт будить его пушами. Даже если звонок уже закончился, сообщите о нём и сразу завершите.
- VP8 по умолчанию. iPhone декодирует его программно, аппаратный H.264 простаивает, телефон греется. Ставьте H.264 первым.
RTCEAGLVideoViewв 2026 году. OpenGL ES устарел,RTCMTLVideoViewна Metal быстрее и экономнее.
Какие метрики снимать с каждого звонка
Метрики собирайте из RTCStatsReport каждые 5 секунд и отправляйте в систему мониторинга. Как тестировать поток, мы описали в статье «Как проверить качество WebRTC-стрима».
| Группа | Метрика | Ориентир |
|---|---|---|
| Качество | Задержка туда и обратно (RTT) | медиана ниже 200 мс |
| Качество | Потери пакетов / джиттер | ниже 2% / ниже 30 мс |
| Качество | Замирания видео | меньше 1 в минуту |
| Продукт | Время установки звонка на мобильной сети | меньше 2 секунд |
| Продукт | Успешные первые звонки | больше 95% |
| Продукт | Обрывы посреди звонка | меньше 1,5% |
| Надёжность | Успешные переподключения сигнализации при смене сети | больше 99% |
Первые три продуктовые метрики сильнее всего связаны с удержанием. На одном проекте сокращение времени установки звонка на 800 мс в медленных мобильных сетях подняло удержание первой недели на 0,4 процентного пункта.
FAQ
Поддерживает ли iOS WebRTC?
Да. Safari и WKWebView поддерживают RTCPeerConnection с iOS 14.3, а нативный фреймворк WebRTC подключается через Swift Package Manager (сборка stasel/WebRTC). Для приложения со звонками почти всегда берут нативный фреймворк: он даёт CallKit, аппаратный H.264 и контроль над звуком.
Нужен ли TURN-сервер для iOS-приложения?
Да, для любого приложения в продакшене. По нашей статистике 15–20% звонков идут через ретранслятор из-за NAT мобильных операторов, VPN и корпоративных сетей; без TURN такие звонки молча не соединяются.
Как добавить CallKit в приложение с WebRTC?
Зарегистрируйте PushKit с типом voIP, на каждый VoIP-пуш сразу вызывайте reportNewIncomingCall, а медиа запускайте в provider(_:didActivate:). Приложение на iOS 13 SDK и новее, не сообщившее о звонке в CallKit, система завершает.
Какую библиотеку WebRTC подключать на iOS в 2026 году?
Сборку stasel/WebRTC через Swift Package Manager; последний релиз на ноябрь 2026 года — 155.0.0 от 12 октября. Официальный под GoogleWebRTC устарел и не обновляется.
Какие видеокодеки работают в WebRTC на iPhone?
Аппаратно только H.264. VP8, VP9 и AV1 работают программно и сильнее тратят батарею. Кодера и декодера HEVC в сборке нет, звук передаётся в Opus.
Сколько времени занимает разработка видеозвонка на iOS?
Звонок один на один с CallKit, сигнализацией, TURN и записью — 6–9 недель с ИИ-инструментами (8–12 недель у команд без них). Групповые звонки через SFU добавляют около 2 недель и с ИИ, и без него; юридические требования к данным добавляют 2–4 недели и не ускоряются.
Что дешевле: свой WebRTC или Agora и LiveKit?
До ~1 млн минут участника в месяц обычно дешевле облачный SDK с учётом времени разработки. Выше ~5 млн минут свой SFU окупается за месяцы; для продукта на российском рынке свой SFU часто выбирают из-за 152-ФЗ, а не из-за цены.
Можно ли звонить с iPhone на Android и в браузер?
Да. WebRTC кросс-платформенный: iPhone соединяется с Android, Chrome, Safari и десктопом. Чтобы не греть iPhone программным декодированием, ставьте H.264 первым кодеком на всех клиентах.
Почему звук в iOS-звонке с эхом или «роботизированный»?
Почти всегда это двойное эхоподавление: программное эхоподавление WebRTC работает поверх встроенного в iOS. Используйте .playAndRecord с режимом .voiceChat и выключите программное эхоподавление.
Работает ли WebRTC на iPad и Mac?
Да. Один XCFramework покрывает iOS и iPadOS 15+, macOS 12+ и Mac Catalyst. На iPad в режимах Split View и Stage Manager захват камеры может прерываться, это нужно обработать в интерфейсе.
Что почитать дальше
Основы WebRTC
Что такое WebRTC: как работает, STUN, TURN и SFU (2026)
WebRTC — это открытый стандарт видеосвязи в браузере. Как он работает: сигналинг, SDP, ICE, STUN и TURN, P2P и SFU, безопасность и цена TURN в 2026 году.
Архитектура
Какой тип архитектуры выбрать для видеочата: P2P, MCU или SFU?
Одной из главных сильных сторон WebRTC — гибкость. В этой статье мы разберём разницу между P2P, MCU и SFU, чтобы помочь вам выбрать подходящее решение.
Интерфейс iOS
SwiftUI или UIKit для видеозвонков на iOS: гид 2026
SwiftUI, UIKit или гибрид для iOS-видеозвонков: замеры на 12 плитках, правила iOS 27, PiP и CallKit, компоненты LiveKit и Stream, выбор за 5 вопросов.
Swift 6
Swift 6 и iOS: создаём современное видеочат-приложение
Видеочат на iOS с Swift 6: архитектура на основе actor, WebRTC, собственный SFU, ИИ на устройстве, Liquid Glass в iOS 26 и тестирование на Swift.
Безопасность
Как работает безопасность WebRTC — просто и понятно
О безопасности WebRTC: как создать защищённый видеочат и избежать утечек данных пользователей. Понятно и по делу.
Итог: что сделать до первого релиза
WebRTC на iOS давно решённая задача, если помнить, что задач на самом деле пять. Выберите архитектуру по числу участников, путь «свой стек или SDK» по объёму минут и сроку, заложите около двух недель на CallKit, PushKit и AVAudioSession. Ставьте H.264 первым, выдавайте временные учётные данные TURN и с первого дня меряйте время установки звонка, долю успешных первых звонков и обрывы. Для российского рынка добавьте серверы и данные в России.
Если нужен короткий путь, наша команда с опытом WebRTC берёт на себя iOS-клиент, сигнализацию, TURN и SFU. Расскажите о сценарии и ожидаемой нагрузке, и мы предложим стек и план работ.
Обсудим видеозвонки в вашем iOS-приложении?
30 минут без презентаций: ваша схема, наши заметки и три следующих шага, что исправить или построить.
