hls.js: подробный разбор

Автор: Николай СапуновОбновлено: август 202632 мин чтения
Содержание статьи +

Кратко

Открытая библиотека hls.js – это решение для воспроизведения HTTP Live Streaming (HLS) во всех современных браузерах, кроме Safari. К 2026 году она станет самым популярным видеоплеером в вебе: несколько миллионов загрузок в неделю на npm, стабильная версия v1.6 и альфа-версия v1.7, выпущенная в марте. По сути, это не один плеер, а тесно связанный набор контроллеров – stream controller, ABR controller, buffer controller, EME controller и LL-совместимый загрузчик, – которые накладывают формат HLS-плейлистов поверх браузерного API Media Source Extensions (MSE). В этой статье мы разберём архитектуру по контроллерам, покажем семь строк кода, запускающих простейший плеер, опишем продакшен-стратегию восстановления после ошибок и расскажем, какие настройки действительно влияют на адаптивный битрейт, низкую задержку и поддержку multi-DRM. К концу статьи вы поймёте, зачем нужен hls.js, когда использовать его вместо нативного <video>, когда (почти никогда) стоит форкать его и какие фичи из v1.6 ваша команда, возможно, ещё не включила: HLS Interstitials, HEVC поверх MPEG-2 Transport Stream, FairPlay через современный EME и ManagedMediaSource, который наконец позволяет работать в Safari на iPhone.

Зачем это вам

Если вы доставляете видео в браузер, мобильный веб или smart-TV в 2026 году, hls.js почти наверняка уже входит в ваш бандл – или должен быть там, потому что Safari воспроизводит HLS нативно, а все остальные браузеры – нет. Эта статья поможет продакт-менеджеру задать инженеру правильные вопросы об адаптивном битрейте, восстановлении после сбоев и DRM, а фронтенд- или smart-TV-инженеру – сформировать полную ментальную модель библиотеки: контроллеры, события для продакшн-телеметрии, конфигурации, которые меняют поведение ABR без форка, и четыре семейства ошибок, которые нужно обработать в первый же день. Предварительные знания о стриминге не требуются – каждое понятие объясняется по ходу. К концу вы поймёте, почему JavaScript-бандл объёмом 1,5 мегабайта – это всё, что стоит между JPEG и стримом уровня Netflix в открытом вебе, и какая одна настройка включает LL-HLS для 3-секундного лайва без переписывания плеера.

Что делает hls.js и чем он не является

Самое краткое и точное определение таково: hls.js – это JavaScript-библиотека, которая читает HLS-плейлист, скачивает указанные в нём видео-чанки, на лету перепаковывает их в формат, понятный браузеру, передаёт байты в API Media Source Extensions и генерирует поток событий, достаточный для построения полноценного плеера с пользовательским интерфейсом – всё это в рамках открытого пакета с лицензией MIT, устанавливаемого через npm install как обычную зависимость. Это не UI: ни кнопок, ни скина, ни тач-зон. Это не транскодер: каждый байт, который он передаёт в браузер, уже был закодирован вашим пакейджером. И это не мультипротокольный плеер: hls.js воспроизводит только HLS, но не DASH; для DASH используют Shaka Player или dash.js. Сама библиотека описывает себя одной строкой – «HLS.js is a JavaScript library that plays HLS in browsers with support for MSE» (video-dev/ hls.js, README, accessed 2026-05-25) – и эта фраза полностью отражает суть продукта.

Библиотека построена на двух стандартах W3C, разработка и внедрение которых заняли около десяти лет. Первый – Media Source Extensions (W3C, Media Source Extensions™, Recommendation, 17 ноября 2016; более поздние редакции отслеживаются как Media Source Extensions 2 в статусе Candidate Recommendation на протяжении 2025 года), позволяющий JavaScript добавлять произвольные видео- и аудиоданные в SourceBuffer, прикреплённый к <video>. Второй – Encrypted Media Extensions (W3C, Encrypted Media Extensions, Recommendation, 18 сентября 2017; Working Draft обновлён 20 мая 2026), обеспечивающий JavaScript изолированный способ согласования ключей расшифровки с браузерным Content Decryption Module. Без MSE невозможно подавать сегментированное видео в <video> из JavaScript; без EME – воспроизводить платный премиум-контент. Библиотека hls.js интегрирует протокол HLS, описанный в IETF RFC 8216 (HTTP Live Streaming, август 2017) и расширенный Apple в HLS Authoring Specification for Apple Devices (ревизия 2025-09), с этими двумя браузерными API.

