npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

itube-modern-player

v0.2.1

Published

Lightweight, framework-agnostic HTML5 video player: HLS streams, playlists, chapters, VTT subtitles, sprite previews, VAST ads. Ships with a Vue 3 wrapper.

Readme

itube-modern-player

Лёгкий универсальный HTML5-видеоплеер на TypeScript для видео-сайтов и встраиваемых страниц. Ядро — чистый класс без фреймворк-зависимостей (new Player(...)), поверх него — Vue 3-компонент, IIFE-бандл для <script>-подключения и lazy-обёртка для отложенной загрузки. Сделан как замена fluid-player: та же область применения (контентные видео с рекламой, плейлистами и стримами), но без его архитектурных болячек — без глобального реестра инстансов, без россыпи absolute-элементов, с честным destroy() и полной типизацией.

Ядро ~19–23 КБ gzip (+ hls.js, который догружается динамически только для m3u8). Поддержка: все вечнозелёные браузеры, Safari/iOS (нативный HLS, playsinline, нативный fullscreen).

Возможности

  • Источники — MP4/WebM, HLS-стримы (.m3u8, hls.js или нативно в Safari), live-потоки с бейджем LIVE, прогрессивные качества с переключением без потери позиции, HLS-уровни с пунктом Auto.
  • Плейлисты — массив видео без пересоздания плеера, prev/next, панель списка (сайдбар или горизонтальная лента снизу), имя плейлиста, перемешивание, повтор, автопереход.
  • Тайм-коды (главы) — сегментированный таймлайн как у YouTube, название главы в контролах и тултипе, событие chapterchange, источник: массив или VTT-файл.
  • Панель сцен — список глав с превью из VTT-спрайта, клик — переход к сцене.
  • Спрайт-превью — кадр при наведении на таймлайн (WebVTT + #xywh).
  • Диаграмма популярности — «most replayed»-кривая над таймлайном из разреженных точек.
  • Субтитры — VTT-треки (строка/объект/массив), выключены по умолчанию, меню выбора.
  • Реклама — VAST 2–4 (InLine + Wrapper), preroll/midroll/postroll, прямые src-роллы, skip-таймер, click-through, impression/quartile/pause/resume-пиксели, отдельный <video> (контент не трогается), префетч контента во время преролла, watchdog на зависший креатив, частота показа для плейлистов, полный набор событий.
  • Метаданные видео — тайтл, описание, постер, канал (аватар/заглушка, форма, ссылка) в экране паузы.
  • Экран паузы — дефолтный или полностью свой («слот»: элемент/фабрика в ядре, <template #pauseScreen> во Vue).
  • Related-видео — сетка похожих по паузе и/или окончанию.
  • Кнопки действий — лайк/дизлайк (с управляемым состоянием), «добавить в», шеринг (нативный share sheet с фолбэком на событие), жалоба, свои кнопки в ⋯-дропдауне (customaction).
  • Кастомизация — каждая фича отключаема, каждая иконка заменяема (SVG), все строки переводимы, темизация CSS-переменными + styling-проп (акцент, цвета лайков).
  • Локализации — 11 встроенных языков по коду language, свои словари через registerLocale, облегчённый вход без словарей.
  • UX-мелочи — превью-постер, горячие клавиши (на контейнере, не на document), PiP, полноэкранный режим, спиннер буферизации, оверлей ошибок, авто-скрытие контролов.
  • Интеграции — vanilla / Vue 3 / <script>-тег / ленивая загрузка бандла по интерактиву.
  • Типизация — полные типы всех опций, событий и методов из коробки.

Чем отличается от fluid-player и подобных:

  • Никакой каши из absolute-элементов. Поверх видео лежит один overlay-слой на CSS grid, всё внутри — обычный flex-поток. Верстать поверх и кастомизировать — легко.
  • Нет глобального состояния. new Player() сколько угодно раз на странице, никаких реестров инстансов и привязок по id.
  • Честный destroy(). Плеер полностью убирает за собой DOM и слушатели.
  • Всё типизировано. События, опции, методы — полные типы из коробки.
  • Всё отключаемо и заменяемо. Каждая кнопка, каждая надпись, каждая иконка.

Установка

npm install itube-modern-player

hls.js подтягивается автоматически и грузится только когда плееру дают m3u8-источник (динамический импорт) — в бандл проектов без стримов он не попадает. Vue — опциональная peer-зависимость, нужна только если используете itube-modern-player/vue.

Быстрый старт (vanilla JS / TS)

Ядро — обычный класс без фреймворков, это основной способ использования.

import { Player } from 'itube-modern-player'
import 'itube-modern-player/style.css'

const player = new Player('#mount', {
  source: {
    src: 'https://cdn.example.com/stream.m3u8',
    title: 'Название ролика',
    poster: 'https://cdn.example.com/poster.jpg',
  },
})

player.on('ended', () => console.log('done'))
// ...
player.destroy()

Первый аргумент — элемент или селектор контейнера, в который плеер отрендерит себя. destroy() возвращает контейнер в исходное состояние.

Без сборщика (script-tag / CDN)

Отдельный IIFE-бандл, глобальная переменная ITubePlayer — это сам класс:

<link rel="stylesheet" href="https://unpkg.com/itube-modern-player/dist/style.css">
<!-- hls.js нужен только если будут m3u8-источники: -->
<script src="https://unpkg.com/hls.js"></script>
<script src="https://unpkg.com/itube-modern-player/dist/itube-modern-player.iife.js"></script>
<script>
  const player = new ITubePlayer('#mount', {
    source: { src: 'https://cdn.example.com/stream.m3u8', title: 'Demo' },
  })
</script>

Быстрый старт (Vue 3)

<script setup lang="ts">
import { ITubePlayer } from 'itube-modern-player/vue'
import 'itube-modern-player/style.css'
import type { VideoSource } from 'itube-modern-player'

const video: VideoSource = {
  src: '/stream.m3u8',
  title: 'Название',
}
</script>

<template>
  <ITubePlayer
    :source="video"
    :options="{ seekStep: 5, controls: { pip: false } }"
    @ended="onEnded"
    @timeupdate="onTime"
  >
    <!-- слот экрана паузы: реклама, промо, что угодно.
         Scoped-слот даёт доступ к плееру и к close() ("Закрыть и продолжить") -->
    <template #pauseScreen="{ player, close }">
      <MyAdBlock @close="close" />
    </template>
  </ITubePlayer>
</template>

Кастомный экран паузы (рекламный блок)

Вместо дефолтного оверлея (тайтл/описание/канал) можно показать свой блок — например рекламу с кнопкой «Закрыть и продолжить». Контент рендерится full-cover по центру плеера поверх контролов; клик по фону не возобновляет (только ваша кнопка). Доступ к плееру есть во всех сборках:

  • Vue 3 — scoped-слот: #pauseScreen="{ player, close }". player — инстанс (реактивный, null до монтирования), close() — возобновить воспроизведение и закрыть блок.
  • vanilla / IIFE — фабрика, получающая плеер: pauseScreen: (player) => HTMLElement. Кнопка вызывает player.play().
  • В рантайме — player.setPauseScreenContent(el | null).
// vanilla / Vue2 (внутри компонента)
new Player('#mount', {
  pauseScreen: (player) => {
    const box = document.createElement('div')
    box.innerHTML = '<!-- ваш рекламный блок -->'
    const btn = document.createElement('button')
    btn.textContent = 'Закрыть и продолжить'
    btn.onclick = () => player.play()   // возобновляет и скрывает блок
    box.append(btn)
    return box
  },
})
  • :source — одно видео или массив (массив включает режим плейлиста). Реактивен: смена объекта вызывает player.load(), инстанс плеера не пересоздаётся.
  • :options — все остальные PlayerOptions (см. ниже).
  • Все события плеера ретранслируются как события компонента.
  • Доступ к ядру: ref на компонент → componentRef.value.player (ShallowRef<Player>).

Описание видео: VideoSource

const source: VideoSource = {
  src: 'https://cdn.example.com/video.m3u8',  // m3u8 определяется автоматически
  type: 'application/x-mpegurl',              // необязательно, для нестандартных URL
  title: 'Заголовок',
  description: 'Описание — показывается в экране паузы',
  poster: 'poster.jpg',                       // превью-постер до первого запуска
  duration: 1284,                             // для плашки в плейлисте до загрузки метаданных

  // канал/автор — рендерится в экране паузы
  channel: {
    name: 'Мой канал',
    avatar: 'avatar.png',  // без avatar — стилизованная заглушка из первой буквы
    url: 'https://example.com/channel',  // клик по каналу откроет URL
    avatarShape: 'circle', // 'circle' (default) | 'rounded' | 'square'
  },

  // тайм-коды: массив или URL WebVTT-файла глав
  chapters: [
    { start: 0, title: 'Интро' },
    { start: 120, title: 'Основная часть' },  // end заполняется автоматически
  ],

  // субтитры: строка-URL, один трек или массив. По умолчанию ВЫКЛЮЧЕНЫ.
  subtitles: [
    { src: 'subs-ru.vtt', label: 'Русский', srclang: 'ru' },
    { src: 'subs-en.vtt', label: 'English', srclang: 'en', default: true }, // default включает
  ],

  // спрайт-превью при наведении на таймлайн (WebVTT с фрагментами #xywh=)
  thumbnails: 'thumbs.vtt',

  // диаграмма популярности над таймлайном (как "most replayed" у YouTube)
  // разреженные точки {time: сек, value: сырое число кликов/досмотров};
  // внутри: бакетирование → сглаживание → нормализация к максимуму с базовым полом,
  // появляется при наведении на прогресс-бар; выключается controls.heatmap = false
  heatmap: [
    { time: 95, value: 220 },
    { time: 290, value: 900 },
    { time: 430, value: 540 },
  ],

  // альтернативные качества для прогрессивных файлов (для HLS уровни берутся из манифеста)
  // переключение сохраняет позицию и состояние воспроизведения
  qualities: [
    { quality: 1080, label: '1080p', src: 'video-1080.mp4' },
    { quality: 720, label: '720p', src: 'video-720.mp4' },
  ],

  meta: { anyOwnData: true },  // ваши данные, плеер их не трогает
}

Тайм-коды (главы)

Поддержаны нативно: сегментированный таймлайн (как у YouTube), название текущей главы в контрол-баре, название главы в ховер-тултипе, событие chapterchange. Источник — массив { start, end?, title } либо URL VTT-файла глав. end не обязателен — берётся начало следующей главы или конец видео.

Спрайт-превью (thumbnails)

Стандартный формат — WebVTT, каждый cue указывает картинку и регион спрайта:

WEBVTT

00:00:00.000 --> 00:00:05.000
sprite.jpg#xywh=0,0,160,90

00:00:05.000 --> 00:00:10.000
sprite.jpg#xywh=160,0,160,90

Относительные пути резолвятся от URL VTT-файла. Можно и без #xywh — по картинке на cue.


Все опции: PlayerOptions

new Player('#mount', {
  source,            // VideoSource | VideoSource[]
  autoplay: false,
  muted: false,
  loop: false,
  volume: 1,
  playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2],  // пункты меню скорости
  seekStep: 10,      // секунды для стрелок/кнопок перемотки
  keyboard: true,    // горячие клавиши (на контейнере, не на document!)
  className: 'my-player',       // свой класс на контейнер — хук для темизации
  crossOrigin: 'anonymous',     // если VTT/постеры на другом домене
  playsInline: true,

  // ---- стилизация ----
  styling: {
    themeColor: '#00b3a4',     // акцентный цвет (вместо встроенного красного)
    likeColor: 'forestgreen',  // цвет активного лайка (default forestgreen)
    dislikeColor: '#e53935',   // цвет активного дизлайка (не зависит от themeColor)
    borderRadius: 14,          // скругление углов плеера (число → px), '0' — квадратные
    playButtonStyle: 'solid',  // 'solid' (акцентный круг, белая иконка) | 'inverted' (полупрозрачный белый круг, иконка цвета темы)
  },

  // ---- язык интерфейса ----
  // встроенные статические локали: en, ru, de, es, it, ja, ko, zh, pt, ar, hi
  // словарь терминов = тип PlayerLabels; defaultLabels (en) экспортируется как эталон
  // точечные оверрайды — через labels (применяются поверх локали)
  language: 'ru',

  // ---- плейлист ----
  playlist: {
    title: 'Моя подборка', // имя плейлиста: маленький кикер "Плейлист" + имя приоритетным шрифтом
    autoAdvance: true,   // автопереход к следующему
    loop: false,         // репит списка (кнопка в панели плейлиста)
    shuffle: false,      // перемешивание (кнопка в панели плейлиста)
    startIndex: 0,
    layout: 'sidebar',   // 'sidebar' — справа вертикально | 'bottom' — горизонтальная лента снизу
  },

  // ---- панель сцен (тайм-коды с превью из VTT-спрайта) ----
  scenes: {
    layout: 'bottom',    // 'bottom' (default) | 'sidebar'
  },

  // ---- контролы: каждая фича отключаемая ----
  controls: {
    play: true,
    progress: true,
    time: true,
    volume: true,
    fullscreen: true,
    pip: true,
    settings: true,      // меню скорости (иконка-спидометр)
    quality: false,      // меню качества (иконка-шестерёнка); ВЫКЛЮЧЕНО по умолчанию,
                         // кнопка появляется при наличии source.qualities или HLS-уровней
    scenes: true,        // кнопка списка сцен (появляется при наличии chapters)
    heatmap: true,       // диаграмма популярности (нужны данные source.heatmap)
    subtitles: true,     // кнопка появляется только если у источника есть треки
    seekButtons: true,   // или { back: 5, forward: 15 }
    playlist: true,      // prev/next/список — рендерятся ТОЛЬКО в режиме плейлиста
    hideDelay: 2500,     // мс до скрытия контролов при воспроизведении
  },

  // ---- экран паузы ----
  // true (по умолчанию) — заголовок/описание/канал текущего видео
  // false — выключить
  // HTMLElement или (player) => HTMLElement — свой контент (full-cover оверлей,
  // напр. реклама с «Закрыть и продолжить»; фабрика получает плеер).
  // Во Vue — scoped-слот #pauseScreen="{ player, close }". См. раздел про кастомный экран паузы.
  pauseScreen: true,

  // ---- related-видео ----
  related: {
    title: 'Смотрите также',
    showOn: ['ended', 'pause'],   // когда показывать; по умолчанию ['ended']
    // что делает клик (событие relatedclick эмитится в любом случае):
    // 'player' (default) — загрузить source в плеер | 'newWindow' — открыть url в новой вкладке | 'currentTab' — перейти в текущей
    clickBehavior: 'player',
    items: [
      {
        title: 'Другой ролик',
        poster: 'p.jpg',
        duration: '12:34',
        source: { src: 'other.mp4', title: 'Другой ролик' },  // клик загрузит в плеер
        // или url: 'https://...' — клик откроет ссылку
      },
    ],
  },

  // ---- реклама (см. раздел «Реклама») ----
  adConfig: {
    adList: [
      { roll: 'preRoll', vastTag: 'https://ads.example.com/vast.xml' },
      { roll: 'midRoll', vastTag: 'https://ads.example.com/vast2.xml', timer: 300 },
      { roll: 'postRoll', src: 'https://cdn.example.com/ad.mp4', clickUrl: 'https://sponsor.example.com' },
    ],
    skipDelay: 5,          // сек до кнопки «Пропустить»; -1 — без пропуска
    playOn: 'every',       // 'every' — реклама на каждом видео плейлиста, 'first' — только на первом
    maxWrapperDepth: 3,    // лимит VAST Wrapper-редиректов
    requestTimeout: 8000,  // таймаут запроса VAST-тега, мс
    mediaTimeout: 10000,   // если креатив не стартовал/завис на столько мс — aderror и сразу контент
  },

  // ---- кнопки действий (все по умолчанию ВЫКЛЮЧЕНЫ) ----
  actions: {
    like: true,      // видимая кнопка 👍 → событие action {id:'like'}
    dislike: true,   // видимая кнопка 👎 → событие action {id:'dislike'}
    likeState: 'like', // начальное состояние оценки ('like' | 'dislike' | null);
                       // менять в рантайме: player.setLikeState(...)
    addTo: true,     // в дропдауне ⋯ → action {id:'addTo'}
    share: true,     // в дропдауне ⋯: нативный share-диалог устройства,
                     // если его нет — событие action {id:'share'}
    report: true,    // в дропдауне ⋯ → action {id:'report'}
    // свои пункты дропдауна: клик эмитит customaction {id}
    custom: [
      { id: 'download', title: 'Скачать', icon: '<svg ...>...</svg>' }, // icon: URL картинки или raw SVG
    ],
  },

  // ---- кастомизация каждой кнопки ----
  icons: {
    play: '<svg viewBox="0 0 24 24">...</svg>',   // любая иконка — ваш SVG
    pause: '<svg ...>...</svg>',
    // полный список имён: тип IconName
  },

  // ---- все надписи (i18n) ----
  labels: {
    play: 'Смотреть',
    skipAd: 'Пропустить рекламу',
    related: 'Похожие видео',
    // полный список: тип PlayerLabels
  },
})

IconName: play, pause, replay, bigPlay, volumeHigh, volumeLow, volumeMute, fullscreen, fullscreenExit, pip, settings, subtitles, list, next, previous, seekForward, seekBack, close.


Реклама

Формат списка совместим по духу с fluid-player: массив роллов, каждый — preRoll, midRolltimer — секундой срабатывания) или postRoll. Источник ролла:

  • vastTag — URL VAST-тега. Поддержано: VAST 2/3/4, InLine + Wrapper (редиректы с лимитом глубины), выбор MediaFile (прогрессивный mp4/webm приоритетнее), ClickThrough, Impression, квартильные TrackingEvents (start / firstQuartile / midpoint / thirdQuartile / complete / skip / click), skipoffset (время или процент). VPAID не поддержан намеренно — это исполнение стороннего JS в вашей странице.
  • src — прямой URL медиафайла без VAST (плюс опциональный clickUrl).

