Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

История или legacy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

После установки прямого соединения клиенты начинают обмениваться медиаданными друг с другом. 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()
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. Наконец получить удаленный видеопоток.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 — самый простой, который просто маппит внешний порт на внутренний, то Боб сразу же сможет отправить Алисе пакет, установить соединение, и они поговорят.

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

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

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

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

Самая вредная штука — это Symmetric NAT.

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

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

Все варианты собраны в одной таблице ниже.

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

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

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

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

STUN позволяет определить тип NAT.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Видео и аудио

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

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

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

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

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

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

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

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

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

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

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

Signaling

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

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

Так выглядит простой signaling:

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

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

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

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

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

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

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

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

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

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

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

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

Получив offer, он может посмотреть, какие там были кодеки, сам сгенерировать свои, написать, что у него есть, и тоже отправить. Что может Боб? Candidates своих тоже сгенерируют оба устройства и отправят на signaling. Но у Боба два телефона, и там разные codec negotiation, поэтому signaling это все придержит на себе и будет держать в очереди до тех пор, пока не узнает, на каком устройстве снимут трубку.

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

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

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

Какие еще проблемы вас подстерегают?

Классно было бы, если бы они не пытались конкурировать, — на уровне signaling добавить команду, которая скажет, что если кто-то пришел вторым, то надо переключиться в режим, когда ты просто принимаешь вызов, и сразу же снять трубку.
Бывает такая штука как встречный звонок: один звонит другому, другой звонит в ответ.

То есть у вас должна быть на клиенте очередь отправки. Бывает так, что сеть пропадает, сообщения теряются, поэтому все нужно делать через очереди. У сервера также есть очередь на отправку, и тоже с подтверждением. Сообщения, которые вы отправляете с клиента, должны удаляться из очереди исключительно после того, как сервер подтвердил, что он их обработал.

Так это все реализовано у нас внутри с учетом того, что у нас сервис 24/7, мы хотим иметь возможность терять дата-центры, перекладывать и обновлять версию нашего ПО.

Ссылка на слайде на видео и ссылка на текстовую версию

На Zookeeper мы делаем Leader Election, который определяет тот сервер signaling, который сейчас управляет этим conversation. Клиенты коннектятся по web-socket на какой-то load balancer, он отправляет на signaling-серверы в разных дата-центрах, разные клиенты могут прийти на разные серверы. Если сервер не является лидером этого conversation, он просто все сообщения прокидывает другому.

На самом деле не важно, что использовать. Дальше мы используем некоторое распределенное хранилище, у нас это NewSQL поверх Cassandra. Можно куда угодно сохранять состояние всех очередей, которые есть на signaling, для того чтобы, если пропадет сервер signaling, выключится электричество или еще что-то случится, сработал Leader Election на Zookeeper, поднялся другой сервер, который станет лидером, из базы восстановит все очереди сообщений и начнет отправлять.

Алгоритм выглядит так:

  • Клиент отправляет какое-то сообщение, допустим, свой внешний IP на signaling
  • Signaling принимает, записывает в базу.
  • После того, как понимает, что все пришло, отвечает, что он это сообщение принял.
  • Клиент удаляет это сообщение из своей очереди.

Все пакеты снабжаются уникальными номерами, чтобы не путаться.

С точки зрения базы данных у нас используется надстройка над Cassandra, которая позволяет делать на ней транзакции (видео как раз про это).

Итак, вы узнали:

  • что такое iceServers и как их передать;
  • что есть Session Description Protocol;
  • что его необходимо сгенерировать и отправить на другую сторону;
  • что его нужно принять из signaling и передать в WebRTC на другой стороне, обменяться внешними IP адресами;
  • и начать отправлять видео!

Мы получили:

  • звонки с delay ниже среднего по рынку;
  • мы не сильно нагрели телефоны;
  • время ответа в нашем приложении на топовом уровне.

Здорово!

Security. Man in the middle attack for WebRTC

