Миллион видеозвонков в сутки или «Позвони маме!»

С точки зрения пользователя, сервисы звонков выглядят довольно просто: заходишь на страницу к другому пользователю, звонишь, он снимает трубку, вы с ним разговариваете. Снаружи кажется, что все просто, но немногие знают, как сделать такой сервис. А вот Александр Тоболь (alatobol) не только знает, но и охотно делится своим опытом.

rquncxhtqvyydpdbneoatkwfvke.jpeg

Далее текстовая версия доклада на HighLoad++ Siberia, из которой вы узнаете:

  • как работают сервисы видеозвонков под капотом;
  • как красиво пробить NAT — это будет интересно и специалистам из игровой сферы, которым необходимо peer-to-peer соединение;
  • как устроен WebRTC, какие протоколы в него входят;
  • как можно тюнить WebRTC через BigData.



О спикере: Александр Тоболь руководит разработкой платформ Видео и Ленты в ok.ru.

История видеозвонков


Первое устройство для видеозвонков появилось в 1960, оно называлось пикчерфоном, использовало выделенные сети и стоило крайне дорого. В 2006 Skype добавил в свое приложение видеозвонки. В 2010 году Flash поддержал протокол RTMFP, и мы в Одноклассниках запустили видеозвонки на Flash. В 2016 году Chrome прекратил поддержку Flash, и в августе 2017 мы перезапустили звонки на новой технологии, о чем я сегодня и расскажу. Доработав сервис, еще за полгода мы получили существенный прирост успешно совершенных звонков. Недавно у нас появились еще и маски в звонках.
kn9uuyg18yggcfabvmzgs-cfygg.jpeg

Архитектура и ТЗ


Так как мы работаем в социальной сети, то технических заданий у нас нет, и что такое ТЗ мы не знаем. Обычно вся идея умещается на одну страничку и выглядит примерно так.
qf9xjum4vvjczd3-momvzhht9cw.jpeg

Пользователь хочет звонить другим пользователям, используя веб или iOS/Android-приложения. У другого пользователя может быть несколько устройств. Звонок приходит на все устройства, пользователь снимает трубку на одном из них, они разговаривают. Всё просто.

Технические характеристики


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

Пользователя точно раздражает, если он снимает трубку и вынужден ждать, пока установится соединение.
8gkkqbq6ioqxnn0ro6dcq9kwhno.jpeg

Пользователя раздражает, если качество звонка низкое — что-то прерывается, видео рассыпается, звук булькает.
2lrtudpp-tw5cjsknhzamgj--pi.jpeg

Но больше всего пользователя раздражает задержка в звонках. Latency — это одна из важных характеристик звонков. При latency в разговоре порядка 5 секунд абсолютно невозможно вести диалог.
z-tnlfghw6laz3j25e3kbwob9mi.jpeg

Мы определили для себя приемлемые характеристики:

  • Старт — мы решили, что хорошо начинать звонок за секунду. Т.е. connecting после того, как пользователь ответил, должен занимать не более 1 секунды.
  • Качество — очень субъективный показатель. Можно мерить, например, соотношение сигнал-шум (SNR), но есть еще пропадающие кадры и другие артефакты. Мы измеряли качество скорее субъективно и оценивали потом уже счастье пользователей.
  • Latency должна быть меньше 0,5 секунды. Если Latency больше 0,5 секунды, то вы уже слышите задержки и начинаете перебивать друг друга.


clyxj0ajsqq5coamt5orttz-kxc.jpeg

Polycom — система конференц-связи, установленная у нас в офисах. Средние задержки polycom у нас порядка 1,3 секунды. При такой задержке не всегда друг друга понимаешь. Если задержка увеличится до 2 секунд, то вести будет диалог невозможно.
3_yyqqbvvq5zoavfa5q9tzvqhiq.jpeg

Так как у нас уже была запущена платформа, мы примерно ожидали, что у нас будет миллион звонков в день. Это тысяча звонков параллельно. Если все звонки пустить через сервер, будет тысяча звонков по мегабиту на звонок. Это всего 1 гигабит/сек одного железного сервера будет достаточно.

Интернет против TTX


Что может помешать добиться таких классных характеристик? Интернет!
qbaqsyfaf6vln4e882armfugx1g.jpeg

В интернете существуют такие вещи, как round-trip time (RTT), который не побороть, есть переменная полоса пропускания, есть NAT.

