ts-video-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.
- package/LICENSE.md +21 -0
- package/README.md +438 -0
- package/dist/cdn.d.ts +10 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.min.js +1046 -0
- package/dist/cdn.min.js.map +54 -0
- package/dist/cdn.test.d.ts +2 -0
- package/dist/cdn.test.d.ts.map +1 -0
- package/dist/chunk-36x4yhjn.js +765 -0
- package/dist/chunk-36x4yhjn.js.map +12 -0
- package/dist/chunk-eajts2jv.js +4243 -0
- package/dist/chunk-eajts2jv.js.map +42 -0
- package/dist/chunk-z1ehbz9f.js +380 -0
- package/dist/chunk-z1ehbz9f.js.map +19 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/core/audio-gain.d.ts +67 -0
- package/dist/core/audio-gain.d.ts.map +1 -0
- package/dist/core/caption-styles.d.ts +121 -0
- package/dist/core/caption-styles.d.ts.map +1 -0
- package/dist/core/chapters.d.ts +150 -0
- package/dist/core/chapters.d.ts.map +1 -0
- package/dist/core/error-overlay.d.ts +71 -0
- package/dist/core/error-overlay.d.ts.map +1 -0
- package/dist/core/events.d.ts +119 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/features.d.ts +64 -0
- package/dist/core/features.d.ts.map +1 -0
- package/dist/core/i18n.d.ts +132 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/index.d.ts +37 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/live.d.ts +130 -0
- package/dist/core/live.d.ts.map +1 -0
- package/dist/core/media-session.d.ts +38 -0
- package/dist/core/media-session.d.ts.map +1 -0
- package/dist/core/orientation.d.ts +22 -0
- package/dist/core/orientation.d.ts.map +1 -0
- package/dist/core/popover.d.ts +42 -0
- package/dist/core/popover.d.ts.map +1 -0
- package/dist/core/remote-playback.d.ts +195 -0
- package/dist/core/remote-playback.d.ts.map +1 -0
- package/dist/core/settings-menu.d.ts +132 -0
- package/dist/core/settings-menu.d.ts.map +1 -0
- package/dist/core/spinner.d.ts +67 -0
- package/dist/core/spinner.d.ts.map +1 -0
- package/dist/core/state.d.ts +142 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/text-tracks.d.ts +162 -0
- package/dist/core/text-tracks.d.ts.map +1 -0
- package/dist/core/themes.d.ts +213 -0
- package/dist/core/themes.d.ts.map +1 -0
- package/dist/core/thumbnails.d.ts +124 -0
- package/dist/core/thumbnails.d.ts.map +1 -0
- package/dist/core/tooltips.d.ts +100 -0
- package/dist/core/tooltips.d.ts.map +1 -0
- package/dist/elements/base.d.ts +3 -0
- package/dist/elements/base.d.ts.map +1 -0
- package/dist/elements/index.d.ts +33 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/index.js +55 -0
- package/dist/elements/index.js.map +9 -0
- package/dist/elements/media-airplay-button.d.ts +13 -0
- package/dist/elements/media-airplay-button.d.ts.map +1 -0
- package/dist/elements/media-error-display.d.ts +13 -0
- package/dist/elements/media-error-display.d.ts.map +1 -0
- package/dist/elements/media-fullscreen-button.d.ts +13 -0
- package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
- package/dist/elements/media-live-button.d.ts +13 -0
- package/dist/elements/media-live-button.d.ts.map +1 -0
- package/dist/elements/media-mute-button.d.ts +13 -0
- package/dist/elements/media-mute-button.d.ts.map +1 -0
- package/dist/elements/media-pip-button.d.ts +13 -0
- package/dist/elements/media-pip-button.d.ts.map +1 -0
- package/dist/elements/media-play-button.d.ts +14 -0
- package/dist/elements/media-play-button.d.ts.map +1 -0
- package/dist/elements/media-playback-rate-button.d.ts +10 -0
- package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
- package/dist/elements/media-preview-time.d.ts +18 -0
- package/dist/elements/media-preview-time.d.ts.map +1 -0
- package/dist/elements/media-progress-bar.d.ts +16 -0
- package/dist/elements/media-progress-bar.d.ts.map +1 -0
- package/dist/elements/media-remote-playback-button.d.ts +16 -0
- package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
- package/dist/elements/media-seek-button.d.ts +9 -0
- package/dist/elements/media-seek-button.d.ts.map +1 -0
- package/dist/elements/media-settings-menu.d.ts +32 -0
- package/dist/elements/media-settings-menu.d.ts.map +1 -0
- package/dist/elements/media-time-display.d.ts +21 -0
- package/dist/elements/media-time-display.d.ts.map +1 -0
- package/dist/elements/media-time-group.d.ts +22 -0
- package/dist/elements/media-time-group.d.ts.map +1 -0
- package/dist/elements/media-time-separator.d.ts +13 -0
- package/dist/elements/media-time-separator.d.ts.map +1 -0
- package/dist/elements/media-volume-slider.d.ts +16 -0
- package/dist/elements/media-volume-slider.d.ts.map +1 -0
- package/dist/elements/utils.d.ts +27 -0
- package/dist/elements/utils.d.ts.map +1 -0
- package/dist/elements/video-player.d.ts +23 -0
- package/dist/elements/video-player.d.ts.map +1 -0
- package/dist/elements/video-skin.d.ts +23 -0
- package/dist/elements/video-skin.d.ts.map +1 -0
- package/dist/features/captions.d.ts +8 -0
- package/dist/features/captions.d.ts.map +1 -0
- package/dist/features/create-player.d.ts +45 -0
- package/dist/features/create-player.d.ts.map +1 -0
- package/dist/features/fullscreen.d.ts +8 -0
- package/dist/features/fullscreen.d.ts.map +1 -0
- package/dist/features/gestures.d.ts +8 -0
- package/dist/features/gestures.d.ts.map +1 -0
- package/dist/features/index.d.ts +26 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +31 -0
- package/dist/features/index.js.map +9 -0
- package/dist/features/keyboard.d.ts +8 -0
- package/dist/features/keyboard.d.ts.map +1 -0
- package/dist/features/pip.d.ts +8 -0
- package/dist/features/pip.d.ts.map +1 -0
- package/dist/features/playback.d.ts +8 -0
- package/dist/features/playback.d.ts.map +1 -0
- package/dist/features/quality.d.ts +8 -0
- package/dist/features/quality.d.ts.map +1 -0
- package/dist/features/types.d.ts +26 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/volume.d.ts +8 -0
- package/dist/features/volume.d.ts.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8149 -0
- package/dist/index.js.map +31 -0
- package/dist/layouts/index.d.ts +58 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/player.d.ts +90 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/plugins/ads.d.ts +311 -0
- package/dist/plugins/ads.d.ts.map +1 -0
- package/dist/plugins/analytics.d.ts +203 -0
- package/dist/plugins/analytics.d.ts.map +1 -0
- package/dist/plugins/end-screen.d.ts +184 -0
- package/dist/plugins/end-screen.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +13 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +20 -0
- package/dist/plugins/index.js.map +9 -0
- package/dist/plugins/skip-segments.d.ts +159 -0
- package/dist/plugins/skip-segments.d.ts.map +1 -0
- package/dist/plugins/watermark.d.ts +157 -0
- package/dist/plugins/watermark.d.ts.map +1 -0
- package/dist/providers/base.d.ts +56 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/dash.d.ts +94 -0
- package/dist/providers/dash.d.ts.map +1 -0
- package/dist/providers/hls.d.ts +182 -0
- package/dist/providers/hls.d.ts.map +1 -0
- package/dist/providers/html5.d.ts +64 -0
- package/dist/providers/html5.d.ts.map +1 -0
- package/dist/providers/index.d.ts +41 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/vimeo.d.ts +157 -0
- package/dist/providers/vimeo.d.ts.map +1 -0
- package/dist/providers/youtube.d.ts +134 -0
- package/dist/providers/youtube.d.ts.map +1 -0
- package/dist/stx/index.d.ts +59 -0
- package/dist/stx/index.d.ts.map +1 -0
- package/dist/stx/index.js +480 -0
- package/dist/stx/index.js.map +11 -0
- package/dist/types.d.ts +724 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui/components.d.ts +121 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +16 -0
- package/dist/ui/index.js.map +9 -0
- package/package.json +109 -0
- package/src/cdn.test.ts +10 -0
- package/src/cdn.ts +60 -0
- package/src/config.ts +25 -0
- package/src/core/audio-gain.ts +171 -0
- package/src/core/caption-styles.ts +568 -0
- package/src/core/chapters.ts +551 -0
- package/src/core/error-overlay.ts +354 -0
- package/src/core/events.ts +552 -0
- package/src/core/features.ts +287 -0
- package/src/core/i18n.ts +590 -0
- package/src/core/index.ts +249 -0
- package/src/core/live.ts +472 -0
- package/src/core/media-session.ts +137 -0
- package/src/core/orientation.ts +57 -0
- package/src/core/popover.ts +143 -0
- package/src/core/remote-playback.ts +661 -0
- package/src/core/settings-menu.ts +630 -0
- package/src/core/spinner.ts +310 -0
- package/src/core/state.ts +476 -0
- package/src/core/text-tracks.ts +599 -0
- package/src/core/themes.ts +607 -0
- package/src/core/thumbnails.ts +457 -0
- package/src/core/tooltips.ts +370 -0
- package/src/elements/base.ts +2 -0
- package/src/elements/index.ts +82 -0
- package/src/elements/media-airplay-button.ts +78 -0
- package/src/elements/media-error-display.ts +47 -0
- package/src/elements/media-fullscreen-button.ts +72 -0
- package/src/elements/media-live-button.ts +65 -0
- package/src/elements/media-mute-button.ts +69 -0
- package/src/elements/media-pip-button.ts +66 -0
- package/src/elements/media-play-button.ts +69 -0
- package/src/elements/media-playback-rate-button.ts +59 -0
- package/src/elements/media-preview-time.ts +75 -0
- package/src/elements/media-progress-bar.ts +200 -0
- package/src/elements/media-remote-playback-button.ts +108 -0
- package/src/elements/media-seek-button.ts +50 -0
- package/src/elements/media-settings-menu.ts +302 -0
- package/src/elements/media-time-display.ts +99 -0
- package/src/elements/media-time-group.ts +41 -0
- package/src/elements/media-time-separator.ts +35 -0
- package/src/elements/media-volume-slider.ts +140 -0
- package/src/elements/utils.ts +82 -0
- package/src/elements/video-player.ts +175 -0
- package/src/elements/video-skin.ts +127 -0
- package/src/features/captions.ts +56 -0
- package/src/features/create-player.ts +91 -0
- package/src/features/fullscreen.ts +44 -0
- package/src/features/gestures.ts +44 -0
- package/src/features/index.ts +44 -0
- package/src/features/keyboard.ts +89 -0
- package/src/features/pip.ts +47 -0
- package/src/features/playback.ts +68 -0
- package/src/features/quality.ts +19 -0
- package/src/features/types.ts +28 -0
- package/src/features/volume.ts +46 -0
- package/src/index.ts +635 -0
- package/src/layouts/index.ts +672 -0
- package/src/player.ts +821 -0
- package/src/plugins/ads.ts +1045 -0
- package/src/plugins/analytics.ts +673 -0
- package/src/plugins/end-screen.ts +793 -0
- package/src/plugins/index.ts +61 -0
- package/src/plugins/skip-segments.ts +537 -0
- package/src/plugins/watermark.ts +537 -0
- package/src/providers/base.ts +197 -0
- package/src/providers/dash.ts +588 -0
- package/src/providers/hls.ts +624 -0
- package/src/providers/html5.ts +473 -0
- package/src/providers/index.ts +97 -0
- package/src/providers/vimeo.ts +533 -0
- package/src/providers/youtube.ts +542 -0
- package/src/stx/index.ts +606 -0
- package/src/types.ts +874 -0
- package/src/ui/components.ts +910 -0
- package/src/ui/index.ts +29 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thumbnail Sprites / Seek Preview
|
|
3
|
+
*
|
|
4
|
+
* Provides thumbnail preview functionality during seeking.
|
|
5
|
+
*
|
|
6
|
+
* @module core/thumbnails
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export interface ThumbnailSprite {
|
|
14
|
+
/** URL to the sprite image */
|
|
15
|
+
url: string
|
|
16
|
+
/** Width of each thumbnail in pixels */
|
|
17
|
+
width: number
|
|
18
|
+
/** Height of each thumbnail in pixels */
|
|
19
|
+
height: number
|
|
20
|
+
/** Number of columns in the sprite */
|
|
21
|
+
cols: number
|
|
22
|
+
/** Number of rows in the sprite */
|
|
23
|
+
rows: number
|
|
24
|
+
/** Duration each thumbnail represents in seconds */
|
|
25
|
+
interval: number
|
|
26
|
+
/** Starting time offset in seconds */
|
|
27
|
+
startTime?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ThumbnailCue {
|
|
31
|
+
/** Start time in seconds */
|
|
32
|
+
startTime: number
|
|
33
|
+
/** End time in seconds */
|
|
34
|
+
endTime: number
|
|
35
|
+
/** URL to the thumbnail image (or sprite) */
|
|
36
|
+
url: string
|
|
37
|
+
/** X position in sprite (if using sprite) */
|
|
38
|
+
x?: number
|
|
39
|
+
/** Y position in sprite (if using sprite) */
|
|
40
|
+
y?: number
|
|
41
|
+
/** Width of thumbnail */
|
|
42
|
+
width: number
|
|
43
|
+
/** Height of thumbnail */
|
|
44
|
+
height: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ThumbnailsConfig {
|
|
48
|
+
/** Source for thumbnails - URL to VTT file or sprite config */
|
|
49
|
+
src?: string | ThumbnailSprite | ThumbnailCue[]
|
|
50
|
+
/** Whether to preload thumbnail images */
|
|
51
|
+
preload?: boolean
|
|
52
|
+
/** Custom thumbnail renderer */
|
|
53
|
+
renderer?: (cue: ThumbnailCue, container: HTMLElement) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// VTT Thumbnail Parser
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a WebVTT file containing thumbnail cues
|
|
62
|
+
*
|
|
63
|
+
* VTT format for thumbnails:
|
|
64
|
+
* ```
|
|
65
|
+
* WEBVTT
|
|
66
|
+
*
|
|
67
|
+
* 00:00:00.000 --> 00:00:05.000
|
|
68
|
+
* sprite.jpg#xywh=0,0,160,90
|
|
69
|
+
*
|
|
70
|
+
* 00:00:05.000 --> 00:00:10.000
|
|
71
|
+
* sprite.jpg#xywh=160,0,160,90
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export async function parseThumbnailVTT(url: string): Promise<ThumbnailCue[]> {
|
|
75
|
+
const response = await fetch(url)
|
|
76
|
+
const text = await response.text()
|
|
77
|
+
|
|
78
|
+
const cues: ThumbnailCue[] = []
|
|
79
|
+
const lines = text.split('\n')
|
|
80
|
+
|
|
81
|
+
let i = 0
|
|
82
|
+
|
|
83
|
+
// Skip WEBVTT header
|
|
84
|
+
while (i < lines.length && !lines[i].includes('-->')) {
|
|
85
|
+
i++
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Parse cues
|
|
89
|
+
while (i < lines.length) {
|
|
90
|
+
const line = lines[i].trim()
|
|
91
|
+
|
|
92
|
+
// Find timestamp line
|
|
93
|
+
if (line.includes('-->')) {
|
|
94
|
+
const [startStr, endStr] = line.split('-->')
|
|
95
|
+
const startTime = parseVTTTime(startStr.trim())
|
|
96
|
+
const endTime = parseVTTTime(endStr.trim())
|
|
97
|
+
|
|
98
|
+
// Next line should be the thumbnail URL
|
|
99
|
+
i++
|
|
100
|
+
if (i < lines.length) {
|
|
101
|
+
const urlLine = lines[i].trim()
|
|
102
|
+
if (urlLine) {
|
|
103
|
+
const cue = parseThumbnailUrl(urlLine, startTime, endTime)
|
|
104
|
+
if (cue) {
|
|
105
|
+
cues.push(cue)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
i++
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return cues
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse VTT timestamp to seconds
|
|
119
|
+
*/
|
|
120
|
+
function parseVTTTime(timeStr: string): number {
|
|
121
|
+
const parts = timeStr.split(':')
|
|
122
|
+
let seconds = 0
|
|
123
|
+
|
|
124
|
+
if (parts.length === 3) {
|
|
125
|
+
// HH:MM:SS.mmm
|
|
126
|
+
seconds += Number.parseFloat(parts[0]) * 3600
|
|
127
|
+
seconds += Number.parseFloat(parts[1]) * 60
|
|
128
|
+
seconds += Number.parseFloat(parts[2])
|
|
129
|
+
}
|
|
130
|
+
else if (parts.length === 2) {
|
|
131
|
+
// MM:SS.mmm
|
|
132
|
+
seconds += Number.parseFloat(parts[0]) * 60
|
|
133
|
+
seconds += Number.parseFloat(parts[1])
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return seconds
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse thumbnail URL with optional sprite coordinates
|
|
141
|
+
*/
|
|
142
|
+
function parseThumbnailUrl(urlLine: string, startTime: number, endTime: number): ThumbnailCue | null {
|
|
143
|
+
// Check for xywh fragment
|
|
144
|
+
const xywhMatch = urlLine.match(/(.+)#xywh=(\d+),(\d+),(\d+),(\d+)/)
|
|
145
|
+
|
|
146
|
+
if (xywhMatch) {
|
|
147
|
+
return {
|
|
148
|
+
startTime,
|
|
149
|
+
endTime,
|
|
150
|
+
url: xywhMatch[1],
|
|
151
|
+
x: Number.parseInt(xywhMatch[2], 10),
|
|
152
|
+
y: Number.parseInt(xywhMatch[3], 10),
|
|
153
|
+
width: Number.parseInt(xywhMatch[4], 10),
|
|
154
|
+
height: Number.parseInt(xywhMatch[5], 10),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Plain URL - try to extract dimensions from filename or use defaults
|
|
159
|
+
return {
|
|
160
|
+
startTime,
|
|
161
|
+
endTime,
|
|
162
|
+
url: urlLine,
|
|
163
|
+
width: 160,
|
|
164
|
+
height: 90,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Sprite Thumbnail Generator
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generate thumbnail cues from a sprite configuration
|
|
174
|
+
*/
|
|
175
|
+
export function generateSpriteCues(sprite: ThumbnailSprite, duration: number): ThumbnailCue[] {
|
|
176
|
+
const cues: ThumbnailCue[] = []
|
|
177
|
+
const totalThumbnails = sprite.cols * sprite.rows
|
|
178
|
+
const startTime = sprite.startTime || 0
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < totalThumbnails; i++) {
|
|
181
|
+
const cueStart = startTime + i * sprite.interval
|
|
182
|
+
const cueEnd = cueStart + sprite.interval
|
|
183
|
+
|
|
184
|
+
// Don't create cues beyond video duration
|
|
185
|
+
if (cueStart >= duration) break
|
|
186
|
+
|
|
187
|
+
const col = i % sprite.cols
|
|
188
|
+
const row = Math.floor(i / sprite.cols)
|
|
189
|
+
|
|
190
|
+
cues.push({
|
|
191
|
+
startTime: cueStart,
|
|
192
|
+
endTime: Math.min(cueEnd, duration),
|
|
193
|
+
url: sprite.url,
|
|
194
|
+
x: col * sprite.width,
|
|
195
|
+
y: row * sprite.height,
|
|
196
|
+
width: sprite.width,
|
|
197
|
+
height: sprite.height,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return cues
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// =============================================================================
|
|
205
|
+
// Thumbnails Class
|
|
206
|
+
// =============================================================================
|
|
207
|
+
|
|
208
|
+
export class Thumbnails {
|
|
209
|
+
private cues: ThumbnailCue[] = []
|
|
210
|
+
private preloadedImages = new Map<string, HTMLImageElement>()
|
|
211
|
+
private container: HTMLElement | null = null
|
|
212
|
+
private _currentCue: ThumbnailCue | null = null
|
|
213
|
+
private config: ThumbnailsConfig
|
|
214
|
+
|
|
215
|
+
constructor(config: ThumbnailsConfig = {}) {
|
|
216
|
+
this.config = config
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Initialize thumbnails from config
|
|
221
|
+
*/
|
|
222
|
+
async initialize(duration: number): Promise<void> {
|
|
223
|
+
const { src } = this.config
|
|
224
|
+
|
|
225
|
+
if (!src) return
|
|
226
|
+
|
|
227
|
+
if (typeof src === 'string') {
|
|
228
|
+
// VTT URL
|
|
229
|
+
this.cues = await parseThumbnailVTT(src)
|
|
230
|
+
}
|
|
231
|
+
else if (Array.isArray(src)) {
|
|
232
|
+
// Direct cue array
|
|
233
|
+
this.cues = src
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Sprite config
|
|
237
|
+
this.cues = generateSpriteCues(src, duration)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Preload images if enabled
|
|
241
|
+
if (this.config.preload) {
|
|
242
|
+
this.preloadImages()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Preload all unique thumbnail images
|
|
248
|
+
*/
|
|
249
|
+
private preloadImages(): void {
|
|
250
|
+
const uniqueUrls = new Set(this.cues.map(cue => cue.url))
|
|
251
|
+
|
|
252
|
+
for (const url of uniqueUrls) {
|
|
253
|
+
if (!this.preloadedImages.has(url)) {
|
|
254
|
+
const img = new Image()
|
|
255
|
+
img.src = url
|
|
256
|
+
this.preloadedImages.set(url, img)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get thumbnail cue for a specific time
|
|
263
|
+
*/
|
|
264
|
+
getCueForTime(time: number): ThumbnailCue | null {
|
|
265
|
+
for (const cue of this.cues) {
|
|
266
|
+
if (time >= cue.startTime && time < cue.endTime) {
|
|
267
|
+
return cue
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Create thumbnail preview element
|
|
275
|
+
*/
|
|
276
|
+
createPreviewElement(): HTMLElement {
|
|
277
|
+
this.container = document.createElement('div')
|
|
278
|
+
this.container.className = 'tsvp-thumbnail-preview'
|
|
279
|
+
this.container.style.cssText = `
|
|
280
|
+
position: absolute;
|
|
281
|
+
bottom: 100%;
|
|
282
|
+
left: 50%;
|
|
283
|
+
transform: translateX(-50%);
|
|
284
|
+
margin-bottom: 12px;
|
|
285
|
+
pointer-events: none;
|
|
286
|
+
opacity: 0;
|
|
287
|
+
transition: opacity var(--tsvp-transition-normal, 200ms) var(--tsvp-transition-easing, ease);
|
|
288
|
+
z-index: 10;
|
|
289
|
+
background: var(--tsvp-thumbnail-border, rgba(0, 0, 0, 0.8));
|
|
290
|
+
border-radius: var(--tsvp-thumbnail-radius, 4px);
|
|
291
|
+
padding: 2px;
|
|
292
|
+
box-shadow: var(--tsvp-thumbnail-shadow, 0 4px 12px rgba(0, 0, 0, 0.4));
|
|
293
|
+
overflow: hidden;
|
|
294
|
+
`
|
|
295
|
+
|
|
296
|
+
const inner = document.createElement('div')
|
|
297
|
+
inner.className = 'tsvp-thumbnail-inner'
|
|
298
|
+
inner.style.cssText = `
|
|
299
|
+
width: var(--tsvp-thumbnail-width, 160px);
|
|
300
|
+
height: var(--tsvp-thumbnail-height, 90px);
|
|
301
|
+
background-size: cover;
|
|
302
|
+
background-position: center;
|
|
303
|
+
border-radius: calc(var(--tsvp-thumbnail-radius, 4px) - 2px);
|
|
304
|
+
overflow: hidden;
|
|
305
|
+
`
|
|
306
|
+
this.container.appendChild(inner)
|
|
307
|
+
|
|
308
|
+
const time = document.createElement('div')
|
|
309
|
+
time.className = 'tsvp-thumbnail-time'
|
|
310
|
+
time.style.cssText = `
|
|
311
|
+
position: absolute;
|
|
312
|
+
bottom: 0;
|
|
313
|
+
left: 0;
|
|
314
|
+
right: 0;
|
|
315
|
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
|
316
|
+
color: #fff;
|
|
317
|
+
font-size: var(--tsvp-font-size-sm, 12px);
|
|
318
|
+
padding: 4px 6px 2px;
|
|
319
|
+
text-align: center;
|
|
320
|
+
font-family: var(--tsvp-font-family, sans-serif);
|
|
321
|
+
`
|
|
322
|
+
this.container.appendChild(time)
|
|
323
|
+
|
|
324
|
+
return this.container
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Update thumbnail preview for a specific time
|
|
329
|
+
*/
|
|
330
|
+
updatePreview(time: number, position?: number): void {
|
|
331
|
+
if (!this.container) return
|
|
332
|
+
|
|
333
|
+
const cue = this.getCueForTime(time)
|
|
334
|
+
const inner = this.container.querySelector('.tsvp-thumbnail-inner') as HTMLElement
|
|
335
|
+
const timeEl = this.container.querySelector('.tsvp-thumbnail-time') as HTMLElement
|
|
336
|
+
|
|
337
|
+
if (cue && inner) {
|
|
338
|
+
// Custom renderer
|
|
339
|
+
if (this.config.renderer) {
|
|
340
|
+
this.config.renderer(cue, inner)
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
// Default rendering
|
|
344
|
+
if (cue.x !== undefined && cue.y !== undefined) {
|
|
345
|
+
// Sprite thumbnail
|
|
346
|
+
inner.style.backgroundImage = `url(${cue.url})`
|
|
347
|
+
inner.style.backgroundPosition = `-${cue.x}px -${cue.y}px`
|
|
348
|
+
inner.style.backgroundSize = 'auto'
|
|
349
|
+
inner.style.width = `${cue.width}px`
|
|
350
|
+
inner.style.height = `${cue.height}px`
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Individual thumbnail
|
|
354
|
+
inner.style.backgroundImage = `url(${cue.url})`
|
|
355
|
+
inner.style.backgroundPosition = 'center'
|
|
356
|
+
inner.style.backgroundSize = 'cover'
|
|
357
|
+
inner.style.width = `${cue.width}px`
|
|
358
|
+
inner.style.height = `${cue.height}px`
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Update time display
|
|
363
|
+
if (timeEl) {
|
|
364
|
+
timeEl.textContent = formatTime(time)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this._currentCue = cue
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Update horizontal position if provided
|
|
371
|
+
if (position !== undefined) {
|
|
372
|
+
this.container.style.left = `${position}px`
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Show thumbnail preview
|
|
378
|
+
*/
|
|
379
|
+
show(): void {
|
|
380
|
+
if (this.container) {
|
|
381
|
+
this.container.style.opacity = '1'
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Hide thumbnail preview
|
|
387
|
+
*/
|
|
388
|
+
hide(): void {
|
|
389
|
+
if (this.container) {
|
|
390
|
+
this.container.style.opacity = '0'
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Get all cues
|
|
396
|
+
*/
|
|
397
|
+
getCues(): ThumbnailCue[] {
|
|
398
|
+
return [...this.cues]
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Add cues manually
|
|
403
|
+
*/
|
|
404
|
+
addCues(cues: ThumbnailCue[]): void {
|
|
405
|
+
this.cues.push(...cues)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get current cue
|
|
410
|
+
*/
|
|
411
|
+
getCurrentCue(): ThumbnailCue | null {
|
|
412
|
+
return this._currentCue
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Clear all cues
|
|
417
|
+
*/
|
|
418
|
+
clear(): void {
|
|
419
|
+
this.cues = []
|
|
420
|
+
this.preloadedImages.clear()
|
|
421
|
+
this._currentCue = null
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Destroy thumbnails instance
|
|
426
|
+
*/
|
|
427
|
+
destroy(): void {
|
|
428
|
+
this.clear()
|
|
429
|
+
if (this.container?.parentNode) {
|
|
430
|
+
this.container.parentNode.removeChild(this.container)
|
|
431
|
+
}
|
|
432
|
+
this.container = null
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// =============================================================================
|
|
437
|
+
// Factory
|
|
438
|
+
// =============================================================================
|
|
439
|
+
|
|
440
|
+
export function createThumbnails(config?: ThumbnailsConfig): Thumbnails {
|
|
441
|
+
return new Thumbnails(config)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// =============================================================================
|
|
445
|
+
// Helpers
|
|
446
|
+
// =============================================================================
|
|
447
|
+
|
|
448
|
+
function formatTime(seconds: number): string {
|
|
449
|
+
const h = Math.floor(seconds / 3600)
|
|
450
|
+
const m = Math.floor((seconds % 3600) / 60)
|
|
451
|
+
const s = Math.floor(seconds % 60)
|
|
452
|
+
|
|
453
|
+
if (h > 0) {
|
|
454
|
+
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
|
455
|
+
}
|
|
456
|
+
return `${m}:${s.toString().padStart(2, '0')}`
|
|
457
|
+
}
|