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

14/7/2020
·
Обновлено
7.8.2026
WebRTC в iOS или как добавить видеозвонок в свое приложение? — обложка

Если вы когда-нибудь задумывались о создании приложения для онлайн-конференций или хотели добавить в своё приложение видеозвонки, то, скорее всего, слышали о WebRTC. Но материалов по этой технологии не так уж много, и почти все они рассчитаны на разработчиков. Сегодня мы не будем углубляться в технические детали, а просто попробуем понять, что такое WebRTC и почему видеосвязь — это вовсе не магия. Надеюсь, эта статья поможет вам разобраться, что же это за технология.

Кратко о WebRTC

WebRTC (Web Real Time Communications) — это протокол, который обеспечивает передачу аудио и видео в режиме реального времени. Он работает как по протоколу UDP, так и по TCP, и может переключаться между ними. Главное преимущество WebRTC — возможность устанавливать прямое соединение между пользователями (p2p, или «пиринговое» соединение): медиапоток передаётся напрямую, без участия сервера. Однако для использования p2p-соединений важно учитывать особенности как самого протокола WebRTC, так и особенности таких соединений.

STUN и TURN

Сети обычно проектируются с использованием частных IP-адресов. Эти адреса используются внутри организации, чтобы устройства могли общаться локально — они не маршрутизируются в интернете. Чтобы устройство с приватным IP-адресом могло обращаться к ресурсам за пределами локальной сети, его адрес нужно преобразовать в публичный. Такой перевод выполняет NAT (Network Address Translation). NAT не является основной темой статьи, поэтому подробнее о нём можно почитать здесь. Нам важно знать лишь, что в роутере есть таблица NAT, и чтобы к нашему клиенту приходили пакеты, в этой таблице должна быть специальная запись. Чтобы создать такую запись, клиент должен отправить что-то удалённому клиенту. Проблема в том, что ни один из них не знает своего внешнего IP-адреса. Для решения этой проблемы используются STUN и TURN-серверы. Сразу оговоримся: двух клиентов можно соединить и без STUN и TURN, но это возможно только в том случае, если они находятся в одной сети.

Начнем со STUN-сервера. STUN — это сервер, подключённый напрямую к интернету. Он получает пакет, в котором указан внешний адрес клиента, отправившего этот пакет, и отправляет его обратно. Клиент узнаёт свой внешний адрес и порт, который нужен, чтобы роутер понимал, какой именно клиент отправил пакет — ведь несколько устройств из внутренней сети могут одновременно обращаться к внешним ресурсам. Так в таблице NAT создаётся нужная запись.

TURN – это улучшенный STUN-сервер: он может работать как обычный STUN, но его существование оправдано определёнными задачами. Существует несколько типов NAT, и некоторые из них запоминают не только внешний IP-адрес, но и порт STUN-сервера, и блокируют пакеты, приходящие не от него. В результате NAT не пропускает пакеты от удалённого клиента, и прямое соединение не устанавливается. В таких случаях как раз нужен TURN. Также, например, в сетях 3G невозможно организовать прямое p2p-соединение, и тогда TURN-сервер выступает в роли ретранслятора, хотя клиенты продолжают считать, что общаются напрямую.

Сигнальный сервер

Отлично, мы поняли, зачем нужны STUN и TURN серверы, но это не единственная особенность WebRTC. WebRTC не умеет передавать данные о соединении — значит, с его помощью нельзя напрямую соединить клиентов. Нам нужно как-то организовать обмен информацией о соединении. Что именно передаётся и зачем — разберём ниже. Для этого используется сигнальный сервер. Подойдёт любой способ передачи данных, лишь бы стороны могли обмениваться информацией. В нашей компании, например, обычно используют вебсокеты.

Видеозвонок 1 на 1

Мы поговорили о STUN и TURN-серверах, о том, зачем нужен сигнальный сервер. Но до сих пор непонятно, как создать работающий звонок. Теперь разберёмся, какие шаги нужно выполнить, чтобы организовать видеозвонок.

Для начала скажем, что с помощью WebRTC ваш iPhone может подключиться к любому устройству — не обязательно, чтобы оба участника общались с iPhone. Соединение возможно и с устройством на Android, и с десктопным компьютером.

Имеем двух клиентов: инициатор звонка и ожидающий звонок.

Чтобы позвонить оппоненту, инициатор должен:

  1. Получить свой локальный медиапоток. Медиапоток — это поток видео- и аудиоданных. Каждый поток может включать несколько медиатреков, а каждый медиатрек — несколько медиаканалов.

Медиапотоков может быть несколько — например, с фронтальной камеры и с рабочего стола. Медиапоток синхронизирует свои медиатреки, но разные медиапотоки между собой не синхронизированы. То есть звук и видео с камеры будут синхронизированы, но не с видео с экрана. Медиаканалы внутри одного медиатрека тоже синхронизированы. Получение локального медиапотока в коде выглядит примерно так:

 
 func startLocalStream() { 
    let stream = streamsContainer.stream(forIdentifier: PublishStreamModel.publish) 
    stream.startCameraCapturer(processDeviceRotations: false, prefferedFrameSize: CGSize(width: 640,height: 480), prefferedFrameRate: 15) 
}
 

2. Сформировать offer, то есть предложить начать звонок

 
 if self.positioningType == .caller { 
    self.prepareAndSendOffer() 
} 
 

