Проблема первого зрителя, или непростая конвертация WebRTC видеопотоков в HLS


    Егор закрыл крышку ноутбука и потер красные от недосыпа глаза. "Клиенты продолжают жаловаться на зависания стрима, новый пакет исправлений совсем не помог! Что же делать с этим (censored) HLS?" — произнес он в пустоту кабинета.


    Браузер это не только гипертекст, но и стример


    Плеерами браузеры обзавелись достаточно давно, а вот с энкодером видео и стримингом история другая. Сейчас почти в любом браузере свежей версии можно найти модули энкодинга, стриминга, декодирования и воспроизведения. Эти функции доступны через JavaScript API, а реализация называется Web Real Time Communications или WebRTC. Эта встроенная в браузеры библиотека умеет достаточно много: захват видео с встроенной, виртуальной или USB камеры, компрессия кодеками H.264, VP8, VP9, отправка в сеть по SRTP протоколу, т.е. функционирует как софтверный видеокодер-стример. В итоге видим браузер, у которого под капотом работает что-то похожее на ffmpeg или gstreamer, который хорошо жмет видео, стримит по RTP и играет видеопотоки.


    WebRTC дает простор для реализации разнообразных стриминговых кейсов на JavaScript:


    • стримить поток из браузера на сервер для записи и последующей раздачи
    • раздавать потоки peer-to-peer
    • играть поток другого пользователя и отправлять свой (видеочат)
    • конвертировать сервером другие протоколы, например RTMP, RTSP, и т.д., и играть в браузере как WebRTC

    Рафинированные скрипты управления потоками могут выглядеть так:


    //Запуск трансляции из браузера на сервер
    
    session.createStream({name:”mystream”}).publish();
    
    //Воспроизведение трансляции браузером
    
    session.createStream({name:”mystream”}).play();

    HLS работает там, где не работает WebRTC


    WebRTC работает в последних версиях браузеров, однако имеют место два следующих фактора: 1) Не все пользователи своевременно обновляют браузеры и вполне могут сидеть на каком-нибудь Хроме трехлетней выдержки. 2) Чуть ли не раз в неделю выходят обновления и появляются новые браузеры, WebView, а также другие клиенты и мессенджеры, умеющие серфить интернет. Стоит ли говорить, что далеко не во всех из них присутствует поддержка WebRTC, а если и присутствует, то может быть довольно урезанной. Смотрите как дела обстоят сейчас:



    Отдельная головная боль — всеми любимые яблочные устройства. Они относительно недавно получили поддержку WebRTC и, порой удивляют особенностями поведения по сравнению с православными webkit-браузерами. И там где не работает или не очень хорошо работает WebRTC, отлично работает HLS. В связи с этим, требуется совместимость, и что-то вроде конвертера, который позволит преобразовать WebRTC в HLS и проиграть его практически на любом устройстве.


    Изначально HLS не был задуман для потоков реального времени. Действительно, какой может быть видеореалтайм по HTTP? Задача HLS — нарезать видео на кусочки и ровно, не торопясь, доставить их до плеера, путем скачивания одного за другим. HLS плеер ожидает строго сформированного и ровного видеопотока. И здесь возникает конфликт, так как WebRTC напротив, может позволить себе терять пакеты из-за требований реалтайма и низкой задержки и иметь плавающий FPS / GOP и непостоянный битрейт — быть полной противоположностью HLS в плане предсказуемости и размеренности потока.


    Очевидный подход — депакетизация WebRTC (SRTP) и последующая конвертация в HLS, может не работать в нативном HLS плеере Apple или работать в непригодном для продакшена виде с фризами. Под нативным плеером здесь понимается плеер, который используется в яблочных iOS Safari, Mac OS Safari, Apple TV.


    Поэтому, если вы заметили фриз HLS в нативном плеере, возможно это оно, и источником стрима является WebRTC или другой динамический стрим с неровной разметкой. Кроме этого, в реализации нативных Apple плееров встречается поведение, которое можно понять только опытным путем. Например, сервер должен начать отправку HLS сегментов немедленно, сразу после отдачи m3u8 плейлиста. Промедление в секунду грозит фризом. Если в процессе поменялся конфиг битстрима (что довольно частое явление при WebRTC стриминге), также будет фриз.


    Борьба с фризами в нативных плеерах


    Таким образом, прямая и честная депакетизация WebRTC и пакетизация в HLS в общем случае не работает. В сервере потокового видео Web Call Server (WCS) мы решаем проблему двумя способами, а третий предлагаем в качестве альтернативы:


    1) Транскодирование.


    Это наиболее надежный способ, позволяющий выровнять WebRTC поток под требования HLS, выставить нужный GOP, FPS, и т.д. Однако в некоторых случаях транскодирование не является хорошим решением, например транскодирование 4к потоков VR видео — так себе идея. Такие тяжелые потоки транскодировать очень дорого в плане процессорного времени или ресурсов GPU.



    2) Адаптации и выравнивание WebRTC потока на лету под требования HLS.


    Это специальные парсеры, которые анализируют H.264 битстрим и корректирует его под особенности / баги нативных HLS плееров Apple. Здесь надо признать, что ненативные плееры вроде video.js и hls.js более толерантны к потокам с динамическим битрейтом и FPS коим является WebRTC и не тормозят там, где эталонная по сути реализация Apple HLS встает в вечный фриз.



    3) Использовать в качестве источника потока RTMP вместо WebRTC.


    Несмотря на то, что флэш отошел от дел, RTMP протокол активно используется для стриминга, взять тот же OBS Studio. И надо признать, что RTMP энкодеры производят в целом более ровные потоки чем WebRTC и поэтому практически не дают фризов в HLS, т.е. Конвертация RTMP > HLS с точки зрения фризов выглядит гораздо более годной в том числе и в нативных HLS плеерах. Поэтому если стриминг осуществляется с десктопа и OBS, то для конвертации в HLS лучше использовать его. Если же источником является Chrome браузер, то RTMP уже воспользоваться не получится без установки плагинов, и здесь только WebRTC.



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


    WebRTC в HLS на CDN


    Отдельные неприятности могут поджидать в распределенной системе, когда между источником WebRTC потока и HLS плеером находится несколько серверов доставки WebRTC стримов, а именно CDN, в нашем случае на базе WCS сервера. Выглядит это так: есть Origin — сервер, который принимает WebRTC поток, есть Edge — серверы, которые раздают этот поток в том числе и по HLS. Серверов может быть много, что обеспечивает возможность горизонтального масштабирования системы. Например, к одному Origin — серверу можно подключить 1000 HLS серверов, в этом случае емкость системы масштабируется в 1000 раз.



    Проблема уже была обозначена немного выше, и возникает эта проблема как правило в нативных плеерах: iOS Safari, Mac OS Safari, Apple TV. Под нативным имеется в виду плеер, который работает с прямым указанием урла плей листа в теге, например <video src="https://host/test.m3u8"/>. Как только плеер запросил плей-лист, а это действие является фактически первым шагом воспроизведения HLS потока, сервер обязан сразу, без какой-либо задержки, начать отдавать сегменты HLS видео. Если сервер не начинает отдавать сегменты немедленно, плеер решает что его обманули и останавливает воспроизведение. Опять же, такое поведение характерно именно для нативных HLS плееров Apple, но мы не можем сказать пользователям — “не используйте пожалуйста iPhone Mac и Apple TV для воспроизведения HLS потоков”, пользователи не поймут.


    Итак, при попытке проиграть HLS стрим на Edge сервере, сервер должен немедленно начать отдачу сегментов, но как он это сделает если по факту стрима у него нет? Действительно, при попытке воспроизведения стрим на этом сервере отсутствует. Логика CDN работает по принципу Lazy Loading — мы не погоним стрим на сервер до тех пор, пока кто-то этот стрим на этом сервере не запросит. Возникает проблема первого подключившегося — первый, кто запросил HLS поток с Edge — сервера и имел неосторожность сделать это с нативного плеера Apple, получит фриз по той причине, что должно пройти какое-то время для того чтобы заказать этот стрим с Origin сервера, получить его на Edge и приступить к HLS нарезке. Даже если это займет три секунды, плеер это не спасет. Он уйдет в фриз.



    Здесь снова вырисовываются два решения: одно нормальное, другое — не очень. Можно было бы отказаться от подхода Lazy Loading в CDN и рассылать трафик всем узлам вне зависимости от того, есть там зрители или нет. Решение, возможно пригодное для тех, кто не ограничен в трафике и вычислительных ресурсах. Origin будет гнать трафик на все Edge серверы, в результате все серверы и сеть между ними будут постоянно загружены. Пожалуй эта схема подошла бы только для каких-то специфических решений с малым количеством входящих потоков. При тиражировании большого количества потоков такая схема будет явно неэффективна по ресурсам. И если вспомнить, что мы решаем всего лишь “проблему первого подключившегося из нативного браузера”, то понятно, что оно того не стоит.



    Второй вариант более элегантный, но тоже обходной. Мы отдаем первому подключившемуся пользователю видео картинку, но это пока еще не тот стрим, который он желает увидеть — это прелоадер. Так как мы что-то должны отдать уже сейчас и сделать это немедленно, а исходного стрима у нас нет (он еще заказывается и доставляется с Origin-а), мы принимаем решение попросить клиента немного подождать и показать ему видео прелоадера с двигающейся анимацией. Пользователь ждет несколько секунд, прелоадер крутится, и когда доходит реальный стрим, пользователю начинается показ реального стрима. В результате первый пользователь увидел прелоадер, а последующие подключившиеся наконец-то увидели нормальный HLS стрим, пришедший из CDN, работающей по принципу Lazy Loading. Инженерная проблема решена.


    Но не до конца


    Казалось бы, все работает здорово. CDN функционирует, HLS потоки забираются с краевых серверов Edge и решена проблема первого подключившегося. И здесь появляется еще один подводный камень — мы отдаем прелоадер в фиксированном соотношении сторон 16:9, а в CDN могут входить потоки любых форматов: 16:9, 4:3, 2:1 (VR видео). И это является проблемой, потому что если отдать плееру прелоадер в формате 16:9, а заказанный стрим окажется в формате 4:3, то нативный плеер снова ждет фриз.


    Поэтому встает новая задача — требуется знать с каким именно соотношением сторон поток входит в CDN и отдавать прелоадер в том же соотношении. Особенностью WebRTC потоков является сохранение соотношения сторон при изменении разрешения и при транскодировании — если браузер решает понизить разрешение, он понижает его в том же соотношении. Если сервер решает транскодировать поток, он сохраняет соотношение сторон в той же пропорции. Поэтому логично, что если мы хотим показать прелоадер для HLS, мы показываем его в том же соотношении сторон, в котором заходит стрим.



    CDN работает следующим образом: когда на Origin-сервер заходит трафик, он сообщает остальным серверам в сети, в том числе Edge-серверам о новом потоке. Проблема в том, что в этот момент разрешение исходного потока может быть еще не известно. Разрешение несут конфиги H.264 битстрима вместе с ключевым фреймом. Поэтому может случиться так, что Edge сервер получит информацию что стрим есть, но не будет знать о его разрешении и соотношении сторон, что не позволит ему корректно сгенерировать прелоадер. В связи с этим необходимо сигнализировать о наличии стрима в CDN только при наличии ключевого фрейма — это гарантированно даст Edge-серверу информацию о размерах и позволит сгенерировать корректный прелоадер чтобы предотвратить “проблему первого подключившегося зрителя”.



    Итоги


    Конвертация WebRTC в HLS в общем случае дает фризы при воспроизведении в нативных плеерах Apple. Проблема решаема анализом и корректировкой битстрима H.264 под требования HLS от Apple либо транскодирования, либо с помощью миграции на RTMP протокол и энкодер в качестве источника потока. В распределенной сети с ленивой загрузкой потоков существует проблема первого подключившегося зрителя, которая решается с помощью прелоадера и определения разрешения на стороне Origin сервера — точки входа потока в CDN.


    Ссылки


    Web Call Server — WebRTC сервер


    CDN для стриминга WebRTC с низкой задержкой — CDN на базе WCS


    Воспроизведение WebRTC и RTMP видеопотоков по HLS — Функции сервера по конвертации потоков из различных источников в HLS

    Flashphoner
    Компания

    Комментарии 15

      0
      Спасибо большое за интересную статью. А вот не подскажите ли вы насколько сложно решается обратная задача — вещать с произвольного источника, скажем ffmpeg, прямо в браузер? Для этого обязательна вся эта обвязка с установкой соединения и т.д., или можно напрямую дать WebRTC в зубы ссылку на RTP поток принимать без посредников? (для простоты допустим что все происходит в одной локальной сети и нет никаких промежуточных CDN)
        0
        вещать с произвольного источника, скажем ffmpeg, прямо в браузер?
        или можно напрямую дать WebRTC в зубы ссылку на RTP поток принимать без посредников?

        Если смотреть на эту задачу с точки зрения сети, то получается так:

        Допустим на одном компьютере 192.168.1.10 у вас ffmpeg и он умеет WebRTC (не знаю так ли это, не тестировали), но допустим, ffmpeg умеет вести себя как Chrome и является WebRTC-пиром.

        Допустим на втором компьютере 192.168.1.11 установлен Chrome (WebRTC-браузер).

        Чтобы «вещать прямо в браузер», вам нужно видеотрафик так или иначе передать с устройства 192.168.1.10 на 192.168.1.11 по TCP или UDP протоколу.
        Тогда браузер сможет принять видеопоток, декодировать и отобразить в плеере и вещание состоится.

        Чтобы передать трафик, нужно открыть как минимум 1 порт и там и там.
        Допустим, передавать будем по UDP и открываем порты .10:3310 и .11:3311 соответственно.

        Ок, порты забиндили. Теперь эти два устройства должны друг другу как-то сообщить о своих портах. И здесь два варианта:

        1. Использовать третье устройство — посредник, через которого можно обменяться информацией о портах.
        2. Сделать этим посредником условный ffmpeg.


        После того, как устройства .10 и .11 обменялись портами, у них есть все необходимое для установки соединения и передачи трафика.

        Т.е. ответ на вопрос — да, можно вещать напрямую из источника в браузер.

        Для этого источник должен:
        1) Поддерживать WebRTC-стек.
        2) Выступать сигнальным сервером для обмена SDP (портами).
        3) Кодировать видео один раз и тиражировать браузерам копии.

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

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

        Поэтому для общего случая и для продакшена рабочий вариант — доставить поток на внешний сервер и оттуда этот поток уже распространять по браузерам. В локальной сети тоже самое, только сервер устанавливается локально.
          0
          Спасибо. Концептуально понятно как работает WebRTC-стек, в целом. А есть возможность в API браузера не использовать весь стек целиком, а отдельно настроить только те компоненты, которые непосредственно принимают RTP пакеты? Попробую уточнить, ипользуя аналогию с MPEG-DASH — это тоже стек нескольких протоколов и стандартов: HTTPS, MPD, MSE, ISOBMFF и т.д., но в конечной реализации никто не заставляет принимать сегменты по HTTPS и парсить .mpd файл. Вполне можно взять отдельно MSE и скармливать ему сегменты по WEB Sockets или генерировать их на лету. Можно ли выделить аналогичное подмножество в WebRTC со стороны браузера?
          0
          Вы в принципе не можете выдать ссылку на RTP-поток, он в другую сторону работает. Вы говорите источнику, на какой ip и на какой порт гнать RTP, а не на каком порту слушать.
          Из этого и следует невозможность дать WebRTC в зубы ссылку — браузер открывает свои порты, и требует нормальной аутентификации и нормального шифрования. Поэтому вам требуется полноценный WebRTC стек.
            0
            Я не точно выразился. Я не имел ввиду ссылку буквально. Я имел ввиду информацию (типа SDP), где будет описано куда я сейчас буду вещать и в каком формате. То что вы описывает, это скорее RTSP. RTP — это просто меди пакеты с небольшим заголовком, так что их вполне можно вещать на источник на указанный порт (например по UDP), без установки сессии.
            Но как я уже понял проблема даже не в этом, а в том что в WebRTC, поверх UDP и TCP, еще лежит SCTP — со своими каналами и т.д., а это уже нестандартный транспорт со всеми вытекающими…
              0
              А куда вы будете вещать? Вам нужен ip и порт клиентской машины, притом порт, на котором будет слушать браузер. Их вам нужно передать на источник каким-либо образом, так что именно этим и занимается WebRTC.
              SCTP вообще ни при чем, его как-раз и можно опустить за ненадобностью.

              Вообще, если есть ffmpeg в качестве источника, то проще воспользоваться hls или mpd. Один модуль для nginx, простая настройка, ну и плюс библиотка для mpd на клиенте.
              WebRTC нужен, если критична маленькая задержка или требуется видео от пользователя.
                0
                А куда вы будете вещать?
                Еще раз. Я сейчас не утверждаю что так устроен WebRTC, я рассуждаю гипотетически. Для того что бы вещать по RTP нужно только открыть порт на клиенте и вещать туда. В конце концов, можно жестко выставить порт на клиенте и источнике (я не собираюсь это делать на практике, мне просто хотелось бы разобрать стек WebRTC по косточкам).
                SCTP вообще ни при чем, его как-раз и можно опустить за ненадобностью.
                А вот это очень интересно. Просто у меня сложилось ощущение что SCTP там прибит гвоздями и без него никак. Т.е. вы хотите сказать что можно использовать любой другой транспорт? А каким образом и где это настраивается, вы могли бы подсказать?
                Вообще, если есть ffmpeg в качестве источника, то проще воспользоваться hls или mpd. Один модуль для nginx, простая настройка, ну и плюс библиотка для mpd на клиенте.
                WebRTC нужен, если критична маленькая задержка или требуется видео от пользователя.
                В моем случае не проще, т.к.:
                1. MSE, с некоторого времени, требует что бы в потоке границы сегментов были строго по IDR, а у меня в потоках IDR может не быть по несколько десятков минут.
                2. Мои потоки запросто могут «подрываться», а из-за MSE, восстановление возможно только со следущего IDR.
                Короче у меня вещание идет не с «жесткой» заранее приготовленной копии, а с реальных камер или эфирных кабелей.
                  0
                  Просто у меня сложилось ощущение что SCTP там прибит гвоздями и без него никак. Т.е. вы хотите сказать что можно использовать любой другой транспорт? А каким образом и где это настраивается, вы могли бы подсказать?

                  В WebRTC SCTP используется не для видео/аудио, а для передачи данных. Видео же гонится по обычному UDP или TCP.

                  просто хотелось бы разобрать стек WebRTC по косточкам

                  Я писал свой WebRTC сервер, для видео/аудио, могу вкратце пояснить:
                  1. Сначала обмениваемся SDP друг с другом, поясняем, кто какие кодеки умеет, какие будут использоваться логины/пароли для ICE
                  2. Обмениваемся ip и портами, как локальными, так и внешними — STUN/TURN.
                  3. После чего, начинаем посылать ICE пакеты друг другу по полученным адресам
                  4. Как только будет коннект по каким-либо ip, начинаем передачу ключей шифрования для трафика.
                  5. После чего берем обычные RTP пакеты, шифруем их SRTP полученными в предыдущем пункте ключами, и отправляем по ip/port, полученными в результате ICE-коннекта из пункта 3.

                  Все это вполне реализуемо самому. Если не хочется заморачиваться — берем чужой медиасервер (например kurento), отправляем на него RTP-поток, забираем с него по WebRTC.
                    0
                    Спасибо вам большое за развернутый ответ. Теперь стало еще понятнее, но осталось еще несколько вопросов:
                    SDP это протокол описания, а как происходит сам обмен?
                    Как я понимаю, ICE — Interactive Connectivity Establishment — это протокол или просто некая стандартизированная процедура?
                    Если я всегда нахожусь в локальной сети, могу ли я избежать работы с STUN/TURN серверами или это не обойти и это обязательно?
                      0
                      Как вы обмениваетесь SDP — это строго на ваше усмотрение, в WebRTC нет механизма обмена SDP.
                      ICE — это протокол.
                      Если вы всегда в локальной сети, то STUN необязателен. TURN тем более. Даже если вы укажете STUN сервера, то коннект пойдет через локальные ip адреса, у них приоритет выше.
                        0
                        Спасибо вам огромное за исчерпывающие ответы.
          0
          Проблему с HLS можно частично решить уменьшением размера чанка, обычно он 10 сек, от того и загрузка потока для первого да и для остальных пользователей требует больше времени. Если ваш софт позволяет, уменьшите его до 5 сек например и сравните.
            0

            Скорось подключения вырастит, но неравномернось загрузки также возрастет. Уменьшая размер чанка вы потеряете на ожидании каждого следущего, т. к. в дело вступит задержка round-trip.

              0
              С WebRTC источником проблема в том, что вы не можете контролировать генерацию ключевых кадров в стриме, а HLS чанки режутся по ключевым кадрам.

              Поэтому мы не можем задать точное время чанка. Мы можем указать, например «не менее 2 секунд». В этом случае мы дождемся от Хрома по WebRTC ключевого кадра, проверим прошло ли две секунды, и если прошло, то нарежем чанк.

              Мы не можем принудить браузер высылать ключевые фреймы в жестком интервале 2 секунды. Но можем каждые две секунды просить у браузера ключевой кадр через PLI фидбэк, и для этого есть специальная настройка. Браузер может дать, а может и не дать по запросу. Чаще дает.

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

              В любом случае, уменьшение длины чанков не спасает внутри CDN, а с одним сервером такой проблемы нет. Вне зависимости от размера чанков, даже если бы мы транскодировали поток и нарезали чанки по 1 секунде, в CDN между запросом плей листа и доставкой потока с Origin-сервера пройдет время и плеер перестанет играть поток. Поэтому пока рабочий вариант — только прелоадер первому зрителю.
                0
                можем каждые две секунды просить у браузера ключевой кадр через PLI фидбэк

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

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

            Самое читаемое