Привет! Я Иван Шафран, уже 4 года работаю с WebRTC, ВКонтакте руковожу командой звонков на Android. В этой статье на примере VK Звонков расскажу, что можно сделать, чтобы улучшить качество сервисов для аудио- и видеосвязи. Обсудим достоинства и недостатки WebRTC. Расскажу, как работать с аудио, видео и режимом демонстрации экрана и какие есть варианты сбора статистики.
Статья написана на основе доклада «VK Звонки: Поднимаем планку качества WebRTC-звонков» на Mobius Spring 2024.
Какие функции есть в VK Звонках
VK Звонками пользуются около 20 млн человек в месяц. У нас нет ограничений по числу участников и продолжительности встречи. В сервисе реализованы совместный просмотр видео, демонстрация экрана в 4К, анимированные аватары, умное шумоподавление и AR-технология замены фона. Есть и стандартный набор инструментов: трансляции, планирование, администрирование и запись звонков.
Но все эти возможности не имеют смысла, если в звонке кого-то не слышно, видео тормозит, а демонстрация экрана размыта. Рассмотрим несколько приёмов, которые помогут этого избежать.
WebRTC
Наши звонки работают на технологии WebRTC. С 2011 года она используется в проекте Chromium от Google. Позднее появилась в Яндекс Браузере, Google Chrome, Microsoft Edge, Safari и Mozilla.
SDK WebRTC доступен на iOS, Android и десктопных операционных системах. При желании инструмент можно встроить даже в холодильник или робот-пылесос.
WebRTC — технология обмена сообщениями в режиме реального времени. Это фреймворк с открытым исходным кодом, который предоставляет набор базовых инструментов для самостоятельной сборки работающего сервиса. Однако, чтобы добиться высокого качества, продукт придётся дорабатывать.
Качество и оптимизация
Рассмотрим несколько практических приёмов по оптимизации аудио и видео, а также режима демонстрации экрана. Начнём с простых и перейдём к более сложным советам, чтобы вы смогли найти что-то полезное для любого проекта.
Аудио
Главное, без чего звонок не состоится. Пользователи готовы потерпеть временную потерю видео, но не звука.
Подсветка говорящего
Посмотрите на изображение ниже. Как понять, что в звонке сейчас кто-то говорит? В этом помогает подсветка говорящего на экране.
Стандартный приём — обводка говорящих: например, зелёным цветом, как на изображении ниже.
У WebRTC есть набор технических характеристик соединения, что позволяет узнать степень громкости каждого аудиопотока и идентифицировать говорящего. Для этого используется класс RTCAudioSourceStats и его поле audioLevel.
На графике видно, что громкость аудиопотока — это не бинарный признак, и чаще всего её уровень оказывается выше нуля из-за фонового шума.
Исправить эту проблему можно, считывая средний уровень громкости на протяжении звонка. Например, используя «скользящее среднее», чтобы подстроиться под изменяющиеся условия. На практике это работает хорошо, но вы также можете подключить детектор голоса (Voice Activity Detector) из поставки WebRTC. По умолчанию он работает в пайплайне на аудиовыходе, но его также можно использовать для входа. В групповых звонках такую информацию эффективнее будет подсчитывать на бэкенде, чтобы снизить нагрузку на клиента.
Следующая UX-задача — дать понять пользователю, что он говорит с выключенным микрофоном. Сделать это можно при помощи подхода, описанного ранее, и это сработает, так как микрофон не отключает приложение для звонков, а лишь останавливает передачу данных. Это повышает скорость включения микрофона, иначе часть речи будет съедена задержкой при инициализации.
Разрешения для звонков на Android
Для работы звонков могут понадобиться следующие разрешения:
• доступ к аудио;
• доступ к видео;
• демонстрация экрана;
• доступ к памяти устройства;
• доступ к подключённым Bluetooth-устройствам.
Все разрешения, кроме аудио, можно раздать по мере необходимости. Так пользователю не придётся отвечать на множество запросов при старте звонка. Если же разрешить доступ к аудио при включении микрофона по кнопке, мы начнём терять соединение на другой стороне.
Всё из-за того, что мы используем аудиоканал, чтобы определить проблемы с сетью. Если аудиоданные не идут, значит, что-то не так.
Чтобы исправить проблему, мы:
• сделали форк WebRTC;
• нашли класс org.webrtc.audio.WebRtcAudioRecord;
•стали отправлять тишину до получения разрешения на использование микрофона.
Аудиокодеки
Аудиокодеки для WebRTC просты в использовании. Есть бесплатный открытый кодек Opus, который подходит для работы с разными битрейтами.
В этом кодеке есть функция исправления будущих потерь пакетов — FEC, или Forward Error Correction. Принцип её работы в том, что к аудиопакетам добавляются фрагменты предыдущих пакетов. Если что-то потеряется, мы сможем восстановить аудио.
Это полезный механизм, однако стандартный FEC имеет ряд недостатков:
• включается, когда потери уже пошли;
• имеет ограничения по доле в пакетах;
• сам учитывается как часть битрейта и затрудняет настройку.
В качестве альтернативы FEC можно попробовать механизм RED — Redundant Coding, то есть излишнее кодирование.
Принцип его работы схож с FEC. RED улучшает качество звука, добавляя целые предыдущие пакеты в аудиоканал, хотя это более низкое качество по сравнению с видео и возможностями современных сетей. Тем не менее RED отлично проявляет себя в сетях с большой долей потерь.
На графике выше приведены данные по количеству синтезированных аудиопакетов. WebRTC формирует пакеты, если произошла потеря, компенсируя заметные «разрывы» речи.
При A/B-тестировании мы также заметили существенное снижение качества синтезированных аудиопакетов. Другие метрики изменились не сильно.
Видео
Видео — второй по важности канал связи между пользователями.
Для оптимизации передачи данных видеокодеки используют механизм «различия» между кадрами, что нагружает устройство. По этой причине мобильные устройства часто нагреваются. Рассмотрим несколько вариантов решения проблемы.
Базовые настройки
Кодеков для видео больше, чем для аудио. Самым распространённым и надёжным считается устаревший H.264. Он поддерживается на большинстве устройств.
Ещё одна базовая настройка, на которую стоит обратить внимание, — разрешение. Десктопам и мобильным устройствам достаточно разрешений 720p, HD или ниже. Если вы снизите разрешение, это позволит оптимизировать весь пайплайн — от захвата кадров камерой до отправки их через сеть. Не стоит пренебрегать этой настройкой, если ваш сценарий использования сервиса это позволяет.
Эксперименты с кодеками
При достаточной пропускной способности канала разница между кодеками не сильно заметна, так как мы не отображаем видео в высоком разрешении. В условиях плохой пропускной способности выигрывают кодеки, которые лучше сжимают видео и при этом незначительно теряют в качестве.
У более новых популярных кодеков лучше качество передачи. Они также лучше сжимают в плохих сетях. Однако у их новизны есть минус: на большинстве Android-устройств есть только их программная реализация, которая выполняется на ЦПУ. Аппаратная поддержка — кодирование и декодирование на выделенном чипе — есть не у всех устройств.
Мы можем использовать новые аппаратные кодеки, если они доступны. Если нет, то используем новые кодеки в программном виде при плохой пропускной способности сети. В последнем варианте картинка будет лучше, но батарея станет садиться быстрее. Нагрузку на центральный процессор можно дополнительно облегчить, если уменьшить частоту кадров и снизить разрешение. Это будет выглядеть лучше, чем «рассыпанный» кадр от H.264 при плохой сети.
Проблемы больших звонков
При звонках один на один почти все функции без ошибок работают «из коробки». Но если на экране появляется более шести видео, могут начаться проблемы с аппаратными кодеками. Они реализованы на чипе и имеют ограничения по одновременному количеству декодеров. Таким образом, придётся обратиться к программному декодированию после превышения определённого числа видео в звонке.
Если же мы пойдём ещё дальше, то возникнут проблемы со стороны OpenGL. Закончатся так называемые контексты, которые отвечают за состояние движка. Они нужны для работы на потоках, где выполняются вычисления OpenGL. Как правило, на устройствах доступно около 30 контекстов. На каждое видео по умолчанию выделяется два OpenGL-контекста — на декодирование и на отрисовку. Мы сможем увидеть около 10 видео вместо возможных 15 из-за запоздания с «релизом» контекстов и их использованием при кодировании.
Для решения проблемы с контекстами можно переписать рендеринг — так, чтобы он выполнялся на одном потоке для всех видео.
Для больших звонков на 100 и более участников есть широкий простор для оптимизаций. Решения для VK Звонков мы разбирали на Mobius. Посмотреть видео можно по ссылке.
Демонстрация экрана
Демонстрация экрана — одна из самых востребованных дополнительных функций звонка. В зависимости от сценария на первом месте для вас может быть плавность картинки и игрового контента либо чёткость текста и графики для презентаций.
У WebRTC на Android уже есть класс для демонстрации экрана ScreenCapturerAndroid. Однако он работает посредственно для обоих сценариев. Сложнее всего с ним добиться чёткого текста. Также отсутствует возможность передать аудиодорожку с телефона. Обсудим варианты решения этих проблем.
Передача аудио
По умолчанию WebRTC сама выделяет медиаканалы для микрофона, камеры и демонстрации экрана. При этом канала для аудио с телефона нет. Это может быть связано с тем, что API для доступа к такому аудио появился лишь в Android 10, а WebRTC под Android реализовали задолго до этого.
Как и с демонстрацией экрана, для получения аудио нужно запросить разрешение у пользователя. Используйте API по ссылке.
После этого нам нужно поработать с классом AudioRecord, который предоставляет доступ к буферам аудиопакетов. Необходимо настроить пайплайн работы с этим классом — запросить данные на отдельном потоке и передать их в медиатрек.
Дальше мы можем работать по двум сценариям. Первый — замиксовать аудио с телефона в медиапоток микрофона. Для этого решения не нужны настройки других клиентов. Из минусов — шумодав, который может отсекать музыку как шум. При этом необходимо продолжать отправлять данные при выключенном микрофоне.
Второй способ — выделить отдельный медиаканал и поддерживать его приём на всех клиентах. Для групповых звонков VK Видео мы миксуем всё аудио на сервере и получаем один аудиопоток.
Тест на зрение
При использовании стандартной демонстрации экрана зачастую мы будем видеть нечёткую картинку с текстом. При смене слайдов потребуется время на стабилизацию, а при низкой пропускной способности сети мы и вовсе не сможем ничего прочитать. Разберёмся, что можно сделать в этом случае.
Подсказки для WebRTC
Во фреймворке WebRTC на случай проблем с чёткостью картинки предусмотрены подсказки. С помощью ContentHint можно указать, что мы ожидаем увидеть текст, используя команду TEXT. С помощью DegradationPreference можно указать на необходимость сохранять разрешение MAINTAIN_RESOLUTION и возможность жертвовать частотой обновления кадров.
Стоит учитывать: подсказки не дают гарантии, что на демонстрации презентации с текстом будет необходимая чёткость экрана.
Своя версия демонстрации экрана
Если результат демонстрации экрана нас не устраивает, можно написать свою версию. Самое сложное возьмут на себя готовые библиотеки.
В решении будет две подсистемы. Одна станет отправлять кадры, вторая — получать и отображать. Полный путь данных выглядит так:
• получаем кадр с камеры (работает в рамках старой логики);
• кодируем кадр кодеком;
• нарезаем кодированный кадр на пакеты для отправки;
• отправляем пакеты;
• другая сторона принимает пакеты;
• из этих пакетов собирается кодированный кадр;
• используется кодек для декодирования;
• кадр передаётся в пайплайн обработки;
• данные отображаются на экране.
Для кодирования и декодирования кадров будем использовать класс MediaCodec, запросив у него кодек Vp9. У этого кодека есть два вида API: синхронный и асинхронный. Можно использовать любой подход, но в случае асинхронного проще следить за тем, успевает ли кодек справляться с работой, — он сам выдаёт входные и выходные буферы по готовности.
Для передачи пакетов будем использовать WebRTC DataChannel, так как медиатреки недоступны в самописном варианте передачи. Дата-канал можно представить как протокол WebSocket, но с возможностью передавать бинарные данные и устанавливать очередь отправки.
За кодирование и декодирование отвечает кодек. За передачу данных — дата-канал. Перед разработчиками стоит задача нарезать данные на пакеты и собрать их на месте. Нужно самостоятельно контролировать заторы, если мы не справляемся из-за большой нагрузки или низкой пропускной способности сети. Это одновременно и сложность, и преимущество, так как разрешение не придётся снижать до пиксельного текста, как это делает стандартный медиатрек. Мы будем в первую очередь уменьшать частоту обновления кадров.
В итоге получим чёткий текст на видео.
Статистика
Рекомендую замерять все эксперименты с аудио, видео или демонстрацией экрана. Без замеров мы не узнаем, улучшилось качество звонков или стало хуже.
Локальная отладка
Для локальной отладки можно воспользоваться логами WebRTC. Приятная опция — наличие встроенной страницы с отладкой в браузерах Сhromium. Чтобы увидеть эту информацию в Google Chrome, откройте страницу по адресу chrome://webrtc-internals. Параллельно запустите VK Звонки. Для теста создайте пустой звонок, а затем откройте ссылку в режиме инкогнито. Тогда в звонке будет два участника, установится соединение и на странице отладки появятся графики по параметрам соединения.
Замеры на большой аудитории
Для снятия метрик медиатреков в WebRTC доступны методы getStats.
Один из методов возвращает набор готовых к использованию метрик, но может отличаться в зависимости от браузера. Для Android это не будет проблемой. Однако стоит учитывать, что метод получил отметку «устаревший» и будет удалён в новых версиях. Нам стоит его избегать.
Второй метод getStats стандартизирован, хотя на практике не всё реализовано в точном соответствии с документацией. Актуальное описание характеристик можно найти здесь. В отличие от старого метода здесь есть «сырые» данные, которые нужно самим комбинировать в осмысленные метрики. Также нужно быть аккуратными с типами данных. Они приходят в виде строк и могут быть как Int, Long, Float, так и BigInteger в зависимости от отправленных пакетов.
Выводы
WebRTC предоставляет базовую функциональность, которая работает «из коробки» на хорошем уровне. Мы можем улучшить работу отдельных компонентов, используя простые настройки. Если важны продвинутые функции, придётся писать собственные решения.
Комментарии (5)
Senyaak
09.10.2024 15:17Пока с друзьями искали рабочий способ разблокирования дискорда - сидели через вк.. К сожалению по качеству звука у дискорд нет аналогов :(
ALapinskas
09.10.2024 15:17WebRTC - сейчас везде, скайп, ms teams, zoom и дискорд тут не исключение, если у вас p2p звонок, то качество будет зависеть от вашего соединения, а если групповой - от расположения серверов программы, к примеру, если сервер в Штатах, а вы в России, данным придется проделать длинный путь и качество пострадает.
Senyaak
09.10.2024 15:17Да не - там самые лучшие фильтры звуковые. Минимальное количество искажений и посторонних шумов при ужасных микрофонах... К WebRTC притензий нет :)
Diversus
В связи с блокировкой Discord сегодня как раз тестировали ваш сервис VK Звонки на замену. На самом деле по качеству аудио/видео вообще вопросов нет. А вот, чего не хватает, так это постоянных комнат для общения. Чтобы каждый раз не создавать комнату/беседу, а создать один раз и в любой момент можно было в нее войти. При этом, запланированные и повторяющиеся встречи создать можно, а постоянные не удаляемые нельзя. Что странно.
Есть вообще такое в планах?
IvanShafran Автор
Привет!
Рад, что на практике попробовали наши звонки.
Наши групповые звонки умеют жить бесконечно. Более того, это поведение по умолчанию. Достаточно создателю и админам при выходе из звонка не нажимать "завершить звонок для всех", а просто "выйти из звонка". В таком случае ссылка на звонок будет жить вечно, пока кто-то не закроет его для всех.
Опишу чуть подробнее сценарии:
Создание звонка из чата. После создания звонка в чате плашка звонка будет прикреплена к верхушке там же, где запиненные сообщения. Также у чата сверху будет трубка звонка, по которой можно будет сразу подключиться
Создание звонка из списка чатов. Если создавать пустой звонок из списка чатов, то по умолчанию он имеет скрытый чат, пока в нём нет сообщений. Это сделано, чтобы не спамить пустыми чатами после временных созвонов. Из-за этого такой звонок становится чуть сложнее потом найти, поэтому можно сохранить ссылку на такой звонок где-то. Либо зайти в чат и написать сообщение, тогда чат станет доступен из списка чатов.
Чтобы попасть в такой пустой звонок обратно есть несколько вариантов:
Найти чат звонка и подключиться по запиненной ссылке или трубке в шапке чата
Зайти в раздел звонков. В истории звонков найти нужный и подключиться
По сохранной ссылке заранее ссылке
Ещё несколько лайфхаков:
Важный чат со звонком можно закрепить в списке чатов наверху
Важный чат со звонком можно добавить в отдельную папку, тогда его можно будет быстрее найти
Если вдруг ссылка на звонок утекла случайно, то её можно обновить настройках звонка. Звонок останется прежним, но ссылка будет новая