Предварительно мы измерили скорость передачи в сетях наших пользователей.
bmgez6efih7z_lqwidcknso8edw.jpeg

Разбили по типу подключения, посмотрели среднее RTT, packet loss, скорость, и решили, что будем тестировать звонки на средних значениях каждой из этих сетей.
uio4k8pmxmyxyclfwwawehqijes.jpeg

В интернете бывают и другие неприятности:

  • Packet loss — мы намерили 0,6% random packet loss (congestion packet loss при избыточном количестве пакетов мы не учитываем).
  • Reordering — вы отправляете пакеты в одном порядке, а сеть их пересортировывает.
  • Jitter — отдаете видео или аудио поток с определенным интервалом, а на сторону клиента пакеты приходят склеенные пачками, например, за счет буферизации на сетевых устройствах.
  • NAT — у нас получилось, что больше 97% пользователей находятся за NAT. Дальше поговорим, почему, что и как.


z_uomck64smc12_beaupdnutgp4.jpeg

Рассмотрим перечисленные выше параметры сети на простом примере.

Я у себя из офиса попинговал сайт Новосибирского государственного университета и получил такой странный ping.
wiamatloo1eviw58g3zgzbrsc2e.jpeg

Средний jitter в этом примере равен 30 мс, то есть средний интервал между соседними временами ping — порядка 30 мс, а средний ping — 105 мс.

Что важно в звонках, почему будем бороться за p2p?
gbnyyxij20hrfo96scwubxdzovu.jpeg

Очевидно, что, если между нашими пользователями, которые пытаются в Санкт-Петербурге поговорить друг с другом, удалось установить p2p соединение, а не через сервер, который находится в Новосибирске, мы сэкономим порядка 100 мс round-trip и трафик на этот сервис.

Поэтому большая часть статьи посвящена тому, как сделать хороший p2p.

История или legacy


Как я уже говорил, у нас был сервис звонков с 2010 года, а теперь мы его перезапустили.
sirulur_jaalnldpkmvbjx7ct-q.jpeg

В 2006 году, когда запускался Skype, Flash купил компанию Amicima, которая делала RTMFP. У Flash уже был протокол RTMP, который в принципе можно использовать для звонков, и он часто используется для стриминга. Позже Flash открыл спецификацию на RTMP. Интересно, зачем им нужен был RTMFP? В 2010 мы использовали именно RTMFP.

Сравним требования к протоколам звонков и протоколам реального стриминга и посмотрим, где эта граница.
ihj9xiris6praclbyxpxelwc1i4.jpeg

Протокол RTMP — это скорее протокол видеостриминга. Он использует TCP, у него есть накапливающаяся задержка. Если у вас хорошее интернет-соединение, звонки на RTMP будут работать.

Протокол RTMFP, несмотря на отличие всего в одну букву, — это протокол UDP. Он лишен проблем буферизации — тех, которые есть на TCP; лишен head-of-line блокировок — это когда у вас пропал один пакет, и TCP не отдает следующие пакеты до тех пор, пора не отправит повторно потерянный. RTMFP умел справляться с NAT и переживал смену IP адреса клиентов. Поэтому мы запускали web на RTMFP в 2010 году.
pvrx163tvieetirsg8nkfrov7qm.jpeg

Потом только в 2011 году появился initial draft WebRTC, еще не совсем работоспособный. В 2012 мы начали поддержку звонков на iOS/Android, потом происходило что-то еще, и в 2016 Chrome прекратил поддержку Flash. Нам пришлось что-то делать.
uuubcssxa_w9-ost03_y6dqq6ic.jpeg

Мы посмотрели на все протоколы VoIP: как всегда, для того чтобы сделать что-то, мы начинаем с изучения конкурентов.

Конкуренты или с чего начать


Мы выбрали самых популярных конкурентов: Skype, WhatsApp, Google Duo (похож на Hangouts) и ICQ.

Для начала измерили задержку.
yvbrd2tluf9yzf-xqqibngdooh8.jpeg

Сделать это просто. Выше фотография, на которой:

  • Секундомер (см. телефон вверху слева), на котором видно время (03:08).
  • Ближний телефон совершает звонок и в качестве видео снимает первый телефон. С того момента, как изображение попало в камеру телефона, и вы его увидели, прошло порядка 100 мс.
  • Звонок на другой телефон (белый) и еще одно время. Здесь задержка порядка 310 мс у Google Duo.


