WebRTC: как два браузера договариваются о голосовых и видеозвонках

rv_bwaajw0e0qnii-unhvfeouni.png


Спойлер: никак. За них это делает разработчик.

Когда много лет назад начали убивать Flash, пострадали не только браузерные игры. Flash традиционно была сильна в голосовых и видеозвонках: прямой доступ к микрофону, камере, динамикам, возможность работать с UDP-пакетами. В HTML5 заменой стала технология WebRTC. Та самая, которая несколько месяцев назад наконец-то приземлилась в Safari и Edge. Теперь можно звонить с веб-страницы, открытой на iPhone, на другую веб-страницу, например, открытую в Firefox Quantum на линуксе.

Одна из «фишек» WebRTC, которой не было у Flash — это возможность P2P-соединений между браузерами. Но чтобы peer-to-peer работал, программисту придется помучиться. О том, как браузеры договариваются куда слать UDP-пакеты, и что при этом должен сделать разработчик — под катом.

«Сигнализация» — то, о чем стараются не говорить


Большинство тюториалов по WebRTC — это рассказ про крутую замену Flash, голосовые и видеозвонки из браузеров, красивая история про peer-to-peer и десятимегабитный видеопоток без задержек при видеозвонке с вашего iPhone на Windows-ноутбук, при условии что они подключены к одному WiFi. В качестве же кода обычно показывают несколько строк JavaScript, убедительно демонстрирующих как все просто.

Фокус в том, что обычно демонстрируется обертка над WebRTC. И кроме сокрытия от разработчика кишок RTCPeerConnection и MediaDevices.getUserMedia, такие обертки прячут от разработчика все коммуникации между двумя браузерами, используя для этого собственное облако и стек технологий: будь это PubNub, Twilio или наш Voximplant. Делать работу за разработчика — хорошо и правильно. Но упрощая стек технологий, мы часто подкладываем себе мину замедленного действия, когда непонимание происходящих «под капотом» процессов приводит к срыву сроков, работающих через раз решениях и «техническим проблемам», о которых так любит с придыханием рассказывать техподдержка.

nt4remwzfi5gaotpmtdmj3j-xj8.jpeg


Этот рассказ — про сигнализацию в WebRTC, как ее делаем мы и другие компании, а также как ее можете сделать вы, если захотите создать свое решение «с нуля» и без использования готовых сервисов.

Зачем нужен сервер при P2P-звонке


Слыша словосочетание «peer-to-peer», мы обычно вспоминаем торренты. У которых вроде как центрального сервера нет. Что такое «сигнализация» в WebRTC и где у нее сервер?

Предположим, вы сделали веб-страницу с WebRTC и JavaScript-кодом. Открыли ее на трех ноутбуках, подключенных к вашему WiFi и хотите, чтобы первый ноутбук сделал видеозвонок на третий. Как WebRTC на первом ноутбуке узнает, что нужно подключаться именно к третьему? Как бы мы поступили на месте разработчиков WebRTC?

Первый пришедший в голову способ — это передать WebRTC первого ноутбука IP-адрес третьего ноутбука и пусть отсылает UDP-пакеты. Но такой способ будет работать, только если оба устройства подключены к одной сети и эта сеть позволяет им принимать пакеты друг от друга (сюрприз — публичный WiFi в отелях и на площадках конференций чаще всего не позволяет). А что если у нас не одна, а три точки доступа WiFi? И все три ноутбука подключены к разным точкам доступа и имеют один и тот же виртуальный IP-адрес, например »192.168.0.5». Куда браузеру, запущенному на первом ноутбуке, отправлять пакеты?

Можно предположить, что в такой ситуации звонка не будет, и нам в любом случае потребуется внешний сервер с «настоящим» IP-адресом, через который браузеры на обоих ноутбуках смогут общаться друг с другом. Но авторы WebRTC посчитали, что голос и видео — это трафикоемкие коммуникации, и если миллионы пользователей Skype for Web или Google Hangouts будут звонить через публичные сервера, то эти сервера лопнут. Создатели WebRTC наделили технологию возможностью «пробивать» NAT и устанавливать P2P-подключения, даже если оба устройства имеют виртуальные IP-адреса и не могут напрямую обмениваться пакетами. Расплатой стала та самая «сигнализация». Разработчик не может просто передать WebRTC IP-адрес второго устройства или внешнего сервера. Ему нужно помочь обоим браузерам внимательно осмотреть сеть и договориться друг с другом. И для этого ему нужен свой Signaling Server.

