“О нет, этот HLS меня доконает” — сказал Игорь и долил горячего чая в кружку с толстыми краями. “Клиенты снова жалуются на фризы, а ведь мы только вчера выкатили очередной релиз с исправлениями.”

Браузер как стример и плеер

Браузер сегодня умеет захватывать видео с вебкамеры, сжимать кодеками и отправлять в сеть, т.е. функционировать как видео-кодировщик. Проще говоря, современный браузер содержит в себе модуль кодировщика (encoder + streamer + decoder + player), имеющий Javascript API. Эта встроенная в браузер подсистема или, если угодно библиотека, называется общим термином WebRTC — Web Real Time Communications.

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

Раз браузер и стример и плеер, он может выполнять соответствующие функции:

  • отправить видеопоток на сервер и записать
  • отправить видеопоток и транслировать
  • отправить видеопоток p2p
  • отправить свой видеопоток и одновременно играть чужой (видеочат или видеоконференция)
  • играть видеопоток с любого источника, который конвертируется в WebRTC на стороне сервера (например с RTSP камеры или RTMP потока)

Делается это все с помощью Javascript API. Если предельно упростить пример кода на Javascript, он может выглядеть примерно так:

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

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

 

Где нет WebRTC, там есть HLS

Вроде бы все хорошо — публикуем потоки, играем потоки в браузерах, в том числе и в мобильных, и даже в iOS Safari. Все работает до тех пор, пока не встретится браузер, не поддерживающий WebRTC, а такие браузеры сегодня, надо признать, не редкость. Там, где не работает WebRTC, как правило хорошо работает HLS. Особенно это прослеживается на Apple устройствах вроде приставок Apple TV.

Возникает вполне естественная потребность — играть потоки и по HLS и по WebRTC, т. е. Предоставить разработчику выбор чем конкретно проигрывать поток пользователя. И далее все зависит от источника потока. Дело в том, что HLS был задуман много лет назад в основном для того, чтобы играть VOD и Live трансляции. Тогда не предъявлялось каких-либо серьезных требований к реалтайму, задержкам, непривередливости в выборе кодеков, и т.д.

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

Если депакетизировать WebRTC поток и просто сконвертировать его в HLS — это будет работать, но не везде. А именно, это может не работать в нативном HLS плеере, который живет в браузерах iOS Safari, Mac OS Safari, Apple TV. Поэтому если вы при работе с конвертером WebRTC > HLS увидите фризы видно на айфоне или Apple TV, не пугайтесь. Возможно, это оно.

Почему так происходит: нативный Apple плеер для HLS имеет свое представление о том, каким должен быть входящий на него поток. В спецификациях Apple упоминается: H.264 кодек, AAC 128 kHz семплинг рейт, GOP 30 и т.д. И еще часть неявных требований, которых нет в спецификациях, зашиты в самой реализации нативных плееров и их можно вычленить только опытным путем. Например: сервер должен отдавать HLS сегменты сразу после отдачи m3u8 плейлиста, поток должен быть с неизменной конфигурацией H.264 битстрима, и т.д. Если что-то упустили, будет фриз.

 

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

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

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

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

scheme_transcoding_WebRTC_HLS_WCS_RTSP_RTMP_iOS_browser_MacOS_CDN

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

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

scheme_adaptation_WebRTC_HLS_WCS_RTSP_RTMP_iOS_browser_MacOS_CDN

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

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

scheme_converting_WebRTC_HLS_WCS_RTSP_RTMP_iOS_browser_MacOS_CDN

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

 

WebRTC в HLS на CDN

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

scheme_cdn_hls_WebRTC_HLS_WCS_RTSP_RTMP_iOS_browser_MacOS_CDN

Проблема уже была обозначена немного выше, и возникает эта проблема как правило в нативных плеерах: 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 нарезке. Даже если это займет три секунды, плеер это не спасет. Он уйдет в фриз.

scheme_stream_request_WebRTC_HLS_WCS_RTSP_RTMP_iOS_browser_MacOS_CDN

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

scheme_no_Lazy_Loading_WebRTC_HLS_WCS_RTSP_RTMP_iOS_browser_MacOS_CDN

Второй вариант более элегантный, но тоже обходной. Мы отдаем первому подключившемуся пользователю видео картинку, но это пока еще не тот стрим, который он желает увидеть — это прелоадер. Так как мы что-то должны отдать уже сейчас и сделать это немедленно, а исходного стрима у нас нет (он еще заказывается и доставляется с 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, мы показываем его в том же соотношении сторон, в котором заходит стрим.

scheme_size_preloader_WebRTC_HLS_WCS_RTSP_RTMP_iOS_browser_MacOS_CDN

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

 

scheme_key_frame_WebRTC_HLS_WCS_RTSP_RTMP_iOS_browser_MacOS_CDN

 

Итоги

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