@therealtinhtute/baroiplayer 1.0.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 (244) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/context/media.js +2 -0
  4. package/dist/context/media.js.map +1 -0
  5. package/dist/context/media.mjs +15 -0
  6. package/dist/context/media.mjs.map +1 -0
  7. package/dist/context/store.js +2 -0
  8. package/dist/context/store.js.map +1 -0
  9. package/dist/context/store.mjs +5 -0
  10. package/dist/context/store.mjs.map +1 -0
  11. package/dist/context-BMteAJDN.js +1140 -0
  12. package/dist/context-BMteAJDN.js.map +1 -0
  13. package/dist/context-DCJ_scmO.cjs +23 -0
  14. package/dist/context-DCJ_scmO.cjs.map +1 -0
  15. package/dist/core-events-B5W_j8yU.js +480 -0
  16. package/dist/core-events-B5W_j8yU.js.map +1 -0
  17. package/dist/core-events-CN0-Vhhq.cjs +2 -0
  18. package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
  19. package/dist/core-plugins-Bq9KLany.js +696 -0
  20. package/dist/core-plugins-Bq9KLany.js.map +1 -0
  21. package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
  22. package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
  23. package/dist/core.js +37 -0
  24. package/dist/core.js.map +1 -0
  25. package/dist/core.mjs +1217 -0
  26. package/dist/core.mjs.map +1 -0
  27. package/dist/enhanced-components-DHv0MFvG.js +1073 -0
  28. package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
  29. package/dist/enhanced-components-DTIObgB7.cjs +2 -0
  30. package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
  31. package/dist/hooks-C_r8wexn.cjs +4 -0
  32. package/dist/hooks-C_r8wexn.cjs.map +1 -0
  33. package/dist/hooks-Da2wVXlF.js +1630 -0
  34. package/dist/hooks-Da2wVXlF.js.map +1 -0
  35. package/dist/index.css +1 -0
  36. package/dist/index.js +112 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +6126 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/minimal-components-DSq5sQpy.js +2002 -0
  41. package/dist/minimal-components-DSq5sQpy.js.map +1 -0
  42. package/dist/minimal-components-DflWiihN.cjs +2 -0
  43. package/dist/minimal-components-DflWiihN.cjs.map +1 -0
  44. package/dist/minimal-components.css +1 -0
  45. package/dist/mona-loading.gif +0 -0
  46. package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
  47. package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
  48. package/dist/performance-monitor-rMW1RgV3.js +302 -0
  49. package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
  50. package/dist/utils-BnscpYYd.js +2798 -0
  51. package/dist/utils-BnscpYYd.js.map +1 -0
  52. package/dist/utils-D_czS_0K.cjs +9 -0
  53. package/dist/utils-D_czS_0K.cjs.map +1 -0
  54. package/dist/utils.js +2 -0
  55. package/dist/utils.js.map +1 -0
  56. package/dist/utils.mjs +7 -0
  57. package/dist/utils.mjs.map +1 -0
  58. package/package.json +114 -0
  59. package/src/adapters/videojs-adapter.tsx +470 -0
  60. package/src/assets/mona-loading.gif +0 -0
  61. package/src/components/AnimateLoading.tsx +115 -0
  62. package/src/components/LoadingState.tsx +248 -0
  63. package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
  64. package/src/components/audio-player.tsx +85 -0
  65. package/src/components/basic/index.ts +37 -0
  66. package/src/components/enhanced/advanced-features.tsx +316 -0
  67. package/src/components/enhanced/debug-panel.tsx +422 -0
  68. package/src/components/enhanced/enhanced-controls.tsx +600 -0
  69. package/src/components/enhanced/enhanced-media-player.tsx +509 -0
  70. package/src/components/enhanced/error-handling.tsx +345 -0
  71. package/src/components/enhanced/index.ts +39 -0
  72. package/src/components/enhanced/media-player-enhanced.tsx +349 -0
  73. package/src/components/examples/basic-video-player.tsx +55 -0
  74. package/src/components/examples/custom-controls-example.tsx +63 -0
  75. package/src/components/examples/enhanced-controls-example.tsx +114 -0
  76. package/src/components/examples/enhanced-media-player-example.tsx +293 -0
  77. package/src/components/examples/loading-states-demo.tsx +142 -0
  78. package/src/components/examples/react-19-demo.tsx +381 -0
  79. package/src/components/examples/volume-orientation-demo.tsx +114 -0
  80. package/src/components/icon-demo.tsx +209 -0
  81. package/src/components/icons/index.tsx +166 -0
  82. package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
  83. package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
  84. package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
  85. package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
  86. package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
  87. package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
  88. package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
  89. package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
  90. package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
  91. package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
  92. package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
  93. package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
  94. package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
  95. package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
  96. package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
  97. package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
  98. package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
  99. package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
  100. package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
  101. package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
  102. package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
  103. package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
  104. package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
  105. package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
  106. package/src/components/legacy.tsx +93 -0
  107. package/src/components/media-metadata-display.tsx +100 -0
  108. package/src/components/media-player-audio.tsx +39 -0
  109. package/src/components/media-player-controls.tsx +226 -0
  110. package/src/components/media-player-error.tsx +163 -0
  111. package/src/components/media-player-fullscreen.tsx +109 -0
  112. package/src/components/media-player-hls.tsx +111 -0
  113. package/src/components/media-player-loading.tsx +76 -0
  114. package/src/components/media-player-play.tsx +75 -0
  115. package/src/components/media-player-root.tsx +340 -0
  116. package/src/components/media-player-seek.tsx +168 -0
  117. package/src/components/media-player-time.tsx +99 -0
  118. package/src/components/media-player-tooltip.tsx +58 -0
  119. package/src/components/media-player-video.tsx +39 -0
  120. package/src/components/media-player-volume.tsx +155 -0
  121. package/src/components/media-player.tsx +257 -0
  122. package/src/components/media-suspense.tsx +362 -0
  123. package/src/components/player-settings-form.tsx +163 -0
  124. package/src/components/react-player-wrapper.tsx +651 -0
  125. package/src/components/simple-video-player.tsx +60 -0
  126. package/src/components/ui/enhanced-seek-bar.tsx +235 -0
  127. package/src/components/ui/fullscreen-button.tsx +60 -0
  128. package/src/components/ui/media-resource-preloader.tsx +395 -0
  129. package/src/components/ui/play-button.tsx +107 -0
  130. package/src/components/ui/seek-bar.tsx +161 -0
  131. package/src/components/ui/time-display.tsx +40 -0
  132. package/src/components/ui/volume-control.tsx +143 -0
  133. package/src/components/unified/base-media-player.tsx +246 -0
  134. package/src/components/unified/media-player.tsx +75 -0
  135. package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
  136. package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
  137. package/src/components/variants/minimal/MinimalControls.tsx +142 -0
  138. package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
  139. package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
  140. package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
  141. package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
  142. package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
  143. package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
  144. package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
  145. package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
  146. package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
  147. package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
  148. package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
  149. package/src/components/variants/minimal/components/RightControls.tsx +173 -0
  150. package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
  151. package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
  152. package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
  153. package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
  154. package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
  155. package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
  156. package/src/components/variants/minimal/index.ts +23 -0
  157. package/src/components/video-player.tsx +100 -0
  158. package/src/context/error-handling.ts +394 -0
  159. package/src/context/media-context.tsx +208 -0
  160. package/src/context/media-provider.tsx +265 -0
  161. package/src/context/media-store.ts +379 -0
  162. package/src/context/persistence.ts +453 -0
  163. package/src/core/events/event-system.ts +442 -0
  164. package/src/core/events/hooks.ts +302 -0
  165. package/src/core/events/index.ts +22 -0
  166. package/src/core/events/middleware.ts +269 -0
  167. package/src/core/index.ts +22 -0
  168. package/src/core/optimization/bundle-optimizer.tsx +308 -0
  169. package/src/core/performance/cicd.ts +96 -0
  170. package/src/core/performance/index.ts +38 -0
  171. package/src/core/performance/media-loading-optimizer.ts +538 -0
  172. package/src/core/performance/performance-budgets.ts +432 -0
  173. package/src/core/performance/performance-monitor.tsx +476 -0
  174. package/src/core/performance/performance-testing.ts +459 -0
  175. package/src/core/plugins/base-plugin.ts +224 -0
  176. package/src/core/plugins/enhanced-plugin-system.ts +411 -0
  177. package/src/core/plugins/examples/analytics-plugin.ts +242 -0
  178. package/src/core/plugins/index.ts +39 -0
  179. package/src/core/plugins/registry.ts +327 -0
  180. package/src/core/plugins/types.ts +159 -0
  181. package/src/core/plugins/utils.ts +316 -0
  182. package/src/core/progressive-loading.tsx +322 -0
  183. package/src/core/providers/enhanced-hls-provider.ts +339 -0
  184. package/src/core/providers/service-provider.tsx +168 -0
  185. package/src/core/utils.tsx +38 -0
  186. package/src/features/media-session/media-session-manager.ts +429 -0
  187. package/src/features/picture-in-picture/pip-manager.ts +429 -0
  188. package/src/features/playlist/playlist-manager.ts +606 -0
  189. package/src/features/subtitles/subtitle-manager.ts +441 -0
  190. package/src/features/subtitles/subtitle-parser.ts +280 -0
  191. package/src/features/thumbnails/thumbnail-generator.ts +519 -0
  192. package/src/hooks/accessibility/index.ts +27 -0
  193. package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
  194. package/src/hooks/accessibility/use-focus-management.ts +176 -0
  195. package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
  196. package/src/hooks/accessibility/use-screen-reader.ts +182 -0
  197. package/src/hooks/core/index.ts +47 -0
  198. package/src/hooks/legacy.ts +27 -0
  199. package/src/hooks/use-enhanced-media-player.ts +468 -0
  200. package/src/hooks/use-intelligent-preloading.ts +358 -0
  201. package/src/hooks/use-keyboard-controls.ts +115 -0
  202. package/src/hooks/use-media-actions.ts +175 -0
  203. package/src/hooks/use-media-metadata.ts +89 -0
  204. package/src/hooks/use-media-optimistic.ts +270 -0
  205. package/src/hooks/use-media-player.ts +312 -0
  206. package/src/hooks/use-media-state.ts +190 -0
  207. package/src/hooks/use-memory-optimization.ts +358 -0
  208. package/src/hooks/use-optimistic-controls.ts +92 -0
  209. package/src/hooks/use-optimized-media.ts +263 -0
  210. package/src/hooks/use-touch-gestures.ts +417 -0
  211. package/src/hooks/useEnhancedSubtitles.ts +258 -0
  212. package/src/hooks/useKeyboardShortcuts.ts +74 -0
  213. package/src/hooks/useMediaControls.ts +52 -0
  214. package/src/hooks/useScreenshot.ts +57 -0
  215. package/src/hooks/useSubtitles.ts +91 -0
  216. package/src/hooks/useVideoPlayer.ts +232 -0
  217. package/src/icons/components/FullscreenIcon.tsx +37 -0
  218. package/src/icons/components/SettingsIcon.tsx +38 -0
  219. package/src/icons/components/animated/LoadingIcon.tsx +43 -0
  220. package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
  221. package/src/icons/components/media/PauseIcon.tsx +39 -0
  222. package/src/icons/components/media/PlayIcon.tsx +38 -0
  223. package/src/icons/components/volume/VolumeIcon.tsx +39 -0
  224. package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
  225. package/src/icons/index.ts +56 -0
  226. package/src/icons/provider.tsx +64 -0
  227. package/src/icons/types.ts +40 -0
  228. package/src/index-enhanced.ts +35 -0
  229. package/src/index-legacy.ts +326 -0
  230. package/src/index.css +102 -0
  231. package/src/index.ts +216 -0
  232. package/src/lib/deprecation.ts +51 -0
  233. package/src/lib/format-time.ts +37 -0
  234. package/src/lib/index.ts +3 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/globals.css +462 -0
  237. package/src/styles/minimal-player.css +589 -0
  238. package/src/styles/pixel-art.css +46 -0
  239. package/src/styles/videojs.css +100 -0
  240. package/src/test/setup.ts +79 -0
  241. package/src/types/events.ts +47 -0
  242. package/src/types/formats.ts +95 -0
  243. package/src/types/index.ts +190 -0
  244. package/src/types/videojs-plugins.d.ts +14 -0
