@therealtinhtute/baroiplayer 1.1.0 → 1.2.1

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 (142) hide show
  1. package/README.md +1 -2
  2. package/dist/context/media.js +1 -2
  3. package/dist/context/media.mjs +3 -15
  4. package/dist/context/store.js +1 -2
  5. package/dist/context/store.mjs +3 -5
  6. package/dist/context-Bzd5QCKM.cjs +7 -0
  7. package/dist/context-Bzd5QCKM.cjs.map +1 -0
  8. package/dist/context-DD_SH4tT.js +1053 -0
  9. package/dist/context-DD_SH4tT.js.map +1 -0
  10. package/dist/core-events-BRnLMB7s.cjs +2 -0
  11. package/dist/core-events-BRnLMB7s.cjs.map +1 -0
  12. package/dist/core-events-v_mKdj5p.js +417 -0
  13. package/dist/core-events-v_mKdj5p.js.map +1 -0
  14. package/dist/core-plugins-CMBv-yVh.cjs +2 -0
  15. package/dist/core-plugins-CMBv-yVh.cjs.map +1 -0
  16. package/dist/core-plugins-CqCxLkbd.js +754 -0
  17. package/dist/core-plugins-CqCxLkbd.js.map +1 -0
  18. package/dist/core.js +7 -29
  19. package/dist/core.js.map +1 -1
  20. package/dist/core.mjs +947 -1220
  21. package/dist/core.mjs.map +1 -1
  22. package/dist/enhanced-components-BwjBqSA1.js +2858 -0
  23. package/dist/enhanced-components-BwjBqSA1.js.map +1 -0
  24. package/dist/enhanced-components-C-ka-XLp.cjs +2 -0
  25. package/dist/enhanced-components-C-ka-XLp.cjs.map +1 -0
  26. package/dist/hooks-B-IIWPJr.js +1842 -0
  27. package/dist/hooks-B-IIWPJr.js.map +1 -0
  28. package/dist/hooks-DcHzmBwA.cjs +4 -0
  29. package/dist/hooks-DcHzmBwA.cjs.map +1 -0
  30. package/dist/index.js +27 -97
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +5613 -6210
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/minimal-components-BYsMQde5.js +1610 -0
  35. package/dist/minimal-components-BYsMQde5.js.map +1 -0
  36. package/dist/minimal-components-CsxQbDp7.cjs +2 -0
  37. package/dist/minimal-components-CsxQbDp7.cjs.map +1 -0
  38. package/dist/performance-monitor-DCSijLJg.js +312 -0
  39. package/dist/performance-monitor-DCSijLJg.js.map +1 -0
  40. package/dist/performance-monitor-wNFKmqMr.cjs +2 -0
  41. package/dist/performance-monitor-wNFKmqMr.cjs.map +1 -0
  42. package/dist/rolldown-runtime-BmowowmI.js +14 -0
  43. package/dist/rolldown-runtime-mLOUI7ql.cjs +1 -0
  44. package/dist/utils-D2gQ7c2R.js +64 -0
  45. package/dist/utils-D2gQ7c2R.js.map +1 -0
  46. package/dist/utils-DJaAwiZ9.cjs +2 -0
  47. package/dist/utils-DJaAwiZ9.cjs.map +1 -0
  48. package/dist/utils.js +1 -2
  49. package/dist/utils.mjs +3 -11
  50. package/package.json +28 -22
  51. package/src/components/AnimateLoading.tsx +14 -10
  52. package/src/components/accessibility/keyboard-help-overlay.tsx +2 -0
  53. package/src/components/enhanced/debug-panel.tsx +1 -1
  54. package/src/components/enhanced/enhanced-media-player.tsx +68 -25
  55. package/src/components/examples/loading-states-demo.tsx +1 -2
  56. package/src/components/examples/react-19-demo.tsx +6 -10
  57. package/src/components/examples/volume-orientation-demo.tsx +1 -2
  58. package/src/components/legacy.tsx +6 -6
  59. package/src/components/media-player-video.tsx +5 -12
  60. package/src/components/media-player.tsx +2 -2
  61. package/src/components/ui/blur-poster.tsx +18 -15
  62. package/src/components/ui/enhanced-seek-bar.tsx +2 -2
  63. package/src/components/ui/media-resource-preloader.tsx +21 -23
  64. package/src/components/unified/base-media-player.tsx +22 -10
  65. package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +58 -38
  66. package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +6 -6
  67. package/src/components/variants/minimal/components/VideoElement.tsx +3 -1
  68. package/src/core/optimization/bundle-optimizer.tsx +26 -27
  69. package/src/core/performance/media-loading-optimizer.ts +2 -1
  70. package/src/core/plugins/__tests__/registry.test.ts +1 -1
  71. package/src/core/plugins/enhanced-plugin-system.ts +26 -8
  72. package/src/core/plugins/registry.ts +319 -92
  73. package/src/core/plugins/types.ts +21 -3
  74. package/src/core/providers/enhanced-hls-provider.ts +11 -40
  75. package/src/core/providers/lazy-provider.tsx +5 -42
  76. package/src/features/media-session/media-session-manager.ts +4 -26
  77. package/src/features/picture-in-picture/pip-manager.ts +2 -2
  78. package/src/features/playlist/playlist-manager.ts +26 -18
  79. package/src/features/subtitles/subtitle-manager.ts +1 -1
  80. package/src/features/subtitles/subtitle-parser.ts +19 -16
  81. package/src/features/thumbnails/thumbnail-generator.ts +12 -5
  82. package/src/hooks/__tests__/currentTime-fix.test.ts +149 -0
  83. package/src/hooks/accessibility/use-accessibility-preferences.ts +1 -1
  84. package/src/hooks/accessibility/use-focus-management.ts +2 -2
  85. package/src/hooks/accessibility/use-keyboard-shortcuts.ts +2 -2
  86. package/src/hooks/accessibility/use-screen-reader.ts +1 -1
  87. package/src/hooks/core/index.ts +1 -1
  88. package/src/hooks/use-buffer-health.ts +1 -1
  89. package/src/hooks/use-enhanced-media-player.ts +107 -9
  90. package/src/hooks/use-focus-trap.tsx +2 -2
  91. package/src/hooks/use-intelligent-preloading.ts +23 -3
  92. package/src/hooks/use-keyboard-shortcuts.ts +2 -2
  93. package/src/hooks/use-media-player.ts +74 -43
  94. package/src/hooks/use-memory-optimization.ts +15 -15
  95. package/src/hooks/use-smart-preload.ts +1 -2
  96. package/src/hooks/useKeyboardShortcuts.ts +34 -2
  97. package/src/hooks/useScreenshot.ts +60 -44
  98. package/src/hooks/useVideoPlayer.ts +50 -4
  99. package/src/index-legacy.ts +3 -5
  100. package/src/test/accessibility-utils.ts +7 -7
  101. package/src/test/media-mocks.ts +41 -28
  102. package/src/test/setup.ts +22 -4
  103. package/src/types/assets.d.ts +29 -0
  104. package/src/types/index.ts +13 -0
  105. package/dist/context/media.js.map +0 -1
  106. package/dist/context/media.mjs.map +0 -1
  107. package/dist/context/store.js.map +0 -1
  108. package/dist/context/store.mjs.map +0 -1
  109. package/dist/context-BYwDoSRX.js +0 -1172
  110. package/dist/context-BYwDoSRX.js.map +0 -1
  111. package/dist/context-eNS62_ac.cjs +0 -23
  112. package/dist/context-eNS62_ac.cjs.map +0 -1
  113. package/dist/core-events-B1iM2F46.cjs +0 -2
  114. package/dist/core-events-B1iM2F46.cjs.map +0 -1
  115. package/dist/core-events-DunPHKRE.js +0 -484
  116. package/dist/core-events-DunPHKRE.js.map +0 -1
  117. package/dist/core-plugins-CROGvQUy.js +0 -686
  118. package/dist/core-plugins-CROGvQUy.js.map +0 -1
  119. package/dist/core-plugins-DtsHBOtF.cjs +0 -2
  120. package/dist/core-plugins-DtsHBOtF.cjs.map +0 -1
  121. package/dist/enhanced-components-DUw2KrmQ.js +0 -1142
  122. package/dist/enhanced-components-DUw2KrmQ.js.map +0 -1
  123. package/dist/enhanced-components-DXBCHU0v.cjs +0 -2
  124. package/dist/enhanced-components-DXBCHU0v.cjs.map +0 -1
  125. package/dist/hooks-BW-JjVgP.cjs +0 -4
  126. package/dist/hooks-BW-JjVgP.cjs.map +0 -1
  127. package/dist/hooks-CyX6De5M.js +0 -1693
  128. package/dist/hooks-CyX6De5M.js.map +0 -1
  129. package/dist/minimal-components-Bdcpxav5.cjs +0 -2
  130. package/dist/minimal-components-Bdcpxav5.cjs.map +0 -1
  131. package/dist/minimal-components-DlRK7Qu_.js +0 -1927
  132. package/dist/minimal-components-DlRK7Qu_.js.map +0 -1
  133. package/dist/performance-monitor-C5k6FDXw.js +0 -301
  134. package/dist/performance-monitor-C5k6FDXw.js.map +0 -1
  135. package/dist/performance-monitor-lJmuUjQQ.cjs +0 -2
  136. package/dist/performance-monitor-lJmuUjQQ.cjs.map +0 -1
  137. package/dist/utils-CFOue_9K.cjs +0 -9
  138. package/dist/utils-CFOue_9K.cjs.map +0 -1
  139. package/dist/utils-Dzpl2ArK.js +0 -2844
  140. package/dist/utils-Dzpl2ArK.js.map +0 -1
  141. package/dist/utils.js.map +0 -1
  142. package/dist/utils.mjs.map +0 -1
