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,599 @@
1
+ /**
2
+ * ts-video-player Text Tracks
3
+ *
4
+ * Comprehensive text track management for captions and subtitles.
5
+ * Supports VTT, SRT, and external track loading.
6
+ *
7
+ * @module core/text-tracks
8
+ */
9
+
10
+ import type { TextTrack, TextTrackCue } from '../types'
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Text track ready state
18
+ */
19
+ export enum TextTrackReadyState {
20
+ /** Not loaded */
21
+ NotLoaded = 0,
22
+ /** Loading */
23
+ Loading = 1,
24
+ /** Ready */
25
+ Ready = 2,
26
+ /** Error */
27
+ Error = 3,
28
+ }
29
+
30
+ /**
31
+ * Text track init options
32
+ */
33
+ export interface TextTrackInit {
34
+ /** Track kind */
35
+ kind: TextTrack['kind']
36
+ /** Track label */
37
+ label: string
38
+ /** Track language (BCP 47) */
39
+ language: string
40
+ /** Track source URL */
41
+ src?: string
42
+ /** Whether this is the default track */
43
+ default?: boolean
44
+ /** Track content (VTT/SRT string) */
45
+ content?: string
46
+ }
47
+
48
+ /**
49
+ * VTT cue init options
50
+ */
51
+ export interface VTTCueInit {
52
+ /** Cue ID */
53
+ id?: string
54
+ /** Start time in seconds */
55
+ startTime: number
56
+ /** End time in seconds */
57
+ endTime: number
58
+ /** Cue text */
59
+ text: string
60
+ /** Vertical setting */
61
+ vertical?: '' | 'rl' | 'lr'
62
+ /** Line position */
63
+ line?: number | 'auto'
64
+ /** Line alignment */
65
+ lineAlign?: 'start' | 'center' | 'end'
66
+ /** Position */
67
+ position?: number | 'auto'
68
+ /** Position alignment */
69
+ positionAlign?: 'line-left' | 'center' | 'line-right' | 'auto'
70
+ /** Size (percentage) */
71
+ size?: number
72
+ /** Alignment */
73
+ align?: 'start' | 'center' | 'end' | 'left' | 'right'
74
+ }
75
+
76
+ // =============================================================================
77
+ // Text Track List
78
+ // =============================================================================
79
+
80
+ /**
81
+ * Manages a list of text tracks
82
+ */
83
+ export class TextTrackList {
84
+ private tracks: ManagedTextTrack[] = []
85
+ private listeners = new Set<(tracks: TextTrack[]) => void>()
86
+ private video: HTMLVideoElement | null = null
87
+
88
+ constructor(video?: HTMLVideoElement) {
89
+ this.video = video || null
90
+ }
91
+
92
+ /**
93
+ * Get all tracks
94
+ */
95
+ getAll(): TextTrack[] {
96
+ return this.tracks.map((t) => t.toJSON())
97
+ }
98
+
99
+ /**
100
+ * Get track by ID
101
+ */
102
+ get(id: string): TextTrack | null {
103
+ const track = this.tracks.find((t) => t.id === id)
104
+ return track?.toJSON() || null
105
+ }
106
+
107
+ /**
108
+ * Get currently showing track
109
+ */
110
+ getShowing(): TextTrack | null {
111
+ const track = this.tracks.find((t) => t.mode === 'showing')
112
+ return track?.toJSON() || null
113
+ }
114
+
115
+ /**
116
+ * Add a new text track
117
+ */
118
+ async add(init: TextTrackInit): Promise<TextTrack> {
119
+ const id = `track-${this.tracks.length}`
120
+ const track = new ManagedTextTrack({
121
+ id,
122
+ ...init,
123
+ })
124
+
125
+ this.tracks.push(track)
126
+
127
+ // Load track if src provided
128
+ if (init.src) {
129
+ await track.load(init.src)
130
+ } else if (init.content) {
131
+ track.parseContent(init.content)
132
+ }
133
+
134
+ // Add to video element if available
135
+ if (this.video) {
136
+ this.addToVideo(track)
137
+ }
138
+
139
+ this.notify()
140
+ return track.toJSON()
141
+ }
142
+
143
+ /**
144
+ * Remove a text track
145
+ */
146
+ remove(id: string): void {
147
+ const index = this.tracks.findIndex((t) => t.id === id)
148
+ if (index !== -1) {
149
+ const track = this.tracks[index]
150
+ track.dispose()
151
+ this.tracks.splice(index, 1)
152
+ this.notify()
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Set track mode
158
+ */
159
+ setMode(id: string, mode: TextTrack['mode']): void {
160
+ const track = this.tracks.find((t) => t.id === id)
161
+ if (!track) return
162
+
163
+ // Disable other showing tracks if enabling this one
164
+ if (mode === 'showing') {
165
+ for (const t of this.tracks) {
166
+ if (t.id !== id && t.mode === 'showing') {
167
+ t.mode = 'disabled'
168
+ }
169
+ }
170
+ }
171
+
172
+ track.mode = mode
173
+ this.notify()
174
+ }
175
+
176
+ /**
177
+ * Get active cues at a specific time
178
+ */
179
+ getActiveCues(time: number): TextTrackCue[] {
180
+ const showing = this.tracks.find((t) => t.mode === 'showing')
181
+ if (!showing) return []
182
+
183
+ return showing.cues.filter((cue) => time >= cue.startTime && time <= cue.endTime)
184
+ }
185
+
186
+ /**
187
+ * Subscribe to track changes
188
+ */
189
+ subscribe(listener: (tracks: TextTrack[]) => void): () => void {
190
+ this.listeners.add(listener)
191
+ return () => this.listeners.delete(listener)
192
+ }
193
+
194
+ /**
195
+ * Clear all tracks
196
+ */
197
+ clear(): void {
198
+ for (const track of this.tracks) {
199
+ track.dispose()
200
+ }
201
+ this.tracks = []
202
+ this.notify()
203
+ }
204
+
205
+ private addToVideo(track: ManagedTextTrack): void {
206
+ if (!this.video) return
207
+
208
+ // Create native track element
209
+ const trackEl = document.createElement('track')
210
+ trackEl.kind = track.kind
211
+ trackEl.label = track.label
212
+ trackEl.srclang = track.language
213
+ if (track.src) trackEl.src = track.src
214
+ if (track.default) trackEl.default = true
215
+
216
+ this.video.appendChild(trackEl)
217
+ }
218
+
219
+ private notify(): void {
220
+ const tracks = this.getAll()
221
+ this.listeners.forEach((l) => l(tracks))
222
+ }
223
+ }
224
+
225
+ // =============================================================================
226
+ // Managed Text Track
227
+ // =============================================================================
228
+
229
+ class ManagedTextTrack {
230
+ readonly id: string
231
+ readonly kind: TextTrack['kind']
232
+ readonly label: string
233
+ readonly language: string
234
+ readonly src?: string
235
+ readonly default: boolean
236
+
237
+ private _mode: TextTrack['mode'] = 'disabled'
238
+ private _cues: TextTrackCue[] = []
239
+ private _readyState: TextTrackReadyState = TextTrackReadyState.NotLoaded
240
+
241
+ constructor(init: TextTrackInit & { id: string }) {
242
+ this.id = init.id
243
+ this.kind = init.kind
244
+ this.label = init.label
245
+ this.language = init.language
246
+ this.src = init.src
247
+ this.default = init.default || false
248
+
249
+ if (init.default) {
250
+ this._mode = 'showing'
251
+ }
252
+ }
253
+
254
+ get mode(): TextTrack['mode'] {
255
+ return this._mode
256
+ }
257
+
258
+ set mode(value: TextTrack['mode']) {
259
+ this._mode = value
260
+ }
261
+
262
+ get cues(): TextTrackCue[] {
263
+ return this._cues
264
+ }
265
+
266
+ get readyState(): TextTrackReadyState {
267
+ return this._readyState
268
+ }
269
+
270
+ /**
271
+ * Load track from URL
272
+ */
273
+ async load(url: string): Promise<void> {
274
+ this._readyState = TextTrackReadyState.Loading
275
+
276
+ try {
277
+ const response = await fetch(url)
278
+ if (!response.ok) {
279
+ throw new Error(`Failed to load track: ${response.status}`)
280
+ }
281
+
282
+ const content = await response.text()
283
+ this.parseContent(content)
284
+ this._readyState = TextTrackReadyState.Ready
285
+ } catch (error) {
286
+ this._readyState = TextTrackReadyState.Error
287
+ console.warn('[ts-video-player] Failed to load text track:', error)
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Parse track content (VTT or SRT)
293
+ */
294
+ parseContent(content: string): void {
295
+ // Detect format
296
+ if (content.trim().startsWith('WEBVTT')) {
297
+ this._cues = parseVTT(content)
298
+ } else {
299
+ this._cues = parseSRT(content)
300
+ }
301
+ this._readyState = TextTrackReadyState.Ready
302
+ }
303
+
304
+ /**
305
+ * Add a cue
306
+ */
307
+ addCue(cue: VTTCueInit): void {
308
+ this._cues.push({
309
+ id: cue.id || `cue-${this._cues.length}`,
310
+ startTime: cue.startTime,
311
+ endTime: cue.endTime,
312
+ text: cue.text,
313
+ position: cue.position,
314
+ line: cue.line,
315
+ align: cue.align,
316
+ })
317
+ }
318
+
319
+ /**
320
+ * Remove a cue
321
+ */
322
+ removeCue(id: string): void {
323
+ const index = this._cues.findIndex((c) => c.id === id)
324
+ if (index !== -1) {
325
+ this._cues.splice(index, 1)
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Convert to plain object
331
+ */
332
+ toJSON(): TextTrack {
333
+ return {
334
+ id: this.id,
335
+ kind: this.kind,
336
+ label: this.label,
337
+ language: this.language,
338
+ src: this.src,
339
+ default: this.default,
340
+ mode: this._mode,
341
+ cues: this._cues,
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Dispose the track
347
+ */
348
+ dispose(): void {
349
+ this._cues = []
350
+ }
351
+ }
352
+
353
+ // =============================================================================
354
+ // VTT Parser
355
+ // =============================================================================
356
+
357
+ /**
358
+ * Parse VTT content
359
+ */
360
+ function parseVTT(content: string): TextTrackCue[] {
361
+ const cues: TextTrackCue[] = []
362
+ const lines = content.split(/\r?\n/)
363
+
364
+ let i = 0
365
+
366
+ // Skip header
367
+ while (i < lines.length && !lines[i].includes('-->')) {
368
+ i++
369
+ }
370
+
371
+ while (i < lines.length) {
372
+ const line = lines[i].trim()
373
+
374
+ // Check for cue timing line
375
+ if (line.includes('-->')) {
376
+ const [start, end] = line.split('-->').map((t) => parseVTTTime(t.trim()))
377
+
378
+ // Collect cue text
379
+ i++
380
+ const textLines: string[] = []
381
+ while (i < lines.length && lines[i].trim() !== '') {
382
+ textLines.push(lines[i])
383
+ i++
384
+ }
385
+
386
+ cues.push({
387
+ id: `cue-${cues.length}`,
388
+ startTime: start,
389
+ endTime: end,
390
+ text: textLines.join('\n'),
391
+ })
392
+ }
393
+
394
+ i++
395
+ }
396
+
397
+ return cues
398
+ }
399
+
400
+ /**
401
+ * Parse VTT time format (00:00:00.000 or 00:00.000)
402
+ */
403
+ function parseVTTTime(time: string): number {
404
+ const parts = time.split(':')
405
+ let seconds = 0
406
+
407
+ if (parts.length === 3) {
408
+ // HH:MM:SS.mmm
409
+ seconds = parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + parseFloat(parts[2])
410
+ } else if (parts.length === 2) {
411
+ // MM:SS.mmm
412
+ seconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1])
413
+ }
414
+
415
+ return seconds
416
+ }
417
+
418
+ // =============================================================================
419
+ // SRT Parser
420
+ // =============================================================================
421
+
422
+ /**
423
+ * Parse SRT content
424
+ */
425
+ function parseSRT(content: string): TextTrackCue[] {
426
+ const cues: TextTrackCue[] = []
427
+ const blocks = content.trim().split(/\r?\n\r?\n/)
428
+
429
+ for (const block of blocks) {
430
+ const lines = block.split(/\r?\n/)
431
+ if (lines.length < 2) continue
432
+
433
+ // Find timing line
434
+ let timingIndex = 0
435
+ while (timingIndex < lines.length && !lines[timingIndex].includes('-->')) {
436
+ timingIndex++
437
+ }
438
+
439
+ if (timingIndex >= lines.length) continue
440
+
441
+ const timingLine = lines[timingIndex]
442
+ const [start, end] = timingLine.split('-->').map((t) => parseSRTTime(t.trim()))
443
+
444
+ // Collect text
445
+ const textLines = lines.slice(timingIndex + 1)
446
+
447
+ cues.push({
448
+ id: lines[0] || `cue-${cues.length}`,
449
+ startTime: start,
450
+ endTime: end,
451
+ text: textLines.join('\n'),
452
+ })
453
+ }
454
+
455
+ return cues
456
+ }
457
+
458
+ /**
459
+ * Parse SRT time format (00:00:00,000)
460
+ */
461
+ function parseSRTTime(time: string): number {
462
+ // SRT uses comma for milliseconds
463
+ const normalized = time.replace(',', '.')
464
+ return parseVTTTime(normalized)
465
+ }
466
+
467
+ // =============================================================================
468
+ // Caption Renderer
469
+ // =============================================================================
470
+
471
+ export interface CaptionRendererOptions {
472
+ /** Container element */
473
+ container: HTMLElement
474
+ /** Font size */
475
+ fontSize?: number
476
+ /** Font family */
477
+ fontFamily?: string
478
+ /** Text color */
479
+ color?: string
480
+ /** Background color */
481
+ backgroundColor?: string
482
+ /** Background opacity */
483
+ backgroundOpacity?: number
484
+ /** Text shadow */
485
+ textShadow?: boolean
486
+ }
487
+
488
+ /**
489
+ * Render captions on screen
490
+ */
491
+ export class CaptionRenderer {
492
+ private container: HTMLElement
493
+ private element: HTMLElement | null = null
494
+ private options: Required<Omit<CaptionRendererOptions, 'container'>>
495
+
496
+ constructor(options: CaptionRendererOptions) {
497
+ this.container = options.container
498
+ this.options = {
499
+ fontSize: options.fontSize || 18,
500
+ fontFamily: options.fontFamily || 'sans-serif',
501
+ color: options.color || '#ffffff',
502
+ backgroundColor: options.backgroundColor || '#000000',
503
+ backgroundOpacity: options.backgroundOpacity || 0.75,
504
+ textShadow: options.textShadow !== false,
505
+ }
506
+
507
+ this.createCaptionElement()
508
+ }
509
+
510
+ private createCaptionElement(): void {
511
+ this.element = document.createElement('div')
512
+ this.element.className = 'ts-video-player__captions'
513
+ this.element.setAttribute('aria-live', 'polite')
514
+ this.element.setAttribute('aria-atomic', 'true')
515
+ this.applyStyles()
516
+ this.container.appendChild(this.element)
517
+ }
518
+
519
+ private applyStyles(): void {
520
+ if (!this.element) return
521
+
522
+ const bgColor = this.hexToRgba(this.options.backgroundColor, this.options.backgroundOpacity)
523
+
524
+ Object.assign(this.element.style, {
525
+ position: 'absolute',
526
+ bottom: '10%',
527
+ left: '50%',
528
+ transform: 'translateX(-50%)',
529
+ maxWidth: '80%',
530
+ textAlign: 'center',
531
+ fontSize: `${this.options.fontSize}px`,
532
+ fontFamily: this.options.fontFamily,
533
+ color: this.options.color,
534
+ backgroundColor: bgColor,
535
+ padding: '4px 8px',
536
+ borderRadius: '4px',
537
+ lineHeight: '1.4',
538
+ pointerEvents: 'none',
539
+ zIndex: '10',
540
+ whiteSpace: 'pre-wrap',
541
+ textShadow: this.options.textShadow ? '0 1px 2px rgba(0,0,0,0.5)' : 'none',
542
+ })
543
+ }
544
+
545
+ /**
546
+ * Update the displayed text
547
+ */
548
+ setText(text: string): void {
549
+ if (!this.element) return
550
+
551
+ if (text) {
552
+ this.element.textContent = text
553
+ this.element.style.display = 'block'
554
+ } else {
555
+ this.element.style.display = 'none'
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Update renderer options
561
+ */
562
+ setOptions(options: Partial<Omit<CaptionRendererOptions, 'container'>>): void {
563
+ Object.assign(this.options, options)
564
+ this.applyStyles()
565
+ }
566
+
567
+ /**
568
+ * Destroy the renderer
569
+ */
570
+ destroy(): void {
571
+ this.element?.remove()
572
+ this.element = null
573
+ }
574
+
575
+ private hexToRgba(hex: string, alpha: number): string {
576
+ const r = parseInt(hex.slice(1, 3), 16)
577
+ const g = parseInt(hex.slice(3, 5), 16)
578
+ const b = parseInt(hex.slice(5, 7), 16)
579
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`
580
+ }
581
+ }
582
+
583
+ // =============================================================================
584
+ // Factory Functions
585
+ // =============================================================================
586
+
587
+ /**
588
+ * Create a new text track list
589
+ */
590
+ export function createTextTrackList(video?: HTMLVideoElement): TextTrackList {
591
+ return new TextTrackList(video)
592
+ }
593
+
594
+ /**
595
+ * Create a caption renderer
596
+ */
597
+ export function createCaptionRenderer(options: CaptionRendererOptions): CaptionRenderer {
598
+ return new CaptionRenderer(options)
599
+ }