@@ -0,0 +1,441 @@
1
+ import { SubtitleParser, type SubtitleCue, type ParsedSubtitles } from './subtitle-parser'
2
+
3
+ export interface SubtitleTrack {
4
+ id: string
5
+ url: string
6
+ label: string
7
+ language: string
8
+ isDefault?: boolean
9
+ format?: 'vtt' | 'srt'
10
+ parsedSubtitles?: ParsedSubtitles
11
+ loadingState?: 'idle' | 'loading' | 'loaded' | 'error'
12
+ error?: string
13
+ }
14
+
15
+ export interface SubtitleStyle {
16
+ fontSize?: number
17
+ fontFamily?: string
18
+ color?: string
19
+ backgroundColor?: string
20
+ borderColor?: string
21
+ borderWidth?: number
22
+ opacity?: number
23
+ position?: 'top' | 'center' | 'bottom'
24
+ alignment?: 'left' | 'center' | 'right'
25
+ shadow?: boolean
26
+ }
27
+
28
+ export interface SubtitleManagerOptions {
29
+ tracks?: SubtitleTrack[]
30
+ defaultStyle?: SubtitleStyle
31
+ enabledByDefault?: boolean
32
+ preloadTracks?: boolean
33
+ }
34
+
35
+ export class SubtitleManager {
36
+ private tracks: Map<string, SubtitleTrack> = new Map()
37
+ private activeTrackId: string | null = null
38
+ private currentCues: SubtitleCue[] = []
39
+ private isEnabled: boolean = false
40
+ private style: SubtitleStyle
41
+ private eventListeners: Map<string, Set<Function>> = new Map()
42
+
43
+ constructor(options: SubtitleManagerOptions = {}) {
44
+ this.style = {
45
+ fontSize: 16,
46
+ fontFamily: 'Arial, sans-serif',
47
+ color: '#ffffff',
48
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
49
+ borderColor: '#000000',
50
+ borderWidth: 1,
51
+ opacity: 1,
52
+ position: 'bottom',
53
+ alignment: 'center',
54
+ shadow: true,
55
+ ...options.defaultStyle
56
+ }
57
+
58
+ this.isEnabled = options.enabledByDefault ?? false
59
+
60
+ if (options.tracks) {
61
+ this.addTracks(options.tracks)
62
+
63
+ if (options.preloadTracks) {
64
+ this.preloadAllTracks()
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Add multiple subtitle tracks
71
+ */
72
+ addTracks(tracks: SubtitleTrack[]): void {
73
+ tracks.forEach(track => this.addTrack(track))
74
+ }
75
+
76
+ /**
77
+ * Add a single subtitle track
78
+ */
79
+ addTrack(track: SubtitleTrack): void {
80
+ const trackWithDefaults: SubtitleTrack = {
81
+ ...track,
82
+ loadingState: 'idle',
83
+ format: track.format || this.detectFormat(track.url)
84
+ }
85
+
86
+ this.tracks.set(track.id, trackWithDefaults)
87
+ this.emit('trackAdded', trackWithDefaults)
88
+
89
+ // Set as active if it's the default track and no track is currently active
90
+ if (track.isDefault && !this.activeTrackId) {
91
+ this.setActiveTrack(track.id)
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Remove a subtitle track
97
+ */
98
+ removeTrack(trackId: string): void {
99
+ const track = this.tracks.get(trackId)
100
+ if (track) {
101
+ this.tracks.delete(trackId)
102
+
103
+ if (this.activeTrackId === trackId) {
104
+ this.activeTrackId = null
105
+ this.currentCues = []
106
+ this.emit('cuesChanged', [])
107
+ }
108
+
109
+ this.emit('trackRemoved', track)
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Get all available tracks
115
+ */
116
+ getTracks(): SubtitleTrack[] {
117
+ return Array.from(this.tracks.values())
118
+ }
119
+
120
+ /**
121
+ * Get a specific track by ID
122
+ */
123
+ getTrack(trackId: string): SubtitleTrack | undefined {
124
+ return this.tracks.get(trackId)
125
+ }
126
+
127
+ /**
128
+ * Set the active subtitle track
129
+ */
130
+ async setActiveTrack(trackId: string | null): Promise<void> {
131
+ this.activeTrackId = trackId
132
+ this.currentCues = []
133
+
134
+ if (trackId) {
135
+ const track = this.tracks.get(trackId)
136
+ if (track && !track.parsedSubtitles && track.loadingState === 'idle') {
137
+ await this.loadTrack(trackId)
138
+ }
139
+ }
140
+
141
+ this.emit('activeTrackChanged', trackId)
142
+ this.emit('cuesChanged', [])
143
+ }
144
+
145
+ /**
146
+ * Get the currently active track
147
+ */
148
+ getActiveTrack(): SubtitleTrack | null {
149
+ return this.activeTrackId ? this.tracks.get(this.activeTrackId) || null : null
150
+ }
151
+
152
+ /**
153
+ * Enable or disable subtitles
154
+ */
155
+ setEnabled(enabled: boolean): void {
156
+ this.isEnabled = enabled
157
+
158
+ if (!enabled) {
159
+ this.currentCues = []
160
+ this.emit('cuesChanged', [])
161
+ }
162
+
163
+ this.emit('enabledChanged', enabled)
164
+ }
165
+
166
+ /**
167
+ * Check if subtitles are enabled
168
+ */
169
+ isSubtitlesEnabled(): boolean {
170
+ return this.isEnabled
171
+ }
172
+
173
+ /**
174
+ * Update subtitle styling
175
+ */
176
+ updateStyle(newStyle: Partial<SubtitleStyle>): void {
177
+ this.style = { ...this.style, ...newStyle }
178
+ this.emit('styleChanged', this.style)
179
+ }
180
+
181
+ /**
182
+ * Get current subtitle style
183
+ */
184
+ getStyle(): SubtitleStyle {
185
+ return { ...this.style }
186
+ }
187
+
188
+ /**
189
+ * Update current time and get active cues
190
+ */
191
+ updateTime(currentTime: number): SubtitleCue[] {
192
+ if (!this.isEnabled || !this.activeTrackId) {
193
+ if (this.currentCues.length > 0) {
194
+ this.currentCues = []
195
+ this.emit('cuesChanged', [])
196
+ }
197
+ return []
198
+ }
199
+
200
+ const activeTrack = this.tracks.get(this.activeTrackId)
201
+ if (!activeTrack?.parsedSubtitles) {
202
+ if (this.currentCues.length > 0) {
203
+ this.currentCues = []
204
+ this.emit('cuesChanged', [])
205
+ }
206
+ return []
207
+ }
208
+
209
+ const activeCues = SubtitleParser.getActiveCues(
210
+ activeTrack.parsedSubtitles.cues,
211
+ currentTime
212
+ )
213
+
214
+ // Check if cues have changed
215
+ const cuesChanged =
216
+ activeCues.length !== this.currentCues.length ||
217
+ activeCues.some((cue, index) =>
218
+ !this.currentCues[index] || cue.id !== this.currentCues[index].id
219
+ )
220
+
221
+ if (cuesChanged) {
222
+ this.currentCues = activeCues
223
+ this.emit('cuesChanged', activeCues)
224
+ }
225
+
226
+ return activeCues
227
+ }
228
+
229
+ /**
230
+ * Get current active cues
231
+ */
232
+ getCurrentCues(): SubtitleCue[] {
233
+ return [...this.currentCues]
234
+ }
235
+
236
+ /**
237
+ * Load a subtitle track
238
+ */
239
+ async loadTrack(trackId: string): Promise<void> {
240
+ const track = this.tracks.get(trackId)
241
+ if (!track || track.loadingState === 'loading' || track.loadingState === 'loaded') {
242
+ return
243
+ }
244
+
245
+ // Update loading state
246
+ track.loadingState = 'loading'
247
+ this.emit('trackLoadingStateChanged', { trackId, state: 'loading' })
248
+
249
+ try {
250
+ const response = await fetch(track.url)
251
+ if (!response.ok) {
252
+ throw new Error(`Failed to fetch subtitles: ${response.status} ${response.statusText}`)
253
+ }
254
+
255
+ const content = await response.text()
256
+ const format = track.format || SubtitleParser.detectFormat(content)
257
+
258
+ if (format === 'unknown') {
259
+ throw new Error('Unsupported subtitle format')
260
+ }
261
+
262
+ const parsedSubtitles = SubtitleParser.parse(content, format)
263
+ track.parsedSubtitles = parsedSubtitles
264
+ track.loadingState = 'loaded'
265
+ track.format = format
266
+
267
+ this.emit('trackLoadingStateChanged', { trackId, state: 'loaded' })
268
+ this.emit('trackLoaded', track)
269
+
270
+ } catch (error) {
271
+ track.loadingState = 'error'
272
+ track.error = error instanceof Error ? error.message : 'Unknown error'
273
+
274
+ this.emit('trackLoadingStateChanged', { trackId, state: 'error', error: track.error })
275
+ this.emit('trackError', { trackId, error: track.error })
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Preload all available tracks
281
+ */
282
+ async preloadAllTracks(): Promise<void> {
283
+ const loadPromises = Array.from(this.tracks.keys()).map(trackId =>
284
+ this.loadTrack(trackId)
285
+ )
286
+
287
+ await Promise.allSettled(loadPromises)
288
+ }
289
+
290
+ /**
291
+ * Search for cues containing text
292
+ */
293
+ searchCues(query: string, trackId?: string): Array<{ trackId: string, cue: SubtitleCue }> {
294
+ const results: Array<{ trackId: string, cue: SubtitleCue }> = []
295
+ const searchTracks = trackId ? [trackId] : Array.from(this.tracks.keys())
296
+
297
+ for (const id of searchTracks) {
298
+ const track = this.tracks.get(id)
299
+ if (!track?.parsedSubtitles) continue
300
+
301
+ for (const cue of track.parsedSubtitles.cues) {
302
+ const cleanText = SubtitleParser.cleanText(cue.text).toLowerCase()
303
+ if (cleanText.includes(query.toLowerCase())) {
304
+ results.push({ trackId: id, cue })
305
+ }
306
+ }
307
+ }
308
+
309
+ return results
310
+ }
311
+
312
+ /**
313
+ * Export subtitles in a specific format
314
+ */
315
+ exportSubtitles(trackId: string, format: 'vtt' | 'srt'): string | null {
316
+ const track = this.tracks.get(trackId)
317
+ if (!track?.parsedSubtitles) return null
318
+
319
+ const { cues, metadata } = track.parsedSubtitles
320
+
321
+ if (format === 'vtt') {
322
+ return this.exportAsVTT(cues, metadata)
323
+ } else {
324
+ return this.exportAsSRT(cues)
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Event system methods
330
+ */
331
+ on(event: string, callback: Function): () => void {
332
+ if (!this.eventListeners.has(event)) {
333
+ this.eventListeners.set(event, new Set())
334
+ }
335
+
336
+ this.eventListeners.get(event)!.add(callback)
337
+
338
+ return () => {
339
+ const listeners = this.eventListeners.get(event)
340
+ if (listeners) {
341
+ listeners.delete(callback)
342
+ }
343
+ }
344
+ }
345
+
346
+ private emit(event: string, data?: any): void {
347
+ const listeners = this.eventListeners.get(event)
348
+ if (listeners) {
349
+ listeners.forEach(callback => callback(data))
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Detect subtitle format from URL extension
355
+ */
356
+ private detectFormat(url: string): 'vtt' | 'srt' {
357
+ const extension = url.split('.').pop()?.toLowerCase()
358
+ return extension === 'srt' ? 'srt' : 'vtt'
359
+ }
360
+
361
+ /**
362
+ * Export cues as WebVTT format
363
+ */
364
+ private exportAsVTT(cues: SubtitleCue[], metadata?: any): string {
365
+ let output = 'WEBVTT\n\n'
366
+
367
+ if (metadata) {
368
+ Object.entries(metadata).forEach(([key, value]) => {
369
+ output += `${key}: ${value}\n`
370
+ })
371
+ output += '\n'
372
+ }
373
+
374
+ cues.forEach((cue, index) => {
375
+ if (cue.id) {
376
+ output += `${cue.id}\n`
377
+ }
378
+
379
+ const startTime = this.formatTime(cue.startTime, 'vtt')
380
+ const endTime = this.formatTime(cue.endTime, 'vtt')
381
+
382
+ output += `${startTime} --> ${endTime}`
383
+
384
+ if (cue.settings) {
385
+ const settings = []
386
+ Object.entries(cue.settings).forEach(([key, value]) => {
387
+ settings.push(`${key}:${value}`)
388
+ })
389
+ if (settings.length > 0) {
390
+ output += ` ${settings.join(' ')}`
391
+ }
392
+ }
393
+
394
+ output += `\n${cue.text}\n\n`
395
+ })
396
+
397
+ return output
398
+ }
399
+
400
+ /**
401
+ * Export cues as SRT format
402
+ */
403
+ private exportAsSRT(cues: SubtitleCue[]): string {
404
+ let output = ''
405
+
406
+ cues.forEach((cue, index) => {
407
+ const startTime = this.formatTime(cue.startTime, 'srt')
408
+ const endTime = this.formatTime(cue.endTime, 'srt')
409
+
410
+ output += `${index + 1}\n`
411
+ output += `${startTime} --> ${endTime}\n`
412
+ output += `${cue.text}\n\n`
413
+ })
414
+
415
+ return output
416
+ }
417
+
418
+ /**
419
+ * Format time for different subtitle formats
420
+ */
421
+ private formatTime(seconds: number, format: 'vtt' | 'srt'): string {
422
+ const hours = Math.floor(seconds / 3600)
423
+ const minutes = Math.floor((seconds % 3600) / 60)
424
+ const secs = Math.floor(seconds % 60)
425
+ const milliseconds = Math.floor((seconds % 1) * 1000)
426
+
427
+ const separator = format === 'srt' ? ',' : '.'
428
+
429
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}${separator}${milliseconds.toString().padStart(3, '0')}`
430
+ }
431
+
432
+ /**
433
+ * Cleanup resources
434
+ */
435
+ destroy(): void {
436
+ this.tracks.clear()
437
+ this.eventListeners.clear()
438
+ this.activeTrackId = null
439
+ this.currentCues = []
440
+ }
441
+ }
@@ -0,0 +1,280 @@
1
+ export interface SubtitleCue {
2
+ id?: string
3
+ startTime: number
4
+ endTime: number
5
+ text: string
6
+ settings?: {
7
+ line?: number | 'auto'
8
+ position?: number
9
+ size?: number
10
+ align?: 'start' | 'center' | 'end'
11
+ vertical?: 'rl' | 'lr'
12
+ }
13
+ }
14
+
15
+ export interface ParsedSubtitles {
16
+ cues: SubtitleCue[]
17
+ metadata?: {
18
+ title?: string
19
+ language?: string
20
+ description?: string
21
+ }
22
+ }
23
+
24
+ export class SubtitleParser {
25
+ /**
26
+ * Parse subtitle content based on format
27
+ */
28
+ static parse(content: string, format: 'vtt' | 'srt'): ParsedSubtitles {
29
+ switch (format) {
30
+ case 'vtt':
31
+ return this.parseVTT(content)
32
+ case 'srt':
33
+ return this.parseSRT(content)
34
+ default:
35
+ throw new Error(`Unsupported subtitle format: ${format}`)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Auto-detect subtitle format from content
41
+ */
42
+ static detectFormat(content: string): 'vtt' | 'srt' | 'unknown' {
43
+ const trimmedContent = content.trim()
44
+
45
+ if (trimmedContent.startsWith('WEBVTT')) {
46
+ return 'vtt'
47
+ }
48
+
49
+ // Check for SRT pattern (number, timestamp, text)
50
+ const srtPattern = /^\d+\s*\n\d{2}:\d{2}:\d{2},\d{3}\s*-->\s*\d{2}:\d{2}:\d{2},\d{3}/m
51
+ if (srtPattern.test(trimmedContent)) {
52
+ return 'srt'
53
+ }
54
+
55
+ return 'unknown'
56
+ }
57
+
58
+ /**
59
+ * Parse WebVTT format
60
+ */
61
+ private static parseVTT(content: string): ParsedSubtitles {
62
+ const lines = content.split('\n')
63
+ const cues: SubtitleCue[] = []
64
+ const metadata: any = {}
65
+
66
+ let i = 0
67
+
68
+ // Skip WEBVTT header and metadata
69
+ if (lines[0]?.startsWith('WEBVTT')) {
70
+ i = 1
71
+ // Parse metadata in header
72
+ while (i < lines.length && lines[i].trim() !== '') {
73
+ const line = lines[i].trim()
74
+ if (line.includes(':')) {
75
+ const [key, value] = line.split(':').map(s => s.trim())
76
+ metadata[key.toLowerCase()] = value
77
+ }
78
+ i++
79
+ }
80
+ }
81
+
82
+ // Parse cues
83
+ while (i < lines.length) {
84
+ // Skip empty lines
85
+ if (!lines[i]?.trim()) {
86
+ i++
87
+ continue
88
+ }
89
+
90
+ let cueId: string | undefined
91
+ let timingLine: string
92
+
93
+ // Check if this line is a timing line or cue identifier
94
+ const isTimingLine = lines[i].includes('-->')
95
+ if (isTimingLine) {
96
+ timingLine = lines[i]
97
+ i++
98
+ } else {
99
+ // This is a cue identifier
100
+ cueId = lines[i].trim()
101
+ i++
102
+ timingLine = lines[i] || ''
103
+ i++
104
+ }
105
+
106
+ // Parse timing line
107
+ const timing = this.parseVTTTiming(timingLine)
108
+ if (!timing) {
109
+ i++
110
+ continue
111
+ }
112
+
113
+ // Collect cue text
114
+ const textLines: string[] = []
115
+ while (i < lines.length && lines[i]?.trim() !== '') {
116
+ textLines.push(lines[i])
117
+ i++
118
+ }
119
+
120
+ if (textLines.length > 0) {
121
+ cues.push({
122
+ id: cueId,
123
+ startTime: timing.start,
124
+ endTime: timing.end,
125
+ text: textLines.join('\n'),
126
+ settings: timing.settings
127
+ })
128
+ }
129
+
130
+ i++
131
+ }
132
+
133
+ return { cues, metadata }
134
+ }
135
+
136
+ /**
137
+ * Parse SRT format
138
+ */
139
+ private static parseSRT(content: string): ParsedSubtitles {
140
+ const cues: SubtitleCue[] = []
141
+ const blocks = content.split(/\n\s*\n/).filter(block => block.trim())
142
+
143
+ for (const block of blocks) {
144
+ const lines = block.split('\n').map(line => line.trim()).filter(line => line)
145
+ if (lines.length < 3) continue
146
+
147
+ const id = lines[0]
148
+ const timingLine = lines[1]
149
+ const textLines = lines.slice(2)
150
+
151
+ const timing = this.parseSRTTiming(timingLine)
152
+ if (!timing) continue
153
+
154
+ cues.push({
155
+ id,
156
+ startTime: timing.start,
157
+ endTime: timing.end,
158
+ text: textLines.join('\n')
159
+ })
160
+ }
161
+
162
+ return { cues }
163
+ }
164
+
165
+ /**
166
+ * Parse VTT timing line with settings
167
+ */
168
+ private static parseVTTTiming(timingLine: string): {
169
+ start: number
170
+ end: number
171
+ settings?: SubtitleCue['settings']
172
+ } | null {
173
+ const parts = timingLine.split(/\s+/)
174
+ const arrowIndex = parts.findIndex(part => part === '-->')
175
+
176
+ if (arrowIndex === -1 || arrowIndex === 0 || arrowIndex === parts.length - 1) {
177
+ return null
178
+ }
179
+
180
+ const startTime = this.parseTimeString(parts[arrowIndex - 1])
181
+ const endTime = this.parseTimeString(parts[arrowIndex + 1])
182
+
183
+ if (startTime === null || endTime === null) {
184
+ return null
185
+ }
186
+
187
+ // Parse cue settings
188
+ const settings: SubtitleCue['settings'] = {}
189
+ const settingsParts = parts.slice(arrowIndex + 2)
190
+
191
+ for (const setting of settingsParts) {
192
+ const [key, value] = setting.split(':')
193
+ if (!key || !value) continue
194
+
195
+ switch (key) {
196
+ case 'line':
197
+ settings.line = value === 'auto' ? 'auto' : parseInt(value)
198
+ break
199
+ case 'position':
200
+ settings.position = parseInt(value)
201
+ break
202
+ case 'size':
203
+ settings.size = parseInt(value)
204
+ break
205
+ case 'align':
206
+ if (['start', 'center', 'end'].includes(value)) {
207
+ settings.align = value as 'start' | 'center' | 'end'
208
+ }
209
+ break
210
+ case 'vertical':
211
+ if (['rl', 'lr'].includes(value)) {
212
+ settings.vertical = value as 'rl' | 'lr'
213
+ }
214
+ break
215
+ }
216
+ }
217
+
218
+ return {
219
+ start: startTime,
220
+ end: endTime,
221
+ settings: Object.keys(settings).length > 0 ? settings : undefined
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Parse SRT timing line
227
+ */
228
+ private static parseSRTTiming(timingLine: string): {
229
+ start: number
230
+ end: number
231
+ } | null {
232
+ const match = timingLine.match(/(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/)
233
+ if (!match) return null
234
+
235
+ const startTime = this.parseTimeString(match[1].replace(',', '.'))
236
+ const endTime = this.parseTimeString(match[2].replace(',', '.'))
237
+
238
+ if (startTime === null || endTime === null) return null
239
+
240
+ return { start: startTime, end: endTime }
241
+ }
242
+
243
+ /**
244
+ * Parse time string to seconds
245
+ */
246
+ private static parseTimeString(timeStr: string): number | null {
247
+ const match = timeStr.match(/(\d{2}):(\d{2}):(\d{2})\.(\d{3})/)
248
+ if (!match) return null
249
+
250
+ const hours = parseInt(match[1])
251
+ const minutes = parseInt(match[2])
252
+ const seconds = parseInt(match[3])
253
+ const milliseconds = parseInt(match[4])
254
+
255
+ return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000
256
+ }
257
+
258
+ /**
259
+ * Find active cues at a given time
260
+ */
261
+ static getActiveCues(cues: SubtitleCue[], currentTime: number): SubtitleCue[] {
262
+ return cues.filter(cue =>
263
+ currentTime >= cue.startTime && currentTime <= cue.endTime
264
+ )
265
+ }
266
+
267
+ /**
268
+ * Clean subtitle text (remove HTML tags, etc.)
269
+ */
270
+ static cleanText(text: string): string {
271
+ return text
272
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
273
+ .replace(/&lt;/g, '<')
274
+ .replace(/&gt;/g, '>')
275
+ .replace(/&amp;/g, '&')
276
+ .replace(/&quot;/g, '"')
277
+ .replace(/&#39;/g, "'")
278
+ .trim()
279
+ }
280
+ }