Рисунок 1. Два пути воспроизведения HLS в браузере. Safari использует нативный путь; всем остальным нужен hls.js.

Это можно проверить одной строкой в новой вкладке: Hls.isSupported() возвращает true в Chrome, Edge, Firefox и Opera, поскольку в них поддерживается MSE, и возвращает false в Safari, где MSE настолько ограничен, что hls.js переходит на нативный <video>, который сам обрабатывает HLS-URL. На iOS Safari (и iPadOS Safari) правильный подход – вообще не подключать hls.js при первом рендере, указать video.src в URL плейлиста и передать работу AVFoundation: фреймворк Apple уже нативно понимает HLS, обеспечивает аппаратное декодирование и энергосберегающее поведение, чего JavaScript-решение достичь не может. На всех остальных браузерах используется путь через hls.js.

Почему эта библиотека вообще существует

HLS придумала Apple, ратифицировала Apple и впервые внедрила на своих устройствах. Safari поддерживает HLS нативно, потому что AVFoundation – медиафреймворк macOS и iOS – умеет обрабатывать плейлисты .m3u8 и сегменты .ts с 2009 года. Остальные браузерные движки – Blink (Chrome, Edge, Opera) и Gecko (Firefox) – отказались от встроенной поддержки HLS-демультиплексора: во-первых, потому что протокол воспринимался как контролируемый Apple, а во-вторых, потому что API MSE изначально проектировалось так, чтобы любая JavaScript-библиотека могла реализовать нужный стриминг-протокол. В результате в открытом вебе образовался вакуум: индустриальный стандарт стриминга работает «из коробки» только на iPhone – и больше нигде.

hls.js создал Guillaume du Pontavice в Dailymotion в 2015 году, чтобы заполнить этот пробел. Первоначальная задача была скромной: распарсить HLS-манифест, скачать сегменты MPEG-2 Transport Stream, преобразовать их в фрагментированный MP4 (формат, понятный MSE), и передать байты в SourceBuffer. За одиннадцать лет библиотека обросла контроллерами – ABR, буфером, EME, аудиодорожками, субтитрами, content steering, interstitials – и превратилась в полноценный кросс-браузерный HLS-плеер. Сейчас над ним работает рабочая группа, в которую входят Rob Walch (principal engineer в JW Player) и разработчики из Mux, Akamai, Bitmovin, Cloudflare и десятков стриминговых вендоров. По состоянию на май 2026 года у репозитория на GitHub было 16 500 звёзд и 2 700 форков, а npm-пакет к началу 2026 года превысил отметку в несколько миллионов загрузок в неделю (npm registry, hls.js, accessed 2026-05-25). Среди пользователей в продакшене – JW Player, Mux, Wowza, Akamai, Bitmovin, веб-клиент Twitch и десятки OTT-сервисов через интеграции с JW Player и Video.js.

Политический подтекст здесь важен, потому что он определяет дорожную карту. hls.js не внедряет поведение, которого нет в Safari; когда Apple добавляет новую функцию в HLS Authoring Specification – например, interstitials, content steering, Pathway Cloning, HEVC поверх MPEG-2 TS – hls.js следует за ней. А когда рабочая группа вносит что-то, чего Apple ещё не реализовала – например, эвристики ретраев, параметры настройки ABR, дополнительные механизмы восстановления ошибок – это реализуется как конфигурация, а не как отклонение от спецификации. У библиотеки нет собственного мнения о HLS: она транслирует позицию Apple в браузерах, которые Apple не контролирует.

Архитектура в одном абзаце

Работающий экземпляр hls.js – это небольшой граф объектов, подвешенных к классу Hls верхнего уровня. Конструктор Hls создаёт контроллеры: stream controller, управляющий машиной состояний получения сегментов; level controller, отвечающий за мультивариантный плейлист; ABR controller, выбирающий следующий уровень качества; buffer controller, взаимодействующий с объектами SourceBuffer из MSE; EME controller, который общается с браузерным CDM при наличии DRM; подсистему loader, выполняющую HTTP-запросы; и transmuxer worker, преобразующий MPEG-2 TS в fMP4. Все они связаны через единую шину событий. Вы вызываете hls.attachMedia(video), чтобы привязать экземпляр к <video>, затем hls.loadSource(url) – чтобы начать загрузку плейлиста, и слушаете события для управления интерфейсом. Всё остальное – настройка.