@@ -6,7 +6,7 @@
6
6
  import Hls, { HlsConfig } from "hls.js"
7
7
  import { MediaPlayerConfig } from "../../types"
8
8
 
9
- export interface EnhancedHLSConfig extends HlsConfig {
9
+ export interface EnhancedHLSConfig extends Partial<HlsConfig> {
10
10
  /** Enable machine learning-enhanced adaptive bitrate streaming */
11
11
  enableAdvancedABR?: boolean
12
12
  /** Optimize for low-latency streaming */
@@ -60,25 +60,6 @@ export const ENHANCED_HLS_DEFAULTS: EnhancedHLSConfig = {
60
60
  }
61
61
  },
62
62
 
63
- // Progressive segment loading for better performance
64
- segmentLoadPolicy: {
65
- default: {
66
- maxTimeToFirstByteMs: 9000,
67
- maxLoadTimeMs: 20000,
68
- timeoutRetry: {
69
- maxNumRetry: 3,
70
- retryDelayMs: 2000,
71
- maxRetryDelayMs: 20000,
72
- backoff: "linear"
73
- },
74
- errorRetry: {
75
- maxNumRetry: 5,
76
- retryDelayMs: 3000,
77
- maxRetryDelayMs: 30000,
78
- backoff: "linear"
79
- }
80
- }
81
- },
82
63
 
83
64
  // Enhanced manifest parsing
84
65
  manifestLoadPolicy: {
@@ -102,7 +83,7 @@ export const ENHANCED_HLS_DEFAULTS: EnhancedHLSConfig = {
102
83
 
103
84
  // Level selection optimization
104
85
  startLevel: -1, // Auto-select start level
105
- initialBitrate: 0, // Auto-detect
86
+ // initialBitrate: 0, // Auto-detect (not in HlsConfig)
106
87
  preferManagedMediaSource: true,
107
88
 
108
89
  // Debug and monitoring
@@ -172,11 +153,6 @@ export class EnhancedHLSProvider {
172
153
  this.hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
173
154
  this.performanceMonitor.onLevelSwitched(data)
174
155
  })
175
-
176
- // Quality optimization events
177
- this.hls.on(Hls.Events.BITRATE_SWITCHING, (event, data) => {
178
- this.performanceMonitor.onBitrateSwitch(data)
179
- })
180
156
  }
181
157
 
182
158
  /**
@@ -202,16 +178,16 @@ export class EnhancedHLSProvider {
202
178
 
203
179
  case Hls.ErrorDetails.FRAG_LOAD_TIMEOUT:
204
180
  // Timeout - increase timeout and retry
205
- if (this.hls) {
181
+ if (this.hls && this.hls.config.fragLoadPolicy?.default?.timeoutRetry) {
206
182
  this.hls.config.fragLoadPolicy.default.timeoutRetry.maxNumRetry = 8
207
183
  this.hls.config.fragLoadPolicy.default.timeoutRetry.retryDelayMs = 5000
208
184
  }
209
185
  break
210
186
 
211
187
  case Hls.ErrorDetails.LEVEL_LOAD_ERROR:
212
- // Manifest error - try recovery with alternative
213
- if (options.hlsConfig?.fallbackUrl) {
214
- this.hls?.loadSource(options.hlsConfig.fallbackUrl)
188
+ // Manifest error - try recovery
189
+ if (this.hls && data.fatal) {
190
+ this.hls.recoverMediaError()
215
191
  }
216
192
  break
217
193
 
@@ -229,11 +205,6 @@ export class EnhancedHLSProvider {
229
205
  }
230
206
  break
231
207
  }
232
-
233
- // Custom error handler if provided
234
- if (options.onError) {
235
- options.onError(data)
236
- }
237
208
  }
238
209
 
239
210
  /**
@@ -242,7 +213,7 @@ export class EnhancedHLSProvider {
242
213
  public updateConfig(newConfig: Partial<EnhancedHLSConfig>): void {
243
214
  this.config = { ...this.config, ...newConfig }
244
215
  if (this.hls) {
245
- this.hls.config = this.config
216
+ Object.assign(this.hls.config, this.config)
246
217
  }
247
218
  }
248
219
 
@@ -289,11 +260,11 @@ class HLSPerformanceMonitor {
289
260
 
290
261
  public attach(hls: Hls): void {
291
262
  // Monitor video element for dropped frames
292
- const video = hls.media
293
- if (video) {
263
+ const video = hls.media as HTMLVideoElement | undefined
264
+ if (video && 'getVideoPlaybackQuality' in video) {
294
265
  const checkDroppedFrames = () => {
295
- if (video.getVideoPlaybackQuality) {
296
- const quality = video.getVideoPlaybackQuality()
266
+ if (video && 'getVideoPlaybackQuality' in video) {
267
+ const quality = (video as any).getVideoPlaybackQuality()
297
268
  this.metrics.droppedFrames = quality.droppedVideoFrames
298
269
  }
299
270
  }
@@ -1,46 +1,9 @@
1
- import { type ReactNode, Suspense, lazy } from 'react'
1
+ import { type ReactNode, Suspense } from 'react'
2
2
 
3
- export const EnhancedHLSProvider = lazy(() =>
4
- import(
5
- /* webpackChunkName: "hls-provider" */
6
- './enhanced-hls-provider'
7
- ).then((module) => ({ default: module.EnhancedHLSProvider }))
8
- )
9
-
10
- export const SubtitleParser = lazy(() =>
11
- import(
12
- /* webpackChunkName: "subtitle-parser" */
13
- '../features/subtitles/subtitle-parser'
14
- ).then((module) => ({ default: module.SubtitleParser }))
15
- )
16
-
17
- export const SubtitleManager = lazy(() =>
18
- import(
19
- /* webpackChunkName: "subtitle-manager" */
20
- '../features/subtitles/subtitle-manager'
21
- ).then((module) => ({ default: module.SubtitleManager }))
22
- )
23
-
24
- export const BasePlugin = lazy(() =>
25
- import(
26
- /* webpackChunkName: "base-plugin" */
27
- '../plugins/base-plugin'
28
- ).then((module) => ({ default: module.BasePlugin }))
29
- )
30
-
31
- export const PluginRegistry = lazy(() =>
32
- import(
33
- /* webpackChunkName: "plugin-registry" */
34
- '../plugins/registry'
35
- ).then((module) => ({ default: module.PluginRegistry }))
36
- )
37
-
38
- export const AnalyticsPlugin = lazy(() =>
39
- import(
40
- /* webpackChunkName: "analytics-plugin" */
41
- '../plugins/examples/analytics-plugin'
42
- ).then((module) => ({ default: module.AnalyticsPlugin }))
43
- )
3
+ export async function loadEnhancedHLSProvider() {
4
+ const module = await import('./enhanced-hls-provider')
5
+ return module.EnhancedHLSProvider
6
+ }
44
7
 