Ключевое отличие от fluid-player: реклама играет в отдельном <video>, наложенном поверх. Контентное видео не трогается — его позиция, HLS-сессия, выбранные субтитры и качество переживают любой рекламный брейк без «восстановления состояния».

Поведение:

  • preRoll — перед стартом видео, midRoll — на секунде timer, postRoll — после окончания; роллов каждого типа может быть несколько.
  • playOn: 'first' — рекламный список отрабатывает только на первом видео плейлиста; 'every' (по умолчанию) — на каждом.
  • Любой фейл (недоступный тег, битый XML, неиграющий медиафайл, таймаут) → событие aderror и немедленный переход к контенту. Реклама никогда не блокирует ролик.
  • Кнопка пропуска появляется через skipDelay секунд (или skipoffset из VAST, если он есть).
  • Префетч контента: пока играет преролл, основное видео буферизует первые фрагменты в фоне (preload="auto" на время брейка; для HLS hls.js грузит сегменты сразу после attach) — после рекламы контент стартует мгновенно.

События: adstart, adend, adskip, adclick, aderror.

player.on('aderror', ({ roll, error }) => report(roll, error))


Справочник API

Конструктор

new Player(target: string | HTMLElement, options?: PlayerOptions)

target — элемент-контейнер или CSS-селектор. Плеер рендерит свой DOM внутрь и полностью убирает его в destroy(). Инстансов на странице — сколько угодно, глобального состояния нет.

