itube-modern-player 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +630 -0
  3. package/dist/core/dom.d.ts +18 -0
  4. package/dist/core/events.d.ts +10 -0
  5. package/dist/core/icons.d.ts +3 -0
  6. package/dist/core/labels.d.ts +2 -0
  7. package/dist/core/localeRegistry.d.ts +7 -0
  8. package/dist/core/locales.d.ts +11 -0
  9. package/dist/core.cjs +5 -0
  10. package/dist/core.cjs.map +1 -0
  11. package/dist/core.js +1668 -0
  12. package/dist/core.js.map +1 -0
  13. package/dist/coreEntry.d.ts +14 -0
  14. package/dist/features/ads/manager.d.ts +48 -0
  15. package/dist/features/ads/vast.d.ts +9 -0
  16. package/dist/features/chapters.d.ts +12 -0
  17. package/dist/features/heatmap.d.ts +17 -0
  18. package/dist/features/hls.d.ts +26 -0
  19. package/dist/features/thumbnails.d.ts +11 -0
  20. package/dist/global.d.ts +2 -0
  21. package/dist/index.cjs +2 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +28 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/itube-modern-player.iife.js +5 -0
  27. package/dist/itube-modern-player.iife.js.map +1 -0
  28. package/dist/labels-C3gAZEm-.js +41 -0
  29. package/dist/labels-C3gAZEm-.js.map +1 -0
  30. package/dist/labels-DTgTxMuq.cjs +2 -0
  31. package/dist/labels-DTgTxMuq.cjs.map +1 -0
  32. package/dist/lazy.cjs +2 -0
  33. package/dist/lazy.cjs.map +1 -0
  34. package/dist/lazy.d.ts +23 -0
  35. package/dist/lazy.js +60 -0
  36. package/dist/lazy.js.map +1 -0
  37. package/dist/locales.cjs +2 -0
  38. package/dist/locales.cjs.map +1 -0
  39. package/dist/locales.js +380 -0
  40. package/dist/locales.js.map +1 -0
  41. package/dist/localesEntry.d.ts +8 -0
  42. package/dist/player.d.ts +142 -0
  43. package/dist/style.css +1 -0
  44. package/dist/types.d.ts +443 -0
  45. package/dist/ui/controls.d.ts +90 -0
  46. package/dist/ui/menu.d.ts +29 -0
  47. package/dist/ui/overlays.d.ts +51 -0
  48. package/dist/ui/playlistPanel.d.ts +16 -0
  49. package/dist/ui/progress.d.ts +41 -0
  50. package/dist/ui/scenesPanel.d.ts +19 -0
  51. package/dist/vue.cjs +2 -0
  52. package/dist/vue.cjs.map +1 -0
  53. package/dist/vue.d.ts +67 -0
  54. package/dist/vue.js +89 -0
  55. package/dist/vue.js.map +1 -0
  56. package/package.json +86 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksandr Shevchenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,630 @@