45
8
  export type LazyHLSProvider = Promise<{
46
9
  default: new (config?: any) => { isSupported(): boolean; initialize(): any; destroy(): void }
@@ -1,4 +1,4 @@
1
- export interface MediaMetadata {
1
+ export interface CustomMediaMetadata {
2
2
  title?: string
3
3
  artist?: string
4
4
  album?: string
@@ -79,7 +79,7 @@ export class MediaSessionManager {
79
79
  /**
80
80
  * Set media metadata
81
81
  */
82
- setMetadata(metadata: MediaMetadata): void {
82
+ setMetadata(metadata: CustomMediaMetadata): void {
83
83
  if (!this.isInitialized) return
84
84
 
85
85
  try {
@@ -209,18 +209,6 @@ export class MediaSessionManager {
209
209
  this.callbacks.onStop?.()
210
210
  break
211
211
 
212
- case "togglemicrophone":
213
- this.callbacks.onToggleMicrophone?.()
214
- break
215
-
216
- case "togglecamera":
217
- this.callbacks.onToggleCamera?.()
218
- break
219
-
220
- case "hangup":
221
- this.callbacks.onHangup?.()
222
- break
223
-
224
212
  default:
225
213
  console.warn(`Unhandled media session action: ${action}`)
226
214
  }
@@ -339,9 +327,9 @@ export class MediaSessionManager {
339
327
  */
340
328
  async updateFromMediaElement(
341
329
  mediaElement: HTMLMediaElement,
342
- additionalInfo?: Partial<MediaMetadata>
330
+ additionalInfo?: Partial<CustomMediaMetadata>
343
331
  ): Promise<void> {
344
- const metadata: MediaMetadata = {
332
+ const metadata: CustomMediaMetadata = {
345
333
  title: additionalInfo?.title || document.title || "Media Player",
346
334
  artist: additionalInfo?.artist || "Unknown Artist",
347
335
  album: additionalInfo?.album || "Unknown Album",
@@ -421,13 +409,3 @@ export class MediaSessionManager {
421
409
  export function createMediaSessionManager(config?: MediaSessionConfig): MediaSessionManager {
422
410
  return new MediaSessionManager(config)
423
411
  }
424
-
425
- // Type definitions for better TypeScript support
426
- declare global {
427
- interface Navigator {
428
- mediaSession: MediaSession
429
- }
430
- }
431
-
432
- // Export types for use in other modules
433
- export type { MediaSessionAction, MediaSessionActionDetails, MediaSessionPlaybackState }
@@ -188,11 +188,11 @@ export class EnhancedPiPManager implements PiPManager {
188
188
  this.emit("enter", { type: "document", window: this.pipWindow })
189
189
 
190
190
  // Set up window event listeners
191
- this.pipWindow.addEventListener("unload", () => {
191
+ if (this.pipWindow) this.pipWindow.addEventListener("unload", () => {
192
192
  this.handleDocumentPiPClose()
193
193
  })
194
194
 
195
- this.pipWindow.addEventListener("pagehide", () => {
195
+ if (this.pipWindow) this.pipWindow.addEventListener("pagehide", () => {
196
196
  this.handleDocumentPiPClose()
197
197
  })
198
198
  } catch (error) {
@@ -122,12 +122,14 @@ export class PlaylistManager {
122
122
  indicesToRemove.forEach((index) => {
123
123
  if (index >= 0 && index < this.items.length) {
124
124
  const [removedItem] = this.items.splice(index, 1)
125
- const [originalItem] = this.originalOrder.splice(
126
- this.originalOrder.findIndex((item) => item.id === removedItem.id),
127
- 1
128
- )
125
+ if (removedItem) {
126
+ const originalIndex = this.originalOrder.findIndex((item) => item.id === removedItem.id)
127
+ if (originalIndex >= 0) {
128
+ this.originalOrder.splice(originalIndex, 1)
129
+ }
129
130
 
130
- removedItems.push(removedItem)
131
+ removedItems.push(removedItem)
132
+ }
131
133
 
132
134
  // Adjust current index
133
135
  if (index < this.currentIndex) {
@@ -171,6 +173,7 @@ export class PlaylistManager {
171
173
  }
172
174
 
173
175
  const [movedItem] = this.items.splice(fromIndex, 1)
176
+ if (!movedItem) return false
174
177
  this.items.splice(toIndex, 0, movedItem)
175
178
 
176
179
  // Update current index
@@ -202,7 +205,7 @@ export class PlaylistManager {
202
205
  */
203
206
  getCurrentItem(): PlaylistItem | null {
204
207
  return this.currentIndex >= 0 && this.currentIndex < this.items.length
205
- ? this.items[this.currentIndex]
208
+ ? this.items[this.currentIndex] || null
206
209
  : null
207
210
  }
208
211
 
@@ -225,7 +228,7 @@ export class PlaylistManager {
225
228
  this.emit("currentItemChanged", {
226
229
  currentItem: this.items[index],
227
230
  currentIndex: index,
228
- previousItem: previousIndex >= 0 ? this.items[previousIndex] : undefined
231
+ previousItem: previousIndex >= 0 ? this.items[previousIndex] || undefined : undefined
229
232
  })
230
233
  return true
231
234
  }
@@ -245,7 +248,7 @@ export class PlaylistManager {
245
248
  */
246
249
  getNext(): PlaylistItem | null {
247
250
  const nextIndex = this.getNextIndex()
248
- return nextIndex >= 0 ? this.items[nextIndex] : null
251
+ return nextIndex >= 0 && nextIndex < this.items.length ? this.items[nextIndex] || null : null
249
252
  }
250
253
 
251
254
  /**
@@ -253,7 +256,7 @@ export class PlaylistManager {
253
256
  */
254
257
  getPrevious(): PlaylistItem | null {
255
258
  const prevIndex = this.getPreviousIndex()
256
- return prevIndex >= 0 ? this.items[prevIndex] : null
259
+ return prevIndex >= 0 && prevIndex < this.items.length ? this.items[prevIndex] || null : null
257
260
  }
258
261
 
259
262
  /**
@@ -271,9 +274,9 @@ export class PlaylistManager {
271
274
  // Check history first for better navigation
272
275
  if (this.historyIndex > 0) {
273
276
  this.historyIndex--
274
- const historyIndex = this.history[this.historyIndex]
275
- if (historyIndex >= 0 && historyIndex < this.items.length) {
276
- this.currentIndex = historyIndex
277
+ const histIndex = this.history[this.historyIndex]
278
+ if (histIndex !== undefined && histIndex >= 0 && histIndex < this.items.length) {
279
+ this.currentIndex = histIndex
277
280
  this.emit("currentItemChanged", {
278
281
  currentItem: this.items[this.currentIndex],
279
282
  currentIndex: this.currentIndex
@@ -297,7 +300,7 @@ export class PlaylistManager {
297
300
  * Get item by index
298
301
  */
299
302
  getItem(index: number): PlaylistItem | null {
300
- return index >= 0 && index < this.items.length ? this.items[index] : null
303
+ return index >= 0 && index < this.items.length ? this.items[index] || null : null
301
304
  }
302
305
 
303
306
  /**
@@ -440,17 +443,22 @@ export class PlaylistManager {
440
443
  // Fisher-Yates shuffle
441
444
  for (let i = this.items.length - 1; i > 0; i--) {
442
445
  const j = Math.floor(Math.random() * (i + 1))
443
- ;[this.items[i], this.items[j]] = [this.items[j], this.items[i]]
446
+ const temp = this.items[i]
447
+ if (temp && this.items[j]) {
448
+ this.items[i] = this.items[j]
449
+ this.items[j] = temp
450
+ }
444
451
  }
445
452
 
446
453
  // Move current item to the beginning if it exists
447
454
  if (currentItem) {
448
455
  const currentItemIndex = this.items.findIndex((item) => item.id === currentItem.id)
449
456
  if (currentItemIndex > 0) {
450
- ;[this.items[0], this.items[currentItemIndex]] = [
451
- this.items[currentItemIndex],
452
- this.items[0]
453
- ]
457
+ const temp = this.items[0]
458
+ if (temp && this.items[currentItemIndex]) {
459
+ this.items[0] = this.items[currentItemIndex]
460
+ this.items[currentItemIndex] = temp
461
+ }
454
462
  }
455
463
  this.currentIndex = 0
456
464
  }
@@ -376,7 +376,7 @@ export class SubtitleManager {
376
376
  output += `${startTime} --> ${endTime}`
377
377
 
378
378
  if (cue.settings) {
379
- const settings = []
379
+ const settings: string[] = []
380
380
  Object.entries(cue.settings).forEach(([key, value]) => {
381
381
  settings.push(`${key}:${value}`)
382
382
  })
@@ -69,11 +69,13 @@ export class SubtitleParser {
69
69
  if (lines[0]?.startsWith("WEBVTT")) {
70
70
  i = 1
71
71
  // Parse metadata in header
72
- while (i < lines.length && lines[i].trim() !== "") {
73
- const line = lines[i].trim()
72
+ while (i < lines.length && lines[i]?.trim() !== "") {
73
+ const line = lines[i]?.trim() || ""
74
74
  if (line.includes(":")) {
75
75
  const [key, value] = line.split(":").map((s) => s.trim())
76
- metadata[key.toLowerCase()] = value
76
+ if (key && value) {
77
+ metadata[key.toLowerCase()] = value
78
+ }
77
79
  }
78
80
  i++
79
81
  }
@@ -91,13 +93,13 @@ export class SubtitleParser {
91
93
  let timingLine: string
92
94
 
93
95
  // Check if this line is a timing line or cue identifier
94
- const isTimingLine = lines[i].includes("-->")
96
+ const isTimingLine = lines[i]?.includes("-->")
95
97
  if (isTimingLine) {
96
- timingLine = lines[i]
98
+ timingLine = lines[i] || ""
97
99
  i++
98
100
  } else {
99
101
  // This is a cue identifier
100
- cueId = lines[i].trim()
102
+ cueId = lines[i]?.trim()
101
103
  i++
102
104
  timingLine = lines[i] || ""
103
105
  i++
@@ -113,7 +115,8 @@ export class SubtitleParser {
113
115
  // Collect cue text
114
116
  const textLines: string[] = []
115
117
  while (i < lines.length && lines[i]?.trim() !== "") {
116
- textLines.push(lines[i])
118
+ const line = lines[i]
119
+ if (line) textLines.push(line)
117
120
  i++
118
121
  }
119
122
 
@@ -148,7 +151,7 @@ export class SubtitleParser {
148
151
  if (lines.length < 3) continue
149
152
 
150
153
  const id = lines[0]
151
- const timingLine = lines[1]
154
+ const timingLine = lines[1] || ""
152
155
  const textLines = lines.slice(2)
153
156
 
154
157
  const timing = this.parseSRTTiming(timingLine)
@@ -180,8 +183,8 @@ export class SubtitleParser {
180
183
  return null
181
184
  }
182
185
 
183
- const startTime = this.parseTimeString(parts[arrowIndex - 1])
184
- const endTime = this.parseTimeString(parts[arrowIndex + 1])
186
+ const startTime = this.parseTimeString(parts[arrowIndex - 1] || "")
187
+ const endTime = this.parseTimeString(parts[arrowIndex + 1] || "")
185
188
 
186
189
  if (startTime === null || endTime === null) {
187
190
  return null
@@ -235,8 +238,8 @@ export class SubtitleParser {
235
238
  const match = timingLine.match(/(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/)
236
239
  if (!match) return null
237
240
 
238
- const startTime = this.parseTimeString(match[1].replace(",", "."))
239
- const endTime = this.parseTimeString(match[2].replace(",", "."))
241
+ const startTime = this.parseTimeString(match[1]?.replace(",", ".") || "")
242
+ const endTime = this.parseTimeString(match[2]?.replace(",", ".") || "")
240
243
 
241
244
  if (startTime === null || endTime === null) return null
242
245
 
@@ -250,10 +253,10 @@ export class SubtitleParser {
250
253
  const match = timeStr.match(/(\d{2}):(\d{2}):(\d{2})\.(\d{3})/)
251
254
  if (!match) return null
252
255
 
253
- const hours = parseInt(match[1])
254
- const minutes = parseInt(match[2])
255
- const seconds = parseInt(match[3])
256
- const milliseconds = parseInt(match[4])
256
+ const hours = parseInt(match[1] || "0", 10)
257
+ const minutes = parseInt(match[2] || "0", 10)
258
+ const seconds = parseInt(match[3] || "0", 10)
259
+ const milliseconds = parseInt(match[4] || "0", 10)
257
260
 
258
261
  return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000
259
262
  }
@@ -393,6 +393,11 @@ export class ThumbnailGenerator {
393
393
  reject(new Error("Seek timeout"))
394
394
  }, 5000)
395
395
 
396
+ if (!this.videoElement) {
397
+ reject(new Error("Video element not initialized"))
398
+ return
399
+ }
400
+
396
401
  this.videoElement.addEventListener("seeked", onSeeked)
397
402
  this.videoElement.addEventListener("error", onError)
398
403
  this.videoElement.currentTime = Math.max(0, Math.min(time, this.videoElement.duration))
@@ -436,12 +441,14 @@ export class ThumbnailGenerator {
436
441
  private addToCache(key: string, thumbnail: GeneratedThumbnail): void {
437
442
  // Remove oldest items if cache is full
438
443
  if (this.cache.size >= this.config.maxCacheSize) {
439
- const oldestKey = this.cache.keys().next().value
440
- const oldThumbnail = this.cache.get(oldestKey)
441
- if (oldThumbnail) {
442
- URL.revokeObjectURL(oldThumbnail.objectUrl)
444
+ const oldestKey = this.cache.keys().next().value as string | undefined
445
+ if (oldestKey) {
446
+ const oldThumbnail = this.cache.get(oldestKey)
447
+ if (oldThumbnail) {
448
+ URL.revokeObjectURL(oldThumbnail.objectUrl)
449
+ }
450
+ this.cache.delete(oldestKey)
443
451
  }
444
- this.cache.delete(oldestKey)
445
452
  }
446
453
 
447
454
  this.cache.set(key, thumbnail)
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest"
2
+ import { renderHook, act } from "@testing-library/react"
3
+ import { useVideoPlayer } from "../useVideoPlayer"
4
+ import React from "react"
5
+
6
+ describe("currentTime non-finite value fix", () => {
7
+ let mockVideoElement: Partial<HTMLVideoElement>
8
+ let containerRef: React.RefObject<HTMLDivElement>
9
+
10
+ beforeEach(() => {
11
+ // Create a mock div element
12
+ const mockDiv = {
13
+ requestFullscreen: vi.fn()
14
+ } as unknown as HTMLDivElement
15
+
16
+ mockVideoElement = {
17
+ currentTime: NaN,
18
+ duration: NaN,
19
+ volume: 1,
20
+ muted: false,
21
+ play: vi.fn().mockResolvedValue(undefined),
22
+ pause: vi.fn(),
23
+ addEventListener: vi.fn(),
24
+ removeEventListener: vi.fn()
25
+ }
26
+
27
+ containerRef = {
28
+ current: mockDiv
29
+ }
30
+
31
+ vi.spyOn(global, "document" as any).mockReturnValue({
32
+ querySelector: vi.fn().mockReturnValue(mockVideoElement)
33
+ })
34
+ })
35
+
36
+ it("should handle NaN duration when seeking forward", () => {
37
+ const { result } = renderHook(() => useVideoPlayer(containerRef))
38
+
39
+ const videoRef = result.current.videoRef
40
+ if (videoRef.current) {
41
+ Object.assign(videoRef.current, mockVideoElement)
42
+ }
43
+
44
+ act(() => {
45
+ result.current.controls.seekForward(10)
46
+ })
47
+
48
+ // Should not throw error and should not set non-finite value
49
+ expect(mockVideoElement.currentTime).toBeNaN()
50
+ })
51
+
52
+ it("should handle NaN currentTime when seeking backward", () => {
53
+ mockVideoElement.currentTime = NaN
54
+
55
+ const { result } = renderHook(() => useVideoPlayer(containerRef))
56
+
57
+ const videoRef = result.current.videoRef
58
+ if (videoRef.current) {
59
+ Object.assign(videoRef.current, mockVideoElement)
60
+ }
61
+
62
+ act(() => {
63
+ result.current.controls.seekBackward(10)
64
+ })
65
+
66
+ // Should handle NaN gracefully - sets to 0
67
+ expect(() => result.current.controls.seekBackward(10)).not.toThrow()
68
+ })
69
+
70
+ it("should handle valid values correctly", () => {
71
+ mockVideoElement.currentTime = 50
72
+ mockVideoElement.duration = 100
73
+
74
+ const { result } = renderHook(() => useVideoPlayer(containerRef))
75
+
76
+ const videoRef = result.current.videoRef
77
+ if (videoRef.current) {
78
+ Object.assign(videoRef.current, mockVideoElement)
79
+
80
+ // Mock the currentTime setter to track calls
81
+ let setTimeValue: number | undefined
82
+ Object.defineProperty(videoRef.current, "currentTime", {
83
+ get: () => mockVideoElement.currentTime,
84
+ set: (value: number) => {
85
+ setTimeValue = value
86
+ mockVideoElement.currentTime = value
87
+ },
88
+ configurable: true
89
+ })
90
+
91
+ act(() => {
92
+ result.current.controls.seekForward(10)
93
+ })
94
+
95
+ // Should set valid time value
96
+ expect(setTimeValue).toBe(60)
97
+ }
98
+ })
99
+
100
+ it("should prevent seeking when duration is 0", () => {
101
+ mockVideoElement.currentTime = 10
102
+ mockVideoElement.duration = 0
103
+
104
+ const { result } = renderHook(() => useVideoPlayer(containerRef))
105
+
106
+ const videoRef = result.current.videoRef
107
+ if (videoRef.current) {
108
+ Object.assign(videoRef.current, mockVideoElement)
109
+
110
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
111
+
112
+ act(() => {
113
+ result.current.controls.seekForward(10)
114
+ })
115
+
116
+ expect(consoleSpy).toHaveBeenCalledWith("Cannot seek forward: duration is not available")
117
+ consoleSpy.mockRestore()
118
+ }
119
+ })
120
+
121
+ it("should clamp seek values to valid range", () => {
122
+ mockVideoElement.currentTime = 95
123
+ mockVideoElement.duration = 100
124
+
125
+ const { result } = renderHook(() => useVideoPlayer(containerRef))
126
+
127
+ const videoRef = result.current.videoRef
128
+ if (videoRef.current) {
129
+ Object.assign(videoRef.current, mockVideoElement)
130
+
131
+ let setTimeValue: number | undefined
132
+ Object.defineProperty(videoRef.current, "currentTime", {
133
+ get: () => mockVideoElement.currentTime,
134
+ set: (value: number) => {
135
+ setTimeValue = value
136
+ mockVideoElement.currentTime = value
137
+ },
138
+ configurable: true
139
+ })
140
+
141
+ act(() => {
142
+ result.current.controls.seekForward(10)
143
+ })
144
+
145
+ // Should clamp to duration (100, not 105)
146
+ expect(setTimeValue).toBe(100)
147
+ }
148
+ })
149
+ })
@@ -139,7 +139,7 @@ export function useFocusVisible() {
139
139
  if (typeof window === "undefined") return
140
140
 
141
141
  let hadKeyboardEvent = true
142
- let keyboardThrottleTimeoutID: NodeJS.Timeout
142
+ let keyboardThrottleTimeoutID: NodeJS.Timeout | undefined
143
143
 
144
144
  const handleKeyDown = (e: KeyboardEvent) => {
145
145
  if (e.metaKey || e.altKey || e.ctrlKey) {
@@ -92,13 +92,13 @@ export function useFocusManagement({
92
92
  // Shift + Tab
93
93
  if (document.activeElement === firstFocusable) {
94
94
  event.preventDefault()
95
- lastFocusable.focus()
95
+ lastFocusable?.focus()
96
96
  }
97
97
  } else {
98
98
  // Tab
99
99
  if (document.activeElement === lastFocusable) {
100
100
  event.preventDefault()
101
- firstFocusable.focus()
101
+ firstFocusable?.focus()
102
102
  }
103
103
  }
104
104
  }
@@ -57,10 +57,10 @@ export function useKeyboardShortcuts({
57
57
 
58
58
  useEffect(() => {
59
59
  const element = target || document
60
- element.addEventListener("keydown", handleKeyDown)
60
+ element.addEventListener("keydown", handleKeyDown as EventListener)
61
61
 
62
62
  return () => {
63
- element.removeEventListener("keydown", handleKeyDown)
63
+ element.removeEventListener("keydown", handleKeyDown as EventListener)
64
64
  }
65
65
  }, [handleKeyDown, target])
66
66
 
@@ -12,7 +12,7 @@ export function useScreenReader({
12
12
  debounceMs = 100
13
13
  }: UseScreenReaderOptions = {}) {
14
14
  const liveRegionRef = useRef<HTMLDivElement | null>(null)
15
- const timeoutRef = useRef<NodeJS.Timeout>()
15
+ const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
16
16
 
17
17
  // Create or get the live region for announcements
18
18
  const getLiveRegion = useCallback((priority: AnnouncementPriority) => {