Свойства

| Свойство | Тип | Описание | | --- | --- | --- | | container | HTMLElement | Корневой элемент плеера (.imp-player) | | video | HTMLVideoElement | Контентный видео-элемент (прямой доступ для нестандартных нужд) | | paused | boolean | На паузе | | ended | boolean | Дошло до конца | | currentTime | number | Текущая секунда | | duration | number | Длительность (0, пока метаданные не загружены) | | live | boolean | Live-поток (duration === Infinity) | | bufferedEnd | number | Конец буферизованного диапазона, сек | | volume | number | Громкость 0..1 | | muted | boolean | Заглушен | | playbackRate | number | Текущая скорость | | playbackRates | number[] | Доступные скорости (из опций) | | qualityLevels | Level[] | { index, label }[] — HLS-уровни или прогрессивные качества | | currentQuality | number | Индекс выбранного качества, -1 = auto | | qualityAutoAvailable | boolean | Есть ли пункт Auto (только HLS) | | subtitleTracks | SubtitleTrack[] | Треки текущего источника | | activeSubtitle | number | Индекс включённого трека, -1 = выкл | | playlist | VideoSource[] | Текущий список | | index | number | Индекс играющего элемента | | source | VideoSource \| null | Текущий источник | | hasPlaylist | boolean | Больше одного элемента | | hasNext / hasPrevious | boolean | Есть куда листать (учитывает playlist.loop) | | chapterList | NormalizedChapter[] | Главы текущего видео (после загрузки метаданных) | | chapter | NormalizedChapter \| null | Текущая глава | | isFullscreen | boolean | Полноэкранный режим | | adPlaying | boolean | Идёт рекламный брейк |

