Что нужно от картинки в видеозвонке? Базово — чтобы не пикселила, не замирала и не вешала весь звонок. Это основы real-time видео, и добиться этого не так трудно.
Всё самое интересное начинается, когда вы хотите, чтобы в одном звонке могли участвовать сколько угодно человек. И при этом все могли включать видео, а не просто смотреть; разрешение скриншеринга было бы 4К; шеринг оставался суперчётким при любом интернете и т. д. И чтобы звонки работали на любых платформах и устройствах на мобильном нестабильном интернете.
Как мы добиваемся всего этого в звонках ВКонтакте, какие хаки в настройках используем, как экономим трафик и CPU, как боремся за latency и где нам пришлось идти в обход WebRTC, читайте под катом.
Disclaimer 1. Выводы и заключения по конкурентам сделаны исключительно из общедоступной информации, общих наблюдений и измерений, поэтому могут быть ошибочными.
Disclaimer 2. Огромное влияние на качество передачи любых real-time данных оказывает сеть, но сетевой слой мы обсудим в отдельной статье. Здесь сосредоточимся только на том, что касается передачи качественного видео.
Приступая к любой задаче, нужно составить требования, исходя из которых вы будете строить своё техническое решение. Мы провели исследование, в котором выяснили, чего хотят пользователи, — и ориентировались на эти результаты. Люди называли важные им свойства звонков, и в топ вышли качество, наличие видео, поддержка десктопа и мобильных платформ. Плюс, конечно, отсутствие ограничений по длительности и возможность пользоваться сервисом бесплатно.
Если хотелки пользователей перевести в свойства решения, то получится следующее:
качество видеозвонков складывается из качества аудио (о котором говорим в этой статье), качества видео (разрешения и FPS), низких задержек и стабильной работы;
работа на десктопе и мобильных устройствах предполагает две вещи. Первое — что нужно поддержать WebRTC как стандарт для браузеров. И второе — следует быть очень бережливыми к клиентским ресурсам, чтобы средние смартфоны справлялись с нагрузкой и им хватало пропускной способности мобильной сети;
отсутствие платы и ограничений обязывает экономить и серверные ресурсы.
Вспомним теорию
Теперь, зная вводные, пробежимся по основным понятиям, чтобы лучше представлять проблематику.
Видео, которые мы смотрим в телефоне или ноутбуке, состоят из кадров. Кадры бывают трёх типов:
I-кадры — опорные, или keyframe-кадры, содержат картинку целиком и кодируются независимо от других кадров;
P-кадры кодируют только отличия от предыдущих кадров;
B-кадры кодируют diff от предыдущего и последующего кадров.
В последовательности кадров, из которых складывается видео, типы чередуются и опорные кадры встречаются не слишком часто, а без них хорошая картинка не получится. Для видеозвонков это означает следующее: новый подключившийся участник не может начать проигрывать видео, пока не получит опорный кадр.
Если новый участник просто будет ждать, пока наступит очередь опорника, то получится дополнительная задержка на подключение — она может длиться секунды. Чтобы не ждать и не мучиться с задержкой, можно запросить опорник с помощью full intra request (FIR) по требованию. Но это лишний трафик, потому что опорный кадр кодируется с меньшей степенью сжатия, чем diff.
Видеокодеки
Как фреймы с камеры будут закодированы, то есть какой будет последовательность опорных и остальных типов кадров, как они будут сжаты, сколько их будет в одной секунде и так далее — всё это определяет видеокодек. По сути это алгоритм, который может быть, например, довольно тупым, но очень быстрым, или суперэффективным по битрейту, но тяжёлым.
Кодирование видео и выбор кодека — это всегда компромисс.
Каждый раз в этом вопросе у нас две чаши весов: с одной стороны раскалённый телефон, который по узкому сетевому каналу передаёт отлично сжатое видео, с другой — сэкономленная батарейка и видео с более высоким битрейтом, для отправки которого нужна большая пропускная способность.
Разные кодеки лучше подходят для определённых случаев. Например, H.264 аппаратно ускоренный и поэтому экономит ресурсы, но сильно уступает VP9 по качеству сжатия на низких битрейтах. Ниже условные графики того, как растёт нагрузка на устройство и какое получается качество в зависимости от битрейта — для H.264 слева и для VP9 справа.
Поэтому у нас есть механизм адаптации и мы переключаемся между этими двумя кодеками (или откатываемся на что-то ещё, если они недоступны). Если битрейт выше порога, то используем H.264, если ниже — VP9.
В результате не превращаем телефон в сковородку на высоких битрейтах и вытягиваем качество на низких.
Второй компромисс, на который нужно пойти при выборе кодека, лежит между его современностью и распространённостью поддержки. Например, AV1 — очень классный кодек. По данным Facebook, AV1 позволяет уменьшить битрейт при одинаковом качестве на 50% относительно H.264 и на 30% по сравнению с VP9. Но он пока требует очень много вычислительных ресурсов и не поддерживается аппаратно — а значит, всё равно не будет доступен большинству наших пользователей. Даже если поддержать его, решения для качественной работы с другими кодеками будут необходимы.
Видео в групповом звонке
Вопросы экономии ресурсов и трафика становятся гораздо острее, когда мы переходим от звонков один на один к групповым.
В звонке тет-а-тет нужно закодировать и отдать, получить и декодировать для воспроизведения на клиенте всего одно видео. На это хватит сети в 1–2 Мбит/с, и можно использовать аппаратно ускоренное кодирование. В групповом звонке мы хотим видеть несколько видео — то есть, скорее всего, нужно больше энкодеров, а большинство устройств поддерживает аппаратно ускоренное кодирование только одного потока в один момент времени. Насколько увеличится трафик и будет ли он расти пропорционально числу участников, зависит от топологии.
Для звонков один на один мы всегда, когда это возможно, устанавливаем peer-to-peer соединение, которое выгоднее с точки зрения latency. Для групповых звонков теоретически есть три варианта: установить p2p-соединение каждого с каждым (Mesh), смиксовать на сервере все потоки в один (MCU) или использовать сервер по сути как ретранслятор пакетов (SFU).
Топология MCU — Multipoint Conferencing Unit — подходит для аудио (как мы его реализовали, смотрите в статье об аудиопайплайне звонков ВКонтакте), но не для видео. Ни один популярный сервис не пытается на сервере склеивать сетку из видео разных участников и посылать на клиент один поток. Интуитивно понятно, что это будет очень дорого, потому что потоки должны быть разными для всех участников (без собственного видео — и здесь это не сделать простым вычитанием, как в аудио). Например, при переключении режимов просмотра с окна с главным спикером на сетку надо перезапускать весь микс.
Поэтому для видео обычно используют SFU — прокидывают видео через сервер всем клиентам. Правда, реализации могут сильно отличаться.
В самом простом варианте сервер действительно только перекидывает пакеты, так что дорогое микширование входящих потоков ложится на клиента. Кроме этого, есть ещё два момента, от которых сильно страдает качество картинки:
Шторм опорных кадров (FIR storm). Когда новый подключившийся клиент не хочет ждать опорный кадр, он запрашивает его вне очереди. Из-за этого, во-первых, возникает лишняя нагрузка на отправителя для генерации опорника, во-вторых, его получают все участники звонка и тратят лишний трафик. То же самое с любыми перебоями сети — неполадки у одного получателя влияют на всех в звонке.
Исходящий поток нужно адаптировать, чтобы участник с самой слабой сетью мог его получить. Снова из-за одного страдают все: видят собеседников не чётко, а в крупный квадратик.
Второй вариант для forwarding — simulcast.
Идея заключается в том, чтобы отправляющий клиент на своей стороне кодировал видео сразу в нескольких разрешениях. Сервер при этом всё так же перекидывает видео, но уже может выбрать из нескольких вариантов: например, отправить клиенту с мощным девайсом и стабильной сетью хорошее качество, а клиенту со слабеньким 3G самое маленькое разрешение.
Simulcast частично решает проблему понижения качества для всех и частично шторма опорников, но требует очень много клиентских ресурсов: как CPU- и GPU-вычислений (чтобы батарейка улетала за час), так и гораздо большего сетевого канала на отдачу.
Более продвинутая технология SVC (Scalable Video Coding) делает примерно то же, но внутри кодека. В один закодированный поток укладывается несколько SVC-слоёв с разными качествами. Такой подход потребляет ресурсы немного скромнее и требует чуть меньшей ширины исходящего канала, чем simulcast, но не решает проблему шторма опорных кадров (поток-то в результате один). И главный недостаток SVC — нет поддержки в браузерах. Для нас это сразу делает его непригодным.
В итоге мы, как всегда, вынуждены идти своим путём. Мы знаем, что для наших пользователей важнее всего качество, поэтому понижение битрейта по любому поводу для нас не вариант. Также на примере работы со звуком мы выяснили, что любая дополнительная нагрузка на телефон не только делает из него сковородку, но и добавляет случайные задержки и вообще-то портит качество. Значит, нужно серверное транскодирование.
При серверном транскодировании клиент отправляет на сервер видео в лучшем качестве, которое может закодировать и передать по доступной сети. На сервере это исходное видео транскодируется в несколько качеств поменьше и раздаётся остальным участникам, кому какое подходит. Нет проблем с тем, чтобы подстроиться под сеть каждого участника группового видеозвонка: опорные кадры можно генерировать на сервере, когда надо, и это не влияет ни на отправляющего клиента, ни на остальных собеседников. Одна проблема — это дорого. Но с этим тоже можно бороться.
Качество по запросу
Вообще говоря, в групповом звонке с серверной топологией нетрудно узнать, как в интерфейсе показывается видео каждого участника. У нас есть два основных варианта раскладки:
один говорящий в большом окне по центру и рядом несколько человек в совсем маленьких окошках;
сетка с видео одинакового среднего размера.
Очевидно, что видео в хорошем качестве нужно только для главного спикерского окошка. А если вы пока молчите и ваше видео показывается в миниатюре, то не нужно ни кодировать его в высоком качестве, ни пересылать по сети и через сервер. А если участников много и мы знаем, что ваше видео вообще ни у кого не влезло на текущий экран звонка, то его можно и вовсе не передавать — так сэкономятся и ваши ресурсы, и серверные.
То есть сервер говорит клиенту: «Давай видео не больше, чем такого-то качества, лучше пока не нужно». Если раскладка изменится — например, участник станет спикером или просто кто-то захочет посмотреть его видео внимательнее и развернёт его у себя в приложении, — сервер запросит видео в большем разрешении.
Переключение качества по запросу в исходящем потоке выглядит в webrtc-internals вот так:
Сначала никто не смотрит видео этого клиента — ничего не посылаем. Потом видео попало в большое окошко — битрейт подскочил. Последний отрезок — видео где-то в сетке, битрейт и размер фрейма меньше.
Понятно, что на резких переключениях туда-сюда можно просадить всю выгоду: завалить клиента запросами и замучить кодек постоянным переключением, в том числе генерацией новых опорников. Поэтому не забываем о здравом смысле и тротлим резкие переключения на понижение качества. То есть если вы только что говорили, то мы немного подождём и попересылаем качественное видео — на случай, если вы скоро продолжите. С другой стороны, запрос на повышение качества стараемся выполнить сразу: если уж видео показывается в большом окошке, оно должно выглядеть хорошо.
Кодирование видео на клиенте по запросу помогло нам значительно сэкономить сеть и серверные и клиентские ресурсы:
на 62% меньше клиент-серверного трафика;
на 21% меньше нагрузки на клиентский CPU;
на 12% меньше использование серверного CPU.
И это в самом неоптимальном случае для трёх участников. Если их больше, то больше и вариантов показа. Плюс есть вероятность, что чьё-то видео вообще не потребуется. Соответственно, суммарная выгода будет выше.
Но и это ещё не всё. Допустим, у всех участников примерно одинаковая сеть и никто не смотрит видео от определённого клиента на главном экране. Значит, нам нужно раздать его только в одном исходном качестве — среднем. И только если подключится кто-то новый с худшей сетью, для кого понадобится понизить битрейт, мы запустим транскодирование на сервере и нарежем минимальное качество. По факту, у нас 40% видео не требует транскодирования и пересылается в исходном виде, как в чистом SFU.
Итого: топология и кодирование в групповом видеозвонке
Выбирая групповую топологию для работы с видео, сразу откажитесь от MCU и помните главные недостатки разных вариантов реализации SFU:
чистый SFU — много входящего трафика, шторм опорными кадрами и другие проблемы;
simulcast — лишний исходящий трафик и большая нагрузка на клиентские устройства;
SVC — лучше, чем simulcast, но не работает в браузерах;
Quality on-demand — наш подход бережёт ресурсы пользователей и перекладывает нагрузку на сервер.
Конкретный выбор зависит от ваших требований и возможностей. Простой SFU из коробки есть в open-source проектах и нормально сработает для небольшого числа участников. Но 50 человек в звонке для готового типового SFU — это предел. Браузер, использующий WebRTC для передачи видео, скорее всего, начнёт разваливаться ещё раньше.
Мы выбрали серверное транскодирование для того, чтобы не нагружать клиентов, но стараемся экономить и CPU сервера:
транскодируем видео на бэкенде по запросу. Когда можем, пересылаем оригинальный поток или не посылаем ничего, если видео никто не смотрит;
за счёт серверного транскодирования отдаём опорные кадры по запросу, не влияя на остальных участников звонка. Таким образом ускоряем подключение;
медиана passtrough latency для видео — 40 мс. Такую дополнительную задержку вносит серверное транскодирование, если оно потребовалось (но это бывает редко);
серверные ресурсы: 0,39 CPU на пользователя, из них 0,34 CPU на видео и 0,05 CPU на аудио.
Демонстрация экрана
Демонстрация экрана — необходимая функция, если говорить о сервисе для рабочих и учебных видеоконференций. Но сейчас она есть и у большинства мессенджеров для личного общения — и ВКонтакте не исключение.
Казалось бы, демонстрация экрана — то же видео, только не с камеры. Это так, но сценарий использования и требования к воспроизведению отличаются. Рассмотрим отличия на графиках исходящего трафика из webrtc-internals.
Слева видео с камеры: каждый следующий кадр не сильно отличается от предыдущего, кодек кодирует видео примерно с константным битрейтом без существенных изменений качества.
Справа видео с экрана: большинство кадров ничем (или почти ничем) не отличаются от предыдущих, кодек кодирует их с очень низким битрейтом. Потом переключили окно, перелистнули слайд, быстро проскроллили — и новый кадр резко отличается от прошлого. Соответственно, нужно передать гораздо больше бит, так что получаются большие спайки битрейта.
В WebRTC эту проблему предлагается решать следующим образом: указываем, что отправляем текстовый контент (contentHint=text, degradationPreference=maintain-resolution); WebRTC оптимизирует кодирование под текст и вместо понижения разрешения сокращает фреймрейт.
Но кодек, чтобы уложиться в заданный битрейт при быстром скроллинге, повышает quantization parameter (QP). QP влияет на то, что предпочтёт кодек: повысить битрейт или получить искажения. Чем больше QP, тем выше уровень сжатия и сильнее искажения.
Вот как это выглядит на графиках webrtc-internals (кстати, доступно каждому: открываете chrome://webrtc-internals/ во время видеозвонка и следите, как сервис переключает битрейты и реагирует на изменения в звонке).
QP на левом графике резко вырастает именно тогда, когда начинаешь скроллить расшаренное окно, при этом падает качество. В это же время FPS на правом графике падает до 1 кадра в секунду — так себе плавность.
Google Meet, используя стандартный WebRTC, одновременно повышает quantization parameter, понижает FPS и повышает битрейт. Получается мыло, как на скриншоте ниже.
Когда спикер прекратит скроллить, текст постепенно восстановится и всё опять будет вполне читаемо. Но это «постепенно» может занять секунды, а тем временем будет пора листать дальше и показывать следующий слайд.
Мы не хотим, чтобы текст смазывался, и стремимся адаптироваться к условиям сети, понижая фреймрейт вместо повышения QP. WebRTC не даёт достаточного доступа к настройкам кодека, чтобы этого добиться. Поэтому для демонстрации экрана мы сделали отдельный пайплайн в обход WebRTC.
Adaptation controller смотрит на количество неотправленных данных в исходящем буфере и, если оно превышает порог, выбрасывает фрейм до кодирования.
Video encoder сконфигурирован с ограничением параметра maxQP — чтобы не разрешать ему размазывать текст, даже если битрейт не вписывается в заданный target. В такой конфигурации в связке с adaptation controller даже при недостаточной пропускной способности сети текст остаётся чётким, но снижается FPS.
Отдельно хочется отметить вопрос кодирования видео в браузерах. В них пока не везде есть низкоуровневый API видеокодирования. Но есть экспериментальный WebCodecs API, который доступен только в Chrome в рамках origin trial.
Мы записались в origin trial и в Chrome используем WebCodecs, а для остальных браузеров в качестве фолбэка берём libvpx-библиотеку, скомпилированную под WebAssembly. Плюс решения с WebAssembly — широкая поддержка в браузерах, минус — ниже производительность. Zoom тоже использует WebAssembly для кодирования видео, поэтому даже в веб-версии Zoom, в отличие от Google Meet, при скроллинге вы не увидите таких расплывшихся букв. Так что ждём, когда WebCodecs доедут до всех браузеров.
За счёт кастомного пайплайна мы поддерживаем демонстрацию экрана в 4K на мобильных платформах и в нативном десктопном клиенте. Скриншер на мобильных можно зумить пальцами, поэтому 4К тут не ради красивой цифры, а действительно приносит пользу. Можно подключиться к рабочему миту с телефона и спокойно рассмотреть графики — особенно если коллега шерит Retina-экран с нашего нового десктопного клиента (если захотите проверить качество скриншеринга — скачать клиент можно тут). На вебе можем показывать 4К, а шерить только 2К из-за ограничений браузеров. Здесь мы пока на уровне средних пользователей с мониторами Full HD. Но когда 4–8К будет более распространено, мы уже точно научимся его поддерживать.
Где мы относительно конкурентов
В нашей архитектуре групповых звонков участвует собственный conferencing-сервер, и мы выбрали серверное транскодирование. Нужно проверить, как это сказалось на latency — показателе, который сильно влияет на субъективное восприятие качества.
Задержку по видео измеряем с помощью уже отработанного приёма: снимаем секундомер, фотографируем вместе экран с исходящим видео и экран принимающего клиента, сравниваем.
Вот что получили по видеозвонкам на компьютере на хорошей сети, но по Wi-Fi (сеть по сервисам выровнена).
Отдельно посмотрели на сети, задушенной до 300 Кбит/с. Здесь как раз вступают в игру отработка сбоев, борьба со штормом опорников и ухищрения с кодеками.
С мобильными клиентами тоже особый разговор: там жёстче борьба за нагрузку, но есть аппаратное ускорение. И это нативный клиент, а не универсальная браузерная реализация.
Во всех измерениях у ВКонтакте задержка или на уровне сервисов-конкурентов, или минимальная среди них. Давайте разберёмся почему.
Синхронизация звука и видео
Референсные исследования показывают: если голос (звук) немного отстаёт от видео, это не так заметно, как когда он опережает картинку.
Человеческий мозг легко компенсирует задержку звука, а вот слышать раньше, чем видеть, ему некомфортно. Рассинхронизация звука и видео в диапазоне от −100 до +60 мс незаметна.
Попробуем воспользоваться этим знанием. Посмотрим, сколько занимает процессинг аудио и видео по перцентилям.
Понятно, что на клиентах всё может быть по-разному. Но задержки на нашем бэкенде как раз в тех порядках, что и диапазон незаметного рассинхрона.
Поэтому мы отказались от lipsync. Мы передаём с сервера аудио и видео независимо, и на клиенте они не синхронизируются каким-либо специальным образом — мы надеемся на то, что различия будут несущественными и пользователи их не заметят. Если узнаем, что это мешает пользователям, то сможем включить. Но пока и без lipsync получается хорошо, а главное — с меньшими задержками.
Пайплайн для работы с видео
В итоге наш пайплайн для работы с видео позволил поддержать 4К в том числе для скриншеринга, сэкономить более 60% клиент-серверного трафика и 10–20% CPU клиента и сервера. Так что скоро мы сможем полностью снять ограничение на число участников в звонке, и все они смогут подключаться с видео.
Пайплайн включает:
aдаптивный выбор кодека в зависимости от сети и устройства: используем VP9 для качественного видео на низких битрейтах, а на высоких — H.264 для меньшей нагрузки на устройство;
SFU-топологию для групповых звонков с серверным транскодированием видео в меньшие разрешения — это позволяет экономить клиентские ресурсы и бороться со штормом опорных кадров;
Quality on-demand, или кодирование исходящего видео в качество по запросу, — тоже для уменьшения нагрузки на клиентов и экономии трафика.
демонстрацию экрана в обход WebRTC — так понижаем FPS, но сохраняем чёткость изображения и поддерживаем 4К.
При этом пайплайн не включает синхронизацию аудио и видео, что позволяет нам иметь наименьшую задержку среди популярных сервисов видеоконференций.
В следующей статье соберём все знания о работе с real-time аудио и видео и построим архитектуру сервиса видеозвонков без ограничения на число участников.
Благодарности: собирать данные и готовить статью помогали Иван Григорьев (@Ivan_A), Андрей Петухов, Никита Ткаченко, Андрей Моржухин.
P. S. Сегодня важный день: мы выпускаем SDK наших звонков на рынок. В него мы упаковали все свои технологии для реализации групповых видеозвонков. Прежде эту библиотеку помимо ВКонтакте могли встроить только сервисы из Mail.ru Group: Учи.ру, «Сферум», Одноклассники и Звонки Mail.ru. Теперь, когда все возможности обкатаны на этих масштабных платформах, мы предлагаем их и другим IT-бизнесам.
Что есть внутри SDK, как получить тестовый доступ и интегрировать звонки в свой продукт (бесшовно, с собственным интерфейсом) — рассказываем на лендинге и в релизе.