Рисунок 2. Граф контроллеров hls.js. Каждый контроллер подписан на шину событий; почти любое публичное событие, которое слушает ваше приложение, исходит из одного из этих блоков.

Этот абзац – вся картина. Дальше статья подробно раскрывает каждый блок: называет события, которые он генерирует, и указывает, какие настройки имеют значение.

Stream controller

Stream controller – сердце библиотеки. Он управляет небольшой state machine: STOPPED, IDLE, KEY_LOADING, FRAG_LOADING, WAITING_LEVEL, PARSING, PARSED, BUFFER_FLUSHING, ENDED, ERROR – и последовательно проходит по её состояниям, обрабатывая по одному сегменту за раз. В простейшем случае машина стартует в состоянии IDLE, запрашивает у level controller, какой вариант нужно загружать, переходит в FRAG_LOADING, пока loader получает сегмент, затем – в PARSING, когда байты пришли и transmuxer начинает конвертацию MPEG-2 TS → fMP4. По завершении конвертации она переходит в состояние PARSED, передаёт байты buffer controller для добавления и возвращается в IDLE, чтобы обработать следующий сегмент. Если используется DRM, машина ожидает в KEY_LOADING, пока EME controller получает лицензию. При ребуферинге плеера она приостанавливается в BUFFER_FLUSHING. Когда поток завершается, она переходит в состояние ENDED. При возникновении ошибки машина переходит в ERROR и эмитит Hls.Events.ERROR.

Состояния не академические. Каждое событие, которое вы отслеживаете в продакшене – FRAG_LOADED, LEVEL_LOADED, BUFFER_APPENDED, MANIFEST_PARSED – возникает при конкретном переходе, и порядок этих переходов строго определён. Если вы разрабатываете инструмент для измерения «time to first frame» поверх hls.js (что стоит сделать), вы будете измерять это время как интервал от MEDIA_ATTACHING до первого FRAG_BUFFERED для видео-SourceBuffer, и этот интервал точно соответствует переходу stream controller от состояния STOPPED к PARSED для первого сегмента.

Контроллер уровня и контроллер ABR

Контроллер уровня управляет мультивариантным плейлистом (.m3u8 с тегом EXT-X-STREAM-INF на вариант) и медиа-плейлистами каждого варианта (.m3u8 со списком сегментов в EXTINF). Он один раз получает мультивариантный плейлист, а затем периодически обновляет медиа-плейлист активного варианта – по расписанию для live (раз в target duration) или один раз для VOD. Варианты передаются контроллеру ABR через массив levels.

Контроллер ABR выбирает, какой вариант загружать следующим. Алгоритм по умолчанию в hls.js – это эвристика на основе пропускной способности: отслеживается недавняя скорость скачивания с помощью экспоненциально взвешенного скользящего среднего (EWMA) по последним сегментам, оценка умножается на коэффициент безопасности (по умолчанию 0,7), и выбирается самый высокий доступный вариант, у которого заявленный битрейт ниже скорректированной оценки. Если буфер короткий или загрузка занимает больше времени, чем ожидалось, контроллер может прервать загрузку текущего сегмента и понизить качество; правило примерно такое: «если в буфере меньше двух сегментов и прогнозируемое время загрузки исчерпает буфер – отменить загрузку и попробовать вариант ниже» (video-dev/hls.js, src/controller/abr-controller.ts, ветка master, дата обращения: 25 мая 2026). Контроллер можно заменить: hls.abrController = new MyController(hls) – поддерживаемый способ переопределения.

Две настройки управляют поведением ABR без необходимости форка. abrBandWidthFactor (по умолчанию 0,95) – это коэффициент безопасности, применяемый к оценке пропускной способности при стабильном выборе качества. abrBandWidthUpFactor (по умолчанию 0,7) – более консервативный коэффициент, используемый при повышении качества; асимметрия осознанная: слишком низкий уровень качества снижает восприятие, а слишком высокий – вызывает ребуферы, которые вредят времени просмотра сильнее, чем потеря качества в 100 кбит/с. Ветка v1.7-alpha добавляет abrSwitchInterval как третью настройку, ограничивающую частоту смены уровней качества в секунду; это подавляет паттерн «ABR прыгает между ступенями», который операторы наблюдают на сетях с джиттером (video-dev/hls.js, Release v1.7.0-alpha.1, 5 марта 2026).