Пока не буду раскрывать все карты, но мы делали так, чтобы эти устройства не могли установить p2p соединения. Конечно, измерения проводились в разных сетях, и это просто пример.
rnxzee4grukafmg0gu3cunpemsi.jpeg

Skype все-таки немножко перебивает. У нас получилось, что у Skype, в случае если ему не удастся подключить p2p, задержка составляет 1,1 с.

Тестовая среда у нас была сложная. Мы тестировали в разных условиях (EDGE, 3G, LTE, WiFi), учитывали, что каналы ассиметричные, и я привожу усредненные значения всех измерений.
gbpp6sc5jugssoov8nmzvajlifo.jpeg

Для того чтобы оценить расход батареи, нагрузку на процессор и все остальное, мы решили, что можно просто померить температуру телефона пирометром и считать, что это некоторая средняя нагрузка на GPU телефона на процессор, на аккумулятор. В принципе, очень неприятно подносить к уху горячий телефон, да и держать в руках тоже. Пользователю кажется, что сейчас приложение израсходует всю его батарею.
dlckvdbr8we75haadv9qkgk1jxk.jpeg

В итоге получилось:

  • Самыми медленными по задержке оказались ICQ и Skype, а самым быстрым — Telegram. Это не совсем корректное сравнения, так как у Telegram нет видеозвонков, но зато по аудио у них минимальная latency. Классно работает WhatsApp (порядка 200 мс) и Hangouts — 390 мс.
  • По температуре меньше всех ест Telegram без видео, а больше всех Skype.
  • С точки зрения времени ответа дольше всех соединение устанавливает Telegram, а самый быстрый WhatsApp и Google Duo.


Отлично, у нас появились какие-то метрики!
t8mbn0vv_g1utslspeeevk8g5ie.jpeg

Мы протестировали качество видео и голоса в разных сетях, с разными дропами и всем остальным. В результате пришли к выводу, что самое качественное видео на Google Duo, а голос — на Skype, но это в «плохих» сетях, когда уже есть искажения. В целом все работают примерно средненько. У WhatsApp самая замыленная картинка.

Посмотрим, на чем это все реализовано.
ihap1infndlvdjllso8eszhjnla.jpeg

У Skype свой проприетарный протокол, а все остальные используют или модификацию WebRTC, или вообще напрямую WebRTC. Hangouts, Google Duo, WhatsApp, Facebook Messenger могут работать с вебом, и у них у всех под капотом WebRTC. Они все такие разные, с разными характеристиками, и у них всех один WebRTC! Значит, нужно уметь его правильно готовить. Плюс есть Telegram, у которого за аудио часть отвечают некоторые части WebRTC, есть ICQ, которая форкнула WebRTC очень давно и пошла развиваться своим путем.

WebRTC. Архитектура


3q0x-cy-45bblq5lgoayq2awd5u.jpeg

WebRTC подразумевает наличие signaling-сервера, посредника между клиентами, который используется для обмена сообщениями в процессе установки p2p-соединения между ними. После установки прямого соединения клиенты начинают обмениваться медиаданными друг с другом.

WebRTC. Demo


Начнем с простого демо. Есть простые 5 шагов, как установить WebRTC соединение.

Детальный код примера
1.  // Step #1: Getting local video stream and initializing a peer connection with it (both caller and callee)
2.   
3.  var localStream = null;
4.  var localVideo = document.getElementById('localVideo');
5.   
6.  navigator
7.    .mediaDevices
8.    .getUserMedia({ audio: true, video: true })
9.    .then(stream => {
10.     localVideo.srcObject = stream;
11.     localStream = stream;
12.   });
13.  
14. var pc = new RTCPeerConnection({ iceServers: [...] });
15.  
16. localStream
17.   .getTracks()
18.   .forEach(track => pc.addTrack(track, localStream));
19.  
20. // Step #2: Creating SDP offer (caller)
21.  
22. pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
23.   .then(offer => signaling.send('offer', offer));
24.  
25. // Step #3: Handling SDP offer and sending SDP answer (callee)
26.  
27. signaling.on('offer', offer => {
28.   pc.setRemoteDescription(offer)
29.     .then(() => pc.createAnswer())
30.     .then(answer => signaling.send('answer', answer))
31. });
32.  
33. // Step #4: Handling SDP answer (calleer)
34.  
35. signaling.on('answer', answer => pc.setRemoteDescription(answer));
36.  
37. // Step #5: Exchanging ICE candidates
38.  
39. pc.onicecandidate = event => signaling.send('candidate', event.candidate);
40.  
41. signaling.on('candidate', candidate => pc.addIceCandidate(candidate));
42.  
43. // Step #6: Getting remote video stream (both caller and callee)
44.  
45. var remoteVideo = document.getElementById('remoteVideo');
46.  
47. pc.onaddstream = event => remoteVideo.srcObject = event.streams[0];