Методы

Воспроизведение

| Метод | Описание | | --- | --- | | play(): Promise<void> | Запуск. Если есть несыгранный преролл — сначала отыграет его | | pause(): void | Пауза (во время рекламы игнорируется) | | togglePlay(): void | Переключить | | seek(sec: number): void | Абсолютная перемотка (клампится в 0..duration) | | skip(delta: number): void | Относительная перемотка, например skip(-10) |

Громкость и скорость

| Метод | Описание | | --- | --- | | setVolume(v: number): void | 0..1; значение > 0 снимает mute | | setMuted(m: boolean): void | Установить mute | | toggleMute(): void | Переключить mute | | setPlaybackRate(r: number): void | Скорость воспроизведения |

Качество и субтитры

| Метод | Описание | | --- | --- | | setQuality(index: number): void | Для HLS -1 = auto; для прогрессивных источников переключает файл с сохранением позиции | | setSubtitle(index: number): void | Включить трек по индексу, -1 — выключить |

Плейлист и источники

| Метод | Описание | | --- | --- | | load(source, startIndex = 0): void | Заменить источник(и) без пересоздания плеера — громкость, скорость, подписки и DOM сохраняются. Массив включает режим плейлиста | | next(): void / previous(): void | Переход по плейлисту (учитывает loop и shuffle) | | playItem(i: number): void | Запустить конкретный элемент | | togglePlaylistPanel(): void | Показать/скрыть панель списка | | toggleScenesPanel(): void | Показать/скрыть панель сцен | | setShuffle(b) / shuffle | Режим перемешивания | | setRepeat(b) / repeat | Режим повтора списка |