Стоит отметить: оригинальная Buffer Occupancy Lyapunov-based Adaptation, сокращённо BOLA – статья Park и Chiang, IEEE INFOCOM 2016 – была интегрирована в hls.js как экспериментальный контроллер около 2020 года, но в продакшен-развёртываниях она менее заметна, чем throughput-ориентированный ABR; у проекта dash.js исторически более отполированная реализация BOLA. В hls.js по умолчанию для большинства стримов используется throughput-ориентированный подход, а BOLA доступна как опция в конфиге. Отдельная статья Learn подробно разбирает алгоритм BOLA и объясняет, в каких случаях то или иное семейство алгоритмов оказывается эффективнее.

Контроллер буфера

Контроллер буфера – тонкий слой над API Media Source Extensions. Он создаёт MediaSource, подключает его к элементу <video> через URL.createObjectURL, открывает по одному SourceBuffer на трек (обычно один видео, один аудио, иногда один субтитров) и сериализует операции append, remove, end-of-stream, поскольку MSE не поддерживает параллельные операции на одном SourceBuffer. Он также решает технические детали: добавляет init-сегменты фрагментированного MP4 перед медиа-сегментами, рассчитывает допустимые зазоры в буфере при переключении качества и (начиная с версии 1.6) обрабатывает новый класс ошибок MEDIA_SOURCE_REQUIRES_RESET, восстанавливая ситуацию, когда MSE был закрыт, а буфер продолжал считать, что он ещё открыт (video-dev/ hls.js, Release v1.7.0-alpha.1, 5 марта 2026).

В iOS Safari 17 и выше buffer controller также умеет работать через ManagedMediaSource – подмножество MSE, которое Apple внедрила в iOS 17, чтобы наконец разрешить JavaScript-плеерам функционировать на iPhone. ManagedMediaSource – это не полноценный MSE: у него более строгие правила освобождения памяти браузером, а также требование, чтобы элемент source был обёрнут в дочерний <source> для <video>. hls.js автоматически определяет его наличие и перенаправляет работу через него на iOS, если он доступен. Результат: hls.js теперь может воспроизводить MSE-потоки в стиле DASH на iPhone Safari впервые в истории – и кросс-платформенному стеку больше не нужна «iOS-ветка», на которую полагались десять лет. При этом на практике большинство команд всё равно предпочитают нативный AVFoundation на iOS, если контент – HLS, поскольку этот путь обеспечивает аппаратное ускорение от начала до конца.

Контроллер EME

EME-контроллер отвечает за обработку DRM. Когда в манифесте встречается тег EXT-X-KEY, указывающий на систему ключей (Widevine urn:uuid:edef8ba9-..., FairPlay com.apple.streamingkeydelivery или PlayReady urn:uuid:9a04f079-...), EME-контроллер перехватывает управление у stream-контроллера в состоянии KEY_LOADING, вызывает navigator.requestMediaKeySystemAccess, открывает MediaKeySession, запрашивает лицензию по настроенному URL и передаёт ключ в модуль дешифрования контента браузера. Buffer-контроллер не может добавлять зашифрованные данные в буфер до получения ключа – поэтому медленный сервер лицензирования чаще всего становится причиной «чёрного экрана без ошибки» на платных потоках.

Конфигурация в современном hls.js (версии 1.3 и выше) находится по адресу drmSystems:

const hls = new Hls({
  emeEnabled: true,
  drmSystems: {
    'com.widevine.alpha':           { licenseUrl: 'https://drm.example.com/widevine'  },
    'com.microsoft.playready':      { licenseUrl: 'https://drm.example.com/playready' },
    'com.apple.fps':                { licenseUrl: 'https://drm.example.com/fairplay',
                                      serverCertificateUrl: 'https://drm.example.com/fairplay/cert' },
  },
});