В нем написано следующее:

  1. Взять видео и установить peer connection, передать какие-то iceServers (сразу непонятно, что это).
  2. Создать SDP offer и отправить его в signaling, и signaling WebRTC за вас никак не имплементирует.
  3. Потом надо сделать обертку для приходящего из signaling, и это тоже не входит в WebRTC.
  4. Дальше поменяться какими-то candidates.
  5. Наконец получить удаленный видеопоток.


Давайте все же разберемся, что там происходит и что нам нужно реализовывать сами.
wxfzfnkuw7smjite-bdpmadb_ro.jpeg

Смотрим картинку снизу вверх. Есть библиотека WebRTC, которая уже встроена в браузер, поддерживается Chrome, Firefox и др. Вы можете собрать её же под Android/iOS и общаться с ней через API и SDP (Session Description Protocol), в котором описывается сама сессия. Ниже расскажу, что в него входит. Для использования этой библиотеки в своем приложении вы должны установить соединение между абонентами через signaling. Signaling — это тоже ваш сервис, который придется написать самим, WebRTC его не предоставляет.

Далее в статье мы по порядку обсудим сеть, потом видео/аудио, и в конце напишем свой signaling.

WebRTC network или p2p (на самом деле c2s2c)


Кажется, что установить p2p-соединение довольно просто.
he7hsoyzqklbzj715slypgychgc.jpeg

У нас есть Алиса и Боб, которые хотят установить p2p соединение. Они берут свои IP адреса, у них есть signaling-сервер, к которому они оба подключены, и через которые могут обменяться этими адресами. Они обмениваются адресами, и ой! Адреса у них одинаковые, что-то пошло не так!
rnhcujxoa5k0tni2oquj-tx3hfi.jpeg

На самом деле оба пользователя сидят, скорее всего, за Wi-Fi роутерами и это их локальные серые IP-адреса. Роутер обеспечивает им такую функцию, как Network Address Translation (NAT). Как она работает?
yrxur0s_xhoye3i36zod4ropbec.jpeg

У вас есть серая подсеть и внешний IP-адрес. Вы отправляете пакет в интернет со своего серого адреса, NAT подменяет ваш серый адрес на белый и запоминает маппинг: то, с какого порта он отправил, какому пользователю и какому порту соответствует. Когда приходит обратный пакет, он по этому маппингу резолвит и отправляет его отправителю. Все просто.

Ниже иллюстрация, как это выглядит у меня дома.
udardacyxvmzt5mq0modbosd-mc.jpeg

Это мой внутренний IP-адрес и адрес роутера (кстати, тоже серый). Если провести трассировку и посмотреть маршрут, то мы увидим мой Wi-Fi-роутер: пачку серых адресов провайдера и внешний белый IP. Таким образом, на самом деле у меня будет два NAT: один, на котором я на Wi-Fi, и другой еще у провайдера, если я, конечно, не купил себе выделенный внешний IP-адрес.

NAT так популярен, потому что:

  • до сих пор у многих IPv4, и адресов не хватает;
  • NAT вроде как защищает сеть;
  • это стандартная функция роутера: подключаетесь к Wi-Fi, там сразу есть NAT, он работает.


Поэтому только 3% пользователей сидят с внешним IP, а все остальные проходят через NAT.

NAT позволяет совершенно спокойно ходить на любые белые адреса. Но если вы никуда не ходили, то к вам никто не может прийти.
j7clj2pypnpwv_p-odndgflgzpk.jpeg

Для установки p2p соединения это проблема. На самом деле Алиса и Боб не могут отправить друг другу пакеты, если они оба находятся за NAT.

В WebRTC для решения этой проблемы есть протокол STUN. Предлагается развернуть STUN-сервер. Тогда Алиса подключается к STUN-серверу, получает свой IP-адрес, через signaling отправляет его Бобу. Боб тоже получает свой IP-адрес и отправляет Алисе. Они отправляют навстречу друг другу пакеты и таким образом пробивают NAT.
1mmqiz57zbyzzahdgng_y_3hywo.jpeg

