OSMF HLS плагин

http streaming hero

При работе над проектом Together передо мной была поставлена задача воспроизведения видео Apple HLS на Flash-платформе. Доставка видеоконтента в едином формате (в нашем случае — HLS), как правило, весьма удобна и обладает множеством преимуществ. Для работы с видео во Flash есть фреймворк OSMF, который легко расширяется при помощи плагинов и распространяется с открытым исходным кодом. Но есть одна проблема: фреймворк об HLS он ни слухом ни духом :^. Adobe продвигал использование RTMP, а потом — HTTP Dynamic Streaming (HDS), альтернативу Apple HLS. В этой статье мы расскажем про бесплатный HLS плагин, который мы разработали для работы с HLS в OSMF видеоплеерах.

HLS OSMF плагин выложен на GitHub. Рассмотрим же его поближе.

Данный плагин разработан на базе “Matthew’s HLS plugin” (увы, по приведенной ссылке скачать его уже нельзя, но был найден форк на github’e). Я доработал этот плагин, обеспечив корректную работу multi-bitrate streaming и добавив поддержку DVR streaming. Часть, отвечающую за обработку видеопотока, я не трогал: она корректно справлялась со своей задачей. По сути, эта часть вытаскивает из видеопотока MPEG TS видео в формате H.264 и при помощи NetStream appendBytes отправляет его на воспроизведение. А вот часть, отвечающая за работу с m3u8-плейлистом, была полностью переписана. Механизм работы плагина сделан идентично HDS (менеджер работы с видеопотоком и менеджер работы с индекс-файлом).

Сначала рассмотрим примеры использования плагина.

Использование:

1. Для StrobeMediaPlayback видеоплеера:

— подключить HLSDynamicPlugin.swf так же, как и любой другой плагин. Т.е в переменной flashvars прописать:

1
2
3
4
5
6
7
flashvars = {

 …,

 hls_plugin: ‘lt;url/to/HLSDynamicPlugin.swfgt;’

}

— статично подключить HLSPlugin.swc к проекту StrobeMediaPlayback и в функции onChromeProviderComplete(event:Event) после строчки:

1
factory = injector.getInstance(MediaFactory);

добавить код:

1
2
3
factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD, onLoadPlugin);
factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD_ERROR, onError);
factory.loadPlugin(new PluginInfoResource(new HLSPluginInfo()));

2. Для собственного OSMF-плеера есть два варианта:

— статично подключить HLSPlugin.swc к проекту и загрузить при помощи DefaultMediaFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private function initPlayer():void{
var factory:DefaultMediaFactory = new DefaultMediaFactory();
factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD, onLoadPlugin);
factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD_ERROR, onError);
factory.loadPlugin(new PluginInfoResource(new HLSPluginInfo()));
var res:URLResource = new URLResource( HLS_VIDEO );
var element:MediaElement = factory.createMediaElement(res);
if(element == null)
throw new Error('Unsupported media type!');
var player:MediaPlayer = new MediaPlayer(element);
var container:MediaContainer = new MediaContainer();
container.addMediaElement(element);
container.scaleX = .75;
container.scaleY = .75;
addChild(container);
}

— использовать HLSDynamicPlugin.swf, который динамически загружать при помощи DefaultMediaFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private function initPlayer():void{
var factory:DefaultMediaFactory = new DefaultMediaFactory();
factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD, onLoadPlugin);
factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD_ERROR, onError);
factory.loadPlugin(new URLResource(URL_TO_PLUGIN));
function onComplete(e:ParseEvent):void{
var res:URLResource = new URLResource( HLS_VIDEO );
var element:MediaElement = factory.createMediaElement(res);
if(element == null)
throw new Error('Unsupported media type!');
var player:MediaPlayer = new MediaPlayer(element);
var container:MediaContainer = new MediaContainer();
container.addMediaElement(element);
container.scaleX = .75;
container.scaleY = .75;
addChild(container);
}
function onError(e:ParseEvent):void{
trace("plugin load error!");
}
}

После этих манипуляций остается передать плееру ссылку на видеопоток (m3u8-плейлист) и наслаждаться просмотром видео.

Теперь рассмотрим технические особенности реализации плагина.

Что же у нас “под капотом”?

Для начала необходимо ознакомиться с документацией: acceptchecksnow.com HLS Specification.

В ней вы найдете очень познавательную информацию по таким вопросам, как: “Что такое m3u8-плейлист и что в него пишут?”, “Как правильно делать HLS-видеопоток?”, и “Как правильно этот же поток разобрать на стороне клиента?”. Так что, если вы захотите добавить в плагин обработку какого-то хитрого тега, без этого вам не обойтись.

Теперь, когда вы ознакомились с технологией подготовки видео для плагина, приступим собственно к процессу обработки внутри плагина:

1. По полученной ссылке (из URLResource) на m3u8-плейлист создается M3U8Element. Этот элемент и скачивает требуемый плейлист при помощи M3U8Loader.

2. Скачанный плейлист (по сути своей, текстовый файл) отдается на обработку M3U8PlaylistParser’у. На выходе мы получаем M3U8Playlist, содержащий в себе M3U8Item’ы (или другие M3U8Playlist’ы, в случае multi-bitrate streaming).

3. На этом этапе у нас уже есть “бинарное” представление плейлиста. Казалось бы, вперед — на обработку, ан-нет… Здесь мы должны создать HLSDynamicStreamingResource (наследник DynamicStreamingResource), на основании распарсенных данных плейлиста. Без данной процедуры OSMF не сможет нормально обработать плейлист с потоками разного качества (multi-bitrate streaming).

4. Теперь наш HLS ресурс (HLSDynamicStreamingResource) вновь передается плееру вместо изначально данного URLResource.

