«Трафик! Этот чертов трафик влетит нам в копеечку!»
Томми, основатель онлайн-радио-платформы.

Массовая трансляция видео и аудио генерирует огромное количество трафика. А трафик это деньги. Иногда – большие деньги. И тогда бизнес начинает задумываться – как бы сделать так, чтобы эти деньги не платить. И желания бизнеса – плодотворная почва для интересных решений. Об одном из них мы и поговорим.

В чем суть?

Очевидная особенность медиа-трафика в WebRTC – он может вполне легально отправляться клиентом. С той же статикой в HLS такой фокус не проходит. Так почему бы не использовать эту возможность и передавать трафик не от сервера к клиенту, а от клиента к клиенту - как в пиринговой сети, используя WebRTC p2p соединения в качестве транспорта (WebRTC p2p mesh). Первые Х клиентов получают медиа от сервера и раздают следующим Y клиентов, а те отдают следующим и так далее.

Рис 1. Базовая упрощенная схема
Рис 1. Базовая упрощенная схема

Таким образом мы получаем связный граф. При этом мы хотим, чтобы степень корня графа (то бишь количество клиентов, получающих медиа непосредственно от сервера), было минимально разумным, однако без ущерба стабильности системы.

В борьбе за стабильность

Основная сложность здесь – нестабильность графа или, перефразируя Булгакова, проблема не в том, что пользователи отключаются, а в том, что они отключаются внезапно. Любой элемент графа, любой набор элементов, может в произвольный момент выйти вон. И повлиять на это нельзя. Единственное разумное решение – дублирование соединений. Пользователь А, подключаясь к сети, устанавливает P2P-соединения с несколькими пользователями, каждый из которых готов отдавать трафик (о том, как определять готовность поговорим ниже). Но реально получает трафик он только от одного – одно соединение активно и Х соединений пассивны. В случае обрыва активного соединения, клиент не тратит время на подключение к другому пользователю, а лишь активизирует (о методике управления процессом поговорим ниже) одно из пассивных соединений. В итоге перерыв в медиа-трафике будет минимальным. При обрыве пассивного соединения клиенту стоит установить новое.

Впрочем, и от этого перерыва можно избавиться. У клиента может быть не одно, а несколько активных соединений, причем только одно будет воспроизводиться, остальные будут «играть в пустоту». При этом клиент может контролировать количество пришедших пакетов (хотя частый запрос WebRTC Stats и не рекомендуется) и переключаться между активными соединениям при первых признаках потери связи. Негативным эффектом данного подхода будет рост паразитного трафика между клиентами.

Однако и проблему избыточного трафика можно отчасти решить – для этого можно разделить активные соединения на основные и резервные, безжалостно зарезая качество (например, используя WebCodecs API) у резервных – таким образом, при отключении источника, у клиента будет незначительный (зависит только от частоты проверки соединения) перерыв и после него – короткий период не самого хорошего качества - пока отправитель не переведет соединение в разряд основных. 

Рис 2. Виды активных и пассивных соединений
Рис 2. Виды активных и пассивных соединений

Как не перегрузить узел и определить, способен ли он отдавать трафик

В WebRTC каждый исходящий медиа-поток кодируется отдельно. Это и благо и проклятье, причем, в нашем случае, скорее второе. Ресурсы клиента ограничены и, что самое плохое, у нас нет четкого механизма их оценки. Поэтому единственное что мы можем использовать – косвенные признаки из WebRTC Stats.

На что стоит обратить внимание:

  • Качество входящего канала: поскольку пользователь в любом случае получает медиа, мы можем отталкиваться от метрик этого соединения. https://www.w3.org/TR/webrtc-stats/#inboundrtpstats-dict*
    Какие именно метрики использовать и какие веса им задавать – тема для отдельной статьи, однако базово не стоит доверять клиентам, у которых большое количество потерянных пакетов, большой Jitter buffer и большое же количество отправленных NACK. Не стоит забывать, что соединение – палка о двух концах и проблема может быть и на стороне отправителя, но, в любом случае, лучше не предлагать передавать медиа, если сами его получаем ненадежно.

  • Не стоит использовать соединение, если оба клиента используют симметричный NAT – в этом случае трафик пойдет через TURN-сервер. Лучше вообще не использовать TURN-сервер в данной архитектуре. Отсутствие TURN как раз и даст нам нужный механизм контроля – если клиенты не могут соединиться напрямую, то и не нужно.

  • Следует настороженно относиться к клиентам, подключенным к сотовой сети – помимо плавающей скорости также стоит иметь в виду, что у всех мобильных операторов симметричный NAT. Также мобильный трафик может быть платным, что важно для клиента. Определить тип подключения можно также исходя из WebRTC stats.