Вопрос: у Алисы открыт определенный порт, уже пробит NAT/Firewall на этот порт, и у Боба открыт. Они знают адреса друг друга. Алиса пытается отправить пакет Бобу, он отправляет пакет Алисе. Как вы думаете, они смогут поговорить или нет?

На самом деле, вы правы в любом случае, результат зависит от типа той пары NAT, которая есть у пользователей.
tehdhjdvyvfv9dcrxng950n98h8.jpeg

Network Address Translation


Существует 4 типа NAT:

  1. Full cone NAT;
  2. Restricted cone NAT;
  3. Port restricted cone NAT;
  4. Symmetric NAT.


В базовой версии Алиса отправляет пакет на сервер STUN, у нее открывается какой-то порт. Боб как-то узнает об ее порте и отправляет обратный пакет. Если это Full cone NAT — самый простой, который просто маппит внешний порт на внутренний, то Боб сразу же сможет отправить Алисе пакет, установить соединение, и они поговорят.
phkpzrkuy3k2uzqsl4sub-sla7o.jpeg

Ниже схема взаимодействия: Алиса с какого-то порта отправляет на порт STUN пакет, STUN ей отвечает ее внешним адресом. STUN может ответить с любого адреса, если это Full cone NAT, он все равно пробьет NAT, и Боб может ответить на тот же адрес.
dlpp3anl2az479kengmx5easix4.jpeg

В случае Restricted cone NAT все чуть-чуть сложнее. Он запоминает не просто порт, с которого нужно маппить на внутренний адрес, а еще и внешний адрес, на который вы сходили. То есть если вы установили соединение только к IP STUN-сервера, то никто другой из сети не сможет вам ответить, и тогда пакет Боба не дойдет.
yt-gsg5yyrf99ywt77j2ydntyzy.jpeg

Как решается эта задача? В простой схеме (см. иллюстрацию ниже) так: Алиса отправляет пакет на STUN, он ей отвечает ее IP. STUN может отвечать ей с любого порта, пока это Restricted cone NAT. Боб не может ответить Алисе, потому что у него другой адрес. Алиса ему отвечает пакетом, зная IP адрес Боба. У нее открывается NAT до Боба, Боб ей отвечает. Ура, они поговорили.
izjiare3mege16iftxwal3haqye.jpeg

Чуть-чуть более сложный вариант — Port restricted cone NAT. Все то же самое, только STUN должен отвечать ровно с того порта, на который к нему обращались. Тоже все будет работать.

Самая вредная штука — это Symmetric NAT.
p-sefuxnvpapxpuf8-wqavqkd0u.jpeg

Вначале работает все точно так же — Алиса отправляет пакет STUN-серверу, он отвечает с того же порта. Боб не может ответить Алисе, но она отправляет пакет Бобу. И тут, несмотря на то что Алиса отправляет пакет на порт 4444, маппинг ей выделяет новый порт. Symmetric NAT отличается тем, что при установке каждого нового соединения, он каждый раз на маршрутизаторе выдает новый порт. Соответственно, Боб бьется в тот порт, с которого Алиса ходила на STUN, и они никак не могут соединиться.

В обратную сторону, если Боб с открытым IP-адресом, Алиса может к нему просто прийти, и они установят соединение.

Все варианты собраны в одной таблице ниже.
-cjfo9dclflwku9qz-2p4wl_hre.jpeg

В ней видно, что почти все возможно кроме случаев, когда мы пытаемся установить соединения через Symmetric NAT с Port restricted cone NAT или Symmetric NAT на другом конце.
gplk_darehdjbaxktic84liuwto.jpeg

Как мы выяснили, p2p бесценно для нас в плане задержек (latency), но если установить его не удалось, то WebRTC предлагает нам TURN-сервер. Когда мы поняли, что p2p не установится, мы можем просто подключиться к TURN, который будет проксировать весь трафик. Правда при этом вы будете платить за трафик, а у пользователей, возможно, появится некоторые дополнительные задержки.

Практика


Бесплатные STUN-серверы есть у Google. Можно их поставить в библиотеку, будет работать.

У TURN-серверов есть credential (логин и пароль). Скорее всего, вам придется поднять свой, довольно трудно найти бесплатный.

Примеры бесплатных STUN-серверов от Google:

  • stun: stun.l.google.com:19302
  • stun: stun1.l.google.com:19302
  • stun: stun2.l.google.com:19302
  • stun: stun3.l.google.com:19302


