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.
Files changed (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +438 -0
  3. package/dist/cdn.d.ts +10 -0
  4. package/dist/cdn.d.ts.map +1 -0
  5. package/dist/cdn.min.js +1046 -0
  6. package/dist/cdn.min.js.map +54 -0
  7. package/dist/cdn.test.d.ts +2 -0
  8. package/dist/cdn.test.d.ts.map +1 -0
  9. package/dist/chunk-36x4yhjn.js +765 -0
  10. package/dist/chunk-36x4yhjn.js.map +12 -0
  11. package/dist/chunk-eajts2jv.js +4243 -0
  12. package/dist/chunk-eajts2jv.js.map +42 -0
  13. package/dist/chunk-z1ehbz9f.js +380 -0
  14. package/dist/chunk-z1ehbz9f.js.map +19 -0
  15. package/dist/config.d.ts +9 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/core/audio-gain.d.ts +67 -0
  18. package/dist/core/audio-gain.d.ts.map +1 -0
  19. package/dist/core/caption-styles.d.ts +121 -0
  20. package/dist/core/caption-styles.d.ts.map +1 -0
  21. package/dist/core/chapters.d.ts +150 -0
  22. package/dist/core/chapters.d.ts.map +1 -0
  23. package/dist/core/error-overlay.d.ts +71 -0
  24. package/dist/core/error-overlay.d.ts.map +1 -0
  25. package/dist/core/events.d.ts +119 -0
  26. package/dist/core/events.d.ts.map +1 -0
  27. package/dist/core/features.d.ts +64 -0
  28. package/dist/core/features.d.ts.map +1 -0
  29. package/dist/core/i18n.d.ts +132 -0
  30. package/dist/core/i18n.d.ts.map +1 -0
  31. package/dist/core/index.d.ts +37 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/live.d.ts +130 -0
  34. package/dist/core/live.d.ts.map +1 -0
  35. package/dist/core/media-session.d.ts +38 -0
  36. package/dist/core/media-session.d.ts.map +1 -0
  37. package/dist/core/orientation.d.ts +22 -0
  38. package/dist/core/orientation.d.ts.map +1 -0
  39. package/dist/core/popover.d.ts +42 -0
  40. package/dist/core/popover.d.ts.map +1 -0
  41. package/dist/core/remote-playback.d.ts +195 -0
  42. package/dist/core/remote-playback.d.ts.map +1 -0
  43. package/dist/core/settings-menu.d.ts +132 -0
  44. package/dist/core/settings-menu.d.ts.map +1 -0
  45. package/dist/core/spinner.d.ts +67 -0
  46. package/dist/core/spinner.d.ts.map +1 -0
  47. package/dist/core/state.d.ts +142 -0
  48. package/dist/core/state.d.ts.map +1 -0
  49. package/dist/core/text-tracks.d.ts +162 -0
  50. package/dist/core/text-tracks.d.ts.map +1 -0
  51. package/dist/core/themes.d.ts +213 -0
  52. package/dist/core/themes.d.ts.map +1 -0
  53. package/dist/core/thumbnails.d.ts +124 -0
  54. package/dist/core/thumbnails.d.ts.map +1 -0
  55. package/dist/core/tooltips.d.ts +100 -0
  56. package/dist/core/tooltips.d.ts.map +1 -0
  57. package/dist/elements/base.d.ts +3 -0
  58. package/dist/elements/base.d.ts.map +1 -0
  59. package/dist/elements/index.d.ts +33 -0
  60. package/dist/elements/index.d.ts.map +1 -0
  61. package/dist/elements/index.js +55 -0
  62. package/dist/elements/index.js.map +9 -0
  63. package/dist/elements/media-airplay-button.d.ts +13 -0
  64. package/dist/elements/media-airplay-button.d.ts.map +1 -0
  65. package/dist/elements/media-error-display.d.ts +13 -0
  66. package/dist/elements/media-error-display.d.ts.map +1 -0
  67. package/dist/elements/media-fullscreen-button.d.ts +13 -0
  68. package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
  69. package/dist/elements/media-live-button.d.ts +13 -0
  70. package/dist/elements/media-live-button.d.ts.map +1 -0
  71. package/dist/elements/media-mute-button.d.ts +13 -0
  72. package/dist/elements/media-mute-button.d.ts.map +1 -0
  73. package/dist/elements/media-pip-button.d.ts +13 -0
  74. package/dist/elements/media-pip-button.d.ts.map +1 -0
  75. package/dist/elements/media-play-button.d.ts +14 -0
  76. package/dist/elements/media-play-button.d.ts.map +1 -0
  77. package/dist/elements/media-playback-rate-button.d.ts +10 -0
  78. package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
  79. package/dist/elements/media-preview-time.d.ts +18 -0
  80. package/dist/elements/media-preview-time.d.ts.map +1 -0
  81. package/dist/elements/media-progress-bar.d.ts +16 -0
  82. package/dist/elements/media-progress-bar.d.ts.map +1 -0
  83. package/dist/elements/media-remote-playback-button.d.ts +16 -0
  84. package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
  85. package/dist/elements/media-seek-button.d.ts +9 -0
  86. package/dist/elements/media-seek-button.d.ts.map +1 -0
  87. package/dist/elements/media-settings-menu.d.ts +32 -0
  88. package/dist/elements/media-settings-menu.d.ts.map +1 -0
  89. package/dist/elements/media-time-display.d.ts +21 -0
  90. package/dist/elements/media-time-display.d.ts.map +1 -0
  91. package/dist/elements/media-time-group.d.ts +22 -0
  92. package/dist/elements/media-time-group.d.ts.map +1 -0
  93. package/dist/elements/media-time-separator.d.ts +13 -0
  94. package/dist/elements/media-time-separator.d.ts.map +1 -0
  95. package/dist/elements/media-volume-slider.d.ts +16 -0
  96. package/dist/elements/media-volume-slider.d.ts.map +1 -0
  97. package/dist/elements/utils.d.ts +27 -0
  98. package/dist/elements/utils.d.ts.map +1 -0
  99. package/dist/elements/video-player.d.ts +23 -0
  100. package/dist/elements/video-player.d.ts.map +1 -0
  101. package/dist/elements/video-skin.d.ts +23 -0
  102. package/dist/elements/video-skin.d.ts.map +1 -0
  103. package/dist/features/captions.d.ts +8 -0
  104. package/dist/features/captions.d.ts.map +1 -0
  105. package/dist/features/create-player.d.ts +45 -0
  106. package/dist/features/create-player.d.ts.map +1 -0
  107. package/dist/features/fullscreen.d.ts +8 -0
  108. package/dist/features/fullscreen.d.ts.map +1 -0
  109. package/dist/features/gestures.d.ts +8 -0
  110. package/dist/features/gestures.d.ts.map +1 -0
  111. package/dist/features/index.d.ts +26 -0
  112. package/dist/features/index.d.ts.map +1 -0
  113. package/dist/features/index.js +31 -0
  114. package/dist/features/index.js.map +9 -0
  115. package/dist/features/keyboard.d.ts +8 -0
  116. package/dist/features/keyboard.d.ts.map +1 -0
  117. package/dist/features/pip.d.ts +8 -0
  118. package/dist/features/pip.d.ts.map +1 -0
  119. package/dist/features/playback.d.ts +8 -0
  120. package/dist/features/playback.d.ts.map +1 -0
  121. package/dist/features/quality.d.ts +8 -0
  122. package/dist/features/quality.d.ts.map +1 -0
  123. package/dist/features/types.d.ts +26 -0
  124. package/dist/features/types.d.ts.map +1 -0
  125. package/dist/features/volume.d.ts +8 -0
  126. package/dist/features/volume.d.ts.map +1 -0
  127. package/dist/index.d.ts +84 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +8149 -0
  130. package/dist/index.js.map +31 -0
  131. package/dist/layouts/index.d.ts +58 -0
  132. package/dist/layouts/index.d.ts.map +1 -0
  133. package/dist/player.d.ts +90 -0
  134. package/dist/player.d.ts.map +1 -0
  135. package/dist/plugins/ads.d.ts +311 -0
  136. package/dist/plugins/ads.d.ts.map +1 -0
  137. package/dist/plugins/analytics.d.ts +203 -0
  138. package/dist/plugins/analytics.d.ts.map +1 -0
  139. package/dist/plugins/end-screen.d.ts +184 -0
  140. package/dist/plugins/end-screen.d.ts.map +1 -0
  141. package/dist/plugins/index.d.ts +13 -0
  142. package/dist/plugins/index.d.ts.map +1 -0
  143. package/dist/plugins/index.js +20 -0
  144. package/dist/plugins/index.js.map +9 -0
  145. package/dist/plugins/skip-segments.d.ts +159 -0
  146. package/dist/plugins/skip-segments.d.ts.map +1 -0
  147. package/dist/plugins/watermark.d.ts +157 -0
  148. package/dist/plugins/watermark.d.ts.map +1 -0
  149. package/dist/providers/base.d.ts +56 -0
  150. package/dist/providers/base.d.ts.map +1 -0
  151. package/dist/providers/dash.d.ts +94 -0
  152. package/dist/providers/dash.d.ts.map +1 -0
  153. package/dist/providers/hls.d.ts +182 -0
  154. package/dist/providers/hls.d.ts.map +1 -0
  155. package/dist/providers/html5.d.ts +64 -0
  156. package/dist/providers/html5.d.ts.map +1 -0
  157. package/dist/providers/index.d.ts +41 -0
  158. package/dist/providers/index.d.ts.map +1 -0
  159. package/dist/providers/vimeo.d.ts +157 -0
  160. package/dist/providers/vimeo.d.ts.map +1 -0
  161. package/dist/providers/youtube.d.ts +134 -0
  162. package/dist/providers/youtube.d.ts.map +1 -0
  163. package/dist/stx/index.d.ts +59 -0
  164. package/dist/stx/index.d.ts.map +1 -0
  165. package/dist/stx/index.js +480 -0
  166. package/dist/stx/index.js.map +11 -0
  167. package/dist/types.d.ts +724 -0
  168. package/dist/types.d.ts.map +1 -0
  169. package/dist/ui/components.d.ts +121 -0
  170. package/dist/ui/components.d.ts.map +1 -0
  171. package/dist/ui/index.d.ts +9 -0
  172. package/dist/ui/index.d.ts.map +1 -0
  173. package/dist/ui/index.js +16 -0
  174. package/dist/ui/index.js.map +9 -0
  175. package/package.json +109 -0
  176. package/src/cdn.test.ts +10 -0
  177. package/src/cdn.ts +60 -0
  178. package/src/config.ts +25 -0
  179. package/src/core/audio-gain.ts +171 -0
  180. package/src/core/caption-styles.ts +568 -0
  181. package/src/core/chapters.ts +551 -0
  182. package/src/core/error-overlay.ts +354 -0
  183. package/src/core/events.ts +552 -0
  184. package/src/core/features.ts +287 -0
  185. package/src/core/i18n.ts +590 -0
  186. package/src/core/index.ts +249 -0
  187. package/src/core/live.ts +472 -0
  188. package/src/core/media-session.ts +137 -0
  189. package/src/core/orientation.ts +57 -0
  190. package/src/core/popover.ts +143 -0
  191. package/src/core/remote-playback.ts +661 -0
  192. package/src/core/settings-menu.ts +630 -0
  193. package/src/core/spinner.ts +310 -0
  194. package/src/core/state.ts +476 -0
  195. package/src/core/text-tracks.ts +599 -0
  196. package/src/core/themes.ts +607 -0
  197. package/src/core/thumbnails.ts +457 -0
  198. package/src/core/tooltips.ts +370 -0
  199. package/src/elements/base.ts +2 -0
  200. package/src/elements/index.ts +82 -0
  201. package/src/elements/media-airplay-button.ts +78 -0
  202. package/src/elements/media-error-display.ts +47 -0
  203. package/src/elements/media-fullscreen-button.ts +72 -0
  204. package/src/elements/media-live-button.ts +65 -0
  205. package/src/elements/media-mute-button.ts +69 -0
  206. package/src/elements/media-pip-button.ts +66 -0
  207. package/src/elements/media-play-button.ts +69 -0
  208. package/src/elements/media-playback-rate-button.ts +59 -0
  209. package/src/elements/media-preview-time.ts +75 -0
  210. package/src/elements/media-progress-bar.ts +200 -0
  211. package/src/elements/media-remote-playback-button.ts +108 -0
  212. package/src/elements/media-seek-button.ts +50 -0
  213. package/src/elements/media-settings-menu.ts +302 -0
  214. package/src/elements/media-time-display.ts +99 -0
  215. package/src/elements/media-time-group.ts +41 -0
  216. package/src/elements/media-time-separator.ts +35 -0
  217. package/src/elements/media-volume-slider.ts +140 -0
  218. package/src/elements/utils.ts +82 -0
  219. package/src/elements/video-player.ts +175 -0
  220. package/src/elements/video-skin.ts +127 -0
  221. package/src/features/captions.ts +56 -0
  222. package/src/features/create-player.ts +91 -0
  223. package/src/features/fullscreen.ts +44 -0
  224. package/src/features/gestures.ts +44 -0
  225. package/src/features/index.ts +44 -0
  226. package/src/features/keyboard.ts +89 -0
  227. package/src/features/pip.ts +47 -0
  228. package/src/features/playback.ts +68 -0
  229. package/src/features/quality.ts +19 -0
  230. package/src/features/types.ts +28 -0
  231. package/src/features/volume.ts +46 -0
  232. package/src/index.ts +635 -0
  233. package/src/layouts/index.ts +672 -0
  234. package/src/player.ts +821 -0
  235. package/src/plugins/ads.ts +1045 -0
  236. package/src/plugins/analytics.ts +673 -0
  237. package/src/plugins/end-screen.ts +793 -0
  238. package/src/plugins/index.ts +61 -0
  239. package/src/plugins/skip-segments.ts +537 -0
  240. package/src/plugins/watermark.ts +537 -0
  241. package/src/providers/base.ts +197 -0
  242. package/src/providers/dash.ts +588 -0
  243. package/src/providers/hls.ts +624 -0
  244. package/src/providers/html5.ts +473 -0
  245. package/src/providers/index.ts +97 -0
  246. package/src/providers/vimeo.ts +533 -0
  247. package/src/providers/youtube.ts +542 -0
  248. package/src/stx/index.ts +606 -0
  249. package/src/types.ts +874 -0
  250. package/src/ui/components.ts +910 -0
  251. 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
+ }