Старый шорткат widevineLicenseUrl всё ещё работает, но устарел; новый код следует использовать с drmSystems. Поддержка FairPlay через современный EME – это не устаревший webkit-специфичный путь до появления EME, который Apple внедрила первой, – появилась в версии 1.6, а в 1.6.15 исправили баг с патчингом key-IDA FairPlay, из-за которого возникали ошибки "keyId is null" на некоторых конфигурациях энкодеров (video-dev/hls.js, Release v1.6.15, 19 ноября 2025). Если у вас multi-DRM, фиксируйте версию 1.6.14 или выше. Полная ментальная модель EME – что такое CDM, чем отличается cenc от cbcs, как проходит обмен лицензией от начала до конца – описана в нашем разборе Encrypted Media Extensions (EME).

Семь строк кода

Рабочий hls.js-плеер помещается в твит. Это канонический паттерн:

import Hls from 'hls.js';

const video = document.querySelector('video');
const url   = '/streams/master.m3u8';

if (Hls.isSupported()) {
  const hls = new Hls();
  hls.loadSource(url);
  hls.attachMedia(video);
  hls.on(Hls.Events.MANIFEST_PARSED, () => video.play());
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
  // Safari: пропускаем hls.js, отдаём HLS URL нативной AVFoundation.
  video.src = url;
  video.addEventListener('loadedmetadata', () => video.play());
}

Это всё. Восемь строк, если считать import. Ветвление обязательно, потому что правильный путь в Safari – использовать нативный HLS, а неправильный – подгружать hls.js везде и смириться с тем, что на iPhone в Safari до iOS 17 он не работает. Обратите внимание: Hls.isSupported() проверяет поддержку MSE вообще, а не HLS – он возвращает true в каждом современном браузере, кроме Safari, и false – везде ещё, ровно наоборот тому, где canPlayType правдиво.

Порядок имеет значение. Рекомендуемая последовательность – loadSource, затем attachMedia, и только после этого подписка на события: attachMedia запускает цикл MEDIA_ATTACHING → MEDIA_ATTACHED, на котором stream controller ждёт перед обработкой плейлиста. Обратный порядок тоже работает, но добавляет задержку в один тик событийного цикла. Для low-latency live каждый тик на счету.

Рисунок 3. Последовательность событий от загрузки страницы до первого кадра. Time-to-First-Frame – это временной интервал между событиями MEDIA_ATTACHING и первым FRAG_BUFFERED для видео-трека.

Обработка ошибок, как её отгружают

Ошибки в hls.js поступают через одно событие: Hls.Events.ERROR. Полезная нагрузка – тегированный объект, содержащий четыре поля. Три из них – type, details, fatal – нужно проверять каждый раз, а четвёртое – data – имеет переменную структуру, зависящую от контекста. Всего существует четыре семейства ошибок, каждое из которых соответствует определённому пути восстановления.

Hls.ErrorTypes.NETWORK_ERROR – это всё, что выявила сетевая часть: 404 на манифест, 404 на фрагмент, таймаут фрагмента, HTTP-статус 5xx или прерванный XMLHttpRequest. При fatal задокументированный путь восстановления – hls.startLoad(): перезапустить state machine получения сегментов и попробовать снова. Пример из API-документации библиотеки показывает ровно это, и документация v1.6 разбирает процесс шаг за шагом (video-dev/ hls.js, docs/API.md, master branch, accessed 2026-05-25).

Hls.ErrorTypes.MEDIA_ERROR – это всё, что вытащил уровень MSE или декодер браузера: appendBuffer, который SourceBuffer отверг, QuotaExceededError, ребуфер из-за дыры, которую gap controller не смог преодолеть, ошибка декодера. При fatal-ошибке задокументированный путь – вызвать hls.recoverMediaError() один раз; если вторая MEDIA_ERROR приходит в течение нескольких секунд после первой, нужно вызвать hls.swapAudioCodec(), а затем hls.recoverMediaError() (video-dev/hls.js, docs/API.md, master branch). Смена аудиокодека – это восстановление в конкретном случае, когда декодер браузера не может обработать смену кодека посередине стрима.

Hls.ErrorTypes.KEY_SYSTEM_ERROR покрывает всё, что может пойти не так с EME: отказ в выдаче лицензии, статус ключа internal-error или output-restricted, сбой CDM. Автоматическое восстановление невозможно, потому что причина, как правило, лежит либо в неправильной настройке лицензионного сервера, либо в политике устройства (например, телефон с Widevine L3 пытается воспроизвести 4K-контент без поддержки HDCP). Правильное решение – показать пользователю сообщение «контент недоступен на этом устройстве» и отправить событие в телеметрию.