И бесплатный TURN-сервер с паролями: url: «turn:192.158.29.39:3478? transport=udp», credential: «JZEOEt2V3Qb0y27GRntt2u2PAYA=», username:»28224511:1379330808′.

Мы используем coturn.
yj_dzvkvpkhmol2a8ryscucgoli.jpeg

В результате через p2p соединение у нас проходит 34% траффика, все остальное проксируется через TURN-сервер.

Что еще интересного есть в STUN-протоколе?


STUN позволяет определить тип NAT.
njlo7h_sounawjxlwk-pxsiejy0.jpeg

Ссылка на слайде

При отправке пакета можно указать, что вы хотите получить ответ с такого же порта или попросить STUN ответить с другого порта, с другого IP, или вообще с другого IP и порта. Таким образом за 4 запроса к STUN-серверу можно определить тип NAT.
og0m2hawerjjdabsr43zwygz_cy.jpeg

Мы посчитали типы NAT и получили, что почти у всех пользователей или Symmetric NAT, или Port restricted cone NAT. Отсюда и получается, что только треть пользователей могут установить p2p соединение.

Вы можете спросить, зачем я все это рассказываю, если можно было просто взять STUN у Google, засунуть в WebRTC, и вроде как все заработает.

Потому что на самом деле можно определить тип NAT самим.
ihqn8j2nm2dumsstvyopem7ljxk.jpeg

Это ссылка на Java-приложение, которое ничего хитрого не делает: просто пингует разные порты и разные STUN-серверы, и смотрит, какой порт видит в итоге. Если у вас открытый Full cone NAT, то в ответах STUN сервер будет один и тот же порт. При Restricted cone NAT у вас на каждый запрос к STUN будут приходить разные порты.
veqft1dee6wstziwrkrbemxj5da.jpeg

При Symmetric NAT у меня в офисе получается вот так. Там абсолютно разные порты.

Но иногда встречается интересная закономерность, что на каждое подключение номер порта увеличивается на единицу.
2n7ero_besx7rzunsjuzfjplfpy.jpeg

То есть у многих NAT настроено так, что они увеличивают или уменьшают порт на константу. Эту константу можно найти и таким образом пробить Symmetric NAT.
te6nbfknmebcz7zmrtk35jqwfpk.jpeg

Таким образом пробиваем NAT — ходим на один STUN-сервер, на другой, смотрим разницу, сравниваем и пробуем еще раз отдать свой порт уже с этим инкрементом или декрементом. То есть Алиса пытается отдать Бобу свой порт, уже скорректированный на константу, зная, что в следующий раз он будет именно такой.
l1re-mp1om6zkjqm0j1n5rfh004.jpeg

Так нам удалось наварить еще 12% peer-to-peer.

На самом деле, иногда внешние роутеры с одним и тем же IP ведут себя одинаково. Поэтому если пособирать статистику и если Symmetric NAT — это фича провайдера, а не фича Wi-Fi-роутера пользователя, то дельту можно предсказать, сразу же отправить ее пользователю, чтобы он ей воспользовался и не тратил лишнее время на ее определение.

CDN Relay или что делать, если не удалось установить р2р-соединение


Если мы все же используем TURN-сервер и работаем не в p2p, а в real mode, передавая весь трафик через сервер, можно еще добавить CDN. Если, конечно, у вас есть площадка. У нас есть свои CDN-площадки, поэтому для нас это было довольно просто. Но надо было определить, куда лучше отправлять человека: на CDN-площадку или, допустим, на канал до Москвы. Это не очень тривиальная задача, поэтому мы делали так:

  1. Случайно выдавали некоторым пользователям московские площадки, некоторым — удаленные.
  2. Собирали статистику по IP пользователя, по серверам и по характеристикам сети.
  3. По maxMind сгруппировали подсети, посмотрели статистику и смогли по IP понять, какой для пользователя ближайший TURN-сервер для соединения.


qldgsyqtvaeqqdvyctilcipatau.jpeg

В Новосибирске есть CDN. Если у вас все работает через Москву, то 99 перцентиль RTT — 1,3 секунды. Через CDN все работает сильно быстрее (0,4 секунды).

Всегда ли лучше использовать соединение p2p и не использовать сервер? Интересный пример — это два красноярских провайдера Optibyte и Mobra (возможно, имена изменены). Между ними почему-то связь на p2p сильно хуже, чем через MSK. Наверное, они друг с другом не дружат.
yqendmksjs7vila8zpra1xu_2uc.jpeg

