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
@@ -0,0 +1,142 @@
1
+ import { NormalizedChapter } from './features/chapters';
2
+ import { Level } from './features/hls';
3
+ import { ThumbnailTrack } from './features/thumbnails';
4
+ import { ActionsOptions, ControlsOptions, IconName, PlayerEventMap, PlayerLabels, PlayerOptions, SubtitleTrack, VideoSource } from './types';
5
+ /**
6
+ * Framework-agnostic video player.
7
+ *
8
+ * ```ts
9
+ * import { Player } from 'itube-modern-player'
10
+ * import 'itube-modern-player/style.css'
11
+ *
12
+ * const player = new Player('#mount', { source: { src: 'video.m3u8', title: 'Demo' } })
13
+ * ```
14
+ *
15
+ * Design rules this codebase follows (deliberately unlike fluid-player):
16
+ * - no globals, no instance registry — `new Player()` as many times as you like;
17
+ * - one overlay layer laid out with grid/flex, not a pile of absolute elements;
18
+ * - `destroy()` returns the mount node to its original state;
19
+ * - every event is typed, every string and icon is replaceable.
20
+ */
21
+ export declare class Player {
22
+ readonly container: HTMLElement;
23
+ readonly video: HTMLVideoElement;
24
+ readonly labels: PlayerLabels;
25
+ readonly icons: Record<IconName, string>;
26
+ readonly controlsOptions: Required<ControlsOptions>;
27
+ readonly actionsOptions: ActionsOptions;
28
+ readonly playbackRates: number[];
29
+ readonly seekStep: number;
30
+ /** Set by the controls while the user drags the seek bar. */
31
+ scrubbing: boolean;
32
+ private emitter;
33
+ private mount;
34
+ private options;
35
+ private playlistOptions;
36
+ private abort;
37
+ private sources;
38
+ private currentIndex;
39
+ private sourceController;
40
+ private loadToken;
41
+ private chapters;
42
+ private currentChapter;
43
+ private progressiveQuality;
44
+ private thumbTrack;
45
+ private shuffleMode;
46
+ private controls;
47
+ private poster;
48
+ private pauseScreen;
49
+ private related;
50
+ private playlistPanel;
51
+ private scenesPanel;
52
+ private spinner;
53
+ private errorBox;
54
+ private adManager;
55
+ private playedOnce;
56
+ private idleTimer;
57
+ private destroyed;
58
+ constructor(target: string | HTMLElement, options?: PlayerOptions);
59
+ on<K extends keyof PlayerEventMap>(event: K, fn: (payload: PlayerEventMap[K]) => void): () => void;
60
+ once<K extends keyof PlayerEventMap>(event: K, fn: (payload: PlayerEventMap[K]) => void): () => void;
61
+ off<K extends keyof PlayerEventMap>(event: K, fn: (payload: PlayerEventMap[K]) => void): void;
62
+ /** @internal — UI modules emit through the player. */
63
+ emit<K extends keyof PlayerEventMap>(event: K, payload: PlayerEventMap[K]): void;
64
+ get paused(): boolean;
65
+ get ended(): boolean;
66
+ get currentTime(): number;
67
+ get duration(): number;
68
+ get live(): boolean;
69
+ get bufferedEnd(): number;
70
+ get adPlaying(): boolean;
71
+ play(): Promise<void>;
72
+ pause(): void;
73
+ togglePlay(): void;
74
+ seek(time: number): void;
75
+ /** Relative seek, e.g. `skip(-10)`. */
76
+ skip(delta: number): void;
77
+ get volume(): number;
78
+ get muted(): boolean;
79
+ setVolume(volume: number): void;
80
+ setMuted(muted: boolean): void;
81
+ toggleMute(): void;
82
+ get playbackRate(): number;
83
+ setPlaybackRate(rate: number): void;
84
+ get qualityLevels(): Level[];
85
+ get qualityAutoAvailable(): boolean;
86
+ get currentQuality(): number;
87
+ setQuality(index: number): void;
88
+ get subtitleTracks(): SubtitleTrack[];
89
+ get activeSubtitle(): number;
90
+ setSubtitle(index: number): void;
91
+ get playlist(): VideoSource[];
92
+ get index(): number;
93
+ get source(): VideoSource | null;
94
+ get hasPlaylist(): boolean;
95
+ get hasNext(): boolean;
96
+ get hasPrevious(): boolean;
97
+ /** Shuffle mode: `next()` and auto-advance pick a random item. */
98
+ get shuffle(): boolean;
99
+ setShuffle(on: boolean): void;
100
+ /** Repeat mode: after the last item the list starts over instead of ending. */
101
+ get repeat(): boolean;
102
+ setRepeat(on: boolean): void;
103
+ next(): void;
104
+ previous(): void;
105
+ /** Jump to a playlist item and start playback. */
106
+ playItem(index: number): void;
107
+ togglePlaylistPanel(): void;
108
+ toggleScenesPanel(): void;
109
+ /** Thumbnail track of the current source (used by the seek tooltip and the scenes panel). */
110
+ get thumbnails(): ThumbnailTrack | null;
111
+ /**
112
+ * Replace what the player is playing. A single source resets the playlist
113
+ * to one item; an array installs a new playlist.
114
+ */
115
+ load(source: VideoSource | VideoSource[], startIndex?: number): void;
116
+ get isFullscreen(): boolean;
117
+ toggleFullscreen(): Promise<void>;
118
+ togglePip(): Promise<void>;
119
+ /** Normalized chapters of the current source (after metadata is known). */
120
+ get chapterList(): NormalizedChapter[];
121
+ get chapter(): NormalizedChapter | null;
122
+ /**
123
+ * Share the current video: native share sheet when the device has one,
124
+ * otherwise the `action` event with `id: "share"` so the app can show
125
+ * its own dialog.
126
+ */
127
+ share(): Promise<void>;
128
+ /** Highlight the like or dislike button (`null` clears both). */
129
+ setLikeState(state: 'like' | 'dislike' | null): void;
130
+ /** Update like/dislike counters shown as tooltips above the buttons (`undefined` leaves one untouched). */
131
+ setLikeCounts(likeCount?: number | string, dislikeCount?: number | string): void;
132
+ /** Inject custom pause-screen content (used by the Vue wrapper's slot). */
133
+ setPauseScreenContent(element: HTMLElement | null): void;
134
+ destroy(): void;
135
+ private loadItem;
136
+ private bindVideoEvents;
137
+ private handleEnded;
138
+ private bindIdleHide;
139
+ private showControlsNow;
140
+ private scheduleIdle;
141
+ private bindKeyboard;
142
+ }
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .imp-player{--imp-accent: #e53935;--imp-bg: #000;--imp-text: #fff;--imp-control-bg: rgba(20, 20, 20, .85);--imp-track: rgba(255, 255, 255, .25);--imp-track-buffered: rgba(255, 255, 255, .35);--imp-radius: 8px;--imp-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--imp-controls-height: 48px;--imp-transition: .18s ease;position:relative;width:100%;aspect-ratio:16 / 9;background:var(--imp-bg);color:var(--imp-text);font-family:var(--imp-font);font-size:14px;line-height:1.4;overflow:hidden;border-radius:var(--imp-radius);outline:none;user-select:none;-webkit-user-select:none;container-type:inline-size}.imp-player--fullscreen{border-radius:0}.imp-player [hidden],.imp-ad [hidden]{display:none!important}.imp-video{width:100%;height:100%;display:block;object-fit:contain}.imp-layer{position:absolute;inset:0;display:grid;grid-template-rows:auto 1fr auto;grid-template-columns:minmax(0,1fr);grid-template-areas:"top" "middle" "bottom";pointer-events:none}.imp-layer__middle{grid-area:middle;display:flex;align-items:center;justify-content:center}.imp-center-controls{position:absolute;inset:0;align-items:center;justify-content:center;gap:16px;pointer-events:none;transition:opacity var(--imp-transition)}.imp-player--idle .imp-center-controls{opacity:0}.imp-center-btn{pointer-events:auto;display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;padding:10px;border:0;border-radius:50%;background:#00000073;color:#fff;cursor:pointer;transition:background var(--imp-transition),transform var(--imp-transition)}.imp-center-btn svg{width:100%;height:100%}.imp-center-btn--play{width:56px;height:56px;padding:14px;background:var(--imp-accent)}.imp-center-btn:hover:not(:disabled){background:#0009}.imp-center-btn--play:hover:not(:disabled){background:var(--imp-accent)}.imp-center-btn:active:not(:disabled){transform:scale(.9)}.imp-center-btn:disabled{opacity:.35;cursor:default}.imp-layer__bottom{grid-area:bottom;display:flex;flex-direction:column;background:linear-gradient(transparent,#000000b3);padding:24px 12px 8px;opacity:1;transition:opacity var(--imp-transition)}.imp-player--idle .imp-layer__bottom{opacity:0}.imp-player--idle{cursor:none}.imp-btn{pointer-events:auto;display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:6px;border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;transition:background var(--imp-transition),transform var(--imp-transition)}.imp-btn svg{width:100%;height:100%}.imp-btn:hover:not(:disabled){background:#ffffff26}.imp-btn:focus-visible{outline:2px solid var(--imp-accent);outline-offset:1px}.imp-btn:disabled{opacity:.35;cursor:default}.imp-btn--active{color:var(--imp-accent)}.imp-btn--like.imp-btn--active{color:var(--imp-like, forestgreen)}.imp-btn--dislike.imp-btn--active{color:var(--imp-dislike, #e53935)}.imp-action{position:relative;display:inline-flex}.imp-count-tooltip{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%) scale(.9);padding:3px 7px;border-radius:5px;background:var(--imp-control-bg);color:var(--imp-text);font-size:12px;font-variant-numeric:tabular-nums;line-height:1.2;white-space:nowrap;pointer-events:none;opacity:0;visibility:hidden;transition:opacity var(--imp-transition),transform var(--imp-transition);z-index:5}.imp-action:hover .imp-count-tooltip:not([hidden]){opacity:1;visibility:visible;transform:translate(-50%) scale(1)}.imp-controls{display:flex;flex-direction:column;gap:4px}.imp-controls__row{display:flex;align-items:center;gap:4px;min-height:var(--imp-controls-height)}.imp-controls__group{display:flex;align-items:center;gap:2px;flex:0 0 auto}.imp-controls__row .imp-btn,.imp-controls__menu-anchor,.imp-action,.imp-volume,.imp-controls__time,.imp-controls__live{flex:0 0 auto}.imp-controls__spacer{flex:1;display:flex;align-items:center;justify-content:center;min-width:0}.imp-controls__chapter{font-size:12px;opacity:.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.imp-controls__time{font-variant-numeric:tabular-nums;font-size:13px;margin:0 6px;white-space:nowrap}.imp-controls__live{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:700;letter-spacing:.5px;margin:0 6px}.imp-controls__live:before{content:"";width:8px;height:8px;border-radius:50%;background:var(--imp-accent)}.imp-controls__menu-anchor{position:relative;display:flex}.imp-volume{display:flex;align-items:center}.imp-volume__slider{pointer-events:auto;appearance:none;-webkit-appearance:none;width:0;opacity:0;height:4px;border-radius:2px;background:linear-gradient(to right,var(--imp-text) var(--imp-volume-fill, 100%),var(--imp-track) var(--imp-volume-fill, 100%));cursor:pointer;transition:width var(--imp-transition),opacity var(--imp-transition)}.imp-volume:hover .imp-volume__slider,.imp-volume__slider:focus-visible{width:64px;opacity:1;margin-right:6px}.imp-volume__slider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--imp-text);border:0}.imp-volume__slider::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:var(--imp-text);border:0}.imp-progress{pointer-events:auto;position:relative;height:16px;display:flex;align-items:center;cursor:pointer;touch-action:none}.imp-progress--live{visibility:hidden}.imp-progress__heatmap{position:absolute;left:0;right:0;bottom:10px;height:36px;pointer-events:none;opacity:0;transition:opacity var(--imp-transition)}.imp-progress:hover .imp-progress__heatmap,.imp-progress--scrubbing .imp-progress__heatmap{opacity:1}.imp-progress__heatmap svg{width:100%;height:100%;display:block}.imp-progress__heatmap path{fill:#ffffff59}.imp-progress__buffered{position:absolute;left:0;height:4px;width:0;border-radius:2px;background:var(--imp-track-buffered);transition:height var(--imp-transition)}.imp-progress__track{position:relative;display:flex;gap:2px;width:100%;height:4px;transition:height var(--imp-transition)}.imp-progress:hover .imp-progress__track,.imp-progress--scrubbing .imp-progress__track,.imp-progress:hover .imp-progress__buffered,.imp-progress--scrubbing .imp-progress__buffered{height:6px}.imp-progress__segment{position:relative;flex-basis:0;height:100%;background:var(--imp-track);border-radius:2px;overflow:hidden}.imp-progress__fill{width:100%;height:100%;background:var(--imp-accent);transform:scaleX(0);transform-origin:left}.imp-progress__handle{position:absolute;width:12px;height:12px;border-radius:50%;background:var(--imp-accent);transform:translate(-50%) scale(0);transition:transform var(--imp-transition);pointer-events:none}.imp-progress:hover .imp-progress__handle,.imp-progress--scrubbing .imp-progress__handle{transform:translate(-50%) scale(1)}.imp-progress__tooltip{position:absolute;bottom:22px;transform:translate(-50%);display:flex;flex-direction:column;align-items:center;gap:4px;padding:4px;border-radius:6px;background:var(--imp-control-bg);opacity:0;visibility:hidden;transition:opacity var(--imp-transition);pointer-events:none;white-space:nowrap}.imp-progress__tooltip--visible{opacity:1;visibility:visible}.imp-progress__thumb{border-radius:4px;background-color:#111;background-repeat:no-repeat}.imp-progress__tooltip-chapter{font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis}.imp-progress__tooltip-time{font-size:11px;font-variant-numeric:tabular-nums;opacity:.85}.imp-menu{pointer-events:auto;position:absolute;right:0;bottom:44px;min-width:160px;max-height:280px;overflow-y:auto;background:var(--imp-control-bg);border-radius:var(--imp-radius);padding:6px;backdrop-filter:blur(8px);z-index:5}.imp-menu__backdrop{display:none}.imp-menu__section+.imp-menu__section{margin-top:6px;border-top:1px solid rgba(255,255,255,.12);padding-top:6px}.imp-menu__title{font-size:11px;text-transform:uppercase;letter-spacing:.6px;opacity:.6;padding:4px 8px}.imp-menu__item{display:flex;align-items:center;gap:8px;width:100%;text-align:left;border:0;background:transparent;color:inherit;font:inherit;padding:7px 10px;border-radius:5px;cursor:pointer}.imp-menu__icon{display:inline-flex;width:18px;height:18px;flex-shrink:0}.imp-menu__icon svg,.imp-menu__icon img{width:100%;height:100%;object-fit:contain}.imp-menu__label{white-space:nowrap}.imp-menu__item:hover{background:#ffffff1f}.imp-menu__item--active{color:var(--imp-accent);font-weight:600}.imp-poster{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--imp-bg)}.imp-poster__image{position:absolute;inset:0;background-size:cover;background-position:center}.imp-poster__play{position:relative;width:72px;height:72px;border-radius:50%;background:var(--imp-accent);padding:16px}.imp-poster__play:hover:not(:disabled){background:var(--imp-accent);transform:scale(1.08)}.imp-error{pointer-events:auto;max-width:80%;padding:10px 16px;border-radius:8px;background:#000000bf;border:1px solid rgba(255,255,255,.2);font-size:13px;text-align:center}.imp-spinner{position:absolute;inset:0;margin:auto;width:48px;height:48px;border:4px solid rgba(255,255,255,.25);border-top-color:var(--imp-text);border-radius:50%;animation:imp-spin .8s linear infinite}@keyframes imp-spin{to{transform:rotate(360deg)}}.imp-pause-screen{grid-area:top;pointer-events:auto;padding:20px 24px;background:linear-gradient(rgba(0,0,0,.7),transparent);animation:imp-fade-in .22s ease}.imp-pause-screen--custom{grid-area:1 / 1 / -1 / -1;display:flex;align-items:center;justify-content:center;padding:16px;background:#0009;z-index:4}.imp-pause-screen__custom{pointer-events:auto;max-width:100%;max-height:100%;overflow:auto}.imp-pause-screen__default{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}.imp-pause-screen__heading{flex:1;min-width:0}.imp-channel{display:flex;align-items:center;gap:10px;border:0;padding:0;background:transparent;color:inherit;font:inherit;text-align:left;flex-shrink:0}.imp-channel--link{cursor:pointer}.imp-channel--link:hover .imp-channel__name{text-decoration:underline}.imp-channel__avatar{width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,var(--imp-accent),#7b1fa2);background-size:cover;background-position:center}.imp-channel__avatar--rounded{border-radius:10px}.imp-channel__avatar--square{border-radius:0}.imp-channel__avatar--circle{border-radius:50%}.imp-channel__avatar{background-size:cover;background-position:center;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;flex-shrink:0}.imp-channel__name{font-size:14px;font-weight:600;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}.imp-pause-screen__title{font-size:20px;font-weight:700}.imp-pause-screen__description{margin-top:6px;font-size:14px;opacity:.85;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.imp-sponsor{display:inline-flex;align-items:center;gap:7px;margin-top:10px;padding:5px 11px;border-radius:6px;max-width:100%;background:#00000073;border:1px solid rgba(255,255,255,.25);color:var(--imp-text);font-size:13px;font-weight:600;text-decoration:none;pointer-events:auto;transition:border-color var(--imp-transition),background var(--imp-transition)}.imp-sponsor:hover{border-color:var(--imp-accent);background:#0009}.imp-sponsor__label{flex-shrink:0;padding:1px 6px;border-radius:3px;background:var(--imp-accent);color:#fff;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px}.imp-sponsor__text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@keyframes imp-fade-in{0%{opacity:0}}.imp-related{grid-area:middle;pointer-events:auto;align-self:stretch;display:flex;flex-direction:column;justify-content:center;gap:12px;padding:24px;background:#000c;animation:imp-fade-in .22s ease;z-index:2}.imp-related__close{align-self:flex-end}.imp-related__title{font-size:16px;font-weight:700}.imp-related__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;overflow-y:auto}.imp-related__card{display:flex;flex-direction:column;gap:6px;border:0;padding:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.imp-related__thumb{position:relative;aspect-ratio:16 / 9;border-radius:6px;background-color:#222;background-size:cover;background-position:center;transition:transform var(--imp-transition)}.imp-related__card:hover .imp-related__thumb{transform:scale(1.04)}.imp-related__duration{position:absolute;right:6px;bottom:6px;padding:1px 5px;border-radius:4px;background:#000c;font-size:11px;font-variant-numeric:tabular-nums}.imp-related__card-title{font-size:13px;font-weight:600;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.imp-playlist{position:absolute;display:flex;flex-direction:column;background:var(--imp-control-bg);backdrop-filter:blur(8px);z-index:6;--imp-thumb-w: 88}.imp-playlist--sidebar{top:0;right:0;bottom:0;width:min(320px,80%);animation:imp-slide-in .2s ease}.imp-playlist--bottom{left:0;right:0;bottom:0;max-height:46%;animation:imp-slide-up .2s ease;--imp-thumb-w: 128}.imp-playlist--bottom .imp-playlist__list{flex-direction:row;overflow-x:auto;overflow-y:hidden;padding:0 12px 12px}.imp-playlist--bottom .imp-playlist__item{flex-direction:column;align-items:stretch;flex:0 0 calc(var(--imp-thumb-w) * 1px + 12px)}.imp-playlist--bottom .imp-playlist__thumb{flex:0 0 auto;width:calc(var(--imp-thumb-w) * 1px)}@keyframes imp-slide-up{0%{transform:translateY(100%)}}.imp-playlist__tools{display:flex;align-items:center;gap:2px}.imp-playlist__heading{min-width:0}.imp-playlist__kicker{font-size:11px;font-weight:400;text-transform:uppercase;letter-spacing:.6px;opacity:.6}.imp-playlist__name{font-size:15px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@keyframes imp-slide-in{0%{transform:translate(100%)}}.imp-playlist__header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;font-weight:700}.imp-playlist__list{overflow-y:auto;display:flex;flex-direction:column;padding:0 8px 8px;gap:4px}.imp-playlist__item{display:flex;gap:10px;align-items:center;padding:6px;border:0;border-radius:6px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.imp-playlist__item:hover{background:#ffffff1a}.imp-playlist__item--active{background:#ffffff24;box-shadow:inset 2px 0 0 var(--imp-accent)}.imp-playlist__thumb{flex:0 0 calc(var(--imp-thumb-w) * 1px);aspect-ratio:16 / 9;border-radius:4px;background-color:#222;background-size:cover;background-position:center;overflow:hidden}.imp-scenes__sprite{background-repeat:no-repeat;transform:scale(calc(var(--imp-thumb-w) / var(--imp-sprite-w, 160)));transform-origin:top left}.imp-playlist__title{font-size:13px;font-weight:600;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.imp-playlist__duration{font-size:12px;opacity:.7;font-variant-numeric:tabular-nums}.imp-ad{position:absolute;inset:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--imp-bg);z-index:10}.imp-ad__video{grid-row:1 / -1;grid-column:1;width:100%;height:100%;object-fit:contain;cursor:pointer}.imp-ad__spinner{grid-row:2;grid-column:1;justify-self:center;align-self:center;pointer-events:none}.imp-ad__hud{grid-row:1;grid-column:1;display:flex;align-items:center;gap:10px;padding:12px 14px;pointer-events:none}.imp-ad__badge{padding:3px 8px;border-radius:4px;background:#000000b3;border:1px solid rgba(255,255,255,.4);font-size:12px;font-weight:700}.imp-ad__countdown{font-size:12px;font-variant-numeric:tabular-nums;text-shadow:0 1px 2px rgba(0,0,0,.8)}.imp-ad__actions{grid-row:3;grid-column:1;display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:14px}.imp-ad__skip,.imp-ad__visit{border:1px solid rgba(255,255,255,.5);background:#000000bf;color:inherit;font:inherit;font-weight:600;padding:8px 14px;border-radius:6px;cursor:pointer}.imp-ad__skip:disabled{opacity:.7;cursor:default}.imp-ad__skip:hover:not(:disabled),.imp-ad__visit:hover{background:#fff3}.imp-ad--paused:after{content:"▶";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:48px;background:#0006;pointer-events:none}@media(max-width:767px){.imp-btn{width:28px;height:28px;padding:5px}.imp-controls__row{gap:0;min-height:38px}.imp-controls__group{gap:0}.imp-layer__bottom{padding:12px 6px 5px}.imp-controls__chapter{display:none}.imp-btn--seek-back,.imp-btn--seek-forward{width:26px;height:26px;padding:5px;opacity:.75}.imp-progress{height:12px}.imp-controls__time,.imp-controls__live{font-size:10px;margin:0 3px}.imp-poster__play{width:48px;height:48px;padding:11px}.imp-spinner{width:36px;height:36px;border-width:3px}.imp-pause-screen{padding:8px 10px}.imp-pause-screen__default{align-items:flex-start;gap:8px}.imp-pause-screen__title{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:0}.imp-pause-screen__description{display:none}.imp-channel{align-self:flex-start}.imp-channel__name{display:none}.imp-channel__avatar{width:32px;height:32px}.imp-sponsor{margin-top:6px;padding:4px 8px;font-size:11px;gap:5px}.imp-sponsor__label{font-size:9px}.imp-ad__hud{padding:8px 10px;gap:6px}.imp-ad__badge{font-size:10px;padding:2px 6px}.imp-ad__countdown{font-size:11px}.imp-ad__actions{padding:8px;gap:6px}.imp-ad__skip,.imp-ad__visit{padding:6px 10px;font-size:12px}.imp-ad--paused:after{font-size:36px}.imp-controls__menu-anchor{position:static}.imp-menu{position:absolute;inset:auto 0 0;box-sizing:border-box;min-width:0;max-width:none;max-height:80%;overflow-y:auto;border-radius:var(--imp-radius) var(--imp-radius) 0 0;padding:6px 8px calc(6px + env(safe-area-inset-bottom,0px));box-shadow:0 -8px 24px #00000080;animation:imp-slide-up .18s ease;z-index:6}.imp-menu__backdrop{display:block;position:absolute;inset:0;background:#00000080;pointer-events:auto;z-index:5;animation:imp-fade-in .18s ease}.imp-menu__item{padding:10px}.imp-playlist{inset:0;width:auto;max-height:none;border-radius:0;animation:imp-fade-in .18s ease}.imp-playlist--bottom .imp-playlist__list,.imp-playlist__list{flex-direction:column;overflow-x:hidden;overflow-y:auto}.imp-playlist--bottom .imp-playlist__item{flex-direction:row;align-items:center;flex:0 0 auto}.imp-playlist--bottom .imp-playlist__thumb,.imp-playlist__thumb{flex:0 0 120px;width:120px}}
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Public types of itube-modern-player.
3
+ * Everything here is re-exported from the package root.
4
+ */
5
+ /** A subtitle / captions track attached to a video. */
6
+ export interface SubtitleTrack {
7
+ /** URL of a WebVTT file. */
8
+ src: string;
9
+ /** Human readable label shown in the subtitles menu. */
10
+ label: string;
11
+ /** BCP-47 language tag, e.g. `"en"`, `"ru"`. */
12
+ srclang?: string;
13
+ /** Enable this track by default. */
14
+ default?: boolean;
15
+ }
16
+ /** A single chapter / time-code on the timeline. */
17
+ export interface Chapter {
18
+ /** Start time in seconds. */
19
+ start: number;
20
+ /** End time in seconds. Defaults to the start of the next chapter (or video end). */
21
+ end?: number;
22
+ /** Chapter title shown in the seek tooltip and the chapter label. */
23
+ title: string;
24
+ }
25
+ /** An alternative quality rendition for progressive (non-HLS) sources. */
26
+ export interface QualityLevel {
27
+ /** Stream / file URL for this rendition. */
28
+ src: string;
29
+ /** Label shown in the quality menu, e.g. `"1080p"`. Falls back to `quality`. */
30
+ label?: string;
31
+ /** Quality value, e.g. `1080` or `"hd"` — used as the label when `label` is omitted. */
32
+ quality?: string | number;
33
+ type?: string;
34
+ }
35
+ /** Channel / author info, rendered in the pause-screen header. */
36
+ export interface ChannelInfo {
37
+ name: string;
38
+ /** Avatar image URL. Without it a styled first-letter placeholder is rendered. */
39
+ avatar?: string;
40
+ /** Clicking the channel block opens this URL in a new tab. */
41
+ url?: string;
42
+ /** Avatar shape. Default `"circle"`. */
43
+ avatarShape?: 'circle' | 'rounded' | 'square';
44
+ }
45
+ /**
46
+ * Sponsor / promo link shown under the description on the pause screen.
47
+ * Only rendered when present — pass it only for sponsored videos.
48
+ */
49
+ export interface SponsorLink {
50
+ /** Click-through URL (opens in a new tab, `rel="nofollow noopener"`). */
51
+ url: string;
52
+ /** Link text, e.g. the sponsor name. */
53
+ text: string;
54
+ /** Small badge before the text. Defaults to the localized "Sponsored" label. */
55
+ label?: string;
56
+ }
57
+ /** Custom styling knobs. Extensible — more keys will land here over time. */
58
+ export interface StylingOptions {
59
+ /** Accent color (progress fill, big play button, active menu items). Overrides the built-in red. */
60
+ themeColor?: string;
61
+ /** Color of the active like button. Default `forestgreen`. */
62
+ likeColor?: string;
63
+ /** Color of the active dislike button. Default the built-in red (independent from themeColor). */
64
+ dislikeColor?: string;
65
+ }
66
+ /** Everything the player knows about one video. */
67
+ export interface VideoSource {
68
+ /** Media URL. `.m3u8` is detected automatically and routed through hls.js. */
69
+ src: string;
70
+ /** Explicit MIME type, e.g. `"application/x-mpegurl"` or `"video/mp4"`. */
71
+ type?: string;
72
+ title?: string;
73
+ description?: string;
74
+ /** Poster image, also used as the preview poster before the first play. */
75
+ poster?: string;
76
+ /** Duration in seconds — used in playlist UI before metadata is loaded. */
77
+ duration?: number;
78
+ /** URL of a WebVTT file describing preview thumbnails (sprite via `#xywh=`). */
79
+ thumbnails?: string;
80
+ /**
81
+ * Subtitles: a bare VTT URL, a single track, or an array of tracks.
82
+ * All tracks start disabled unless one has `default: true`.
83
+ */
84
+ subtitles?: string | SubtitleTrack | SubtitleTrack[];
85
+ /** Channel/author shown in the pause screen. */
86
+ channel?: ChannelInfo;
87
+ /** Sponsor/promo link under the description (pause screen). Only for sponsored videos. */
88
+ sponsor?: SponsorLink;
89
+ /** Chapters as an array, or a URL of a WebVTT chapters file. */
90
+ chapters?: Chapter[] | string;
91
+ /** Alternative renditions for progressive sources. Ignored for HLS (levels come from the manifest). */
92
+ qualities?: QualityLevel[];
93
+ /**
94
+ * Popularity samples for the heatmap above the progress bar (YouTube-style
95
+ * "most replayed"). Sparse — only the moments you have data for.
96
+ */
97
+ heatmap?: HeatmapPoint[];
98
+ /** Free-form user data, carried around untouched. */
99
+ meta?: Record<string, unknown>;
100
+ }
101
+ /** An item in the related-videos grid. */
102
+ export interface RelatedItem {
103
+ title: string;
104
+ poster?: string;
105
+ /** Pre-formatted duration badge, e.g. `"12:34"`. */
106
+ duration?: string;
107
+ /** If set, clicking the item loads this source into the player. */
108
+ source?: VideoSource;
109
+ /** If set, clicking the item navigates to this URL instead. */
110
+ url?: string;
111
+ meta?: Record<string, unknown>;
112
+ }
113
+ export interface RelatedOptions {
114
+ items: RelatedItem[];
115
+ /** When to show the grid. Default: `["ended"]`. */
116
+ showOn?: Array<'pause' | 'ended'>;
117
+ /** Heading above the grid. */
118
+ title?: string;
119
+ }
120
+ /** One ad insertion, fluid-player compatible shape. */
121
+ export interface AdRoll {
122
+ roll: 'preRoll' | 'midRoll' | 'postRoll';
123
+ /** URL of a VAST XML tag. */
124
+ vastTag?: string;
125
+ /** Direct media file URL — a minimal alternative to VAST. */
126
+ src?: string;
127
+ /** For midRoll: playback second at which the ad fires. */
128
+ timer?: number;
129
+ /** Click-through URL when `src` is used instead of VAST. */
130
+ clickUrl?: string;
131
+ }
132
+ export interface AdsOptions {
133
+ adList: AdRoll[];
134
+ /** Seconds before the skip button appears. VAST `skipoffset` wins if present. Default 5; `-1` disables skipping. */
135
+ skipDelay?: number;
136
+ /** Max VAST wrapper redirects to follow. Default 3. */
137
+ maxWrapperDepth?: number;
138
+ /** Request timeout for VAST tags, ms. Default 8000. */
139
+ requestTimeout?: number;
140
+ /**
141
+ * Watchdog for the ad media itself, ms. If the creative does not start (or
142
+ * stalls mid-play) for this long, the ad is dropped with `aderror` and
143
+ * content resumes. Default 10000.
144
+ */
145
+ mediaTimeout?: number;
146
+ /**
147
+ * Playlist behaviour: `"every"` (default) runs the ad list for every video,
148
+ * `"first"` runs it only for the first video that plays.
149
+ */
150
+ playOn?: 'every' | 'first';
151
+ }
152
+ /** A consumer-defined entry in the ⋯ actions dropdown. */
153
+ export interface CustomAction {
154
+ id: string;
155
+ title: string;
156
+ /** Icon: image URL or raw `<svg>` markup. Optional. */
157
+ icon?: string;
158
+ }
159
+ export type BuiltinActionId = 'like' | 'dislike' | 'addTo' | 'share' | 'report';
160
+ /**
161
+ * Social action buttons. Everything is OFF by default.
162
+ * `like`/`dislike` render as visible buttons; `addTo`/`share`/`report` and
163
+ * all `custom` entries live in a ⋯ dropdown on the right.
164
+ */
165
+ export interface ActionsOptions {
166
+ like?: boolean;
167
+ dislike?: boolean;
168
+ /** Initial rating state — highlights the corresponding button. Change later via `player.setLikeState()`. */
169
+ likeState?: 'like' | 'dislike' | null;
170
+ /** Initial like count, shown as a tooltip above the button. Update later via `player.setLikeCounts()`. */
171
+ likeCount?: number | string;
172
+ /** Initial dislike count, shown as a tooltip above the button. */
173
+ dislikeCount?: number | string;
174
+ addTo?: boolean;
175
+ /**
176
+ * Share: uses the device's native share sheet (`navigator.share`) when
177
+ * available, otherwise emits the `action` event with `id: "share"`.
178
+ */
179
+ share?: boolean;
180
+ report?: boolean;
181
+ /** Extra dropdown entries. Click emits `customaction` with the entry id. */
182
+ custom?: CustomAction[];
183
+ }
184
+ export interface ControlsOptions {
185
+ play?: boolean;
186
+ progress?: boolean;
187
+ time?: boolean;
188
+ volume?: boolean;
189
+ fullscreen?: boolean;
190
+ pip?: boolean;
191
+ /** Playback-speed menu (speedometer icon). */
192
+ settings?: boolean;
193
+ /** Quality menu (gear icon). OFF by default; needs `source.qualities` or an HLS source. */
194
+ quality?: boolean;
195
+ /** Scene list button — appears when the source has chapters. Default true. */
196
+ scenes?: boolean;
197
+ /** Popularity heatmap above the progress bar (needs `source.heatmap` data). Default true. */
198
+ heatmap?: boolean;
199
+ subtitles?: boolean;
200
+ /** Skip-back / skip-forward buttons. Pass an object to change the step (seconds). */
201
+ seekButtons?: boolean | {
202
+ back?: number;
203
+ forward?: number;
204
+ };
205
+ /** Previous / next / list buttons (only rendered when a playlist is present). */
206
+ playlist?: boolean;
207
+ /** ms of pointer inactivity before controls fade out during playback. Default 2500. */
208
+ hideDelay?: number;
209
+ }
210
+ export interface PlaylistOptions {
211
+ /** Playlist name shown in the panel header under a small "Playlist" kicker. */
212
+ title?: string;
213
+ /** Automatically start the next item when one ends. Default true. */
214
+ autoAdvance?: boolean;
215
+ /** Repeat: jump from the last item back to the first. Default false. Toggleable in the UI. */
216
+ loop?: boolean;
217
+ /** Shuffle: auto-advance picks a random item. Default false. Toggleable in the UI. */
218
+ shuffle?: boolean;
219
+ startIndex?: number;
220
+ /** Panel placement: `"sidebar"` (right, default) or `"bottom"` (horizontal strip). */
221
+ layout?: 'sidebar' | 'bottom';
222
+ }
223
+ /** Scene (chapter) list panel — timecodes with sprite previews. */
224
+ export interface ScenesOptions {
225
+ /** Panel placement. Default `"bottom"`. */
226
+ layout?: 'sidebar' | 'bottom';
227
+ }
228
+ /** Every visible string, overridable for i18n. */
229
+ export interface PlayerLabels {
230
+ play: string;
231
+ pause: string;
232
+ replay: string;
233
+ mute: string;
234
+ unmute: string;
235
+ fullscreen: string;
236
+ exitFullscreen: string;
237
+ pip: string;
238
+ settings: string;
239
+ subtitles: string;
240
+ subtitlesOff: string;
241
+ speed: string;
242
+ quality: string;
243
+ qualityAuto: string;
244
+ next: string;
245
+ previous: string;
246
+ playlist: string;
247
+ scenes: string;
248
+ shuffle: string;
249
+ repeat: string;
250
+ like: string;
251
+ dislike: string;
252
+ addTo: string;
253
+ share: string;
254
+ report: string;
255
+ more: string;
256
+ seekForward: string;
257
+ seekBack: string;
258
+ live: string;
259
+ related: string;
260
+ adLabel: string;
261
+ skipAd: string;
262
+ skipAdIn: string;
263
+ visitAdvertiser: string;
264
+ sponsored: string;
265
+ }
266
+ /** SVG icon overrides — raw `<svg>` markup keyed by icon name. */
267
+ export type IconName = 'play' | 'pause' | 'replay' | 'bigPlay' | 'volumeHigh' | 'volumeLow' | 'volumeMute' | 'fullscreen' | 'fullscreenExit' | 'pip' | 'settings' | 'speed' | 'subtitles' | 'list' | 'scenes' | 'next' | 'previous' | 'seekForward' | 'seekBack' | 'shuffle' | 'repeat' | 'like' | 'dislike' | 'addTo' | 'share' | 'report' | 'more' | 'close';
268
+ export interface PlayerOptions {
269
+ /** Single source or an array (an array enables playlist mode). */
270
+ source?: VideoSource | VideoSource[];
271
+ playlist?: PlaylistOptions;
272
+ autoplay?: boolean;
273
+ muted?: boolean;
274
+ loop?: boolean;
275
+ /** 0..1 */
276
+ volume?: number;
277
+ /** Playback-rate choices for the settings menu. Default `[0.5, 0.75, 1, 1.25, 1.5, 2]`. */
278
+ playbackRates?: number[];
279
+ /** Seconds for keyboard / double-tap / button seeking. Default 10. */
280
+ seekStep?: number;
281
+ /** Enable keyboard shortcuts on the player container. Default true. */
282
+ keyboard?: boolean;
283
+ controls?: ControlsOptions;
284
+ /**
285
+ * Pause-screen "slot". `true` renders the default title/description overlay,
286
+ * an element or a factory lets you inject arbitrary markup.
287
+ */
288
+ pauseScreen?: boolean | HTMLElement | ((player: unknown) => HTMLElement);
289
+ related?: RelatedOptions;
290
+ /** Ad configuration: pre/mid/post rolls, skip timer, playlist frequency. */
291
+ adConfig?: AdsOptions;
292
+ /** @deprecated use `adConfig` */
293
+ ads?: AdsOptions;
294
+ /** Like/dislike/share/… buttons. All off unless enabled. */
295
+ actions?: ActionsOptions;
296
+ /** Custom styling (accent / like / dislike colors). */
297
+ styling?: StylingOptions;
298
+ /** Scene list panel configuration. */
299
+ scenes?: ScenesOptions;
300
+ /**
301
+ * UI language — picks one of the built-in static locales.
302
+ * Individual strings can still be overridden via `labels`.
303
+ */
304
+ language?: LocaleCode;
305
+ labels?: Partial<PlayerLabels>;
306
+ icons?: Partial<Record<IconName, string>>;
307
+ /** Extra class added to the player container — hook for external theming. */
308
+ className?: string;
309
+ /** `crossorigin` attribute for the video element (needed for canvas/VTT on other origins). */
310
+ crossOrigin?: '' | 'anonymous' | 'use-credentials';
311
+ /** Inline playback on iOS. Default true. */
312
+ playsInline?: boolean;
313
+ }
314
+ /** A single popularity sample: how often users sought/replayed near `time`. */
315
+ export interface HeatmapPoint {
316
+ /** Position in seconds. Does not have to cover every second — sparse data is fine. */
317
+ time: number;
318
+ /** Raw popularity value (clicks, replays, views) — normalized internally. */
319
+ value: number;
320
+ }
321
+ /** Supported UI languages. */
322
+ export type LocaleCode = 'en' | 'ru' | 'de' | 'es' | 'it' | 'ja' | 'ko' | 'zh' | 'pt' | 'ar' | 'hi';
323
+ /** Parsed preview-thumbnail cue. */
324
+ export interface ThumbnailCue {
325
+ start: number;
326
+ end: number;
327
+ /** Absolute image URL. */
328
+ src: string;
329
+ /** Sprite region; undefined when the cue points at a whole image. */
330
+ xywh?: {
331
+ x: number;
332
+ y: number;
333
+ w: number;
334
+ h: number;
335
+ };
336
+ }
337
+ /** A resolved, playable ad (after VAST unwrapping). */
338
+ export interface ResolvedAd {
339
+ roll: AdRoll['roll'];
340
+ mediaUrl: string;
341
+ mediaType?: string;
342
+ clickThrough?: string;
343
+ duration?: number;
344
+ /** Seconds until skippable; undefined → use AdsOptions.skipDelay. */
345
+ skipOffset?: number;
346
+ impressions: string[];
347
+ tracking: Partial<Record<'start' | 'firstQuartile' | 'midpoint' | 'thirdQuartile' | 'complete' | 'skip' | 'click' | 'pause' | 'resume', string[]>>;
348
+ adTitle?: string;
349
+ }
350
+ /** Map of every event the player emits → its payload. */
351
+ export interface PlayerEventMap {
352
+ ready: {
353
+ player: unknown;
354
+ };
355
+ play: undefined;
356
+ pause: undefined;
357
+ ended: undefined;
358
+ timeupdate: {
359
+ currentTime: number;
360
+ duration: number;
361
+ };
362
+ progress: {
363
+ buffered: number;
364
+ };
365
+ volumechange: {
366
+ volume: number;
367
+ muted: boolean;
368
+ };
369
+ ratechange: {
370
+ rate: number;
371
+ };
372
+ seeking: {
373
+ currentTime: number;
374
+ };
375
+ seeked: {
376
+ currentTime: number;
377
+ };
378
+ sourcechange: {
379
+ source: VideoSource;
380
+ index: number;
381
+ };
382
+ playlistitemchange: {
383
+ source: VideoSource;
384
+ index: number;
385
+ };
386
+ chapterchange: {
387
+ chapter: Chapter | null;
388
+ };
389
+ fullscreenchange: {
390
+ active: boolean;
391
+ };
392
+ pipchange: {
393
+ active: boolean;
394
+ };
395
+ qualitychange: {
396
+ label: string;
397
+ };
398
+ subtitlechange: {
399
+ track: SubtitleTrack | null;
400
+ };
401
+ relatedshow: undefined;
402
+ relatedclick: {
403
+ item: RelatedItem;
404
+ };
405
+ /** A built-in action button was clicked (share only when native sharing is unavailable). */
406
+ action: {
407
+ id: BuiltinActionId;
408
+ };
409
+ /** A consumer-defined dropdown entry was clicked. */
410
+ customaction: {
411
+ id: string;
412
+ };
413
+ adstart: {
414
+ ad: ResolvedAd;
415
+ };
416
+ adend: {
417
+ ad: ResolvedAd;
418
+ };
419
+ adskip: {
420
+ ad: ResolvedAd;
421
+ };
422
+ adclick: {
423
+ ad: ResolvedAd;
424
+ };
425
+ /** The ad creative was paused (click-through, user tap, autoplay block). */
426
+ adpause: {
427
+ ad: ResolvedAd;
428
+ };
429
+ /** The ad creative resumed after a pause. */
430
+ adresume: {
431
+ ad: ResolvedAd;
432
+ };
433
+ aderror: {
434
+ roll: AdRoll;
435
+ error: Error;
436
+ };
437
+ error: {
438
+ message: string;
439
+ cause?: unknown;
440
+ };
441
+ destroy: undefined;
442
+ }
443
+ export type PlayerEvent = keyof PlayerEventMap;