Hls.ErrorTypes.MUX_ERROR и Hls.ErrorTypes.OTHER_ERROR – редкие случаи: transmuxer не может распарсить повреждённый сегмент или парсер сталкивается с неожиданным токеном в плейлисте. Восстановление невозможно – зафиксируйте ошибку, переключитесь на альтернативный источник, если это возможно, и покажите пользователю стандартное сообщение об ошибке.

Паттерн, который генерирует любой продакшен-плеер, выглядит примерно так:

hls.on(Hls.Events.ERROR, (event, data) => {
  if (!data.fatal) return;             // нефатальная: лог и играем дальше
  switch (data.type) {
    case Hls.ErrorTypes.NETWORK_ERROR:
      telemetry.error('hls.network', data);
      hls.startLoad();                 // перезапустить state machine
      break;
    case Hls.ErrorTypes.MEDIA_ERROR:
      telemetry.error('hls.media', data);
      if (mediaErrorRecoveryAttempted) {
        hls.swapAudioCodec();
        hls.recoverMediaError();
      } else {
        mediaErrorRecoveryAttempted = true;
        hls.recoverMediaError();
        setTimeout(() => { mediaErrorRecoveryAttempted = false; }, 5000);
      }
      break;
    default:
      telemetry.error('hls.fatal', data);
      hls.destroy();
      showUnplayableMessage();
  }
});

Частая ошибка – вызывать hls.destroy() на каждую fatal-ошибку. Destroy отвязывает инстанс от video-элемента и заставляет пользователя перезагрузить страницу; делать это на восстановимом NETWORK_ERROR – это однострочный баг, который отгружается каждый квартал. Сначала прочитайте data.type; destroy – только когда нет пути восстановления.

Рисунок 4. Дерево восстановления. Читать тип первым; destroy() – только при отсутствии других опций.

Low-latency HLS через hls.js

Расширение Apple low-latency HLS – сокращённо LL-HLS – позволяет плееру загружать частичные сегменты до завершения кодирования полного, обеспечивая целевую задержку «от стекла до стекла» 2–5 секунд вместо 10–30 секунд у стандартного HLS (Apple, HLS Authoring Specification for Apple Devices, revision 2025-09, §6 Low-Latency HLS). Библиотека hls.js поддерживает LL-HLS начиная с версии 1.0, а стабильная реализация была достигнута в серии версий 1.6. В сентябре 2023 года Apple убрала из спецификации требование использования HTTP/2 server push, поэтому статьи, утверждающие, что LL-HLS требует HTTP/2 push, устарели. Текущая спецификация использует три механизма низкой задержки: повторную загрузку плейлиста с блокировкой, подсказки предзагрузки и отчёты о вариантах воспроизведения.

Включается через конфиг, а не путём переписывания:

const hls = new Hls({
  lowLatencyMode: true,         // включить LL-HLS (по умолчанию true с v1.4)
  liveSyncDuration:    3,       // целиться в ~3 секунды от живого края
  maxLiveSyncPlaybackRate: 1.1  // догонять live проигрыванием на 1.1x при отставании
});

Каждая из двух ручек длительности заслуживает отдельного пояснения. liveSyncDuration – целевая дистанция в секундах от живого края: слишком низкая – плеер постоянно ребуферит при любом сетевом всплеске, слишком высокая – задержки остаются высокими, и преимущества низкой латентности теряются. maxLiveSyncPlaybackRate позволяет плееру слегка ускорять воспроизведение при отставании, чтобы пользователь мог догнать прямой эфир без заметного seek; значение по умолчанию – 1.0 (функция отключена), большинство LL-HLS-развёртываний используют диапазон 1,05–1,1.

Производительность LL-HLS (LL-HLS) через hls.js ограничена не столько самим плеером, сколько путём между origin-сервером и плеером. CDN без поддержки HTTP/2 (или HTTP/3) и блокирующая перезагрузка плейлиста сводят все преимущества LL-HLS на нет: LL-HLS-плеер на CDN, не поддерживающем LL-HLS, работает так же, как обычный HLS-плеер. Наш разбор LL-HLS описывает требования к CDN.