Мы проанализировали все такие случаи, случайным образом отправляя пользователей на p2p или через MSK, собрали статистику и построили предсказания. Мы знаем, что статистику нужно обновлять, поэтому некоторым пользователям мы специально устанавливаем разные соединения, чтобы проверить, не изменилось ли что-то в сетях.

Мы померили такие простые характеристики, как round time, packet loss, bandwidth — осталось научиться их правильно сравнивать.
yul-nbvpnw5za6ixc80wlgumrse.jpeg

Как понять, что лучше: 2 Mbit/s интернета, 400 мс RTT и 5% packet Loss или 100 Kbit/s, 100 мс задержка и мизерный packet loss?

Точного ответа нет, оценка качества видеозвонка очень субъективна. Поэтому мы после окончания звонка просили пользователей оценить качество в звездочках и по результатам настраивали константы. Получилось, что, например, RTT меньше 300 мс — это уже неважно, дальше важен bitrate.

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

Вернемся к нашему плану статьи, мы все еще обсуждаем сеть.

Как выглядит установка соединения?
-o0bzzfolvd5rhfysec57ebndda.jpeg

Передаем в PeerConnection () STUN и TURN-серверы, устанавливается соединение. Алиса узнает свой IP, отправляет его в signaling; Боб узнает об IP Алисы. Алиса получает IP Боба. Они обмениваются пакетами, возможно, пробивают NAT, возможно, устанавливают TURN и общаются.
yzoarv064wp3s6zwwjpy0ec68hk.jpeg

В 5 шагах установки соединения, которые мы обсуждали ранее, мы разобрались с серверами, поняли, где их взять, и что ICE candidates — это внешние IP-адреса, которыми мы обмениваемся через signaling. Внутренние IP-адреса клиентов, если они находятся в зоне действия одного Wi-Fi, тоже можно пробовать пробить.

Перейдем к части видео.

Видео и аудио


WebRTC поддерживает некоторый набор кодеков видео и аудио, но можно добавить туда свой кодек. Базово поддерживается H.264 и VP8 для видео. VP8 — софтверный кодек, поэтому сильно расходует батарею. H.264 есть не на всех устройствах (обычно он нативный), поэтому приоритет по умолчанию стоит на VP8.

Внутри SDP (Session Description Protocol) существует codec negotiation: когда один клиент отправляет список своих кодеков, другой — своих с приоритетом, и они договариваются, какие кодеки будут использовать для общения. При желании можно поменять приоритет кодеков VP8 и H.264, и за счет этого можно сэкономить батарею на некоторых устройствах, где 264 нативный. Вот пример того, как это можно сделать. Мы так сделали, нам показалось, что пользователи не стали жаловаться на качество, но при этом заряд батареи расходуется сильно меньше.

Для аудио в WebRTC есть OPUS или G711, обычно у всех OPUS всегда работает, ничего с ним делать не надо.

Ниже замеры температуры после 10 минут использования.
pf-qpjvwi8myzm7_q1zlmpe2zbs.jpeg

Понятно, что мы тестировали разные устройства. Это пример айфона, и на нем приложение OK меньше всех тратит батарею, потому что температура устройства меньше всего.

Вторая штука, которую можно включить, если вы пользуетесь WebRTC — это автоматическое отключение видео при очень плохом коннекте.
xgkxab9ccijdbomfmja0rtrhfpg.jpeg

Если у вас меньше 40 Кбит/с, видео выключится. Надо просто выставить флажок при создании соединения, пороговое значение можно настроить через интерфейс. Также можно установить минимальный и максимальный стартовый текущий битрейт.
xw7dtsmnuz7pe0ujabtjapnnkxg.jpeg

Это очень полезная штука. Если при установке соединения вы заранее знаете, какой битрейт вы ожидаете, можно его передать, звонок начнется с него, и не понадобится адаптация битрейта. Плюс, если вы знаете, что у вас на канале часто бывают packet loss или просадки bandwidth, то максимальное значение тоже можно ограничить.

WhatsApp работает с очень мыльным видео, зато с маленькими задержками, так как агрессивно сверху поджимает битрейт.

Мы собрали статистику и с помощью MaxMind и нанесли на карту.
la1bkqswv6zqpbqdd7zrjarq0xu.jpeg

Это примерное стартовое качество, которое мы используем для звонков в разных регионах России.

Signaling