Offer, Answer, ICE кандидаты и другие страшные слова


Итак, как выглядит видеозвонок между двумя браузерами с точки зрения разработчика?

  1. После всей предварительной подготовки и создании необходимых JavaScript-объектов на первом браузере вызывается WebRTC метод createOffer (), который возвращает текстовый пакет в формате SDP (или, в будущем, JSON-сериализуемый объект, если oRTC версия API заборет «классическую»). Этот пакет содержит информацию о том, что за коммуникации хочет разработчик: голос, видео или отсылать данные, какие кодеки есть — вся вот эта история
  2. А вот теперь — сигнализация. Разработчик должен каким-то способом (really, в спецификации так и написано!) передать этот текстовый пакет offer второму браузеру. Например, используя собственный сервер в интернет и WebSocket-подключение от обоих браузеров
  3. Получив offer на втором браузере, разработчик передает его WebRTC с помощью метода setRemoteDescription (). Затем вызывает метод createAnswer (), который возвращает такой же текстовый пакет в формате SDP, но уже для второго браузера и с учетом полученного пакета от первого
  4. Сигнализация продолжается: разработчик передает текстовый пакет answer обратно первому браузеру
  5. Получив answer на первом браузере, разработчик передает его WebRTC с помощью уже упомянутого метода setRemoteDescription (), после чего WebRTC в обоих браузерах минимально осведомлены друг о друге. Можно подключаться? Увы, нет. На самом деле все только начинается
  6. WebRTC в обоих браузерах начинает изучать состояние сетевого подключения (на самом деле в стандарте не указано когда это нужно делать, и для многих браузеров WebRTC начинает изучать сеть сразу же после создания соответствующих объектов, чтобы не создавать потом лишних задержек при подключении). Когда разработчик на первом шаге создавал объекты WebRTC, он должен был как минимум передать адрес STUN-сервера. Это сервер, который в ответ на UDP-пакет «какой у меня IP» передает IP-адрес, с которого получил этот пакет. WebRTC использует STUN-сервера чтобы получить «внешний» IP-адрес, сравнить его с «внутренним» и понять есть ли NAT. И если есть, то какие обратные порты NAT использует для маршрутизации UDP-пакетов
  7. Время от времени WebRTC на обоих браузерах будет вызывать коллбэк onicecandidate, передавая уже знакомый SIP-пакет с информацией для второго участника подключения. В этом пакете содержится информация о внутреннем и внешнем IP-адресах, попытках подключения, портах используемых NAT и так далее. Разработчик использует сигнализацию, чтобы передавать эти пакеты между браузерами. Переданный пакет отдается WebRTC с помощью метода addIceCandidate ()
  8. Через некоторое время WebRTC установит подключение peer-to-peer. Или не сможет, если NAT будет мешать. Для таких случаев разработчик может передать адрес TURN-сервера, который будет использоваться в качестве внешнего соединительного элемента: оба браузера будут передавать через него UDP-пакеты с голосом или видео. Если STUN-сервер можно найти бесплатный (например, есть у google), то TURN-сервер придется поднимать самому. Никому не интересно пропускать через себя терабайты видеотрафика просто так

Все эти нюансы можно скрыть, если воспользоваться готовой платформой. Наш Web SDK правильно настраивает WebRTC, патчит SDP-пакеты, поддерживает WebSocket-подключение к облаку Voximplant и заботится еще о множестве деталей. И конечно же у нас есть собственные STUN- и TURN-сервера, чтобы подключение состоялось в любом случае. Но можно не скрывать нюансы и сделать самому! Доступные в браузерах API сейчас позволяют сделать сигнализацию разными способами, о них — ниже.

Простая сигнализация HTTP-запросами, которая не работает