5. Теперь у нас есть ресурс, чего же мы ждем? Давайте видео! Стоп-стоп-стоп, скажет вам OSMF и при помощи HTTPStreamingHLSFactory превратит наш многострадальный плейлист (ресурс в текущем состоянии) в HTTPStreamingHLSIndexInfo.

6. И вот это самое IndexInfo и есть уже почти готовый к употреблению OSMF’ом продукт. “Почти?!”, — возмутитесь вы. “Да, почти”, потому что это всего-лишь информация на основании которой HTTPStreamingHLSIndexHandler уже загружает основной контент: видеопоток.

7. И что же у нас теперь то происходит? А вот что:

HTTPStreamingHLSIndexHandler получает от OSMF’а команду: getNextFile(). Это происходит либо по таймеру, либо в случае если была задействована перемотка. Соответственно, в ответ OSMF получает запрос на скачивание следующего чанка из плейлиста. После того как наш кусочек видео скачан и HTTPStreamSource перейдет в состояние READ, он говорит HTTPStreamingMP2TSFileHandler’у обработай-ка мне то что я скачал, вызывая функцию processFileSegment() (подробности ее работы разберем чуть позже)

8. Если HTTPStreamingHLSIndexHandler не нашел еще конца плейлиста, он соответственно запрашивает следующий кусочек видеопотока (т.е переходит к п.6), и так продолжается, пока плейлист не закончится.

Казалось бы вот и сказочке конец, но снова — нет :), пытливый читатель явно задался вопросом “А что же у нас происходит с live-трансляцией? Ведь в ней плейлист не заканчивается”. А ничего сложного: когда индекс текущего сегмента становится больше или равным длине списка чанков, и при этом поток является live, HTTPStreamingHLSIndexHandler просто запрашивает перезагрузку плейлиста и переводит OSMF в состояние “Ждем-с…”. Очень важный нюанс: по спецификации при multi-bitrate streaming (и так же поступает плагин) рекомендуется перезагружать только текущий плейлист, а не всё скопом. И, исходя из этого нюанса, обработка обновленного плейлиста сокращается до двух шагов:

— распарсить при помощи M3U8PlaylistParser’a
— обновить текущее HTTPStreamingHLSIndexInfo.

Обработка плейлиста в оригинальной версии плагина от Matthew работала так же во всех случаях, и как следствие без DynamicStreamingResource OSMF не мог корректно обработать плейлисты для multi-bitrate streaming.

Ну и, раз мы затронули тему live streaming, рассмотрим, в чем разница между DVR и обычным live:

Объединяет их по стандарту лишь одно: отсутствие тега #EXT-X-ENDLIST в конце. А разница лишь в способе обновления плейлиста:

— в DVR просто новые чанки “дописываются” в плейлист,
— в чистом live же, происходит “ротация” чанков. Т.е. плейлист имеет фиксированную длину, но каждый раз приходит с новыми чанками и при этом у него число в теге #EXT-X-MEDIA-SEQUENCE увеличивается на количество обновленных чанков.

Собственно, все это и есть процесс работы плагина при воспроизведении HLS-видео.

Краткая анатомия TS-видеопотока

Собственно, разбор TS-сегмента можно найти во все той же спецификации (и упоминаемых там ссылках), ну, а я только покажу, что обрабатывает плагин:

1. Первое, что нам нужно, это байт с кодом 0x47 (для синхронизации). Обработчик ищет его до последнего байта, и если он его не находит, значит, ему пришло «что-то не то».

2. Дальше идут 187 байт сервисных данных, из которых:

— второй байт содержит в себе: error indicator (0x80 бит), payload unit start indicator (0х40 бит), transport priority indicator (0х20 бит)
— следом идет packet ID, исходя из которого и происходит дальнейшая обработка данных
— 4-й байт содержит в себе: scrambling control bits и биты наличия adaptation field и собственно самих данных (payload data), а так же continuity count.

Далее, если установлен бит наличия adaptation field, вычитывается его длина и все… никакой обработки этих данных в плагине нет, и мы переходим непосредственно к самим данным (если они есть, о чем сигнализирует бит payload data):

Исходя из значения packet ID, данные могут быть разными, а именно:

— packet ID == 0: PAT данные; если был установлен payload unit start indicator, то они обрабатываются, если же нет, возвращается пустой ByteArray. Второй вариант нам не интересен, поэтому рассмотрим, что же есть в PAT’e:

— первый байт не используется
— второй содержит table ID
— далее 2 байта длины таблицы
— следующие 5 байт пропускаются
— и из дальнейшего вычитывается pmtPID и сохраняется для дальнейшей работы.
— последние 4 байта CRC данных тоже пропускаются

— packet ID == pmtPID (тот самый, ранее вычитанный): условия те же, что и для PAT. Начинка:

— первый байт не используется
— tableID (обязательно должен быть равен 0х02)
— 2 байта длины таблицы
— 7 байт пропускаются (версии, резервные и т.п)
— длительность programInfo
— programInfo (пропускается)
— байт типа данных: (0x1b — H.264 video, 0x0f — AAC Audio / ADTS, остальные типы плагином не обрабатываются)
— pid пакета данных
— длина остальных данных (используется чтобы их пропустить).
— CRC-байты не используются

— packet ID == audioPID (PID из PMT, если тип был аудио): просто обрабатывает аудиопоток. С процессом обработки вы можете ознакомиться, взглянув на функцию processES в классе HTTPStreamingMP2PESAudio

— packet ID == videoPID (случай тот же, что и у аудио): обработка видеопотока. Так же, как и в случае аудио, рекомендую ознакомиться с функцией processES, но уже в классе HTTPStreamingMP2PESVideo.

Успешного воспроизведения видео вашим плеерам! 😉