Егор закрыл крышку ноутбука и потер красные от недосыпа глаза. "Клиенты продолжают жаловаться на зависания стрима, новый пакет исправлений совсем не помог! Что же делать с этим (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
Комментарии (6)
Speccyfan
18.12.2019 19:36Проблему с HLS можно частично решить уменьшением размера чанка, обычно он 10 сек, от того и загрузка потока для первого да и для остальных пользователей требует больше времени. Если ваш софт позволяет, уменьшите его до 5 сек например и сравните.
Videoman
18.12.2019 20:16Скорось подключения вырастит, но неравномернось загрузки также возрастет. Уменьшая размер чанка вы потеряете на ожидании каждого следущего, т. к. в дело вступит задержка round-trip.
fpn Автор
18.12.2019 22:04С WebRTC источником проблема в том, что вы не можете контролировать генерацию ключевых кадров в стриме, а HLS чанки режутся по ключевым кадрам.
Поэтому мы не можем задать точное время чанка. Мы можем указать, например «не менее 2 секунд». В этом случае мы дождемся от Хрома по WebRTC ключевого кадра, проверим прошло ли две секунды, и если прошло, то нарежем чанк.
Мы не можем принудить браузер высылать ключевые фреймы в жестком интервале 2 секунды. Но можем каждые две секунды просить у браузера ключевой кадр через PLI фидбэк, и для этого есть специальная настройка. Браузер может дать, а может и не дать по запросу. Чаще дает.
Поэтому, в общем случае, без транскодинга, мы можем управлять только нижней границей длины HLS чанка.
В любом случае, уменьшение длины чанков не спасает внутри CDN, а с одним сервером такой проблемы нет. Вне зависимости от размера чанков, даже если бы мы транскодировали поток и нарезали чанки по 1 секунде, в CDN между запросом плей листа и доставкой потока с Origin-сервера пройдет время и плеер перестанет играть поток. Поэтому пока рабочий вариант — только прелоадер первому зрителю.
Videoman
Спасибо большое за интересную статью. А вот не подскажите ли вы насколько сложно решается обратная задача — вещать с произвольного источника, скажем ffmpeg, прямо в браузер? Для этого обязательна вся эта обвязка с установкой соединения и т.д., или можно напрямую дать WebRTC в зубы ссылку на RTP поток принимать без посредников? (для простоты допустим что все происходит в одной локальной сети и нет никаких промежуточных CDN)
fpn Автор
Если смотреть на эту задачу с точки зрения сети, то получается так:
Допустим на одном компьютере 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 соответственно.
Ок, порты забиндили. Теперь эти два устройства должны друг другу как-то сообщить о своих портах. И здесь два варианта:
После того, как устройства .10 и .11 обменялись портами, у них есть все необходимое для установки соединения и передачи трафика.
Т.е. ответ на вопрос — да, можно вещать напрямую из источника в браузер.
Для этого источник должен:
1) Поддерживать WebRTC-стек.
2) Выступать сигнальным сервером для обмена SDP (портами).
3) Кодировать видео один раз и тиражировать браузерам копии.
Фактически, ваш источник должен быть сервером, умеющим захватывать сырое видео с камеры, подцепленной к нему через USB или другой интерфейс.
Понятно что такое вещание будет нормально работать только внутри локальной сети, т.к. если потоков тиражируется много, то они просто не пролезут в глобальную сеть. Т.е. из локальной сети вы сможете вытащить ну 10 толстых потоков на 10 браузеров снаружи, и на этом все.
Поэтому для общего случая и для продакшена рабочий вариант — доставить поток на внешний сервер и оттуда этот поток уже распространять по браузерам. В локальной сети тоже самое, только сервер устанавливается локально.
Videoman
Спасибо. Концептуально понятно как работает WebRTC-стек, в целом. А есть возможность в API браузера не использовать весь стек целиком, а отдельно настроить только те компоненты, которые непосредственно принимают RTP пакеты? Попробую уточнить, ипользуя аналогию с MPEG-DASH — это тоже стек нескольких протоколов и стандартов: HTTPS, MPD, MSE, ISOBMFF и т.д., но в конечной реализации никто не заставляет принимать сегменты по HTTPS и парсить .mpd файл. Вполне можно взять отдельно MSE и скармливать ему сегменты по WEB Sockets или генерировать их на лету. Можно ли выделить аналогичное подмножество в WebRTC со стороны браузера?