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,551 @@
1
+ /**
2
+ * Chapters Support
3
+ *
4
+ * VTT chapters parsing and chapter markers for the progress bar.
5
+ *
6
+ * @module core/chapters
7
+ */
8
+
9
+ // =============================================================================
10
+ // Types
11
+ // =============================================================================
12
+
13
+ export interface Chapter {
14
+ /** Chapter ID */
15
+ id: string
16
+ /** Start time in seconds */
17
+ startTime: number
18
+ /** End time in seconds */
19
+ endTime: number
20
+ /** Chapter title */
21
+ title: string
22
+ /** Optional thumbnail URL */
23
+ thumbnail?: string
24
+ }
25
+
26
+ export interface ChaptersConfig {
27
+ /** VTT chapters URL or array of chapters */
28
+ src?: string | Chapter[]
29
+ /** Show chapter markers on progress bar */
30
+ showMarkers?: boolean
31
+ /** Show chapter title on hover */
32
+ showTitles?: boolean
33
+ /** Marker color */
34
+ markerColor?: string
35
+ /** Active marker color */
36
+ activeMarkerColor?: string
37
+ }
38
+
39
+ // =============================================================================
40
+ // VTT Chapters Parser
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Parse VTT file for chapters
45
+ */
46
+ export async function parseChaptersVTT(url: string): Promise<Chapter[]> {
47
+ const response = await fetch(url)
48
+ const text = await response.text()
49
+
50
+ const chapters: Chapter[] = []
51
+ const lines = text.split('\n')
52
+
53
+ let i = 0
54
+ let chapterIndex = 0
55
+
56
+ // Skip WEBVTT header
57
+ while (i < lines.length && !lines[i].includes('-->')) {
58
+ i++
59
+ }
60
+
61
+ // Parse cues
62
+ while (i < lines.length) {
63
+ const line = lines[i].trim()
64
+
65
+ // Find timestamp line
66
+ if (line.includes('-->')) {
67
+ const [startStr, endStr] = line.split('-->')
68
+ const startTime = parseVTTTime(startStr.trim())
69
+ const endTime = parseVTTTime(endStr.trim())
70
+
71
+ // Next line(s) should be the chapter title
72
+ i++
73
+ let title = ''
74
+ while (i < lines.length && lines[i].trim() !== '') {
75
+ if (title) title += ' '
76
+ title += lines[i].trim()
77
+ i++
78
+ }
79
+
80
+ if (title) {
81
+ chapters.push({
82
+ id: `chapter-${chapterIndex++}`,
83
+ startTime,
84
+ endTime,
85
+ title,
86
+ })
87
+ }
88
+ }
89
+
90
+ i++
91
+ }
92
+
93
+ return chapters
94
+ }
95
+
96
+ /**
97
+ * Parse VTT timestamp to seconds
98
+ */
99
+ function parseVTTTime(timeStr: string): number {
100
+ const parts = timeStr.split(':')
101
+ let seconds = 0
102
+
103
+ if (parts.length === 3) {
104
+ seconds += Number.parseFloat(parts[0]) * 3600
105
+ seconds += Number.parseFloat(parts[1]) * 60
106
+ seconds += Number.parseFloat(parts[2])
107
+ }
108
+ else if (parts.length === 2) {
109
+ seconds += Number.parseFloat(parts[0]) * 60
110
+ seconds += Number.parseFloat(parts[1])
111
+ }
112
+
113
+ return seconds
114
+ }
115
+
116
+ // =============================================================================
117
+ // Chapters Manager
118
+ // =============================================================================
119
+
120
+ export class ChaptersManager {
121
+ private chapters: Chapter[] = []
122
+ private config: Required<ChaptersConfig>
123
+ private markersContainer: HTMLElement | null = null
124
+ private titleElement: HTMLElement | null = null
125
+ private currentChapter: Chapter | null = null
126
+ private duration = 0
127
+
128
+ constructor(config: ChaptersConfig = {}) {
129
+ this.config = {
130
+ src: config.src || [],
131
+ showMarkers: config.showMarkers ?? true,
132
+ showTitles: config.showTitles ?? true,
133
+ markerColor: config.markerColor || 'rgba(255, 255, 255, 0.5)',
134
+ activeMarkerColor: config.activeMarkerColor || 'var(--tsvp-color-primary, #00a8ff)',
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Initialize chapters
140
+ */
141
+ async initialize(duration: number): Promise<void> {
142
+ this.duration = duration
143
+ const { src } = this.config
144
+
145
+ if (!src) return
146
+
147
+ if (typeof src === 'string') {
148
+ this.chapters = await parseChaptersVTT(src)
149
+ }
150
+ else if (Array.isArray(src)) {
151
+ this.chapters = src
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Get all chapters
157
+ */
158
+ getChapters(): Chapter[] {
159
+ return [...this.chapters]
160
+ }
161
+
162
+ /**
163
+ * Get chapter at specific time
164
+ */
165
+ getChapterAtTime(time: number): Chapter | null {
166
+ for (const chapter of this.chapters) {
167
+ if (time >= chapter.startTime && time < chapter.endTime) {
168
+ return chapter
169
+ }
170
+ }
171
+ return null
172
+ }
173
+
174
+ /**
175
+ * Get current chapter
176
+ */
177
+ getCurrentChapter(): Chapter | null {
178
+ return this.currentChapter
179
+ }
180
+
181
+ /**
182
+ * Update current time and chapter
183
+ */
184
+ updateTime(time: number): Chapter | null {
185
+ const chapter = this.getChapterAtTime(time)
186
+
187
+ if (chapter !== this.currentChapter) {
188
+ this.currentChapter = chapter
189
+ this.updateActiveMarker()
190
+ }
191
+
192
+ return chapter
193
+ }
194
+
195
+ /**
196
+ * Seek to chapter by ID
197
+ */
198
+ seekToChapter(id: string): number | null {
199
+ const chapter = this.chapters.find(c => c.id === id)
200
+ return chapter ? chapter.startTime : null
201
+ }
202
+
203
+ /**
204
+ * Get next chapter
205
+ */
206
+ getNextChapter(currentTime: number): Chapter | null {
207
+ for (const chapter of this.chapters) {
208
+ if (chapter.startTime > currentTime) {
209
+ return chapter
210
+ }
211
+ }
212
+ return null
213
+ }
214
+
215
+ /**
216
+ * Get previous chapter
217
+ */
218
+ getPreviousChapter(currentTime: number): Chapter | null {
219
+ let prev: Chapter | null = null
220
+ for (const chapter of this.chapters) {
221
+ if (chapter.startTime >= currentTime) {
222
+ break
223
+ }
224
+ prev = chapter
225
+ }
226
+ return prev
227
+ }
228
+
229
+ /**
230
+ * Create chapter markers element for progress bar
231
+ */
232
+ createMarkersElement(): HTMLElement {
233
+ this.markersContainer = document.createElement('div')
234
+ this.markersContainer.className = 'tsvp-chapter-markers'
235
+ this.markersContainer.style.cssText = `
236
+ position: absolute;
237
+ left: 0;
238
+ right: 0;
239
+ top: 0;
240
+ bottom: 0;
241
+ pointer-events: none;
242
+ `
243
+
244
+ this.renderMarkers()
245
+ return this.markersContainer
246
+ }
247
+
248
+ /**
249
+ * Render chapter markers
250
+ */
251
+ private renderMarkers(): void {
252
+ if (!this.markersContainer || !this.duration) return
253
+
254
+ this.markersContainer.innerHTML = ''
255
+
256
+ for (const chapter of this.chapters) {
257
+ const marker = document.createElement('div')
258
+ marker.className = 'tsvp-chapter-marker'
259
+ marker.dataset.chapterId = chapter.id
260
+
261
+ const position = (chapter.startTime / this.duration) * 100
262
+
263
+ marker.style.cssText = `
264
+ position: absolute;
265
+ left: ${position}%;
266
+ top: 0;
267
+ bottom: 0;
268
+ width: 2px;
269
+ background: ${this.config.markerColor};
270
+ transform: translateX(-50%);
271
+ transition: background var(--tsvp-transition-fast, 100ms);
272
+ `
273
+
274
+ this.markersContainer.appendChild(marker)
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Update active marker styling
280
+ */
281
+ private updateActiveMarker(): void {
282
+ if (!this.markersContainer) return
283
+
284
+ const markers = this.markersContainer.querySelectorAll('.tsvp-chapter-marker')
285
+ markers.forEach((marker) => {
286
+ const el = marker as HTMLElement
287
+ const isActive = this.currentChapter && el.dataset.chapterId === this.currentChapter.id
288
+ el.style.background = isActive ? this.config.activeMarkerColor : this.config.markerColor
289
+ })
290
+ }
291
+
292
+ /**
293
+ * Create chapter title element
294
+ */
295
+ createTitleElement(): HTMLElement {
296
+ this.titleElement = document.createElement('div')
297
+ this.titleElement.className = 'tsvp-chapter-title'
298
+ this.titleElement.style.cssText = `
299
+ position: absolute;
300
+ bottom: 100%;
301
+ left: 50%;
302
+ transform: translateX(-50%);
303
+ margin-bottom: 8px;
304
+ padding: 6px 12px;
305
+ background: var(--tsvp-controls-tooltip-bg, rgba(0, 0, 0, 0.9));
306
+ color: var(--tsvp-controls-tooltip-text, #ffffff);
307
+ font-size: var(--tsvp-font-size-sm, 12px);
308
+ font-family: var(--tsvp-font-family, sans-serif);
309
+ border-radius: var(--tsvp-radius-sm, 4px);
310
+ white-space: nowrap;
311
+ opacity: 0;
312
+ transition: opacity var(--tsvp-transition-normal, 200ms);
313
+ pointer-events: none;
314
+ z-index: 10;
315
+ `
316
+
317
+ return this.titleElement
318
+ }
319
+
320
+ /**
321
+ * Show chapter title
322
+ */
323
+ showTitle(chapter: Chapter): void {
324
+ if (this.titleElement && this.config.showTitles) {
325
+ this.titleElement.textContent = chapter.title
326
+ this.titleElement.style.opacity = '1'
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Hide chapter title
332
+ */
333
+ hideTitle(): void {
334
+ if (this.titleElement) {
335
+ this.titleElement.style.opacity = '0'
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Get chapter at position (for hover preview)
341
+ */
342
+ getChapterAtPosition(percent: number): Chapter | null {
343
+ const time = (percent / 100) * this.duration
344
+ return this.getChapterAtTime(time)
345
+ }
346
+
347
+ /**
348
+ * Add chapters
349
+ */
350
+ addChapters(chapters: Chapter[]): void {
351
+ this.chapters.push(...chapters)
352
+ this.renderMarkers()
353
+ }
354
+
355
+ /**
356
+ * Clear all chapters
357
+ */
358
+ clear(): void {
359
+ this.chapters = []
360
+ this.currentChapter = null
361
+ if (this.markersContainer) {
362
+ this.markersContainer.innerHTML = ''
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Destroy
368
+ */
369
+ destroy(): void {
370
+ this.clear()
371
+ this.markersContainer?.remove()
372
+ this.titleElement?.remove()
373
+ this.markersContainer = null
374
+ this.titleElement = null
375
+ }
376
+ }
377
+
378
+ // =============================================================================
379
+ // Chapter Menu Component
380
+ // =============================================================================
381
+
382
+ export class ChapterMenu {
383
+ private container: HTMLElement
384
+ private chapters: Chapter[] = []
385
+ private onSelect: ((_chapter: Chapter) => void) | null = null
386
+
387
+ constructor() {
388
+ this.container = document.createElement('div')
389
+ this.container.className = 'tsvp-chapter-menu'
390
+ this.applyStyles()
391
+ }
392
+
393
+ private applyStyles(): void {
394
+ this.container.style.cssText = `
395
+ max-height: 300px;
396
+ overflow-y: auto;
397
+ background: var(--tsvp-menu-bg, rgba(20, 20, 20, 0.95));
398
+ border-radius: var(--tsvp-menu-radius, 8px);
399
+ box-shadow: var(--tsvp-menu-shadow, 0 4px 20px rgba(0, 0, 0, 0.5));
400
+ `
401
+ }
402
+
403
+ /**
404
+ * Set chapters
405
+ */
406
+ setChapters(chapters: Chapter[]): void {
407
+ this.chapters = chapters
408
+ this.render()
409
+ }
410
+
411
+ /**
412
+ * Render chapter list
413
+ */
414
+ private render(): void {
415
+ this.container.innerHTML = ''
416
+
417
+ for (const chapter of this.chapters) {
418
+ const item = document.createElement('button')
419
+ item.className = 'tsvp-chapter-menu-item'
420
+ item.type = 'button'
421
+
422
+ item.style.cssText = `
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 12px;
426
+ width: 100%;
427
+ padding: 10px 16px;
428
+ border: none;
429
+ background: transparent;
430
+ color: var(--tsvp-color-text, #ffffff);
431
+ font-size: var(--tsvp-font-size, 14px);
432
+ font-family: var(--tsvp-font-family, sans-serif);
433
+ text-align: left;
434
+ cursor: pointer;
435
+ transition: background var(--tsvp-transition-fast, 100ms);
436
+ `
437
+
438
+ // Thumbnail
439
+ if (chapter.thumbnail) {
440
+ const thumb = document.createElement('img')
441
+ thumb.src = chapter.thumbnail
442
+ thumb.style.cssText = `
443
+ width: 80px;
444
+ height: 45px;
445
+ object-fit: cover;
446
+ border-radius: 4px;
447
+ `
448
+ item.appendChild(thumb)
449
+ }
450
+
451
+ // Info
452
+ const info = document.createElement('div')
453
+ info.style.cssText = 'flex: 1;'
454
+
455
+ const title = document.createElement('div')
456
+ title.textContent = chapter.title
457
+ title.style.fontWeight = 'var(--tsvp-font-weight-bold, 600)'
458
+ info.appendChild(title)
459
+
460
+ const time = document.createElement('div')
461
+ time.textContent = formatTime(chapter.startTime)
462
+ time.style.cssText = `
463
+ font-size: var(--tsvp-font-size-sm, 12px);
464
+ opacity: 0.7;
465
+ margin-top: 2px;
466
+ `
467
+ info.appendChild(time)
468
+
469
+ item.appendChild(info)
470
+
471
+ // Hover effect
472
+ item.addEventListener('mouseenter', () => {
473
+ item.style.background = 'var(--tsvp-menu-item-hover-bg, rgba(255, 255, 255, 0.1))'
474
+ })
475
+ item.addEventListener('mouseleave', () => {
476
+ item.style.background = 'transparent'
477
+ })
478
+
479
+ // Click handler
480
+ item.addEventListener('click', () => {
481
+ if (this.onSelect) {
482
+ this.onSelect(chapter)
483
+ }
484
+ })
485
+
486
+ this.container.appendChild(item)
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Set select handler
492
+ */
493
+ onChapterSelect(handler: (chapter: Chapter) => void): void {
494
+ this.onSelect = handler
495
+ }
496
+
497
+ /**
498
+ * Get element
499
+ */
500
+ getElement(): HTMLElement {
501
+ return this.container
502
+ }
503
+
504
+ /**
505
+ * Highlight current chapter
506
+ */
507
+ setActiveChapter(id: string): void {
508
+ const items = this.container.querySelectorAll('.tsvp-chapter-menu-item')
509
+ items.forEach((item, index) => {
510
+ const el = item as HTMLElement
511
+ const isActive = this.chapters[index]?.id === id
512
+ el.style.background = isActive
513
+ ? 'var(--tsvp-menu-item-active-bg, rgba(0, 168, 255, 0.2))'
514
+ : 'transparent'
515
+ })
516
+ }
517
+
518
+ /**
519
+ * Destroy
520
+ */
521
+ destroy(): void {
522
+ this.container.remove()
523
+ }
524
+ }
525
+
526
+ // =============================================================================
527
+ // Factory
528
+ // =============================================================================
529
+
530
+ export function createChaptersManager(config?: ChaptersConfig): ChaptersManager {
531
+ return new ChaptersManager(config)
532
+ }
533
+
534
+ export function createChapterMenu(): ChapterMenu {
535
+ return new ChapterMenu()
536
+ }
537
+
538
+ // =============================================================================
539
+ // Helpers
540
+ // =============================================================================
541
+
542
+ function formatTime(seconds: number): string {
543
+ const h = Math.floor(seconds / 3600)
544
+ const m = Math.floor((seconds % 3600) / 60)
545
+ const s = Math.floor(seconds % 60)
546
+
547
+ if (h > 0) {
548
+ return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
549
+ }
550
+ return `${m}:${s.toString().padStart(2, '0')}`
551
+ }