Числа, которые реально считают операторы

Счётчики просмотров, привязанные к плееру, неинтересны; цифры ниже – это данные, которые оператор измеряет на продакшен-развёртывании hls.js.

МетрикаОпределениеЗдоровый диапазон (типовое OTT)
Time to first frame (TTFF)Время по часам от MEDIA_ATTACHING до первого FRAG_BUFFERED для видео0,6–1,5 с на широкополосе; 1,5–3 с на сотовой
Rebuffer ratioСуммарное время ребуферов ÷ общее время просмотра в сессииЦель < 0,5 %; в реальности 1–3 %
ABR switch rateЧисло событий LEVEL_SWITCHED в минуту воспроизведения0,2–1,0 в минуту стационарно
Variant entropyДоля времени сессии, проведённая на верхней ступени0,6–0,9 на широкополосе; ниже – нормально на сотовой
Fatal error rateСессий хотя бы с одной fatal Hls.Events.ERROR ÷ все сессииЦель < 0,5 %; в реальности 1–2 %
Live edge distanceДля live: liveSyncPosition − currentTime, усреднённое за минуту± 0,5 с для LL-HLS; ± 3 с для стандартного HLS

Все эти числа извлекаются из потока событий hls.js – отдельной библиотеки инструментирования не требуется. Mux Data, Conviva, Bitmovin Analytics и Datazoom оборачивают события hls.js и отправляют их на бэкенд; наша статья Observability и метрики плеера разбирает схему каждого вендора и объясняет, когда лучше строить решение самому, а когда – покупать готовое. Математика rebuffer ratio одинакова вне зависимости от вендора: суммируем миллисекунды между BUFFER_STALLED и соответствующим RESUME (или следующим циклом play–pause), делим на миллисекунды реального воспроизведения и умножаем на 100. Цель – 0,5 % на сессию продолжительностью 60 минут, что составляет 18 секунд ребуфера, то есть примерно один плохой сегмент на сессию.

Когда форкать (почти никогда) и когда monkey-patch (чаще, чем кажется)

Библиотека с открытым исходным кодом под лицензией MIT, и соблазн форкнуть её «просто ради одной фичи» вполне реален – но почти всегда это ошибка. Причина не юридическая и не моральная, а операционная. hls.js выпускает обновления раз в две–четыре недели, и типичный релиз включает дюжину исправлений багов для редких сценариев воспроизведения на устройствах, которых у вас, скорее всего, нет: Tizen 4.0 2018 года, webOS 5 на LG 2020 года, пятилетняя PlayStation. Как только вы создаёте форк, вы перестаёте получать эти исправления. Правильный подход в порядке приоритета:

Сначала – конфигурация. Большая часть того, ради чего форкают, выставлена опциями конструктора Hls – xhrSetup, fetchSetup, loader, manifestLoadingTimeOut, levelLoadingTimeOut, fragLoadingTimeOut, lowLatencyMode, liveSyncDuration, liveMaxLatencyDuration, весь объект drmSystems, все ABR-коэффициенты. Полный список в src/config.ts на master-ветке, и это первое место, куда стоит посмотреть до открытия issue, не говоря уже о форке.

Второе – заменить контроллер. Библиотека позволяет заменить abrController, audioTrackController, subtitleTrackController и подсистему loader. Хотите кастомный ABR – buffer-ориентированный, на основе обучения или управляемый сервером? Напишите свой класс и назначьте его. Сигнатура базового класса остаётся стабильной в серии v1.6.

Третье – monkey-patch снаружи. Если нужно изменить плейлист после получения (например, переписать URL сегментов, добавив токен, удалить некорректный EXT-X-DATERANGE, перенаправить на резервный origin), задайте свой xhrSetup или fetchSetup и измените ответ в нужном месте. Форк не требуется.

Форк – правильный выбор, только если баг, который нужно исправить, находится в логике transmuxer, парсера или state machine stream controller, и изменение слишком незначительно, чтобы отправлять его upstream. За девять лет использования hls.js в продакшене мы прибегали к этому всего дважды. Оба раза патч занимал одну строку; оба раза мы отправили его upstream и закрыли форк в течение двух месяцев.