UI и прочее

| Метод | Описание | | --- | --- | | toggleFullscreen(): Promise<void> | Полный экран (на iOS — нативный fullscreen видео) | | togglePip(): Promise<void> | Картинка-в-картинке | | setPauseScreenContent(el: HTMLElement \| null): void | Подменить контент экрана паузы (то, что во Vue делает слот) | | setLikeState(s: 'like' \| 'dislike' \| null): void | Подсветить лайк/дизлайк | | share(): Promise<void> | Нативный share-диалог; без него — событие action {id:'share'} | | destroy(): void | Полный демонтаж: DOM, слушатели, hls-сессия, реклама |

События

const off = player.on('timeupdate', fn)  // вернёт функцию отписки
player.once('ready', fn)
player.off('timeupdate', fn)

События: PlayerEventMap

| Событие | Payload | Когда | | --- | --- | --- | | ready | { player } | Плеер создан | | play / pause / ended | — | Воспроизведение | | timeupdate | { currentTime, duration } | Тик времени | | progress | { buffered } | Буферизация | | volumechange | { volume, muted } | Громкость | | ratechange | { rate } | Скорость | | seeking / seeked | { currentTime } | Перемотка | | sourcechange | { source, index } | Источник заменён | | playlistitemchange | { source, index } | Переход по плейлисту | | chapterchange | { chapter \| null } | Сменилась глава | | fullscreenchange | { active } | Полный экран | | pipchange | { active } | PiP | | qualitychange | { label } | Качество (в т.ч. авто-переключение HLS) | | subtitlechange | { track \| null } | Субтитры | | relatedshow | — | Показана сетка related | | relatedclick | { item } | Клик по related-карточке | | action | { id } | Кнопки: like, dislike, addTo, share (если нет нативного шеринга), report | | customaction | { id } | Ваша кнопка из actions.custom | | adstart / adend / adskip / adclick | { ad: ResolvedAd } | Жизненный цикл рекламы | | adpause / adresume | { ad: ResolvedAd } | Креатив поставлен на паузу / возобновлён (+ VAST-пиксели pause/resume) | | aderror | { roll, error } | Ролл не отыграл (контент продолжается автоматически) | | error | { message, cause? } | Ошибка медиа/сети/треков | | destroy | — | Плеер демонтирован |