Данных критериев достаточно чтобы понять – способен ли клиент отдать хотя бы один исходящий поток. И на базе исходящего потока мы можем сделать гораздо более точную оценку. Здесь стоит упомянуть следующие критерии из WebRTC Stats:

  • qualityLimitationReason и qualityLimitationDurations – если мы видим, что качество исходящего медиа ограничено из-за использования процессора или памяти, значит данному узлу уже достаточно получателей, а лучше одного переключить куда-то еще. Если же ограничение вызвано каналом – нужно проверять во всех исходящие соединения, возможно, проблема на стороне получателя.

  • retransmittedPacketsSent, nackCount и roundTripTime нужно оценивать в среднем по всем исходящим соединениям, дабы уменьшить вероятность ложного срабатывания из-за проблем на стороне получателя. 

Также весьма и весьма полезно проводить небольшой тест сразу после подключения – клиент начинает получать от сервера медиа и отправлять ему же в несколько потоков, до достижения некоего лимита подключений или возникновения ограничений из списка выше. Это позволит понять, чего мы можем ждать от клиента в самом начале сессии.

Ранжирование клиентов

Не все клиенты одинаково полезны системе и не лишним будет ввести некую систему ранжирования. Клиенты, способные раздавать больше соединений логично должны подключаться непосредственно к медиа-серверу, будучи на первой линии. Те же кто не может много раздавать, подключен к мобильному Интернету или просто нам не нравится – определяется в аутсайдеры и помещается на нижние ветви дерева, дабы свести ущерб от его возможной нестабильности к минимуму. Как правило, четырех уровней вполне достаточно – «готов в первую линию», «симметричный NAT, готов раздавать тем, кто готов принять», «здесь что-то не так, но попробуем» и «никаких раздач». Критерии, по которым клиент попадает в нужную группу сильно зависят от задачи – нужно ли нам обеспечить максимальное качество, либо максимальное количество.

Управление деревом

На первый взгляд может показаться правильным завести все управление в Data Channel между клиентами, однако здесь есть подводный камень – сигнальный сервер должен знать все о всех соединениях, дабы корректно подключать новых клиентов в граф. Поэтому все управление графом должно идти из сигнального сервера. И да, он при этом должен быть чертовски быстрым.

Отказоустойчивость и масштабируемость

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

Рис 3. Использование нескольких медиа-серверов
Рис 3. Использование нескольких медиа-серверов

Задержка для клиента

Поскольку медиа будет странствовать по графу, у него будет накапливаться задержка и, в зависимости от размеров графа, она может достигать нескольких секунд. Что кратно лучше HLS, для которого приемлемые результаты начинаются от десятков секунд. Для уменьшения задержки нужно увеличивать количество зрителей на узел, что приведет у ухудшению качества / стабильности. Другим способом уменьшения задержки является выбор оптимального узла на стороне получателя – не просто «дай мне свободный источник», но «дай мне свободный источник с наименьшей задержкой», саму же задержку можно достаточно точно определять по roundTripTime.

Так сколько же клиентов будет получать медиа от сервера?

Здесь необходимо искать баланс между надежностью и ресурсами. Удобно устанавливать пороговые значения – условно, первые Х клиентов подключаются только к медиа-серверу (серверам). Все следующие клиенты подключаются к уже существующим клиентам, а, если никто не готов отдавать медиа – к медиа-серверу. Причем, клиентов желательно менять местами, чтобы более «надежные» были в первом ряду. Такой подход позволит автоматически балансировать топологию сети, предоставляя видео от других клиентов, а, при невозможности – от медиа-сервера. 