Эту часть вам, скорее всего, придется написать, если вы захотите сделать звонки. Тут существует много всяких подводных камней. Вспомним, как это выглядит.
ipiverlo5evjhsixis-obij86vs.jpeg

Есть приложение с signaling, которое коннектится и обменивается с SDP, и SDP внизу являются интерфейсом к WebRTC.

Так выглядит простой signaling:
y3zlihhnt8w32ukv-ms7sbelbgm.jpeg

Алиса звонит Бобу. Она подключается, например, по web-socket«ному соединению. Боб получает пуш на свой мобильный телефон или в браузер, или в какое-то открытое соединение, подключается по web-socket«у и после этого у него начинает звонить в кармане телефон. Боб снимает трубку, Алиса ему отправляет свои кодеки и другие особенности WebRTC, которые она поддерживает. Боб ей отвечает тем же самым, и после этого они обмениваются candidates, которых они увидели. Ура, звонок!

Все это выглядит довольно долго. Первое, пока вы установите web-socket«ное соединение, пока придет пуш и все остальное, у Боба в кармане не будет звонить телефон. Алиса будет все время ждать, думать, где же Боб, почему он не берет трубку. После подтверждения это все занимает секунды, и даже на хороших соединениях это может быть 3–5 секунд, а на плохих — все 10.

Надо с этим что-то делать! Вы мне скажете, что можно вообще все сделать очень просто.

kzq0ymgejzqqpmlyso5ri8mvz-k.jpeg

Если уже есть открытое подключение вашего приложения, можно сразу отправить пуш на установку соединения, подключиться к нужному серверу signaling и сразу начать звонить.

Потом еще одна оптимизация. Даже если телефон все еще звонит в кармане, и вы не сняли трубку, можно, на самом деле, поменяться информацией о поддерживаемых кодеках, внешними IP адресами, начать слать пустые видео пакеты, и вообще у вас все будет прогрето. Как только вы снимите трубку, все будет здорово.

Мы так сделали, и казалось, что все классно. Но нет.
hhm5t6dtihar96isxhtsm92pums.jpeg

Первая проблема — пользователи часто отменяют вызов. Они нажимают «Позвонить» и тут же делают отмену. Соответственно, пуш уходит на звонок, а пользователь пропадает (у него пропадает интернет или еще что-то). Тем временем у кого-то звонит телефон, он снимает трубку, и его там не ждут. Поэтому наша примитивная оптимизация с тем, чтобы как можно быстрее начать звонить, на самом деле не работает.
xywegygpnsrlns4osq7nn4i71f0.jpeg

При быстрой отмене вызова есть вторая вредная штука. Если вы генерируете ID вашего conversation на сервере, то вам нужно дождаться ответа. То есть вы создаете звонок, получаете ID, и только после этого вы можете делать все, что хотите: отправлять пакеты, обмениваться, в том числе, отменить вызов. Это очень плохая история, потому что получается, что пока у вас response не пришел, вы по факту не можете ничего отменить с клиента. Поэтому лучше всего генерировать какой-то ID на клиенте типа GUID и говорить, что вы начали звонок. Люди еще часто делают так: позвонил, отменил и сразу же позвонил еще раз. Чтобы это все не перепуталось, делайте GUID и отправляйте его.
1eheobd8oyf6z-je6j0puuj7psy.jpeg

Вроде бы все ничего, но есть еще одна проблема. Если у Боба два телефона, или еще где-то браузер остался открытым, то вся наша магическая схема с тем, чтобы обменяться пакетами, установить соединение, не работает, если он вдруг ответил с другого устройства.

Что же делать? Вернемся к нашей базовой простой медленной схеме signaling и оптимизируем ее, отправим пуш чуть-чуть пораньше. Пользователь начнет побыстрее коннектиться, но это сэкономит какие-то копейки.
apk3kx9qq3nsrzmrervqa548e-g.jpeg

Что же делать с самой долгой частью после того, как он снял трубку и начал обмен?
ffbjqvge9liirsi6spjc7ttlyby.jpeg

Можно поступить следующим образом. Понятное дело, что Алиса знает уже все свои кодеки и может их выслать на оба телефона Боба. Она может порезолвить все свои IP адреса и тоже их отправить на signaling, который их придержит у себя в очереди, но не будет отправлять никому из клиентов, чтобы они начали устанавливать с ней соединение раньше времени.

Что может Боб? Получив offer,

© Habrahabr.ru