1
+ # itube-modern-player
2
+
3
+ Лёгкий универсальный HTML5-видеоплеер на TypeScript для видео-сайтов и встраиваемых страниц. Ядро — чистый класс без фреймворк-зависимостей (`new Player(...)`), поверх него — Vue 3-компонент, IIFE-бандл для `<script>`-подключения и lazy-обёртка для отложенной загрузки. Сделан как замена fluid-player: та же область применения (контентные видео с рекламой, плейлистами и стримами), но без его архитектурных болячек — без глобального реестра инстансов, без россыпи absolute-элементов, с честным `destroy()` и полной типизацией.
4
+
5
+ **Ядро ~19–23 КБ gzip** (+ hls.js, который догружается динамически только для m3u8). Поддержка: все вечнозелёные браузеры, Safari/iOS (нативный HLS, playsinline, нативный fullscreen).
6
+
7
+ ## Возможности
8
+
9
+ - **Источники** — MP4/WebM, HLS-стримы (`.m3u8`, hls.js или нативно в Safari), live-потоки с бейджем LIVE, прогрессивные качества с переключением без потери позиции, HLS-уровни с пунктом Auto.
10
+ - **Плейлисты** — массив видео без пересоздания плеера, prev/next, панель списка (сайдбар или горизонтальная лента снизу), имя плейлиста, перемешивание, повтор, автопереход.
11
+ - **Тайм-коды (главы)** — сегментированный таймлайн как у YouTube, название главы в контролах и тултипе, событие `chapterchange`, источник: массив или VTT-файл.
12
+ - **Панель сцен** — список глав с превью из VTT-спрайта, клик — переход к сцене.
13
+ - **Спрайт-превью** — кадр при наведении на таймлайн (WebVTT + `#xywh`).
14
+ - **Диаграмма популярности** — «most replayed»-кривая над таймлайном из разреженных точек.
15
+ - **Субтитры** — VTT-треки (строка/объект/массив), выключены по умолчанию, меню выбора.
16
+ - **Реклама** — VAST 2–4 (InLine + Wrapper), preroll/midroll/postroll, прямые `src`-роллы, skip-таймер, click-through, impression/quartile/pause/resume-пиксели, отдельный `<video>` (контент не трогается), префетч контента во время преролла, watchdog на зависший креатив, частота показа для плейлистов, полный набор событий.
17
+ - **Метаданные видео** — тайтл, описание, постер, канал (аватар/заглушка, форма, ссылка) в экране паузы.
18
+ - **Экран паузы** — дефолтный или полностью свой («слот»: элемент/фабрика в ядре, `<template #pauseScreen>` во Vue).
19
+ - **Related-видео** — сетка похожих по паузе и/или окончанию.
20
+ - **Кнопки действий** — лайк/дизлайк (с управляемым состоянием), «добавить в», шеринг (нативный share sheet с фолбэком на событие), жалоба, свои кнопки в ⋯-дропдауне (`customaction`).
21
+ - **Кастомизация** — каждая фича отключаема, каждая иконка заменяема (SVG), все строки переводимы, темизация CSS-переменными + `styling`-проп (акцент, цвета лайков).
22
+ - **Локализации** — 11 встроенных языков по коду `language`, свои словари через `registerLocale`, облегчённый вход без словарей.
23
+ - **UX-мелочи** — превью-постер, горячие клавиши (на контейнере, не на document), PiP, полноэкранный режим, спиннер буферизации, оверлей ошибок, авто-скрытие контролов.
24
+ - **Интеграции** — vanilla / Vue 3 / `<script>`-тег / ленивая загрузка бандла по интерактиву.
25
+ - **Типизация** — полные типы всех опций, событий и методов из коробки.
26
+
27
+ Чем отличается от fluid-player и подобных:
28
+
29
+ - **Никакой каши из absolute-элементов.** Поверх видео лежит один overlay-слой на CSS grid, всё внутри — обычный flex-поток. Верстать поверх и кастомизировать — легко.
30
+ - **Нет глобального состояния.** `new Player()` сколько угодно раз на странице, никаких реестров инстансов и привязок по id.
31
+ - **Честный `destroy()`.** Плеер полностью убирает за собой DOM и слушатели.
32
+ - **Всё типизировано.** События, опции, методы — полные типы из коробки.
33
+ - **Всё отключаемо и заменяемо.** Каждая кнопка, каждая надпись, каждая иконка.
34
+
35
+ ---
36
+
37
+ ## Установка
38
+
39
+ ```bash
40
+ npm install itube-modern-player
41
+ ```
42
+
43
+ `hls.js` подтягивается автоматически и грузится **только** когда плееру дают m3u8-источник (динамический импорт) — в бандл проектов без стримов он не попадает. Vue — опциональная peer-зависимость, нужна только если используете `itube-modern-player/vue`.
44
+
45
+ ## Быстрый старт (vanilla JS / TS)
46
+
47
+ Ядро — обычный класс без фреймворков, это основной способ использования.
48
+
49
+ ```ts
50
+ import { Player } from 'itube-modern-player'
51
+ import 'itube-modern-player/style.css'
52
+
53
+ const player = new Player('#mount', {
54
+ source: {
55
+ src: 'https://cdn.example.com/stream.m3u8',
56
+ title: 'Название ролика',
57
+ poster: 'https://cdn.example.com/poster.jpg',
58
+ },
59
+ })
60
+
61
+ player.on('ended', () => console.log('done'))
62
+ // ...
63
+ player.destroy()
64
+ ```
65
+
66
+ Первый аргумент — элемент или селектор контейнера, в который плеер отрендерит себя. `destroy()` возвращает контейнер в исходное состояние.
67
+
68
+ ### Без сборщика (script-tag / CDN)
69
+
70
+ Отдельный IIFE-бандл, глобальная переменная `ITubePlayer` — это сам класс:
71
+
72
+ ```html
73
+ <link rel="stylesheet" href="https://unpkg.com/itube-modern-player/dist/style.css">
74
+ <!-- hls.js нужен только если будут m3u8-источники: -->
75
+ <script src="https://unpkg.com/hls.js"></script>
76
+ <script src="https://unpkg.com/itube-modern-player/dist/itube-modern-player.iife.js"></script>
77
+ <script>
78
+ const player = new ITubePlayer('#mount', {
79
+ source: { src: 'https://cdn.example.com/stream.m3u8', title: 'Demo' },
80
+ })
81
+ </script>
82
+ ```
83
+
84
+ ## Быстрый старт (Vue 3)
85
+
86
+ ```vue
87
+ <script setup lang="ts">
88
+ import { ITubePlayer } from 'itube-modern-player/vue'
89
+ import 'itube-modern-player/style.css'
90
+ import type { VideoSource } from 'itube-modern-player'
91
+
92
+ const video: VideoSource = {
93
+ src: '/stream.m3u8',
94
+ title: 'Название',
95
+ }
96
+ </script>
97
+
98
+ <template>
99
+ <ITubePlayer
100
+ :source="video"
101
+ :options="{ seekStep: 5, controls: { pip: false } }"
102
+ @ended="onEnded"
103
+ @timeupdate="onTime"
104
+ >
105
+ <!-- слот экрана паузы: реклама, промо, что угодно.
106
+ Scoped-слот даёт доступ к плееру и к close() ("Закрыть и продолжить") -->
107
+ <template #pauseScreen="{ player, close }">
108
+ <MyAdBlock @close="close" />
109
+ </template>
110
+ </ITubePlayer>
111
+ </template>
112
+ ```
113
+
114
+ ### Кастомный экран паузы (рекламный блок)
115
+
116
+ Вместо дефолтного оверлея (тайтл/описание/канал) можно показать свой блок — например рекламу с кнопкой «Закрыть и продолжить». Контент рендерится full-cover по центру плеера поверх контролов; клик по фону не возобновляет (только ваша кнопка). Доступ к плееру есть во всех сборках:
117
+
118
+ - **Vue 3** — scoped-слот: `#pauseScreen="{ player, close }"`. `player` — инстанс (реактивный, `null` до монтирования), `close()` — возобновить воспроизведение и закрыть блок.
119
+ - **vanilla / IIFE** — фабрика, получающая плеер: `pauseScreen: (player) => HTMLElement`. Кнопка вызывает `player.play()`.
120
+ - В рантайме — `player.setPauseScreenContent(el | null)`.
121
+
122
+ ```ts
123
+ // vanilla / Vue2 (внутри компонента)
124
+ new Player('#mount', {
125
+ pauseScreen: (player) => {
126
+ const box = document.createElement('div')
127
+ box.innerHTML = '<!-- ваш рекламный блок -->'
128
+ const btn = document.createElement('button')
129
+ btn.textContent = 'Закрыть и продолжить'
130
+ btn.onclick = () => player.play() // возобновляет и скрывает блок
131
+ box.append(btn)
132
+ return box
133
+ },
134
+ })
135
+ ```
136
+
137
+ - `:source` — одно видео или массив (массив включает режим плейлиста). Реактивен: смена объекта вызывает `player.load()`, инстанс плеера **не пересоздаётся**.
138
+ - `:options` — все остальные `PlayerOptions` (см. ниже).
139
+ - Все события плеера ретранслируются как события компонента.
140
+ - Доступ к ядру: `ref` на компонент → `componentRef.value.player` (`ShallowRef<Player>`).
141
+
142
+ ---
143
+
144
+ ## Описание видео: `VideoSource`
145
+
146
+ ```ts
147
+ const source: VideoSource = {
148
+ src: 'https://cdn.example.com/video.m3u8', // m3u8 определяется автоматически
149
+ type: 'application/x-mpegurl', // необязательно, для нестандартных URL
150
+ title: 'Заголовок',
151
+ description: 'Описание — показывается в экране паузы',
152
+ poster: 'poster.jpg', // превью-постер до первого запуска
153
+ duration: 1284, // для плашки в плейлисте до загрузки метаданных
154
+
155
+ // канал/автор — рендерится в экране паузы
156
+ channel: {
157
+ name: 'Мой канал',
158
+ avatar: 'avatar.png', // без avatar — стилизованная заглушка из первой буквы
159
+ url: 'https://example.com/channel', // клик по каналу откроет URL
160
+ avatarShape: 'circle', // 'circle' (default) | 'rounded' | 'square'
161
+ },
162
+
163
+ // тайм-коды: массив или URL WebVTT-файла глав
164
+ chapters: [
165
+ { start: 0, title: 'Интро' },
166
+ { start: 120, title: 'Основная часть' }, // end заполняется автоматически
167
+ ],
168
+
169
+ // субтитры: строка-URL, один трек или массив. По умолчанию ВЫКЛЮЧЕНЫ.
170
+ subtitles: [
171
+ { src: 'subs-ru.vtt', label: 'Русский', srclang: 'ru' },
172
+ { src: 'subs-en.vtt', label: 'English', srclang: 'en', default: true }, // default включает
173
+ ],
174
+
175
+ // спрайт-превью при наведении на таймлайн (WebVTT с фрагментами #xywh=)
176
+ thumbnails: 'thumbs.vtt',
177
+
178
+ // диаграмма популярности над таймлайном (как "most replayed" у YouTube)
179
+ // разреженные точки {time: сек, value: сырое число кликов/досмотров};
180
+ // внутри: бакетирование → сглаживание → нормализация к максимуму с базовым полом,
181
+ // появляется при наведении на прогресс-бар; выключается controls.heatmap = false
182
+ heatmap: [
183
+ { time: 95, value: 220 },
184
+ { time: 290, value: 900 },
185
+ { time: 430, value: 540 },
186
+ ],
187
+
188
+ // альтернативные качества для прогрессивных файлов (для HLS уровни берутся из манифеста)
189
+ // переключение сохраняет позицию и состояние воспроизведения
190
+ qualities: [
191
+ { quality: 1080, label: '1080p', src: 'video-1080.mp4' },
192
+ { quality: 720, label: '720p', src: 'video-720.mp4' },
193
+ ],
194
+
195
+ meta: { anyOwnData: true }, // ваши данные, плеер их не трогает
196
+ }
197
+ ```
198
+
199
+ ### Тайм-коды (главы)
200
+
201
+ Поддержаны нативно: сегментированный таймлайн (как у YouTube), название текущей главы в контрол-баре, название главы в ховер-тултипе, событие `chapterchange`. Источник — массив `{ start, end?, title }` либо URL VTT-файла глав. `end` не обязателен — берётся начало следующей главы или конец видео.
202
+
203
+ ### Спрайт-превью (`thumbnails`)
204
+
205
+ Стандартный формат — WebVTT, каждый cue указывает картинку и регион спрайта:
206
+
207
+ ```
208
+ WEBVTT
209
+
210
+ 00:00:00.000 --> 00:00:05.000
211
+ sprite.jpg#xywh=0,0,160,90
212
+
213
+ 00:00:05.000 --> 00:00:10.000
214
+ sprite.jpg#xywh=160,0,160,90
215
+ ```
216
+
217
+ Относительные пути резолвятся от URL VTT-файла. Можно и без `#xywh` — по картинке на cue.
218
+
219
+ ---
220
+
221
+ ## Все опции: `PlayerOptions`
222
+
223
+ ```ts
224
+ new Player('#mount', {
225
+ source, // VideoSource | VideoSource[]
226
+ autoplay: false,
227
+ muted: false,
228
+ loop: false,
229
+ volume: 1,
230
+ playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2], // пункты меню скорости
231
+ seekStep: 10, // секунды для стрелок/кнопок перемотки
232
+ keyboard: true, // горячие клавиши (на контейнере, не на document!)
233
+ className: 'my-player', // свой класс на контейнер — хук для темизации
234
+ crossOrigin: 'anonymous', // если VTT/постеры на другом домене
235
+ playsInline: true,
236
+
237
+ // ---- стилизация ----
238
+ styling: {
239
+ themeColor: '#00b3a4', // акцентный цвет (вместо встроенного красного)
240
+ likeColor: 'forestgreen', // цвет активного лайка (default forestgreen)
241
+ dislikeColor: '#e53935', // цвет активного дизлайка (не зависит от themeColor)
242
+ },
243
+
244
+ // ---- язык интерфейса ----
245
+ // встроенные статические локали: en, ru, de, es, it, ja, ko, zh, pt, ar, hi
246
+ // словарь терминов = тип PlayerLabels; defaultLabels (en) экспортируется как эталон
247
+ // точечные оверрайды — через labels (применяются поверх локали)
248
+ language: 'ru',
249
+
250
+ // ---- плейлист ----
251
+ playlist: {
252
+ title: 'Моя подборка', // имя плейлиста: маленький кикер "Плейлист" + имя приоритетным шрифтом
253
+ autoAdvance: true, // автопереход к следующему
254
+ loop: false, // репит списка (кнопка в панели плейлиста)
255
+ shuffle: false, // перемешивание (кнопка в панели плейлиста)
256
+ startIndex: 0,
257
+ layout: 'sidebar', // 'sidebar' — справа вертикально | 'bottom' — горизонтальная лента снизу
258
+ },
259
+
260
+ // ---- панель сцен (тайм-коды с превью из VTT-спрайта) ----
261
+ scenes: {
262
+ layout: 'bottom', // 'bottom' (default) | 'sidebar'
263
+ },
264
+
265
+ // ---- контролы: каждая фича отключаемая ----
266
+ controls: {
267
+ play: true,
268
+ progress: true,
269
+ time: true,
270
+ volume: true,
271
+ fullscreen: true,
272
+ pip: true,
273
+ settings: true, // меню скорости (иконка-спидометр)
274
+ quality: false, // меню качества (иконка-шестерёнка); ВЫКЛЮЧЕНО по умолчанию,
275
+ // кнопка появляется при наличии source.qualities или HLS-уровней
276
+ scenes: true, // кнопка списка сцен (появляется при наличии chapters)
277
+ heatmap: true, // диаграмма популярности (нужны данные source.heatmap)
278
+ subtitles: true, // кнопка появляется только если у источника есть треки
279
+ seekButtons: true, // или { back: 5, forward: 15 }
280
+ playlist: true, // prev/next/список — рендерятся ТОЛЬКО в режиме плейлиста
281
+ hideDelay: 2500, // мс до скрытия контролов при воспроизведении
282
+ },
283
+
284
+ // ---- экран паузы ----
285
+ // true (по умолчанию) — заголовок/описание/канал текущего видео
286
+ // false — выключить
287
+ // HTMLElement или (player) => HTMLElement — свой контент (full-cover оверлей,
288
+ // напр. реклама с «Закрыть и продолжить»; фабрика получает плеер).
289
+ // Во Vue — scoped-слот #pauseScreen="{ player, close }". См. раздел про кастомный экран паузы.
290
+ pauseScreen: true,
291
+
292
+ // ---- related-видео ----
293
+ related: {
294
+ title: 'Смотрите также',
295
+ showOn: ['ended', 'pause'], // когда показывать; по умолчанию ['ended']
296
+ items: [
297
+ {
298
+ title: 'Другой ролик',
299
+ poster: 'p.jpg',
300
+ duration: '12:34',
301
+ source: { src: 'other.mp4', title: 'Другой ролик' }, // клик загрузит в плеер
302
+ // или url: 'https://...' — клик откроет ссылку
303
+ },
304
+ ],
305
+ },
306
+
307
+ // ---- реклама (см. раздел «Реклама») ----
308
+ adConfig: {
309
+ adList: [
310
+ { roll: 'preRoll', vastTag: 'https://ads.example.com/vast.xml' },
311
+ { roll: 'midRoll', vastTag: 'https://ads.example.com/vast2.xml', timer: 300 },
312
+ { roll: 'postRoll', src: 'https://cdn.example.com/ad.mp4', clickUrl: 'https://sponsor.example.com' },
313
+ ],
314
+ skipDelay: 5, // сек до кнопки «Пропустить»; -1 — без пропуска
315
+ playOn: 'every', // 'every' — реклама на каждом видео плейлиста, 'first' — только на первом
316
+ maxWrapperDepth: 3, // лимит VAST Wrapper-редиректов
317
+ requestTimeout: 8000, // таймаут запроса VAST-тега, мс
318
+ mediaTimeout: 10000, // если креатив не стартовал/завис на столько мс — aderror и сразу контент
319
+ },
320
+
321
+ // ---- кнопки действий (все по умолчанию ВЫКЛЮЧЕНЫ) ----
322
+ actions: {
323
+ like: true, // видимая кнопка 👍 → событие action {id:'like'}
324
+ dislike: true, // видимая кнопка 👎 → событие action {id:'dislike'}
325
+ likeState: 'like', // начальное состояние оценки ('like' | 'dislike' | null);
326
+ // менять в рантайме: player.setLikeState(...)
327
+ addTo: true, // в дропдауне ⋯ → action {id:'addTo'}
328
+ share: true, // в дропдауне ⋯: нативный share-диалог устройства,
329
+ // если его нет — событие action {id:'share'}
330
+ report: true, // в дропдауне ⋯ → action {id:'report'}
331
+ // свои пункты дропдауна: клик эмитит customaction {id}
332
+ custom: [
333
+ { id: 'download', title: 'Скачать', icon: '<svg ...>...</svg>' }, // icon: URL картинки или raw SVG
334
+ ],
335
+ },
336
+
337
+ // ---- кастомизация каждой кнопки ----
338
+ icons: {
339
+ play: '<svg viewBox="0 0 24 24">...</svg>', // любая иконка — ваш SVG
340
+ pause: '<svg ...>...</svg>',
341
+ // полный список имён: тип IconName
342
+ },
343
+
344
+ // ---- все надписи (i18n) ----
345
+ labels: {
346
+ play: 'Смотреть',
347
+ skipAd: 'Пропустить рекламу',
348
+ related: 'Похожие видео',
349
+ // полный список: тип PlayerLabels
350
+ },
351
+ })
352
+ ```
353
+
354
+ `IconName`: `play, pause, replay, bigPlay, volumeHigh, volumeLow, volumeMute, fullscreen, fullscreenExit, pip, settings, subtitles, list, next, previous, seekForward, seekBack, close`.
355
+
356
+ ---
357
+
358
+ ## Реклама
359
+
360
+ Формат списка совместим по духу с fluid-player: массив роллов, каждый — `preRoll`, `midRoll` (с `timer` — секундой срабатывания) или `postRoll`. Источник ролла:
361
+
362
+ - `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 в вашей странице.
363
+ - `src` — прямой URL медиафайла без VAST (плюс опциональный `clickUrl`).
364
+
365
+ Ключевое отличие от fluid-player: реклама играет в **отдельном** `<video>`, наложенном поверх. Контентное видео не трогается — его позиция, HLS-сессия, выбранные субтитры и качество переживают любой рекламный брейк без «восстановления состояния».
366
+
367
+ Поведение:
368
+
369
+ - `preRoll` — перед стартом видео, `midRoll` — на секунде `timer`, `postRoll` — после окончания; роллов каждого типа может быть несколько.
370
+ - `playOn: 'first'` — рекламный список отрабатывает только на первом видео плейлиста; `'every'` (по умолчанию) — на каждом.
371
+ - Любой фейл (недоступный тег, битый XML, неиграющий медиафайл, таймаут) → событие `aderror` и **немедленный переход к контенту**. Реклама никогда не блокирует ролик.
372
+ - Кнопка пропуска появляется через `skipDelay` секунд (или `skipoffset` из VAST, если он есть).
373
+ - **Префетч контента**: пока играет преролл, основное видео буферизует первые фрагменты в фоне (`preload="auto"` на время брейка; для HLS hls.js грузит сегменты сразу после attach) — после рекламы контент стартует мгновенно.
374
+
375
+ События: `adstart`, `adend`, `adskip`, `adclick`, `aderror`.
376
+
377
+ ```ts
378
+ player.on('aderror', ({ roll, error }) => report(roll, error))
379
+ ```
380
+
381
+ ---
382
+
383
+ ---
384
+
385
+ # Справочник API
386
+
387
+ ## Конструктор
388
+
389
+ ```ts
390
+ new Player(target: string | HTMLElement, options?: PlayerOptions)
391
+ ```
392
+
393
+ `target` — элемент-контейнер или CSS-селектор. Плеер рендерит свой DOM внутрь и полностью убирает его в `destroy()`. Инстансов на странице — сколько угодно, глобального состояния нет.
394
+
395
+ ## Свойства
396
+
397
+ | Свойство | Тип | Описание |
398
+ | --- | --- | --- |
399
+ | `container` | `HTMLElement` | Корневой элемент плеера (`.imp-player`) |
400
+ | `video` | `HTMLVideoElement` | Контентный видео-элемент (прямой доступ для нестандартных нужд) |
401
+ | `paused` | `boolean` | На паузе |
402
+ | `ended` | `boolean` | Дошло до конца |
403
+ | `currentTime` | `number` | Текущая секунда |
404
+ | `duration` | `number` | Длительность (0, пока метаданные не загружены) |
405
+ | `live` | `boolean` | Live-поток (`duration === Infinity`) |
406
+ | `bufferedEnd` | `number` | Конец буферизованного диапазона, сек |
407
+ | `volume` | `number` | Громкость 0..1 |
408
+ | `muted` | `boolean` | Заглушен |
409
+ | `playbackRate` | `number` | Текущая скорость |
410
+ | `playbackRates` | `number[]` | Доступные скорости (из опций) |
411
+ | `qualityLevels` | `Level[]` | `{ index, label }[]` — HLS-уровни или прогрессивные качества |
412
+ | `currentQuality` | `number` | Индекс выбранного качества, `-1` = auto |
413
+ | `qualityAutoAvailable` | `boolean` | Есть ли пункт Auto (только HLS) |
414
+ | `subtitleTracks` | `SubtitleTrack[]` | Треки текущего источника |
415
+ | `activeSubtitle` | `number` | Индекс включённого трека, `-1` = выкл |
416
+ | `playlist` | `VideoSource[]` | Текущий список |
417
+ | `index` | `number` | Индекс играющего элемента |
418
+ | `source` | `VideoSource \| null` | Текущий источник |
419
+ | `hasPlaylist` | `boolean` | Больше одного элемента |
420
+ | `hasNext` / `hasPrevious` | `boolean` | Есть куда листать (учитывает `playlist.loop`) |
421
+ | `chapterList` | `NormalizedChapter[]` | Главы текущего видео (после загрузки метаданных) |
422
+ | `chapter` | `NormalizedChapter \| null` | Текущая глава |
423
+ | `isFullscreen` | `boolean` | Полноэкранный режим |
424
+ | `adPlaying` | `boolean` | Идёт рекламный брейк |
425
+
426
+ ## Методы
427
+
428
+ ### Воспроизведение
429
+
430
+ | Метод | Описание |
431
+ | --- | --- |
432
+ | `play(): Promise<void>` | Запуск. Если есть несыгранный преролл — сначала отыграет его |
433
+ | `pause(): void` | Пауза (во время рекламы игнорируется) |
434
+ | `togglePlay(): void` | Переключить |
435
+ | `seek(sec: number): void` | Абсолютная перемотка (клампится в 0..duration) |
436
+ | `skip(delta: number): void` | Относительная перемотка, например `skip(-10)` |
437
+
438
+ ### Громкость и скорость
439
+
440
+ | Метод | Описание |
441
+ | --- | --- |
442
+ | `setVolume(v: number): void` | 0..1; значение > 0 снимает mute |
443
+ | `setMuted(m: boolean): void` | Установить mute |
444
+ | `toggleMute(): void` | Переключить mute |
445
+ | `setPlaybackRate(r: number): void` | Скорость воспроизведения |
446
+
447
+ ### Качество и субтитры
448
+
449
+ | Метод | Описание |
450
+ | --- | --- |
451
+ | `setQuality(index: number): void` | Для HLS `-1` = auto; для прогрессивных источников переключает файл с сохранением позиции |
452
+ | `setSubtitle(index: number): void` | Включить трек по индексу, `-1` — выключить |
453
+
454
+ ### Плейлист и источники
455
+
456
+ | Метод | Описание |
457
+ | --- | --- |
458
+ | `load(source, startIndex = 0): void` | Заменить источник(и) **без пересоздания плеера** — громкость, скорость, подписки и DOM сохраняются. Массив включает режим плейлиста |
459
+ | `next(): void` / `previous(): void` | Переход по плейлисту (учитывает `loop` и `shuffle`) |
460
+ | `playItem(i: number): void` | Запустить конкретный элемент |
461
+ | `togglePlaylistPanel(): void` | Показать/скрыть панель списка |
462
+ | `toggleScenesPanel(): void` | Показать/скрыть панель сцен |
463
+ | `setShuffle(b)` / `shuffle` | Режим перемешивания |
464
+ | `setRepeat(b)` / `repeat` | Режим повтора списка |
465
+
466
+ ### UI и прочее
467
+
468
+ | Метод | Описание |
469
+ | --- | --- |
470
+ | `toggleFullscreen(): Promise<void>` | Полный экран (на iOS — нативный fullscreen видео) |
471
+ | `togglePip(): Promise<void>` | Картинка-в-картинке |
472
+ | `setPauseScreenContent(el: HTMLElement \| null): void` | Подменить контент экрана паузы (то, что во Vue делает слот) |
473
+ | `setLikeState(s: 'like' \| 'dislike' \| null): void` | Подсветить лайк/дизлайк |
474
+ | `share(): Promise<void>` | Нативный share-диалог; без него — событие `action {id:'share'}` |
475
+ | `destroy(): void` | Полный демонтаж: DOM, слушатели, hls-сессия, реклама |
476
+
477
+ ### События
478
+
479
+ ```ts
480
+ const off = player.on('timeupdate', fn) // вернёт функцию отписки
481
+ player.once('ready', fn)
482
+ player.off('timeupdate', fn)
483
+ ```
484
+
485
+ ## События: `PlayerEventMap`
486
+
487
+ | Событие | Payload | Когда |
488
+ | --- | --- | --- |
489
+ | `ready` | `{ player }` | Плеер создан |
490
+ | `play` / `pause` / `ended` | — | Воспроизведение |
491
+ | `timeupdate` | `{ currentTime, duration }` | Тик времени |
492
+ | `progress` | `{ buffered }` | Буферизация |
493
+ | `volumechange` | `{ volume, muted }` | Громкость |
494
+ | `ratechange` | `{ rate }` | Скорость |
495
+ | `seeking` / `seeked` | `{ currentTime }` | Перемотка |
496
+ | `sourcechange` | `{ source, index }` | Источник заменён |
497
+ | `playlistitemchange` | `{ source, index }` | Переход по плейлисту |
498
+ | `chapterchange` | `{ chapter \| null }` | Сменилась глава |
499
+ | `fullscreenchange` | `{ active }` | Полный экран |
500
+ | `pipchange` | `{ active }` | PiP |
501
+ | `qualitychange` | `{ label }` | Качество (в т.ч. авто-переключение HLS) |
502
+ | `subtitlechange` | `{ track \| null }` | Субтитры |
503
+ | `relatedshow` | — | Показана сетка related |
504
+ | `relatedclick` | `{ item }` | Клик по related-карточке |
505
+ | `action` | `{ id }` | Кнопки: `like`, `dislike`, `addTo`, `share` (если нет нативного шеринга), `report` |
506
+ | `customaction` | `{ id }` | Ваша кнопка из `actions.custom` |
507
+ | `adstart` / `adend` / `adskip` / `adclick` | `{ ad: ResolvedAd }` | Жизненный цикл рекламы |
508
+ | `adpause` / `adresume` | `{ ad: ResolvedAd }` | Креатив поставлен на паузу / возобновлён (+ VAST-пиксели pause/resume) |
509
+ | `aderror` | `{ roll, error }` | Ролл не отыграл (контент продолжается автоматически) |
510
+ | `error` | `{ message, cause? }` | Ошибка медиа/сети/треков |
511
+ | `destroy` | — | Плеер демонтирован |
512
+
513
+ ## Vue 3: `<ITubePlayer>`
514
+
515
+ | Что | API |
516
+ | --- | --- |
517
+ | Пропсы | `source: VideoSource \| VideoSource[]` (реактивный — смена вызывает `load()`), `options: Omit<PlayerOptions, 'source'>` |
518
+ | События | Все события `PlayerEventMap` ретранслируются 1:1 (`@timeupdate`, `@aderror`, `@customaction`, …) |
519
+ | Слоты | `#pauseScreen="{ player, close }"` — контент экрана паузы (scoped: доступ к плееру и `close()`) |
520
+ | Expose | `player: ShallowRef<Player \| null>` — доступ к ядру: `playerRef.value.player.seek(0)` |
521
+
522
+ ## Экспортируемые типы
523
+
524
+ `PlayerOptions, VideoSource, ChannelInfo, SubtitleTrack, Chapter, QualityLevel, PlaylistOptions, ControlsOptions, ActionsOptions, CustomAction, BuiltinActionId, RelatedOptions, RelatedItem, AdsOptions, AdRoll, ResolvedAd, ThumbnailCue, PlayerLabels, IconName, PlayerEvent, PlayerEventMap, Level, SourceController, NormalizedChapter`
525
+
526
+ ## Экспортируемые утилиты
527
+
528
+ | Экспорт | Что делает |
529
+ | --- | --- |
530
+ | `formatTime(sec)` | `3673` → `"1:01:13"` |
531
+ | `locales`, `supportedLanguages`, `getLocale(code)` | Встроенные локали (11 языков) и их коды |
532
+ | `buildHeatmapValues(points, duration)`, `heatmapPath(values)` | Математика диаграммы популярности |
533
+ | `isHlsSource(src, type?)` | Определение m3u8 |
534
+ | `normalizeChapters(chapters, duration)` | Сортировка/заполнение `end` |
535
+ | `loadChaptersVtt(url)` | VTT-файл глав → `Chapter[]` |
536
+ | `ThumbnailTrack.load(url)` | Парсер спрайт-VTT (`cueAt(time)`) |
537
+ | `resolveVast(roll, opts)` | Самостоятельное использование VAST-резолвера |
538
+ | `defaultIcons`, `defaultLabels` | Дефолты для частичного переопределения |
539
+
540
+ ## Темизация
541
+
542
+ Внешний вид настраивается CSS-переменными — на контейнере плеера или любом родителе:
543
+
544
+ ```css
545
+ .my-player {
546
+ --imp-accent: #00bcd4; /* цвет прогресса, кнопки play, активных пунктов */
547
+ --imp-bg: #000;
548
+ --imp-text: #fff;
549
+ --imp-control-bg: rgba(20, 20, 20, 0.85); /* фон меню/панелей */
550
+ --imp-track: rgba(255, 255, 255, 0.25); /* фон таймлайна */
551
+ --imp-radius: 8px;
552
+ --imp-font: Inter, sans-serif;
553
+ --imp-transition: 180ms ease;
554
+ }
555
+ ```
556
+
557
+ Все классы стабильны и начинаются с `imp-` — можно дотюнить точечно обычным CSS.
558
+
559
+ ## Горячие клавиши
560
+
561
+ Слушаются на контейнере плеера (не на `document` — несколько плееров на странице не конфликтуют). `Space`/`K` — play/pause, `←`/`→` — перемотка на `seekStep`, `↑`/`↓` — громкость, `M` — mute, `F` — полный экран, `0–9` — переход на 0–90% длительности, `Home`/`End`.
562
+
563
+ ## Стримы
564
+
565
+ - `*.m3u8` (или `type: 'application/x-mpegurl'`) → hls.js, который загружается динамически при первом использовании; в Safari используется нативный HLS.
566
+ - Live-потоки: автоматически определяются (`duration === Infinity`), вместо таймкода показывается бейдж LIVE, таймлайн скрывается.
567
+ - Качества HLS-уровней попадают в меню настроек (с пунктом Auto).
568
+
569
+ ## Локализации и вес бандла
570
+
571
+ Главный вход `itube-modern-player` включает все 11 локалей (+~4 КБ gzip) — `language: 'ru'` работает без настройки. Если важен каждый килобайт, есть облегчённый вход **без словарей** (в ядре остаётся только английский):
572
+
573
+ ```ts
574
+ import { Player, registerLocale } from 'itube-modern-player/core' // −4 КБ gzip
575
+ import { locales } from 'itube-modern-player/locales' // словари отдельным чанком
576
+
577
+ registerLocale('ru', locales.ru) // регистрируем только нужное
578
+ new Player('#mount', { language: 'ru' })
579
+ ```
580
+
581
+ `registerLocale` принимает и собственные словари — любой объект формы `PlayerLabels` под любым кодом. Незарегистрированный код тихо откатывается к английскому.
582
+
583
+ | Вход | gzip | Локали |
584
+ | --- | --- | --- |
585
+ | `itube-modern-player` | ~23 КБ | все 11 встроены |
586
+ | `itube-modern-player/core` | ~19 КБ | только en, остальное вручную |
587
+ | `itube-modern-player/locales` | ~4 КБ | только словари (отдельный чанк) |
588
+
589
+ ## Ленивая загрузка бандла
590
+
591
+ Точка входа `itube-modern-player/lazy` (~1.5 КБ) для паттерна «бандл плеера грузим после первого интерактива». Мгновенно рисует постер-заглушку (использует те же CSS-классы — подмена незаметна), а полный чанк плеера подтягивает динамическим импортом:
592
+
593
+ ```ts
594
+ import { createLazyPlayer } from 'itube-modern-player/lazy'
595
+ import 'itube-modern-player/style.css'
596
+
597
+ const lazy = createLazyPlayer('#mount', {
598
+ source: { src: 'stream.m3u8', poster: 'poster.jpg', title: '…' },
599
+ // когда качать бандл:
600
+ loadOn: 'interaction', // первый pointerdown/keydown/touch/wheel на странице (по умолчанию)
601
+ // loadOn: 'visible', // когда заглушка вошла во вьюпорт (IntersectionObserver)
602
+ // loadOn: 'immediate' // сразу (но отдельным чанком)
603
+ })
604
+
605
+ lazy.ready.then((player) => { /* полный Player готов */ })
606
+ lazy.destroy() // работает на любой стадии
607
+ ```
608
+
609
+ Клик по заглушке всегда грузит бандл немедленно и запускает воспроизведение.
610
+
611
+ Если нужен полный контроль снаружи — он никуда не делся: обычный `import('itube-modern-player')` по любому вашему триггеру, ядро не делает никаких предположений о моменте своей загрузки.
612
+
613
+ ## Сборка и публикация
614
+
615
+ ```bash
616
+ npm run dev # демо-страница на vite
617
+ # /?proxyads — гонит VAST-теги через дев-прокси /__vast
618
+ # (для окружений, где рекламные домены режутся сетью/блокировщиком)
619
+ npm run typecheck # tsc --noEmit
620
+ npm run build # dist/: ESM + CJS + d.ts + style.css
621
+ npm publish # prepublishOnly прогонит typecheck + build
622
+ ```
623
+
624
+ Экспорты пакета:
625
+
626
+ | Импорт | Что это |
627
+ | --- | --- |
628
+ | `itube-modern-player` | ядро (`Player`, все типы, утилиты) |
629
+ | `itube-modern-player/vue` | Vue 3-компонент `ITubePlayer` |
630
+ | `itube-modern-player/style.css` | стили (подключить один раз) |