Связанный паттерн: форк Hola, поставляющий hls.js-провайдер для JW Player – hola/jwplayer-hlsjs – не является форком в строгом смысле; это тонкий адаптер, интегрирующий hls.js с плагинным API JW Player. У JW Player есть мейнтейнер в рабочей группе hls.js, поэтому развитие проектов идёт синхронно, а дрифт в базовой библиотеке отсутствует.

Частые ошибки и что с ними делать

Один и тот же набор из пяти ошибок повторяется в каждом code review продакшен-интеграции hls.js. Назовём их один раз – и сэкономим четверть времени на отладке.

Первая – не использовать ветвление для Safari. hls.js не запускается в iOS Safari до iOS 17, а даже в iOS 17 нативный путь AVFoundation работает быстрее и экономит заряд батареи при воспроизведении HLS-контента. Ветка, показанная ранее – Hls.isSupported(), иначе откат на canPlayType('application/vnd.apple.mpegurl') – обязательна.

Вторая – вызывать hls.destroy() при каждой ошибке. Паттерн восстановления указан выше; правило: startLoad() для NETWORK_ERROR, recoverMediaError() для MEDIA_ERROR, destroy() – только если восстановление невозможно.

Третья – слушать MANIFEST_LOADED вместо MANIFEST_PARSED. MANIFEST_LOADED фырчит после HTTP-запроса; MANIFEST_PARSED – после того как библиотека разобрала варианты и готова к play(). Вызов play() на MANIFEST_LOADED работает в Chrome и нестабильно падает в Firefox.

Четвёртая – думать, что live currentTime начинается с нуля. Для VOD – да; для live – он начинается с текущего момента, и это может быть число вроде 1719428400 (секунды Unix epoch) или меньше, в зависимости от манифеста. Код интерфейса, отвечающий за отображение прогресс-бара, должен использовать seekable.start(0) и seekable.end(0) как границы, а не 0 и duration.

Пятая – не задавать таймауты под сетевые условия. Значения по умолчанию manifestLoadingTimeOut: 10000, fragLoadingTimeOut: 20000 адекватны для широкополосного соединения, но чрезмерно пессимистичны при использовании спутниковой связи или 3G. Правильный подход – использовать короткие таймауты (3 и 6 секунд) на быстрой сети и увеличивать их до 30 и 60 секунд на медленных каналах, которые удалось определить. Автоматическая настройка таких параметров библиотекой не поддерживается – её нужно реализовывать самостоятельно.

Где здесь Фора Софт

Мы отгружаем hls.js в продакшен-плееры для видеоконференций, OTT, e-learning, телемедицины и видеонаблюдения с момента выхода библиотеки в версии v0.7 – достаточно давно, чтобы «Safari-ветка» и «ветка ManagedMediaSource» стали для нас рефлексом. Доверие к решению строится не на идеальных сценариях, а на истории восстановления: на портировании на smart TV, где buffer controller обходил странности Tizen 4.0, на внедрении LL-HLS, для которого CDN потребовалось три раунда тюнинга, прежде чем liveSyncDuration плеера реально начал удерживать значение, и на multi-DRM-стеке, который доставляет Widevine + FairPlay + PlayReady из одного пакейджера. Если ваша команда разрабатывает видеопродукт в открытом вебе в 2026 году, hls.js должен быть в вашем бандле – независимо от того, написали вы интеграцию сами или унаследовали её. Разница между этими двумя случаями обычно составляет около четверти постлончевой работы над QoE, которую никто изначально не планировал.

Ключевые выводы

  • hls.js – де-факто open-source HLS-плеер для всех браузеров, кроме Safari, с миллионами загрузок в npm каждую неделю в 2026 году.
  • Архитектура представляет собой граф контроллеров, объединённых общей шиной событий: stream, level, ABR, buffer, EME, audio, subtitle, loader, transmuxer.
  • Запуск занимает семь строк кода; ветвление на Hls.isSupported() с откатом на нативный HLS в Safari.
  • Ошибки поступают в Hls.Events.ERROR; восстановление – через startLoad() при сетевых сбоях, через recoverMediaError() при проблемах с медиа, и только в крайнем случае – через destroy().
  • LL-HLS – настраивается через конфиг (lowLatencyMode: true); конечная задержка зависит от CDN и liveSyncDuration.
  • Сначала – конфиг, затем замена контроллера, потом monkey-патч, а форк – почти никогда.

Что читать дальше

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.