3. Передать свой SDP через сигнальный сервер. Что такое SDP? У устройства есть множество параметров, необходимых для установления соединения — например, поддерживаемые кодеки. Все эти параметры собираются в объект SDP, или дескриптор сессии, который передаётся собеседнику через сигнальный сервер. Важно: локальный SDP хранится в текстовом виде и его можно отредактировать перед отправкой — например, чтобы вручную выбрать кодек. Однако такой подход используется редко и не всегда работает.

 
 func stream(_ stream: StreamController?, 
            shouldSendSessionDescriptionsessionDescriptionModel: StreamSessionDescriptionModel, 
            identifier: String, 
            completion: ((Bool)-> ())?) { 
    shouldSendSessionDescription?(sessionDescriptionModel, identifier) 
}
 

4. Передать свои Ice Candidate через сигнальный сервер. Что такое Ice Candidate? SDP помогает установить логическое соединение, но физически клиенты пока не могут найти друг друга. Объекты Ice Candidate содержат информацию о местоположении клиента в сети — с их помощью клиенты находят друг друга и начинают обмениваться медиапотоком. Важно: локальный SDP генерируется один раз, а объектов Ice Candidate может быть несколько. Это нужно потому, что клиент может быть доступен по внутреннему IP-адресу, через TURN-серверы или по внешнему адресу маршрутизатора — и таких адресов может быть несколько. Поэтому для определения местоположения клиента в сети требуется несколько Ice Candidate.

 
 func stream(_ stream: StreamController?, 
            shouldSendCandidate candidateModel: StreamCandidateModel, 
            identifier: String, 
            completion: ((Bool) -> ())?) { 
    shouldSendCandidate?(candidateModel, identifier) 
}
 

5. Принять удалённый медиапоток (поток оппонента) и отобразить его. На iOS для рендеринга видеопотока можно использовать OpenGL или Metal.

 
 func stream(_ stream: StreamController?, shouldShowLocalVideoView videoView: View?, identifier id: String) { 
    guard let video = videoView else { return } 
    self.localVideo = video 
    shouldShowRemoteStream?(video, id) 
}
 

Оппонент в это же время должен выполнить те же шаги, за исключением второго: он формирует answer, а не offer — то есть принимает звонок.

 
 if self.positioningType == .callee && self.peerConnection?.localDescription == nil { 
    self.prepareAndSendAnswer() 
} 
 

На самом деле answer и offer это одно и тоже, отличие состоит лишь в том, что когда ожидающий звонка формирует answer, то есть генерирует свой локальный SDP, он опирается на SDP объект инициатора звонка. Таким образом клиенты будут знать о параметрах устройства друг друга и, к примеру, смогут более корректно подобрать кодек.

Если кратко подытожить, то можно сказать так: клиенты сначала обмениваются SDP (устанавливают логическое соединение), а затем Ice Candidate (устанавливают физическое соединение). Таким образом клиенты успешно соединяются, видят друг друга, слышат друг друга и могут общаться.

Но это ещё не всё, что нужно учесть при работе с WebRTC в iOS. Если оставить всё как есть, пользователи смогут общаться, но только если приложение открыто — чтобы узнать о звонке и ответить, его нужно держать запущенным. Однако эту проблему легко решить: в iOS есть так называемый VoIP-пуш. Что это такое? Это специальный тип пуш-уведомления, созданный именно для работы с голосовыми вызовами.

Регистрируется он так:

 
 // Ссылка фреймворк PushKit 
import PushKit 
// Активируем VoIP регистрацию при запуске 
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool { 
    self.voipRegistration() 
    return true 
} 
// Созздаем VoIP уведомления 
func voipRegistration() { 
    let mainQueue = dispatch_get_main_queue() 
    // Создаем пуш 
    let voipRegistry: PKPushRegistry = PKPushRegistry(mainQueue) 
    // Делигируем на себя 
    voipRegistry.delegate = self 
    // Задаем типа пуша на VoIP 
    voipRegistry.desiredPushTypes = [PKPushTypeVoIP] 
}
 

С помощью этого пуш-уведомления можно показать экран входящего вызова, где пользователь сможет принять или отклонить звонок. Для этого используется следующая функция:

 
 func reportNewIncomingCall(with UUID: UUID, 
                           update: CXCallUpdate, 
                           completion: @escaping (Error?) -> Void) 
 

И совершенно неважно, чем в этот момент занят пользователь — играет в игру или телефон заблокирован. VoIP-пуш имеет самый высокий приоритет, поэтому уведомления приходят всегда, и пользователи легко могут звонить друг другу. При интеграции звонков обязательно нужно подключать VoIP-пуш-уведомления, если вы хотите, чтобы ими реально пользовались. Без них пользоваться звонками крайне неудобно: чтобы вызов прошёл, оба пользователя должны сидеть в приложении и ждать звонка. Это выглядит странно, и в таком случае люди скорее выберут другое приложение.

Заключение

Мы поговорили о некоторых особенностях WebRTC, узнали, что нужно учесть для успешного соединения двух клиентов, разберёмся в последовательности шагов, которые должны выполнить клиенты, чтобы звонок состоялся, а также — что ещё потребуется помимо интеграции WebRTC, чтобы пользователи на iOS могли звонить друг другу. Надеюсь, после прочтения этой статьи WebRTC уже не кажется вам таким пугающим и загадочным, и теперь вы примерно понимаете, что нужно сделать, чтобы внедрить его в свой продукт.

  • Разработка