@therealtinhtute/baroiplayer 1.1.0 → 1.2.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.
- package/dist/core-plugins-By-4-kGs.cjs +2 -0
- package/dist/core-plugins-By-4-kGs.cjs.map +1 -0
- package/dist/{core-plugins-CROGvQUy.js → core-plugins-DSBWcTuX.js} +444 -309
- package/dist/core-plugins-DSBWcTuX.js.map +1 -0
- package/dist/core.js +2 -2
- package/dist/core.mjs +19 -19
- package/dist/{enhanced-components-DUw2KrmQ.js → enhanced-components-B_l3cist.js} +2 -2
- package/dist/{enhanced-components-DUw2KrmQ.js.map → enhanced-components-B_l3cist.js.map} +1 -1
- package/dist/{enhanced-components-DXBCHU0v.cjs → enhanced-components-BspFs9OO.cjs} +2 -2
- package/dist/{enhanced-components-DXBCHU0v.cjs.map → enhanced-components-BspFs9OO.cjs.map} +1 -1
- package/dist/hooks-BXwI8QG6.cjs +4 -0
- package/dist/hooks-BXwI8QG6.cjs.map +1 -0
- package/dist/hooks-Bpi-jDiO.js +1816 -0
- package/dist/hooks-Bpi-jDiO.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -20
- package/dist/index.mjs.map +1 -1
- package/dist/{minimal-components-DlRK7Qu_.js → minimal-components-C4z900Ro.js} +297 -298
- package/dist/minimal-components-C4z900Ro.js.map +1 -0
- package/dist/minimal-components-v4qBp6jv.cjs +2 -0
- package/dist/minimal-components-v4qBp6jv.cjs.map +1 -0
- package/package.json +1 -1
- package/src/components/accessibility/keyboard-help-overlay.tsx +2 -0
- package/src/components/enhanced/debug-panel.tsx +1 -1
- package/src/components/enhanced/enhanced-media-player.tsx +68 -25
- package/src/components/legacy.tsx +6 -6
- package/src/components/media-player-video.tsx +5 -12
- package/src/components/media-player.tsx +2 -2
- package/src/components/ui/enhanced-seek-bar.tsx +2 -2
- package/src/components/ui/media-resource-preloader.tsx +21 -23
- package/src/components/unified/base-media-player.tsx +22 -10
- package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +58 -38
- package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +6 -6
- package/src/core/optimization/bundle-optimizer.tsx +26 -27
- package/src/core/plugins/__tests__/registry.test.ts +1 -1
- package/src/core/plugins/enhanced-plugin-system.ts +26 -8
- package/src/core/plugins/registry.ts +319 -92
- package/src/core/plugins/types.ts +21 -3
- package/src/core/providers/enhanced-hls-provider.ts +11 -40
- package/src/core/providers/lazy-provider.tsx +5 -42
- package/src/features/media-session/media-session-manager.ts +4 -26
- package/src/features/picture-in-picture/pip-manager.ts +2 -2
- package/src/features/playlist/playlist-manager.ts +26 -18
- package/src/features/subtitles/subtitle-manager.ts +1 -1
- package/src/features/subtitles/subtitle-parser.ts +19 -16
- package/src/features/thumbnails/thumbnail-generator.ts +12 -5
- package/src/hooks/__tests__/currentTime-fix.test.ts +149 -0
- package/src/hooks/accessibility/use-accessibility-preferences.ts +1 -1
- package/src/hooks/accessibility/use-focus-management.ts +2 -2
- package/src/hooks/accessibility/use-keyboard-shortcuts.ts +2 -2
- package/src/hooks/accessibility/use-screen-reader.ts +1 -1
- package/src/hooks/core/index.ts +1 -1
- package/src/hooks/use-buffer-health.ts +1 -1
- package/src/hooks/use-enhanced-media-player.ts +107 -9
- package/src/hooks/use-focus-trap.tsx +2 -2
- package/src/hooks/use-intelligent-preloading.ts +23 -3
- package/src/hooks/use-keyboard-shortcuts.ts +2 -2
- package/src/hooks/use-media-player.ts +74 -43
- package/src/hooks/use-memory-optimization.ts +15 -15
- package/src/hooks/use-smart-preload.ts +1 -2
- package/src/hooks/useKeyboardShortcuts.ts +34 -2
- package/src/hooks/useVideoPlayer.ts +50 -4
- package/src/index-legacy.ts +3 -5
- package/src/test/accessibility-utils.ts +7 -7
- package/src/test/media-mocks.ts +41 -28
- package/src/test/setup.ts +22 -4
- package/src/types/index.ts +13 -0
- package/dist/core-plugins-CROGvQUy.js.map +0 -1
- package/dist/core-plugins-DtsHBOtF.cjs +0 -2
- package/dist/core-plugins-DtsHBOtF.cjs.map +0 -1
- package/dist/hooks-BW-JjVgP.cjs +0 -4
- package/dist/hooks-BW-JjVgP.cjs.map +0 -1
- package/dist/hooks-CyX6De5M.js +0 -1693
- package/dist/hooks-CyX6De5M.js.map +0 -1
- package/dist/minimal-components-Bdcpxav5.cjs +0 -2
- package/dist/minimal-components-Bdcpxav5.cjs.map +0 -1
- package/dist/minimal-components-DlRK7Qu_.js.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
|
|
213
|
-
if (
|
|
214
|
-
this.hls
|
|
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
|
|
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
|
|
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
|
|
1
|
+
import { type ReactNode, Suspense } from 'react'
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
import(
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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:
|
|
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<
|
|
330
|
+
additionalInfo?: Partial<CustomMediaMetadata>
|
|
343
331
|
): Promise<void> {
|
|
344
|
-
const metadata:
|
|
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
|
-
|
|
126
|
-
this.originalOrder.findIndex((item) => item.id === removedItem.id)
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
275
|
-
if (
|
|
276
|
-
this.currentIndex =
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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]
|
|
73
|
-
const line = lines[i]
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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]
|
|
239
|
-
const endTime = this.parseTimeString(match[2]
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
95
|
+
lastFocusable?.focus()
|
|
96
96
|
}
|
|
97
97
|
} else {
|
|
98
98
|
// Tab
|
|
99
99
|
if (document.activeElement === lastFocusable) {
|
|
100
100
|
event.preventDefault()
|
|
101
|
-
firstFocusable
|
|
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) => {
|