А как же HLS?

При всей элегантности HLS на момент его создания (а идея была прекрасна – загоняем потоковое медиа в классический HTTP), при всей простоте развертывания на огромные аудитории, его родовые болезни, увы, неистребимы – достаточно большая и, к тому же, произвольная задержка при воспроизведении, отсутствие современных кодеков, отсутствие обратной связи и возможности отправлять трафик. Своя ниша у HLS есть и он будет еще использоваться, но время его уходит, уступая гораздо более сложным, но более эффективным решениям.

Немного вежливости

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

Подводя итог

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

Комментарии (10)


  1. robert_ayrapetyan
    26.12.2024 02:11

    Как именно выбирается "самый быстрый" клиент для соединения? Например, если в графе сотни клиентов, и появляется новый клиент К, как ему подключиться к самому близкому для него (по rtt) клиенту в этом графе?


    1. DaniilMakeev Автор
      26.12.2024 02:11

      Самый быстрый это не обязательно минимальный TTL на последней миле - у разных источников разная накопленная задержка.

      Хорошим решением был бы выбор активного соединения из списка имеющихся основываясь на сумме "зарержка источника" + round trip time между источником и получателем. Т.е.
      * новый пир получает список из 10 источников с минимальной собственной задержкой
      * к каждому из них клиент устанавливает пассивное соединение и смотрит roundTripTime
      * лучшая комбинация выбирается активной.

      Смотреть TTL до всех-всех источников я бы не стал - для этого нужно установить соединение (хотя бы DataChannel) ко всем источникам, на больших сетках это будет знатно грузить и источники и сигнальный.


  1. NutsUnderline
    26.12.2024 02:11

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


    1. DaniilMakeev Автор
      26.12.2024 02:11

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

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

      Идея эта давно витает в воздухе, но из-за узости ниши и высокого порога входа не сильно отсвечивает. И пока opensource-решение не появится - так и останется уделом фанатов и крупняка.


      1. NutsUnderline
        26.12.2024 02:11

        бюджетный ноут вполне способен отдавать 3-5 видео

        как раз часть всей этой моей истории что один друг решил помочь ноутом в ретрансляции и удивился лютой загрузке (это было давно и ноут был реально слабый) :) а вообще "открытием" было что люди действительно смотрели со слабых ПК и ноутов, да еще подключенным фиговым wiFi через всю квартиру, с соседями, или на смартфонах, в общем даже воспроизведение 720p у них не тянуло, в т.ч. из-за канала

        а вот к примеру коллега @nikhotmsk в аналогичном случае таки пробовал peertube, но потом перешел на youtube


        1. nikhotmsk
          26.12.2024 02:11

          Не, на самом деле всё наоборот. Я стараюсь не стримить на youtube из-за блокировок по контенту. PeerTube более лоялен в этом плане, там живые люди проверяют загрузки.


  1. 4external
    26.12.2024 02:11

    у WebRTC P2P mesh меньше overhead, чем у сегментов HLS?


    1. DaniilMakeev Автор
      26.12.2024 02:11

      Не думаю, что разница оверхеда транспорта будет сколь либо значима / определяющя. Гораздо большая разница будет из-за того что в HLS гвоздями прибиты h264/mp3, а в WebRTC можно использовать VP8/VP9(если допустимо)/Opus. Например, чтобы предоставить несколько уровней качества, HLS требует отдельно кодировать каждый поток, а в VP можно использовать Simulcast.


  1. same_one
    26.12.2024 02:11

    Посмотрите проект https://webtorrent.io, это ведь почти оно?

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

    Странно даже было узнать, что peertube не про это.


    1. DaniilMakeev Автор
      26.12.2024 02:11

      Не совсем. Webtorrent, насколько я вижу, допилили фичу, которая уже есть в некоторых торрент-клиентах - запрашивать пакеты в порядке воспроизведения и проигрывать их сразу. Технически это как раз ближе к HLS и работает для существующих видео, не для трансляций реального времени. Фильм так посмотреть можно, живую трансляцию хоккейного матча - увы. Но штука, безусловно, полезная для своих задач.