Vue 3: <ITubePlayer>

| Что | API | | --- | --- | | Пропсы | source: VideoSource \| VideoSource[] (реактивный — смена вызывает load()), options: Omit<PlayerOptions, 'source'> | | События | Все события PlayerEventMap ретранслируются 1:1 (@timeupdate, @aderror, @customaction, …) | | Слоты | #pauseScreen="{ player, close }" — контент экрана паузы (scoped: доступ к плееру и close()) | | Expose | player: ShallowRef<Player \| null> — доступ к ядру: playerRef.value.player.seek(0) |

Экспортируемые типы

PlayerOptions, VideoSource, ChannelInfo, SubtitleTrack, Chapter, QualityLevel, PlaylistOptions, ControlsOptions, ActionsOptions, CustomAction, BuiltinActionId, RelatedOptions, RelatedItem, AdsOptions, AdRoll, ResolvedAd, ThumbnailCue, PlayerLabels, IconName, PlayerEvent, PlayerEventMap, Level, SourceController, NormalizedChapter

Экспортируемые утилиты

| Экспорт | Что делает | | --- | --- | | formatTime(sec) | 3673"1:01:13" | | locales, supportedLanguages, getLocale(code) | Встроенные локали (11 языков) и их коды | | buildHeatmapValues(points, duration), heatmapPath(values) | Математика диаграммы популярности | | isHlsSource(src, type?) | Определение m3u8 | | normalizeChapters(chapters, duration) | Сортировка/заполнение end | | loadChaptersVtt(url) | VTT-файл глав → Chapter[] | | ThumbnailTrack.load(url) | Парсер спрайт-VTT (cueAt(time)) | | resolveVast(roll, opts) | Самостоятельное использование VAST-резолвера | | defaultIcons, defaultLabels | Дефолты для частичного переопределения |

