Меня зовут Ильдар, я техлид в команде Центра развития финансовых технологий (ЦРФТ) Россельхозбанка. Сегодня расскажу о том, как мы внедрили функцию аудиозвонков в наш корпоративный мессенджер для сотрудников. Мы делаем приложение для сотрудников группы РСХБ, которое позволяет получить доступ к популярным корпоративным функциям: новостной ленте, кадровым сервисам, рабочему календарю, справочнику сотрудников и еще многим другим полезным вещам, которые делают рутинные процессы проще и быстрее. Так, например, мессенджер позволяет сотрудникам осуществлять быструю коммуникацию без использования внешних мессенджеров (WhatsApp и Telegram). Сразу отмечу, что это разработка на основе OpenSource‑решения, а именно сервера Synapse от Matirx. Ожидаемо, что теперь сотрудникам нужны были и звонки внутри приложения. На этом мы призадумались: можем ли мы реализовать эту функцию?..  ..конечно, можем!  У нас уже был наработки в этом направлении: наша команда flutter‑разработки уже проводила проверку гипотезы и нам надо было только всё соединить воедино. Но вот вопрос, с чего стоит начать?... ...конечно, со встречи!  С которой, к слову, мы ушли вот с такой картинкой:  Описание работы: Когда пользователь хочет совершить звонок - он переходит в чат с тем, с кем хочет связаться. В чате он нажимает на кнопку вызова абонента. После чего отправляется запрос в матрикс на получения данных о сервере-ретрансляторе (стрелка 1). Матрикс возвращает мобильному приложению (МП) исходящего абонента информацию для подключения к серверу-ретранслятору. МП исходящего абонента отправляет запрос в Matrix на отправку в чат с вызываемого абонента служебного сообщения (не      отображается пользователю) с информацией по подключению к серверу      ретранслятору (стрелка 2). МП звонящего абонента отправляет запрос в Backend приложения для отправки в МП вызываемого абонента silent push-уведомления с информацией, в каком чате находятся данные для подключения к звонку (стрелка      3). Backend приложения «Цифровой офис сотрудника» отправляет silent push-уведомление в МП вызываемого абонента (стрелка 4).  МП вызываемого абонента получает push-уведомление, забирает информацию из служебного сообщения чата и отображает пользователю звонок (стрелка 5).  Если МП вызываемого абонента отвечает на звонок, то переходим к пункту 10. В противном случае звонок отклоняется.  МП вызываемого и МП звонящего абонента обращаются к серверу-ретранслятору для получения информации об установлении соединения (стрелка 6). Матрикс отправляет сообщение в чат с вызываемым абонентом. На фронте мобильного приложения есть flutter-библиотека для работы с Matrix, где также перечислены методы для осуществления функции аудиозвонков, поэтому тут мы использовали существующий пакет, и немного посмотрели реализацию в других проектах 🙂 Для работы описанного алгоритма - приложение должно быть в памяти устройства, читать сообщения из чата, реагировать на пришедшее уведомление о желании одного сотрудника вызвать другого. В реальной жизни операционная система с радостью выгружает неиспользуемые приложения из памяти устройства. Поэтому необходимо было придумать способ доставки сообщения даже на устройство с выгруженным из памяти приложением.  Первым решением было использовать обычные push-уведомления, когда мы хотим сигнализировать пользователю, что ему пришло сообщение в чат.  Но при такой реализации есть существенный минус: это сообщение отображается ОС мобильного устройства. Существует вариант с нетривиальными манипуляциями над уведомлениями, чтобы скрыть стандартное уведомление и отобразить экран вызова звонка. И кроме того пользователь мог вообще отключить у приложения показ всех уведомлений.  После долгих экспериментов решили проверить идею с использованием silent push уведомлений. Они приходят на устройство, даже если приложение свёрнуто или закрыто, и запускают обработчик, в котором мы инициализируем мессенджер. Далее пользователь получает сообщение, и отображается экран с вызовом (со звуковым и вибро-сопровождением). Это решение мы не встречали на просторах Интернета, и, кажется, оно может помочь тем, кто реализует у себя функционал с похожими механизмами. Конечно, чтобы не раздражать пользователей, мы разработали на своей стороне возможность отключения уведомлений о звонках, но уже внутри нашего приложения, а не с помощью ОС. Ещё одна потребность, с которой мы столкнулись, заключалась в передаче информации о звонке сразу в push-уведомлении, чтобы не надо было лезть в чат за дополнительной информацией. Это позволило бы сразу отвечать на звонок. Но в этом случае у нас не оставалась информация о статусе звонка: состоялся, отклонён, завершён.  Конечно, информацию о звонке можно добавить и после самого звонка, но в этом случае, есть вероятность потери информации. Ещё одна причина, почему мы решили забирать данные из чата, а не через push: мы решили оставить маневр для подключения видеозвонков, а там уже необходимо передавать больше метаданных, чем помещается в push. Реализация звонка по умолчанию представляет собой обмен IP-адресами, по которым два абонента могут связаться с друг с другом. И, учитывая, что IP-адреса имеют серую адресацию и находятся за NAT-ом, такое взаимодействие возможно только внутри сети.  В Интернете любому абоненту так не позвонишь, поэтому для нахождения абонентов, которые находятся за NAT-ом, используются STUN-сервера. Они позволяют серверу внутри сети определить свой внешний адрес, способ трансляции адреса и порта во внешней сети. И уже этими данными на этапе получения информации о себе обмениваются абоненты.  Понятно, что для того, чтобы получать информацию об узлах в интернете необходимо самому там находится, т.е. иметь белый IP и не скрываться за NAT-ом. Для обхождения этого ограничения есть TURN-сервера (сервер-ретранслятор), который позволяет в любой топологии сети двум устройствам найти друг друга. Но при этом все пакеты проходят через TURN-сервер (в отличии от STUN, где сервер только сообщает абонентам адреса друг друга), и на него идёт большая нагрузка.  Мы остановились на этом варианте. В качестве сервера-ретранслятора мы использовали coturn-сервер, а рекомендации по его настройки взяли из документации сервера Synapse, как и рекомендации по настройке самого Synapse. Особенность работы TURN-сервера и сервера Synapse - это обмен секретами, где обе системы должны иметь одинаковый секрет (turn_shared_secret), по которому TURN-сервер определяет, что абоненты пришли от конкретного сервера Matrix. Т.к. взаимодействие абонентов идёт по UDP, и соединений может быть несколько, то необходимо открыть достаточно большое количество портов на TURN-сервере (мы открыли 10.000). Какие методы мы использовали при создании аудиозвонков: /_matrix/client/v3/voip/turnServer - получаем информацию о turn-сервере, информацию о котором указали в настройках Synapse. Этот метод вызывается у абонента, который начинает звонок и у вызываемого абонента, когда он получает информацию о входящем звонке и принимает вызов.  m.call.invite - сообщение, которое мы шлём вызываемому абоненту (event в чат (room) с вызываемым абонентом), передаёт id-звонка, информацию о чате, и о том, кто совершает вызов.  m.call.answer - ответное сообщение, которое также отправляется в чат (room) с тем абонентом с которым начинается аудиозвонок.   m.call.select_answer - выбор сообщения в котором содержится подтверждение о начале звонка.   m.call.hangup - завершить звонок, сообщение в чат (room) для прекращения звонка.  m.call.candidates - обмен ICE-кандидатов для установки соединения, которые отправляют друг другу оба вызывающих абонента. Отправляется при старте звонка, а также при изменении состояния абонентов, например, при переключении на другую сеть (с wi-fi на мобильную связь)  Конечно, мы не вызывали методы Матрикс напрямую, а использовали библиотеку, которую я упоминал выше: matrix_api_lite. Но если вы пишете не на flutter, то можно использовать сразу методы API Matrix. Для взаимодействия по WebRTC мы также использовали библиотеку для flutter. Для создания звонка сперва необходим объект соединения RTCPeerConnection, который осуществит связь между устройствами. Он содержит в себе контент (track), которым обмениваются пользователи (голос, видео) и ICE-кандидатов - адреса через которые можно передать этот контент, например, ip-адрес и порт, первоначально ICE-кандидатами мы обмениваемся через сервер Matrix. Поэтому в объекте RTCPeerConnection мы переопределяли следующие методы: onIceCandidate - срабатывает при появлении нового ICE-кандидата в RTCPeerConnection. Соответвенно другая сторона перед этим добавляет кандидатов      peerConnection?.addCandidate(candidate) onIceGatheringState -срабатывает, когда у ICE-кандидата меняется состояние, как только оно в статусе ready - добавляем в список кандидатов onIceConnectionState - срабатывает при изменении состояния соединения, как только статус connected - фиксируем флажок, что соединение установлено onTrack - срабатывает при добавлении нового контента в RTCPeerConnection Далее мы создаём описание предложения (RTCSessionDescription) к вызываемому абоненту с помощью метода createOffer объекта RTCPeerConnection. Когда мы соответственно хотим ответить мы также отправляем в другую сторону свое описание ответа, но только уже для удаленного абонента (remote): Далее для добавления нового контента на передачу вызываемому абоненту мы используем метод addTrack, что вызывает у вызываемого абонента обработчик onTrack, где stream - это контент полученный от устройства абонента: Это первые методы, которые необходимо применить для старта звонка.  Теперь необходимо добавить слушателей чата абонентов и как только в чате появится сообщение с приглашением - мы инициализируем соответствующий description, получаем ICE-кандидатов для соединения и добавляем свой поток в peerConnection и забираем поток из RemoteStream. Чтобы узнать побольше и посмотреть детальнее примеры по работе с библиотекой для flutter, советую почитать тут, тут и ещё вот тут. Конечно, эта статья даёт представление только о базовых принципах старта звонка. Мы не описывали весь тот объём кода, который покрывает весь функционал совершения звонков, задачи по инициализации звонка, работу с устройством, состоянием. Также не описаны моменты, связанные с отключением звука, переключением на гарнитуру и прочие нюансы, которые необходимо учесть при организации звонков. Не стоит думать, что подключаешь две библиотеки и код начинает работать.  Цель статьи - дать общее представление о том, как это работает и что это вообще можно реализовать. К слову сказать, можно на этих же принципах стартовать видеозвонки, но это наши планы на будущее. Коллекция Postman для запросов к Матрикс-серверам Видео о том как организована ip-телефония Рассказ про звонки от ребят из VK