[Перевод] Как работает протокол HLS
Вот уже несколько недель я разрабатываю серверную поддержку коротких видео для компании Bluesky.
Основное назначение этой фичи — обеспечивать потоковый показ небольших (максимум 90 секунд) видеороликов. Показ должен быть бесплатным и при этом не слишком накладным для нас.
Чтобы укладываться в эти ограничения, мы попытались использовать сеть доставки видео-контента (CDN), которая могла бы нести основное бремя поддержки той полосы передачи данных, которая обеспечивала бы показ потокового видео по требованию.
Притом, что такая CDN — это полнофункциональный продукт, мы не хотели чрезмерно замыкаться на конкретном производителе и поэтому решили внести в нашу потоковую платформу некоторые улучшения, тем самым расширив спектр предлагаемых на ней услуг и творчески подойти к реализации протоколов потокового видео.
Вот некоторые вещи, которые мы хотели обеспечивать, и которые при этом не предоставляются «из коробки»:
Вести счёт просмотров, пользовательских сеансов, а также отслеживать, сколько времени длится такой сеанс. Всё это нужно нам для получения более точной обратной связи о том, какова производительность видео.
Обеспечивать динамическую поддержку скрытых субтитров, причём, предусмотреть для этой функции достаточную гибкость, чтобы впоследствии её можно было автоматизировать.
Где-нибудь хранить транскодированную версию исходных видео, чтобы у нас был долговечный «источник истины» для видеоконтента, к которому можно было бы при необходимости обратиться.
В конце каждого потокового видео прикреплять «трейлер» для дальнейшего ветвления роликов в 3-секундные фрагменты в стиле ТикТок.
В этом посте сосредоточимся на вышеупомянутых фичах, имеющих отношение к HLS, а именно на учёте длительности просмотра, скрытых субтитрах и трейлерах.
HLS — это просто набор текстовых файлов
HTTP Live Streaming (HLS) — это стандарт, разработанный компанией Apple в 2009 году. Этот стандарт обеспечивает адаптивный битрейт при live-трансляциях и потоковое видео по технологии VOD (видео по запросу).
В рамках этого поста будет объяснена только потоковая передача HLS VOD.
Плеер, реализующий протокол HLS, может автоматически корректировать качество передаваемого видео, ориентируясь на то, какие условия складываются в сети. Кроме того, сервер, реализующий протокол HLS, должен предоставлять один или более вариантов (variants) потокового медиа, приспосабливаясь под изменение условий в сети. Благодаря такому подходу можно плавно понижать качество потоковой передачи, не прерывая при этом воспроизведения видео.
Вот как HLS реализует эти возможности. Он создаёт ряд файлов «плейлиста», записываемых обычным текстом (.m3u8). Из этих файлов плеер узнаёт, какие варианты битрейта и разрешения предоставляет сервер. Таким образом плеер может «решить», какой вариант (variant) пойдёт в потоковую передачу.
В HLS различаются две разновидности файлов «плейлистов»: Master Playlists и Media Playlists.
Master Playlists
Master Playlist — это первый файл, выбираемый вашим видеоплеером. В нём содержится серия вариантов (variants), указывающих на дочерние Media Playlists. Также в нём описывается примерный битрейт исходников конкретного варианта, а ещё кодеки и разрешения, используемые в этих исходниках.
$ curl https://my.video.host.com/video_15/playlist.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=688540,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360
360p/video.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1921217,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720
720p/video.m3u8
В вышеприведённом файле нужно обратить внимание, прежде всего, на параметры RESOLUTION
и на ссылки {res}/video.m3u8
.
Как правило, сначала медиаплеер пробует самое низкое разрешение, а затем переходит ко всё более высоким по мере того, как наладится сетевое соединение между вами и сервером.
Ссылки в этом файле — это указатели на Media Playlists, обычно задаваемые как относительные пути из Master Playlist. Они устроены так, что, если бы мы хотели взять 720p Media Playlist
, то перешли бы по ссылке https://my.video.host.com/video_15/720p/video.m3u8
.
В Master Playlist также могут содержаться аудио-директивы на много дорожек (для скрытых субтитров), но пока давайте подробнее рассмотрим, что такое Media Playlist.
Media Playlists
Media Playlist — это ещё один вид файла, записываемый обычным текстом. В нём ваш видеоплеер берёт два ключевых источника информации. Во-первых, это список сегментов медиа, Segments (это видео-файлы с расширением .ts
), а во-вторых — заголовки каждого сегмента, сообщающие плееру, в какой среде выполняется такое медиа.
$ curl https://my.video.host.com/video_15/720p/video.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:4
#EXTINF:4.000,
video0.ts
#EXTINF:4.000,
video1.ts
#EXTINF:4.000,
video2.ts
#EXTINF:4.000,
video3.ts
#EXTINF:4.000,
video4.ts
#EXTINF:2.800,
video5.ts
В этом Media Playlist описано видео длительностью 22,8 секунд (пять 4-секундных сегментов + один 2,8-секундный сегмент).
В этом плейлисте описано видео в формате VOD
. Таким образом, нам известно, что в этом плейлисте полностью содержится медиа-информация, необходимая плееру.
В TARGETDURATION
сообщается максимальная длительность каждого Segment, поэтому плееру известно, сколько сегментов буферизовать заранее. В ходе live-передачи плееру также сообщается, как часто обновлять файл плейлиста для обнаружения новых сегментов.
Наконец, заголовки EXTINF
для каждого Segment указывают длительность следующего файла .ts Segment. В свою очередь, по относительным путям video#.ts плеер узнаёт, откуда загружать конкретные медиа-файлы.
Где находятся конкретные медиа-файлы?
К данному моменту видеоплеер загрузил два файла плейлистов .m3u8 и получил множество метаданных о том, как воспроизводить видео, но медиа-файлов как таковых ещё не загрузил.
Именно в файлах .ts, на которые стоят ссылки в Media Playlist, фактически хранятся интересующие нас медиа-файлы. Поэтому, если мы хотим управлять плейлистами, но позволяем CDN выдавать нам сами медиа-файлы, то не может просто взять и переадресовать эти запросы video#.ts в нашу CDN.
Файлы .ts — это короткие медиа-фрагменты, закодированные в формате Transport Stream MPEG-2, которые могут содержать как видео, так и аудио вместе с видео.
Отслеживание просмотров
Вот чем можно воспользоваться для отслеживания просмотров в наших HLS-потоках: как известно, любой видеоплеер должен сначала загрузить Master Playlist.
Когда пользователь запрашивает Master Playlist, можно динамически модифицировать результаты, предоставляя сеансовый ID (SessionID) в ответ на каждый отклик. Так можно отслеживать пользовательские сеансы, не прибегая ни к куки-файлам, ни к заголовкам:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=688540,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360
360p/video.m3u8?session_id=12345
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1921217,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720
720p/video.m3u8?session_id=12345
Теперь, когда видеоплеер выбирает Media Playlists, он будет при этом включать строку-запрос, по которой можно будет идентифицировать сеанс потоковой передачи, убедиться, что мы не учитываем какие-либо просмотры видео по два раза, а также следить, какие именно фрагменты видео были загружены в данном сеансе.
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:4
#EXTINF:4.000,
video0.ts?session_id=12345&duration=4
#EXTINF:4.000,
video1.ts?session_id=12345&duration=4
#EXTINF:4.000,
video2.ts?session_id=12345&duration=4
#EXTINF:4.000,
video3.ts?session_id=12345&duration=4
#EXTINF:4.000,
video4.ts?session_id=12345&duration=4
#EXTINF:2.800,
video5.ts?session_id=12345&duration=2.8
Наконец, когда видеоплеер выберет файлы медиа-сегментов Segment, можно измерить просмотр Segment до того, как переправить его в сеть CDN с кодом 302. Так мы сможем узнать величину сегмента, загруженного при этом сеансе (в видео-секундах), а также какие именно сегменты (Segments) были загружены.
У этого метода есть свои ограничения. Например, если медиаплеер загрузил сегмент, это ещё не означает, что он показал этот фрагмент пользователю. Но это максимум, который мы можем сделать без как следует оснащённого медиа-плеера.
Добавление субтитров
Субтитры включаются в Master Playlist в качестве variant, а затем на них ставится ссылка в каждом из вариантов видео (variants), чтобы плееру было известно, откуда загружать субтитры.
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="en_subtitle",DEFAULT=NO,AUTOSELECT=yes,LANGUAGE="en",FORCED="no",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subtitles/en.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=688540,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,SUBTITLES="subs"
360p/video.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1921217,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720,SUBTITLES="subs"
720p/video.m3u8
Точно как и при работе с Media Playlists, нам потребуется файл Media Playlist для отслеживания субтитров. Из этого источника плеер узнает, откуда загружать исходные файлы, и какая часть длины всего потока в них заключена.
$ curl https://my.video.host.com/video_15/subtitles/en.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:22.8
#EXTINF:22.800,
en.vtt
В нашем случае, поскольку мы выдаём лишь одно короткое видео, мы можем предоставить единственный Segment, указывающий на файл с субтитрами WebVTT, и этот файл соответствует всей длине видео.
Если заглянете в файл en.vtt, то увидите там что-то подобное:
$ curl https://my.video.host.com/video_15/subtitles/en.vtt
WEBVTT
00:00.000 --> 00:02.000
According to all known laws
of aviation,
00:02.000 --> 00:04.000
there is no way a bee
should be able to fly.
00:04.000 --> 00:06.000
Its wings are too small to get
its fat little body off the ground.
...
Медиаплеер способен читать файлы в формате WebVTT и выводить пользователю каждую строчку субтитров именно тогда, когда нужно.
При работе с более длинными видео вы, возможно, захотите дополнительно разделить ваши VTT-файлы на более мелкие сегменты и соответствующим образом обновить субтитры в Media Playlist.
Чтобы предоставлять субтитры в разных версиях и на разных языках, просто добавьте дополнительные строки EXT-X-MEDIA:TYPE=SUBTITLES
в файл Master Playlist и задайте правильные NAME, LANGUAGE
(если они отличаются), а также URI определений дополнительных вариантов субтитров.
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="en_subtitle",DEFAULT=NO,AUTOSELECT=yes,LANGUAGE="en",FORCED="no",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subtitles/en.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="fr_subtitle",DEFAULT=NO,AUTOSELECT=yes,LANGUAGE="fr",FORCED="no",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subtitles/fr.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="ja_subtitle",DEFAULT=NO,AUTOSELECT=yes,LANGUAGE="ja",FORCED="no",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subtitles/ja.m3u8"
Прикрепление субтитров
Если нужно продвигать бренд (и в других случаях, например, добавить рекламу), попробуйте добавлять в плейлист такие сегменты видео, при которых контент ролика или плейлиста менялся бы сам, без необходимости отдельно прикреплять новый контент и перекодировать весь файл исходников.
К счастью, при работе с HLS можно с лёгкостью вставлять сегменты в Media Playlist, воспользовавшись таким ловким фокусом:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:4
#EXTINF:4.000,
video0.ts
#EXTINF:4.000,
video1.ts
#EXTINF:4.000,
video2.ts
#EXTINF:4.000,
video3.ts
#EXTINF:4.000,
video4.ts
#EXTINF:2.800,
video5.ts
#EXT-X-DISCONTINUITY
#EXTINF:3.337,
trailer0.ts
#EXTINF:1.201,
trailer1.ts
#EXTINF:1.301,
trailer2.ts
#EXT-X-ENDLIST
В этом Media Playlist мы используем заголовок HLS EXT-X-DISCONTINUITY
, по которому медиаплеер узнаёт, что у следующих сегментов может отличаться битрейт, разрешение и соотношение сторон.
Если мы предоставим заголовок DISCONTINUITY
, то сможем добавлять в этой точке другие сегменты наряду с обычными, и они смогут указывать на иной источник медиа, разбитый на файлы .ts.
Напомню, что HLS в данном случае позволяет указывать как абсолютные, так и относительные пути, поэтому мы могли бы как предоставить здесь полный URL для этих файлов trailer#.ts, так и виртуально маршрутизировать их, чтобы в них сохранялся контекст пути на просмматриваемое в данный момент видео.
Обратите внимание: здесь не требуется предоставлять заголовок DISCONTINUITY
, а файлы трейлера мы можем называть и по принципу video{6-8}.ts
, если захотим. Но для ясности и для того, чтобы плеер функционировал правильно, заголовок DISCONTINUITY
лучше применять в тех случаях, когда битрейт и разрешение вашего трейлера не совпадают с аналогичными показателями других сегментов данного видео.
Когда видеоплеер приступает к воспроизведению видео, он проследует от video5.ts
к trailer0.ts
без запинки, поэтому покажется, как будтро трейлер входит в состав оригинального видео.
При таком подходе можно динамически менять содержимое трейлера для всех видео, активно кэшировать файлы трейлера .ts Segment для повышения производительности и обходиться без встраивания трейлера в конце каждого файла с исходниками видео.
Заключение
Итак, у нас получился сервис для потоковой передачи видео, и при работе с ним мы можем отслеживать просмотры, смотреть, сколько длятся сеансы, динамически поддерживать скрытые субтитры, а также вставлять в видео рекламные трейлеры, способствуя развитию платформы.
Протокол HLS не назовёшь запредельно сложным. Абсолютное большинство информации в нём заключено в виде человеко-читаемых файлов, записанных обычным текстом. Их легко изучать без подготовки и отслеживать, как они используются в продакшене.
Приступив к этому проекту, я практически ничего не знал о самом протоколе, но смог сказать несколько файлов в формате.m3u8, а затем, углубившись в них, выяснил, как именно работает протокол. После этого я смог написать собственную реализацию HLS-сервера, позволявшую выполнять все функции, необходимые для потоковой передачи видео для нужд Bluesky.
Чтобы подробнее изучить HLS, можете посмотреть здесь официальный документ RFС. В нём описаны все возможности, разобранные выше, и не только.
Надеюсь, этот пост вдохновит вас и на исследование других протоколов, с которыми вы ежедневно работаете. Повозитесь с ними, попробуйте скачивать те файлы, которые обычно браузер интерпретирует за вас, убедитесь сами, насколько просты эти системы, которые на первый взгляд кажутся такими сложными.