Темизация

Внешний вид настраивается CSS-переменными — на контейнере плеера или любом родителе:

.my-player {
  --imp-accent: #00bcd4;        /* цвет прогресса, кнопки play, активных пунктов */
  --imp-bg: #000;
  --imp-text: #fff;
  --imp-control-bg: rgba(20, 20, 20, 0.85);  /* фон меню/панелей */
  --imp-track: rgba(255, 255, 255, 0.25);    /* фон таймлайна */
  --imp-radius: 8px;
  --imp-font: Inter, sans-serif;
  --imp-transition: 180ms ease;
}

Все классы стабильны и начинаются с imp- — можно дотюнить точечно обычным CSS.

Горячие клавиши

Слушаются на контейнере плеера (не на document — несколько плееров на странице не конфликтуют). Space/K — play/pause, / — перемотка на seekStep, / — громкость, M — mute, F — полный экран, 0–9 — переход на 0–90% длительности, Home/End.

Стримы

  • *.m3u8 (или type: 'application/x-mpegurl') → hls.js, который загружается динамически при первом использовании; в Safari используется нативный HLS.
  • По умолчанию подтягивается light-сборка hls.js/light (без alt-audio/субтитров/EME/LL — заметно легче полной) — достаточно для обычного VOD m3u8. Если нужна полная сборка, положите её в window.Hls (плеер подхватит готовый глобал и не будет грузить свой).
  • Live-потоки: автоматически определяются (duration === Infinity), вместо таймкода показывается бейдж LIVE, таймлайн скрывается.
  • Качества HLS-уровней попадают в меню настроек (с пунктом Auto).

Локализации и вес бандла

Главный вход itube-modern-player включает все 11 локалей (+~4 КБ gzip) — language: 'ru' работает без настройки. Если важен каждый килобайт, есть облегчённый вход без словарей (в ядре остаётся только английский):

import { Player, registerLocale } from 'itube-modern-player/core' // −4 КБ gzip
import { locales } from 'itube-modern-player/locales'             // словари отдельным чанком

registerLocale('ru', locales.ru) // регистрируем только нужное
new Player('#mount', { language: 'ru' })

registerLocale принимает и собственные словари — любой объект формы PlayerLabels под любым кодом. Незарегистрированный код тихо откатывается к английскому.

| Вход | gzip | Локали | | --- | --- | --- | | itube-modern-player | ~23 КБ | все 11 встроены | | itube-modern-player/core | ~19 КБ | только en, остальное вручную | | itube-modern-player/locales | ~4 КБ | только словари (отдельный чанк) |

Ленивая загрузка бандла

Точка входа itube-modern-player/lazy (~1.5 КБ) для паттерна «бандл плеера грузим после первого интерактива». Мгновенно рисует постер-заглушку (использует те же CSS-классы — подмена незаметна), а полный чанк плеера подтягивает динамическим импортом:

import { createLazyPlayer } from 'itube-modern-player/lazy'
import 'itube-modern-player/style.css'

const lazy = createLazyPlayer('#mount', {
  source: { src: 'stream.m3u8', poster: 'poster.jpg', title: '…' },
  // когда качать бандл:
  loadOn: 'interaction', // первый pointerdown/keydown/touch/wheel на странице (по умолчанию)
  // loadOn: 'visible',  // когда заглушка вошла во вьюпорт (IntersectionObserver)
  // loadOn: 'immediate' // сразу (но отдельным чанком)
})

lazy.ready.then((player) => { /* полный Player готов */ })
lazy.destroy() // работает на любой стадии

Клик по заглушке всегда грузит бандл немедленно и запускает воспроизведение.

Если нужен полный контроль снаружи — он никуда не делся: обычный import('itube-modern-player') по любому вашему триггеру, ядро не делает никаких предположений о моменте своей загрузки.

SSR и LCP (рекомендованный паттерн)

Сам плеер рендерится на клиенте — он целиком конструирует свой DOM через JS (document/HTMLVideoElement), на сервере его «отрисовать» нельзя, как и любой императивный JS-плеер. Это нормально: на сервере у вас и так нет видео, нужен лишь быстрый постер. Поэтому правильный паттерн — постер серверный, плеер ленивый:

  1. Отрендерьте постер на сервере обычным <img> (не background-image — фон не участвует в LCP и грузится позже). Сделайте его LCP-кандидатом: fetchpriority="high", корректные width/height (чтобы не было layout shift), при необходимости <picture> с srcset под мобайл/десктоп.

    <div id="player" class="my-player-box">
      <img src="/poster-1280.jpg"
           srcset="/poster-480.jpg 480w, /poster-1280.jpg 1280w" sizes="100vw"
           width="1280" height="720" alt="" fetchpriority="high" decoding="async"
           class="my-player-poster">
      <button class="my-player-play" aria-label="Play"></button>
    </div>
  2. Инициализируйте плеер лениво — после первого интерактива/появления во вьюпорте, через itube-modern-player/lazy (или свой динамический import). До инициализации виден ваш серверный постер; JS-бандл и hls.js не блокируют первый экран.

    import { createLazyPlayer } from 'itube-modern-player/lazy'
    createLazyPlayer('#player', { source: { src: '/stream.m3u8', poster: '/poster-1280.jpg' }, loadOn: 'interaction' })

Итог: LCP — это ваш серверный <img>-постер (приходит в первом HTML-ответе, приоритизируется браузером), а вес плеера и стрима подключается только когда пользователь реально собрался смотреть. Так делает и тестовый стенд проекта (постер <picture><img> в шаблоне страницы, плеер поверх по интерактиву).

Если задать source.poster, плеер отрисует свой постер тоже (реальным <img fetchpriority="high">). Он сработает как LCP в чисто клиентских сценариях, но для лучшего LCP всё равно предпочтительнее серверный постер из пункта 1 — он есть в HTML сразу, без ожидания JS.

Сборка и публикация

npm run dev        # демо-страница на vite
                   #   /?proxyads — гонит VAST-теги через дев-прокси /__vast
                   #   (для окружений, где рекламные домены режутся сетью/блокировщиком)
npm run typecheck  # tsc --noEmit
npm run build      # dist/: ESM + CJS + d.ts + style.css
npm publish        # prepublishOnly прогонит typecheck + build

Экспорты пакета:

| Импорт | Что это | | --- | --- | | itube-modern-player | ядро (Player, все типы, утилиты) | | itube-modern-player/vue | Vue 3-компонент ITubePlayer | | itube-modern-player/style.css | стили (подключить один раз) |