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

Кратко о WebRTC

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

STUN и TURN

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

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

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

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

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

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

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

Для начала скажем, что с помощью WebRTC Ваш iPhone может соединиться с любым устройством, не обязательно оба клиента должны общаться c 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 содержат информацию о нахождении клиента в сети, с помощью 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 пуш, что это такое? Это один из видов пуш-уведомления в iOS, он создан как раз для того, чтобы работать со звонками.

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

 
 // Ссылка фреймворк 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 пуш-уведомления, если Вы хотите, чтобы ими кто-то пользовался. Без VoIP звонками пользоваться очень сложно, потому что, чтобы звонок состоялся, нужно, чтобы юзеры сидели в приложении и ждали входящего вызова друг от друга. Это очень странно, в таком случае пользователи скорее выберут другое приложение.

Заключение

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

  • Разработка