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

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

Кратко

Открытая библиотека hls.js – это то, что воспроизводит HTTP Live Streaming, сокращённо HLS, в каждом современном браузере кроме Safari, и в 2026 году это самый установленный видеоплеер веба: несколько миллионов скачиваний в неделю на npm, ветка v1.6 как текущий стабильный релиз и v1.7-alpha, выпущенная в марте. Внутри это не один плеер, а плотно связанный набор контроллеров – stream controller, ABR controller, buffer controller, EME controller и LL-HLS-совместимый загрузчик, – которые накладывают формат плейлистов 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, а frontend- или smart-TV-инженеру – получить полную ментальную модель библиотеки: контроллеры, события для продакшен-телеметрии, конфиги, которые меняют поведение ABR без форка, и четыре семейства ошибок, которые надо обработать в первый же день. Никаких предварительных знаний по стримингу не требуется; каждое понятие объясняется по ходу. К концу вы поймёте, почему JavaScript-бандл в 1,5 мегабайта – это всё, что стоит между JPEG и стримом уровня Netflix в открытом вебе, и какая одна настройка включает LL-HLS для 3-секундного live без переписывания плеера.

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

Самое короткое точное определение такое. hls.js – это JavaScript-библиотека, которая читает HLS-плейлист, скачивает указанные в нём видео-чанки, на лету перепаковывает их в формат, понятный браузеру, передаёт байты в API Media Source Extensions и выставляет поток событий, достаточный, чтобы поверх него собрать целый продакшен-плеер с UI – и всё это из открытого пакета под лицензией 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 November 2016; более поздние редакции отслеживаются как Media Source Extensions 2 в статусе Candidate Recommendation на протяжении 2025), который даёт JavaScript возможность добавлять произвольные видео- и аудио-байты в SourceBuffer, прикреплённый к <video>. Второй – Encrypted Media Extensions (W3C, Encrypted Media Extensions, Recommendation 18 September 2017; Working Draft обновлён 20 May 2026), который даёт JavaScript изолированный способ договариваться о ключах расшифровки с браузерным Content Decryption Module. Без MSE из JavaScript нельзя подавать сегментированное видео в <video>; без EME – нельзя играть платный премиум-контент. hls.js сшивает протокол HLS, описанный в IETF RFC 8216 (HTTP Live Streaming, August 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 и сначала отгрузила на устройствах 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, сделать transmux в фрагментированный MP4 (это понимает MSE) и положить байты в SourceBuffer. За одиннадцать лет библиотека обросла контроллерами – ABR, buffer, EME, audio track, subtitle track, content steering, interstitials – и стала фактическим кросс-браузерным HLS-плеером, который сейчас сопровождает рабочая группа с участием Rob Walch (principal engineer в JW Player) и контрибьюторов из Mux, Akamai, Bitmovin, Cloudflare и десятков стриминг-вендоров. У репозитория на GitHub было 16 500 звёзд и 2 700 форков по состоянию на май 2026, а npm-пакет в начале 2026 пересёк отметку в несколько миллионов скачиваний в неделю (npm registry, hls.js, accessed 2026-05-25). В продакшен-пользователях библиотека перечисляет JW Player, Mux, Wowza, Akamai, Bitmovin, веб-клиент Twitch и десятки OTT-сервисов через интеграции с JW Player и Video.js.

Политический подтекст здесь важен, потому что он формирует road map. 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, который гоняет state machine получения сегментов; 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), чтобы начать скачивать плейлист, и слушаете события, чтобы драйвить UI. Всё остальное – конфигурация.

Рисунок 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 для append и возвращается в 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 для первого сегмента.

Level controller и ABR controller

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

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

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

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

Buffer controller

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

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

EME controller

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

Конфигурация в современном hls.js (v1.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 – не legacy webkit-prefixed pre-EME путь, который Apple отгрузил первым – приехала в v1.6, а в v1.6.15 пофиксили баг с патчингом FairPlay key-ID, который давал ошибки "keyId is null" на некоторых конфигурациях энкодеров (video-dev/hls.js, Release v1.6.15, 19 November 2025). Если у вас multi-DRM, фиксируйте v1.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.js везде и смириться с тем, что в Safari на iPhone до 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: отказ в лицензии, key status internal-error или output-restricted, падение CDM. Автоматического восстановления нет, потому что причина почти всегда – либо неправильная конфигурация лицензионного сервера, либо политика устройства (телефон на Widevine L3 пытается играть 4K, не определился HDCP). Правильный ответ – показать пользователю сообщение «контент недоступен на этом устройстве» и отгрузить событие в телеметрию.

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

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

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 – даёт плееру скачивать частичные сегменты до завершения кодирования полного, с целевой задержкой glass-to-glass 2–5 секунд вместо 10–30 у стандартного HLS (Apple, HLS Authoring Specification for Apple Devices, revision 2025-09, §6 Low-Latency HLS). hls.js поддерживает LL-HLS с v1.0, и поддержка стабилизировалась через серию v1.6. Apple убрала требование HTTP/2 server push из спецификации в сентябре 2023, поэтому статьи, которые описывают LL-HLS как требующий HTTP/2 push, устарели; текущая спецификация использует blocking playlist reload, preload hints и rendition reports как три примитива низкой задержки.

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

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

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

Производительность LL-HLS через hls.js упирается в путь между origin и плеером не меньше, чем в сам плеер. CDN без HTTP/2 (или HTTP/3) и blocking playlist reload сводит выигрыш на нет; LL-HLS-плеер на не-LL-HLS CDN работает как обычный 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 (чаще, чем кажется)

Библиотека open source под MIT, и соблазн форкнуть «вот только ради этой одной фичи» вполне реален и почти всегда неправильный. Причина не юридическая и не моральная – операционная. hls.js делает релиз раз в две-четыре недели, и типовой релиз – это дюжина багфиксов под edge cases воспроизведения на устройствах, которых у вас нет (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-based, learned, server-driven – напишите класс и присвойте. Сигнатура базового класса стабильна по серии 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) или меньше, в зависимости от манифеста. UI-код, который рисует прогресс-бар, должен брать 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» работали уже рефлексом. Доверие зарабатывается не на happy path; оно зарабатывается на истории восстановления, на порте 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-patch, форк – почти никогда.

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

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

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