Первое, что приходит в голову — это простейший HTTP-сервер и xmlHttpRequest/fetch со стороны браузера. Увы, работать будет только для «hello world» из учебника. В реальной жизни сервер ляжет от такого количества запросов. Которые придется делать довольно часто, чтобы нажав «connect» пользователь не ждал несколько минут «установки соединения». И еще их придется делать часто потому, что WebRTC — это realtime история, и offer/answer/ice нужно передавать очень быстро. Задержка даже в несколько секунд может послужить сигналом для WebRTC что «ничего не получается», после чего движок прекратит попытки установить подключение. Как вариант можно попробовать технику «long polling», но на практике она не очень хорошо работает и промежуточная интернет-инфраструктура любит обрывать такие «медленные» HTTP-запросы.

yjklvut4nvcjn8rlv_vulqaauaw.jpeg


WebSockets-сигнализация: most effective tactics available


Большинство решений, использующих WebRTC, для сигнализации использует WebSockets. Протокол уже достаточно «старый», чтобы его поддерживало подавляющее большинство используемых веб-браузеров и сетевого оборудования. А если использовать обертку вроде socket.io или SocketJS, то в тех редких случаях, когда WebSocket не работает, можно деградировать до HTTP long polling, который будет работать «хоть как-то». Со стороны сервера WebSockets-подключение, по которому не передаются данные, почти не потребляет ресурсов, и сервер может спокойно обслуживать десятки тысяч ожидающих звонка веб-страниц.

Какие проблемы могут быть с WebSockets? Ну, подключения иногда обрываются — это надо обрабатывать. Еще у них высокие таймауты keep alive — подключение может выглядеть живым, но на самом деле оно уже оборвано где-то на промежуточном оборудовании. А мы об этом узнаем только когда не придет очередной keep alive пакет, а это может быть десять минут. В течении которых до нас пытаются дозвониться, но не могут. Этот механизм отдан на откуп реализациям браузеров и серверов, так что ping-pong frame со стороны сервера небесполезно будет проверить и подкрутить в случае необходимости.

HTTP/2-сигнализация как современный аналог WebSocket


Когда 2-я версия HTTP станет более популярной, WebSockets и Server Side Events скорее всего уйдут в прошлое. Бинарный канал общения с сервером в обе стороны, по которому можно получить и HTML-страницу, и картинки, и организовать WebRTC сигнализацию — это очень круто. К сожалению, несмотря на поддержку последними версиями популярных браузеров, HTTP/2 все еще опасно использовать для проектов с широкой аудиторией. Причина — в промежуточном оборудовании, составляющим «скелет» интернета. Все эти роутеры, шлюзы, баррикадки и киски двадцатилетней давности часто завершают HTTP/2-соединения, не понимая что это такое и пытаясь «защитить» что-то от кого-то.

WebRTC-сигнализация как пример рекурсии


А еще для сигнализации WebRTC можно использовать другое подключение WebRTC! Звучит странно, но у этого способа есть свои плюсы. Если первое WebRTC-подключение установить между браузером и облаком (как это делается у нас для не P2P-звонков) с какой-нибудь другой сигнализацией, то у такого подключения затем можно использовать Data Channel API. Которое выгодно отличается от WebSockets тем, что может работать не только «как TCP», но и «как UDP», очень быстро отправляя пакеты без гарантированной доставки. Такой способ позволит очень быстро сигнализировать подключения — быстрее, чем WebSockets и HTTP/2. В ряде случаев такой способ это то, что нужно. Например, в играх.

TL; DL


Резюмируя все описанное: перед тем, как WebRTC установит подключение peer-to-peer, разработчик должен обеспечить возможность двум браузерам (или другим устройствам; библиотека libwebrtc от Google позволяет использовать webRTC на всем, что движется компилирует C++) обменяться несколькими текстовыми пакетами. Делать это надо быстро, иначе таймауты и ничего не получится. Платформы делают сигнализацию (и многое другое) за разработчика, но если очень надо, то можно сделать самому. Только помнить о куче нюансов, а потом все отладить.

Иллюстрация до ката с сайта www.elasticrtc.com
Иллюстрация дракона с сайта www.sococo.com/blog/webrtc-signaling-here-be-dragons

© Habrahabr.ru