Поговорим про man in the middle attack for WebRTC. На самом деле, WebRTC очень трудный протокол в плане того, что он базируется на RTP, который еще 1996 года, а SDP пришел в 1998 из SIP.

Внизу огромный список — это куча RFC и прочих расширений к RTP, которые делают из RTP WebRTC.

Соответственно, когда вы обмениваетесь SDP, вам важно знать, какой набор расширений поддерживают клиенты. Первые в списке два интересных RFC — один из них добавляет в пакеты audio level, а другой говорит, что небезопасно передавать открыто audio level в пакетах, и их шифрует. Там есть даже несколько алгоритмов congestion, несколько алгоритмов восстановления потерянных пакетов и всего-всего.

В 2011 году вышел первый драфтовый релиз, в 2013 этот протокол поддержал Firefox, потом он стал собираться на iOS/Android, в 2014 Opera. История WebRTC была сложная. В общем, много-много лет он развивался, но до сих пор не решает одну интересную задачу.

Все здорово, но если это оказался не наш signaling, то в принципе у человека посередине есть возможность «похэншейкаться» и с Алисой, и с Бобом, запроксировать весь трафик и подслушать то, что там происходит.
Когда Алиса и Боб подключаются к signaling, после этого они используют этот канал, устанавливают DTLS Handshake и безопасное соединение.

Есть еще интересное решение — ZRTP, его использует, например, Telegram. Если у вас сервис с высоким доверием, то, конечно, обязательно нужно использовать HTTPS, WSS и т.д.

На самом деле если вы другу скажете, какие у вас эмоджи, он проверит, что у него точно такие, то у вас абсолютно гарантированно безопасное p2p соединение. Многие видели в Telegram эмоджи, когда устанавливается соединение, но мало кто ими пользуется.

Как это работает?

Алиса генерирует некоторые числа, отправляет Бобу их все, кроме одного. Внутри всех этих протоколов изначально используется обычный алгоритм Диффи — Хеллмана. В результате этого обмена у Алисы и у Боба получается некоторое большое число K, про которое человек посередине, который прослушал весь их канал, ничего не знает и не может никак догадаться. Боб тоже генерирует у себя случайное число и отправляет Алисе.

Отследить наличие этого человека посередине нет возможности. Когда между Алисой и Бобом появляется Дейв, они с ним обмениваются этими же ключами, и у них получаются K1 и K2 соответственно. Эти ключи K1 и K2 у Дейва точно будут разные, так как Алиса и Боб генерируют свои ключи случайно. Тогда применяется такой трюк. Так как по голосу можно друг друга идентифицировать, и если эти картинки разные, то кто-то между вами есть и, возможно, он вас слушает. Мы просто берем некоторый хэш от K1 и K2 и отображаем его в эмоджи: в яблоке, в груше — во всем, чем угодно — и люди голосом просто называют те картинки, которые видят.

Результаты

  • Мы «намайнили» NAT type и пробили symmetric NAT.
  • Статистически оценили, что лучше: р2р или relay, качество, CDN; и повысили качество в звездочках с точки зрения пользователей.
  • Поменяли приоритеты кодекам, сэкономили немножко батарею.
  • Минимизировали задержку на signaling.

Не все сразу получилось! На графике видно, что сначала были старые звонки на RTMFP, потом, когда мы перешли на WebRTC, есть небольшой провал, а потом пик вверх. В итоге сейчас у нас количество состоявшихся звонков выросло в 4 раза.

Простая инструкция

Если вам все это не надо, есть очень простая инструкция:
Всё будет звонить, и звонить довольно неплохо.

Послушать ответы на вопросы после доклада

Уже через неделю Александр Тоболь выступит на HighLoad++ с докладом об архитектуре масштабируемой отказоустойчивой платформы 4К-видеостриминга.

Хотя и так понятно, в 19 потоках (10 для докладов и 9 для митапов и мастер-классов) найдется всё, что хоть как-то связано с высокими нагрузками. Какие еще темы будут обсуждаться, смотрите в расписании. Обязательно приходите, если и у ваших сервисов не сотни, но миллионы пользователей.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть