npm.io
0.4.4 • Published 2h ago

itube-modern-player

Licence
MIT
Version
0.4.4
Deps
1
Size
1.2 MB
Vulns
0
Weekly
0

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-спрайта, клик — переход к сцене.
  • Типизированные сцены — несколько разбивок по типам (актёры/локации/действия…, sceneGroups), контрол-дропдаун выбора типа перестраивает таймлайн и список сцен.
  • Спрайт-превью — кадр при наведении на таймлайн (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,                             // для плашки в плейлисте до загрузки метаданных
  // чанки для превью следующего ролика (ховер + end-оверлей), разделяются «·».
  // строка — обычный чанк; { text, icon } — чанк с инлайн-иконкой слева (сырой SVG).
  previewMeta: ['2.3K просмотров', { text: 'User uploaded', icon: '<svg…></svg>' }],

  // канал/автор — рендерится в экране паузы
  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 заполняется автоматически
  ],

  // типизированные сцены: несколько разбивок по типам (актёры/локации/действия…)
  // переключаются контролом «тип сцен»; активная группа задаёт сегменты таймлайна
  // и список сцен (имеет приоритет над chapters). Список типов открытый.
  sceneGroups: [
    {
      id: 'actions',                 // стабильный id
      title: 'Действия',             // подпись на контроле и в дропдауне
      icon: '<svg ...>...</svg>',    // raw SVG или URL картинки (необязательно)
      scenes: [                      // те же {start, end?, title}, что у chapters
        { start: 0, title: 'Интро' },
        { start: 120, title: 'Погоня' },
      ],
    },
    { id: 'locations', title: 'Локации', icon: '...', scenes: [/* … */] },
  ],

  // субтитры: строка-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 не обязателен — берётся начало следующей главы или конец видео.

Типизированные сцены (sceneGroups)

Несколько разбивок видео по типам — актёры, локации, действия и т.п. (список открытый, задаётся по id). Каждая группа: { id, title, icon?, scenes: Chapter[] }. В баре появляется контрол выбора типа (иконка + тайтл текущего типа, дропдаун); переключение перестраивает сегменты прогресс-бара и панель сцен. Активная группа имеет приоритет над chapters.

  • Управление в рантайме: player.setSceneGroup(id), геттеры player.sceneTypes / player.activeSceneType, событие scenetypechange.
  • Отключить контрол: controls.sceneTypes: false. Заголовок дропдауна — лейбл sceneTypes (локализован; переопределяется через labels).
Спрайт-превью (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: 15,      // секунды для стрелок/кнопок перемотки (по умолчанию 15; показывается на кнопке)
  keyboard: true,    // горячие клавиши (на контейнере, не на document!)
  className: 'my-player',       // свой класс на контейнер — хук для темизации
  crossOrigin: 'anonymous',     // если VTT/постеры на другом домене
  playsInline: true,

  // ---- сохранение настроек в localStorage ----
  // true — запоминать громкость/mute и автоплей и восстанавливать при следующей
  // загрузке (восстановленные значения имеют приоритет над volume/muted/autoAdvance).
  // Объект — точечно: { key?: 'itube-player', volume?: true, autoAdvance?: true }.
  // По умолчанию выключено.
  persist: 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,
    // Настройки. Каждая: 'gear' (в едином дропдауне-шестерёнке, по умолчанию) | 'bar'
    // (отдельной кнопкой в баре) | false (выкл). true = 'gear'. Шестерёнка скрыта,
    // если внутри нет ни одной доступной опции.
    speed: 'gear',       // скорость воспроизведения (бывш. `settings`; алиас ещё работает)
    quality: 'gear',     // качество; пункт/кнопка появляется при source.qualities или HLS-уровнях
    subtitles: 'gear',   // субтитры; появляется только если у источника есть треки
    scenes: true,        // кнопка списка сцен (появляется при наличии chapters)
    sceneTypes: true,    // контрол выбора типа сцен (появляется при source.sceneGroups)
    heatmap: true,       // диаграмма популярности (нужны данные source.heatmap)
    seekButtons: true,   // или { back: 5, forward: 15, label: (sec, dir) => `${dir==='back'?'':'+'}${sec}s` }
    // порядок правых контролов слева направо; пусто/нет — встроенный порядок.
    // перечисленные идут первыми, остальные сохраняют дефолтную позицию, ⋯ всегда последняя.
    order: ['gear', 'pip', 'fullscreen'],  // ControlBarItem[]: like|dislike|speed|quality|subtitles|gear|scenes|sceneTypes|playlist|pip|fullscreen|`custom:<id>`
    seekPlacement: 'overlay', // 'overlay' (default) — ±N и play поверх видео на всех экранах
                              // (prev/next в оверлее только на мобиле; на десктопе play/prev/next
                              // в баре) | 'bar' — seek-кнопки в нижнем баре (поведение до 0.3)
    playlist: true,      // prev/next/список — рендерятся ТОЛЬКО в режиме плейлиста
    hidePrev: true,      // скрыть кнопку «prev» в баре (бар = только next); false — вернуть.
                         // На мобиле центр-кластер всё равно показывает prev
    nextPreview: true,   // ховер-превью следующего ролика над кнопкой next (десктоп).
                         // объект: { thumbnail?, title?, duration?, meta? } — какие поля показывать
                         // (иконки чанков задаются в самих source.previewMeta — см. { text, icon })
    hideDelay: 2500,     // мс до скрытия контролов при воспроизведении
  },

  // ---- экран паузы ----
  // true (по умолчанию) — заголовок/описание/канал текущего видео (порядок: спонсор → тайтл → описание)
  // false — выключить
  // { title?, description?, sponsor? } — дефолтный экран, но скрыть отдельные части
  //   (напр. { title: false, description: 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>' },  // в дропдауне ⋯ (placement: 'menu' по умолчанию)
      { id: 'theater', title: 'Театр', icon: '<svg ...>', placement: 'bar' }, // отдельной кнопкой в баре (нужна icon, тултип = title)
    ],
  },

  // ---- кастомизация каждой кнопки ----
  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 Показать/скрыть панель сцен
setSceneGroup(id): void Переключить тип сцен; sceneTypes / activeSceneType — геттеры
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 } Сменилась глава
scenetypechange { group | null } Сменился тип сцен (sceneGroups)
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 стили (подключить один раз)

История изменений

Версионирование по SemVer: major.minor.patch.

0.4.4
  • Акцент на текущем времени в контрол-баре. Лейбл времени разбит на текущее (00:31) и общее ( / 6:33); часть со слешем и длительностью приглушена (opacity: 0.6), чтобы взгляд цеплялся за текущую позицию.
0.4.3
  • Иконки чанков previewMeta (инлайн, per-chunk). previewMeta теперь принимает не только строки, но и { text, icon } — иконка привязана к конкретному чанку и рисуется слева именно от него (как «человечек» у «Загружено пользователем»), а не одним глифом слева от всего блока. Тип — PreviewMetaItem = string | { text, icon }. Мета-строка переносится пословно (инлайн-поток), а не целым чанком; разделитель «·» приклеен к концу чанка и не орфанится в начале строки.
  • Превью следующего ролика по ховеру получило ту же мета-строку, что и end-оверлей (единый рендер), вместе с иконками чанков. Добавлены дефолтные иконки user / channel в набор (options.icons).
0.4.2
  • Сохранение настроек в localStorage. Новая опция persist запоминает громкость/mute и автоплей-next и восстанавливает их при следующей загрузке. true — включить всё с дефолтами; объект { key?, volume?, autoAdvance? } — точечно (свой ключ хранилища, отдельные тумблеры). По умолчанию выключено. Восстановленные значения имеют приоритет над volume / muted / playlist.autoAdvance. Запись best-effort (приватный режим / SSR / квота не роняют плеер).
0.4.1
  • Фикс: end-оверлей «Играть дальше» не кликался. Слой .imp-upnext наследовал pointer-events: none от родителя — клик проходил сквозь оверлей на <video> и просто снимал текущий ролик с паузы (выглядело как «играет то же самое видео»). Теперь оверлей перехватывает клики, курсор над кнопкой — pointer.
  • Заголовок end-оверлея — «NEXT VIDEO». Вынесен в отдельный лейбл nextVideo (раньше использовался next), локализован во всех 11 языках; переопределяется через options.labels.nextVideo.
  • Иконка аплоадера в end-оверлее «Следующее видео» (доработана в 0.4.3 до per-chunk и распространена на ховер-превью — см. выше).
0.4.0
  • Типизированные сцены. VideoSource.sceneGroups — массив групп { id, title, icon?, scenes } (актёры / локации / действия и т.п., список открытый). Активная группа задаёт сегменты таймлайна и список сцен; имеет приоритет над chapters.
  • Контрол выбора типа сцен — дропдаун с иконкой и тайтлом текущего типа; показывается, когда у источника есть sceneGroups. Переключение перестраивает прогресс-бар и панель сцен. Отключается controls.sceneTypes: false. Новое событие scenetypechange, методы player.setSceneGroup(id) / player.sceneTypes / player.activeSceneType. Лейбл sceneTypes локализован во всех 11 языках.
  • Спонсор-ссылка на пауз-скрине — жёлтый бейдж + подчёркнутый текст (как в референс-промо).
  • Автовоспроизведение (свитч в шестерёнке). Тоггл Autoplay в дропдауне настроек (по умолчанию вкл, виден в режиме плейлиста). Вкл — следующий ролик стартует автоматически; выкл — по окончании показывается end-оверлей с превью следующего ролика и кнопкой «Играть дальше». API: player.autoAdvance / player.setAutoAdvance(bool). Лейблы autoplay / playNext — во всех 11 локалях. Меню настроек теперь поддерживает строки-переключатели.
  • Формат подписи перемоткиcontrols.seekButtons.label: (seconds, direction) => string (по умолчанию −15s/+15s).
  • Порядок правых контроловcontrols.order: ControlBarItem[] задаёт порядок слева направо (gear / pip / playlist / like / …). Перечисленные идут первыми, остальные сохраняют дефолтную позицию, ⋯ всегда последняя.
0.3.0

Крупный апдейт UI и набора пропсов (есть смена дефолтов — см. «Миграция»).

  • Единый дропдаун-шестерёнка для настроек. Скорость, качество и субтитры по умолчанию собраны в одну кнопку . Меню — drill-down (как у больших плееров): сначала список настроек строками с текущим значением (Качество — Авто ›, Скорость — 1× ›, Субтитры — Русский ›), клик по строке открывает её опции, «‹» — назад. Каждый пункт можно вынести отдельной кнопкой в бар (controls.speed/quality/subtitles: 'bar') или убрать (false). Кнопка-шестерёнка не показывается, если внутри нет ни одной доступной опции. Сцены и плейлист остаются отдельными кнопками (это управление воспроизведением).
  • В контрол-баре по умолчанию только «next» (кнопка «prev» в баре скрыта — controls.hidePrev: true по умолчанию; false возвращает её). На мобиле центр-кластер по-прежнему показывает и prev, и next.
  • Перемотка ±N и большая кнопка play — оверлеем поверх видео на всех экранах (раньше только на мобиле). prev/next в оверлее — только на мобиле; на десктопе play/prev/next остаются в контрол-баре (рядом друг с другом). Управляется controls.seekPlacement ('overlay' по умолчанию, 'bar' — прежнее десктоп-поведение с seek в нижнем баре). Шаг перемотки виден на кнопке (число в стрелке).
  • Шаг перемотки по умолчанию 15 сек (был 10) и показывается на кнопке (число внутри стрелки). Меняется через seekStep или controls.seekButtons: { back, forward }.
  • Кастом-кнопки прямо в контрол-баре. У actions.custom[] появился placement: 'bar' (иконка + тултип из title); 'menu' (дефолт) — как раньше, пункт в ⋯.
  • Превью следующего ролика по ховеру на кнопке next (десктоп): обложка, тайтл, длительность и доп. строки из source.previewMeta. Конфиг полей — controls.nextPreview.
  • Пауз-скрин: порядок спонсор → тайтл → описание (рекламная ссылка теперь сверху). Новый объект-вариант pauseScreen: { title?, description?, sponsor? } — скрыть отдельные части (когда сайт выводит их вне плеера).

Миграция:

  • controls.settings (булев, включал меню скорости) → переименован в controls.speed со значениями 'gear' | 'bar' | false. Старый settings ещё работает как алиас.
  • controls.quality / controls.subtitles теперь boolean | 'gear' | 'bar' (булевы значения совместимы: true = в шестерёнке).
  • seekStep по умолчанию 15 вместо 10 — задайте seekStep: 10, чтобы вернуть прежний шаг.
  • Seek-кнопки по умолчанию в оверлее, а не в нижнем баре — верните controls: { seekPlacement: 'bar' } при необходимости.
0.2.2

Фиксы UX на реальных iOS-устройствах (в десктоп-девтулзах не воспроизводились):

  • Реклама больше не перекрывает свои кнопки. На iOS <video> композитится в отдельный GPU-слой и всплывал поверх HUD и кнопок «Skip»/«Visit» — тап по скипу проваливался в видео и открывал click-through, рекламу (в т.ч. бесконечные «стримы») нельзя было пропустить. HUD и блок кнопок подняты в собственный контекст наложения (z-index).
  • Кнопки рекламы реагируют на первый тапtouch-action: manipulation убирает 300-мс задержку и дабл-тап-зум iOS.
  • Адекватная тач-модель контролов. На тач-устройствах тап по видео показывает/скрывает контролы (play/pause — центральной кнопкой), а не дёргает воспроизведение. Раньше одно касание порождало сразу несколько событий (показ контролов + переключение play). Поведение мыши на десктопе не изменилось.
  • Скрытые контролы (авто-idle) перестали перехватывать тап «вслепую» — первое касание сначала показывает их.
  • Скрытые контролы полностью неактивны. Кнопки ±10 сек и панель управления больше нельзя нажать в невидимом (idle) состоянии во время проигрывания: pointer-events:none на родителе не отключал потомков с pointer-events:auto, теперь инертен весь скрытый сабтри.
  • Панели сцен и плейлиста на мобиле закрываются при выборе. Раньше после выбора сцены/видео нужно было вручную закрывать full-screen лист крестиком, чтобы увидеть видео. Теперь на мобиле (≤767px) лист закрывается сразу по тапу; десктоп-сайдбар остаётся открытым (видео и так видно рядом).
0.2.1
  • Мелкие багфиксы.
0.2.0
  • styling-проп: акцент темы, цвета лайка/дизлайка, радиус скругления, стиль кнопки play (solid/inverted).
  • related.clickBehavior — поведение клика по похожему видео (player / newWindow / currentTab).
  • HLS-движок грузится через hls.js/light (легче); полный window.Hls, если он уже на странице, имеет приоритет.
  • LCP-постер реальным <img fetchpriority="high">.
  • Восстановление после decode-ошибок воспроизведения.
0.1.0
  • Первый публичный релиз. Ядро без фреймворк-зависимостей + Vue 3-обёртка, IIFE-бандл и lazy-вход. Плейлисты, главы/тайм-коды, панель сцен, спрайт-превью, VTT-субтитры, VAST 2–4 (pre/mid/post-roll), экран паузы, related-видео, кнопки действий, 11 локализаций, темизация CSS-переменными, полная типизация.

Keywords