@therealtinhtute/baroiplayer 1.2.1 → 2.0.0-beta.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.
- package/README.md +90 -71
- package/dist/components/icons/index.d.ts +36 -0
- package/dist/components/icons/index.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/ArrowLeftIcon.d.ts +4 -0
- package/dist/components/icons/svg-icons/ArrowLeftIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/ArrowRightIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/ArrowRightIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/AudioIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/AudioIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/BackwardIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/BackwardIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/CameraIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/CameraIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/CheckIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/CheckIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/EnablePiPIcon.d.ts +4 -0
- package/dist/components/icons/svg-icons/EnablePiPIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/ForwardIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/ForwardIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/FullscreenEnterIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/FullscreenEnterIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/FullscreenExitIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/FullscreenExitIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/LoadingIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/LoadingIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/PauseIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/PauseIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/PlayIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/PlayIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/PlaybackSpeedIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/PlaybackSpeedIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/QualityIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/QualityIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/SettingsIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/SettingsIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/SliderIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/SliderIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/SubtitleIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/SubtitleIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/SubtitleOffIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/SubtitleOffIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/UnPiPIcon.d.ts +4 -0
- package/dist/components/icons/svg-icons/UnPiPIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/VolumeMutedIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/VolumeMutedIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/VolumeOneIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/VolumeOneIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/VolumeThreeIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/VolumeThreeIcon.d.ts.map +1 -0
- package/dist/components/icons/svg-icons/VolumeTwoIcon.d.ts +5 -0
- package/dist/components/icons/svg-icons/VolumeTwoIcon.d.ts.map +1 -0
- package/dist/components/variants/gradient/gradient-audio.d.ts +13 -0
- package/dist/components/variants/gradient/gradient-audio.d.ts.map +1 -0
- package/dist/components/variants/gradient/gradient-controls.d.ts +4 -0
- package/dist/components/variants/gradient/gradient-controls.d.ts.map +1 -0
- package/dist/components/variants/gradient/gradient-glyphs.d.ts +30 -0
- package/dist/components/variants/gradient/gradient-glyphs.d.ts.map +1 -0
- package/dist/components/variants/gradient/gradient-video.d.ts +16 -0
- package/dist/components/variants/gradient/gradient-video.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +750 -5640
- package/dist/index.mjs.map +1 -1
- package/dist/package.json +3 -0
- package/package.json +110 -114
- package/src/components/variants/gradient/gradient-audio.tsx +47 -0
- package/src/components/variants/gradient/gradient-controls.tsx +254 -0
- package/src/components/variants/gradient/gradient-glyphs.test.tsx +69 -0
- package/src/components/variants/gradient/gradient-glyphs.tsx +129 -0
- package/src/components/variants/gradient/gradient-skin.css +211 -0
- package/src/components/variants/gradient/gradient-video.tsx +70 -0
- package/src/index.ts +5 -198
- package/src/test/accessibility-utils.ts +0 -59
- package/dist/context/media.js +0 -1
- package/dist/context/media.mjs +0 -3
- package/dist/context/store.js +0 -1
- package/dist/context/store.mjs +0 -3
- package/dist/context-Bzd5QCKM.cjs +0 -7
- package/dist/context-Bzd5QCKM.cjs.map +0 -1
- package/dist/context-DD_SH4tT.js +0 -1053
- package/dist/context-DD_SH4tT.js.map +0 -1
- package/dist/core-events-BRnLMB7s.cjs +0 -2
- package/dist/core-events-BRnLMB7s.cjs.map +0 -1
- package/dist/core-events-v_mKdj5p.js +0 -417
- package/dist/core-events-v_mKdj5p.js.map +0 -1
- package/dist/core-plugins-CMBv-yVh.cjs +0 -2
- package/dist/core-plugins-CMBv-yVh.cjs.map +0 -1
- package/dist/core-plugins-CqCxLkbd.js +0 -754
- package/dist/core-plugins-CqCxLkbd.js.map +0 -1
- package/dist/core.js +0 -15
- package/dist/core.js.map +0 -1
- package/dist/core.mjs +0 -960
- package/dist/core.mjs.map +0 -1
- package/dist/enhanced-components-BwjBqSA1.js +0 -2858
- package/dist/enhanced-components-BwjBqSA1.js.map +0 -1
- package/dist/enhanced-components-C-ka-XLp.cjs +0 -2
- package/dist/enhanced-components-C-ka-XLp.cjs.map +0 -1
- package/dist/hooks-B-IIWPJr.js +0 -1842
- package/dist/hooks-B-IIWPJr.js.map +0 -1
- package/dist/hooks-DcHzmBwA.cjs +0 -4
- package/dist/hooks-DcHzmBwA.cjs.map +0 -1
- package/dist/minimal-components-BYsMQde5.js +0 -1610
- package/dist/minimal-components-BYsMQde5.js.map +0 -1
- package/dist/minimal-components-CsxQbDp7.cjs +0 -2
- package/dist/minimal-components-CsxQbDp7.cjs.map +0 -1
- package/dist/minimal-components.css +0 -1
- package/dist/mona-loading.gif +0 -0
- package/dist/performance-monitor-DCSijLJg.js +0 -312
- package/dist/performance-monitor-DCSijLJg.js.map +0 -1
- package/dist/performance-monitor-wNFKmqMr.cjs +0 -2
- package/dist/performance-monitor-wNFKmqMr.cjs.map +0 -1
- package/dist/rolldown-runtime-BmowowmI.js +0 -14
- package/dist/rolldown-runtime-mLOUI7ql.cjs +0 -1
- package/dist/utils-D2gQ7c2R.js +0 -64
- package/dist/utils-D2gQ7c2R.js.map +0 -1
- package/dist/utils-DJaAwiZ9.cjs +0 -2
- package/dist/utils-DJaAwiZ9.cjs.map +0 -1
- package/dist/utils.js +0 -1
- package/dist/utils.mjs +0 -3
- package/src/adapters/videojs-adapter.tsx +0 -470
- package/src/assets/mona-loading.gif +0 -0
- package/src/components/AnimateLoading.tsx +0 -111
- package/src/components/LoadingState.tsx +0 -239
- package/src/components/__tests__/media-player-accessibility.test.tsx +0 -510
- package/src/components/__tests__/media-player-react19.test.tsx +0 -457
- package/src/components/accessibility/keyboard-help-overlay.tsx +0 -207
- package/src/components/audio-player.tsx +0 -85
- package/src/components/basic/index.ts +0 -37
- package/src/components/enhanced/advanced-features.tsx +0 -311
- package/src/components/enhanced/debug-panel.tsx +0 -448
- package/src/components/enhanced/enhanced-controls.tsx +0 -597
- package/src/components/enhanced/enhanced-media-player.tsx +0 -566
- package/src/components/enhanced/error-handling.tsx +0 -338
- package/src/components/enhanced/index.ts +0 -39
- package/src/components/enhanced/media-player-enhanced.tsx +0 -383
- package/src/components/examples/basic-video-player.tsx +0 -50
- package/src/components/examples/custom-controls-example.tsx +0 -63
- package/src/components/examples/enhanced-controls-example.tsx +0 -100
- package/src/components/examples/enhanced-media-player-example.tsx +0 -302
- package/src/components/examples/loading-states-demo.tsx +0 -141
- package/src/components/examples/react-19-demo.tsx +0 -349
- package/src/components/examples/volume-orientation-demo.tsx +0 -105
- package/src/components/icon-demo.tsx +0 -196
- package/src/components/legacy.tsx +0 -87
- package/src/components/media-metadata-display.tsx +0 -99
- package/src/components/media-player-audio.tsx +0 -39
- package/src/components/media-player-controls.tsx +0 -223
- package/src/components/media-player-error.tsx +0 -163
- package/src/components/media-player-fullscreen.tsx +0 -109
- package/src/components/media-player-hls.tsx +0 -111
- package/src/components/media-player-loading.tsx +0 -76
- package/src/components/media-player-play.tsx +0 -75
- package/src/components/media-player-root.tsx +0 -338
- package/src/components/media-player-seek.tsx +0 -168
- package/src/components/media-player-time.tsx +0 -99
- package/src/components/media-player-tooltip.tsx +0 -58
- package/src/components/media-player-video.tsx +0 -54
- package/src/components/media-player-volume.tsx +0 -155
- package/src/components/media-player.tsx +0 -292
- package/src/components/media-suspense.tsx +0 -344
- package/src/components/player-settings-form.tsx +0 -158
- package/src/components/react-player-wrapper.tsx +0 -651
- package/src/components/simple-video-player.tsx +0 -62
- package/src/components/ui/blur-poster.tsx +0 -96
- package/src/components/ui/empty-state.tsx +0 -91
- package/src/components/ui/enhanced-seek-bar.tsx +0 -254
- package/src/components/ui/error-state.tsx +0 -124
- package/src/components/ui/fullscreen-button.tsx +0 -81
- package/src/components/ui/loading-skeleton.tsx +0 -107
- package/src/components/ui/media-resource-preloader.tsx +0 -458
- package/src/components/ui/offline-message.tsx +0 -98
- package/src/components/ui/play-button.tsx +0 -99
- package/src/components/ui/seek-bar.tsx +0 -191
- package/src/components/ui/time-display.tsx +0 -61
- package/src/components/ui/volume-control.tsx +0 -190
- package/src/components/unified/base-media-player.tsx +0 -252
- package/src/components/unified/media-player.tsx +0 -75
- package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +0 -587
- package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +0 -573
- package/src/components/variants/minimal/MinimalControls.tsx +0 -124
- package/src/components/variants/minimal/MinimalLoading.tsx +0 -28
- package/src/components/variants/minimal/MinimalMediaPlayer.tsx +0 -318
- package/src/components/variants/minimal/MinimalOverlay.tsx +0 -58
- package/src/components/variants/minimal/MinimalPlayButton.tsx +0 -56
- package/src/components/variants/minimal/MinimalSeekBar.tsx +0 -140
- package/src/components/variants/minimal/MinimalVolumeControl.tsx +0 -75
- package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +0 -206
- package/src/components/variants/minimal/components/CenterPlayButton.tsx +0 -22
- package/src/components/variants/minimal/components/ControlBar.tsx +0 -142
- package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +0 -412
- package/src/components/variants/minimal/components/PlaybackControls.tsx +0 -46
- package/src/components/variants/minimal/components/RightControls.tsx +0 -164
- package/src/components/variants/minimal/components/SeekBar.tsx +0 -127
- package/src/components/variants/minimal/components/SettingsDropdown.tsx +0 -68
- package/src/components/variants/minimal/components/SubtitleDisplay.tsx +0 -21
- package/src/components/variants/minimal/components/TimeDisplay.tsx +0 -15
- package/src/components/variants/minimal/components/VideoElement.tsx +0 -35
- package/src/components/variants/minimal/components/VolumeControl.tsx +0 -220
- package/src/components/variants/minimal/index.ts +0 -23
- package/src/components/video-player.tsx +0 -100
- package/src/context/__tests__/media-store.test.ts +0 -509
- package/src/context/error-handling.ts +0 -398
- package/src/context/media-context.tsx +0 -254
- package/src/context/media-provider.tsx +0 -264
- package/src/context/media-store.ts +0 -387
- package/src/context/persistence.ts +0 -462
- package/src/core/events/event-system.ts +0 -448
- package/src/core/events/hooks.ts +0 -325
- package/src/core/events/index.ts +0 -22
- package/src/core/events/middleware.ts +0 -286
- package/src/core/index.ts +0 -22
- package/src/core/optimization/bundle-optimizer.tsx +0 -312
- package/src/core/performance/cicd.ts +0 -98
- package/src/core/performance/index.ts +0 -38
- package/src/core/performance/media-loading-optimizer.ts +0 -545
- package/src/core/performance/performance-budgets.ts +0 -437
- package/src/core/performance/performance-monitor.tsx +0 -488
- package/src/core/performance/performance-testing.ts +0 -461
- package/src/core/plugins/__tests__/registry.test.ts +0 -595
- package/src/core/plugins/base-plugin.ts +0 -225
- package/src/core/plugins/enhanced-plugin-system.ts +0 -435
- package/src/core/plugins/examples/analytics-plugin.ts +0 -241
- package/src/core/plugins/index.ts +0 -39
- package/src/core/plugins/registry.ts +0 -546
- package/src/core/plugins/types.ts +0 -169
- package/src/core/plugins/utils.ts +0 -317
- package/src/core/progressive-loading.tsx +0 -331
- package/src/core/providers/enhanced-hls-provider.ts +0 -307
- package/src/core/providers/lazy-provider.tsx +0 -14
- package/src/core/providers/service-provider.tsx +0 -164
- package/src/core/utils.tsx +0 -34
- package/src/features/media-session/media-session-manager.ts +0 -411
- package/src/features/picture-in-picture/pip-manager.ts +0 -428
- package/src/features/playlist/playlist-manager.ts +0 -626
- package/src/features/subtitles/subtitle-manager.ts +0 -435
- package/src/features/subtitles/subtitle-parser.ts +0 -284
- package/src/features/thumbnails/thumbnail-generator.ts +0 -528
- package/src/hooks/__tests__/currentTime-fix.test.ts +0 -149
- package/src/hooks/__tests__/use-media-player.test.ts +0 -712
- package/src/hooks/__tests__/use-media-state.test.tsx +0 -316
- package/src/hooks/accessibility/index.ts +0 -27
- package/src/hooks/accessibility/use-accessibility-preferences.ts +0 -190
- package/src/hooks/accessibility/use-focus-management.ts +0 -183
- package/src/hooks/accessibility/use-keyboard-shortcuts.ts +0 -152
- package/src/hooks/accessibility/use-screen-reader.ts +0 -212
- package/src/hooks/core/index.ts +0 -47
- package/src/hooks/legacy.ts +0 -21
- package/src/hooks/use-buffer-health.ts +0 -218
- package/src/hooks/use-enhanced-media-player.ts +0 -575
- package/src/hooks/use-focus-trap.tsx +0 -162
- package/src/hooks/use-intelligent-preloading.ts +0 -387
- package/src/hooks/use-keyboard-controls.ts +0 -115
- package/src/hooks/use-keyboard-shortcuts.ts +0 -255
- package/src/hooks/use-media-actions.ts +0 -184
- package/src/hooks/use-media-metadata.ts +0 -91
- package/src/hooks/use-media-optimistic.ts +0 -264
- package/src/hooks/use-media-player.ts +0 -343
- package/src/hooks/use-media-state.ts +0 -194
- package/src/hooks/use-memory-optimization.ts +0 -360
- package/src/hooks/use-network-status.ts +0 -124
- package/src/hooks/use-optimistic-controls.ts +0 -95
- package/src/hooks/use-optimized-media.ts +0 -283
- package/src/hooks/use-smart-preload.ts +0 -229
- package/src/hooks/use-touch-gestures.ts +0 -461
- package/src/hooks/useEnhancedSubtitles.ts +0 -258
- package/src/hooks/useKeyboardShortcuts.ts +0 -106
- package/src/hooks/useMediaControls.ts +0 -52
- package/src/hooks/useScreenshot.ts +0 -77
- package/src/hooks/useSubtitles.ts +0 -91
- package/src/hooks/useVideoPlayer.ts +0 -278
- package/src/icons/components/FullscreenIcon.tsx +0 -36
- package/src/icons/components/SettingsIcon.tsx +0 -37
- package/src/icons/components/animated/LoadingIcon.tsx +0 -27
- package/src/icons/components/animated/PlayPauseIcon.tsx +0 -42
- package/src/icons/components/media/PauseIcon.tsx +0 -38
- package/src/icons/components/media/PlayIcon.tsx +0 -37
- package/src/icons/components/volume/VolumeIcon.tsx +0 -38
- package/src/icons/components/volume/VolumeOffIcon.tsx +0 -38
- package/src/icons/index.ts +0 -56
- package/src/icons/provider.tsx +0 -58
- package/src/icons/types.ts +0 -60
- package/src/index-enhanced.ts +0 -35
- package/src/index-legacy.ts +0 -324
- package/src/index.css +0 -102
- package/src/lib/__tests__/async-utils.test.ts +0 -291
- package/src/lib/async-utils.ts +0 -194
- package/src/lib/deprecation.ts +0 -51
- package/src/lib/format-time.ts +0 -37
- package/src/lib/index.ts +0 -4
- package/src/lib/utils.ts +0 -6
- package/src/styles/globals.css +0 -462
- package/src/styles/minimal-player.css +0 -621
- package/src/styles/pixel-art.css +0 -41
- package/src/styles/videojs.css +0 -100
- package/src/types/assets.d.ts +0 -29
- package/src/types/events.ts +0 -47
- package/src/types/formats.ts +0 -79
- package/src/types/index.ts +0 -203
- package/src/types/videojs-plugins.d.ts +0 -14
|
@@ -1,528 +0,0 @@
|
|
|
1
|
-
export interface ThumbnailOptions {
|
|
2
|
-
width?: number
|
|
3
|
-
height?: number
|
|
4
|
-
quality?: number
|
|
5
|
-
format?: "jpeg" | "png" | "webp"
|
|
6
|
-
time?: number
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ThumbnailConfig {
|
|
10
|
-
defaultWidth: number
|
|
11
|
-
defaultHeight: number
|
|
12
|
-
defaultQuality: number
|
|
13
|
-
defaultFormat: "jpeg" | "png" | "webp"
|
|
14
|
-
maxCacheSize: number
|
|
15
|
-
enableCaching: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface GeneratedThumbnail {
|
|
19
|
-
blob: Blob
|
|
20
|
-
dataUrl: string
|
|
21
|
-
objectUrl: string
|
|
22
|
-
width: number
|
|
23
|
-
height: number
|
|
24
|
-
timestamp: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ThumbnailSprite {
|
|
28
|
-
image: HTMLImageElement
|
|
29
|
-
thumbnails: Array<{
|
|
30
|
-
x: number
|
|
31
|
-
y: number
|
|
32
|
-
width: number
|
|
33
|
-
height: number
|
|
34
|
-
time: number
|
|
35
|
-
}>
|
|
36
|
-
spriteWidth: number
|
|
37
|
-
spriteHeight: number
|
|
38
|
-
thumbnailWidth: number
|
|
39
|
-
thumbnailHeight: number
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class ThumbnailGenerator {
|
|
43
|
-
private config: ThumbnailConfig
|
|
44
|
-
private cache: Map<string, GeneratedThumbnail> = new Map()
|
|
45
|
-
private videoElement: HTMLVideoElement | null = null
|
|
46
|
-
private canvas: HTMLCanvasElement
|
|
47
|
-
private context: CanvasRenderingContext2D
|
|
48
|
-
|
|
49
|
-
constructor(config?: Partial<ThumbnailConfig>) {
|
|
50
|
-
this.config = {
|
|
51
|
-
defaultWidth: 160,
|
|
52
|
-
defaultHeight: 90,
|
|
53
|
-
defaultQuality: 0.8,
|
|
54
|
-
defaultFormat: "jpeg",
|
|
55
|
-
maxCacheSize: 50,
|
|
56
|
-
enableCaching: true,
|
|
57
|
-
...config
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
this.canvas = document.createElement("canvas")
|
|
61
|
-
const ctx = this.canvas.getContext("2d")
|
|
62
|
-
if (!ctx) {
|
|
63
|
-
throw new Error("Canvas 2D context not supported")
|
|
64
|
-
}
|
|
65
|
-
this.context = ctx
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Set the video element to generate thumbnails from
|
|
70
|
-
*/
|
|
71
|
-
setVideoElement(video: HTMLVideoElement): void {
|
|
72
|
-
this.videoElement = video
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Generate thumbnail at specific time
|
|
77
|
-
*/
|
|
78
|
-
async generateThumbnail(
|
|
79
|
-
time: number,
|
|
80
|
-
options: ThumbnailOptions = {}
|
|
81
|
-
): Promise<GeneratedThumbnail> {
|
|
82
|
-
if (!this.videoElement) {
|
|
83
|
-
throw new Error("Video element not set")
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const cacheKey = this.getCacheKey(time, options)
|
|
87
|
-
|
|
88
|
-
// Check cache first
|
|
89
|
-
if (this.config.enableCaching && this.cache.has(cacheKey)) {
|
|
90
|
-
const cached = this.cache.get(cacheKey)!
|
|
91
|
-
return { ...cached }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const width = options.width || this.config.defaultWidth
|
|
95
|
-
const height = options.height || this.config.defaultHeight
|
|
96
|
-
const quality = options.quality || this.config.defaultQuality
|
|
97
|
-
const format = options.format || this.config.defaultFormat
|
|
98
|
-
|
|
99
|
-
// Setup canvas
|
|
100
|
-
this.canvas.width = width
|
|
101
|
-
this.canvas.height = height
|
|
102
|
-
|
|
103
|
-
// Set video time and wait for seek
|
|
104
|
-
const originalTime = this.videoElement.currentTime
|
|
105
|
-
await this.seekToTime(time)
|
|
106
|
-
|
|
107
|
-
// Draw video frame to canvas
|
|
108
|
-
this.context.clearRect(0, 0, width, height)
|
|
109
|
-
|
|
110
|
-
// Calculate aspect ratio preserving dimensions
|
|
111
|
-
const videoAspect = this.videoElement.videoWidth / this.videoElement.videoHeight
|
|
112
|
-
const canvasAspect = width / height
|
|
113
|
-
|
|
114
|
-
let drawWidth = width
|
|
115
|
-
let drawHeight = height
|
|
116
|
-
let offsetX = 0
|
|
117
|
-
let offsetY = 0
|
|
118
|
-
|
|
119
|
-
if (videoAspect > canvasAspect) {
|
|
120
|
-
// Video is wider, fit to height
|
|
121
|
-
drawHeight = height
|
|
122
|
-
drawWidth = height * videoAspect
|
|
123
|
-
offsetX = (width - drawWidth) / 2
|
|
124
|
-
} else {
|
|
125
|
-
// Video is taller, fit to width
|
|
126
|
-
drawWidth = width
|
|
127
|
-
drawHeight = width / videoAspect
|
|
128
|
-
offsetY = (height - drawHeight) / 2
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
this.context.drawImage(this.videoElement, offsetX, offsetY, drawWidth, drawHeight)
|
|
132
|
-
|
|
133
|
-
// Convert to blob
|
|
134
|
-
const blob = await this.canvasToBlob(format, quality)
|
|
135
|
-
const dataUrl = this.canvas.toDataURL(`image/${format}`, quality)
|
|
136
|
-
const objectUrl = URL.createObjectURL(blob)
|
|
137
|
-
|
|
138
|
-
const thumbnail: GeneratedThumbnail = {
|
|
139
|
-
blob,
|
|
140
|
-
dataUrl,
|
|
141
|
-
objectUrl,
|
|
142
|
-
width,
|
|
143
|
-
height,
|
|
144
|
-
timestamp: time
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Cache thumbnail
|
|
148
|
-
if (this.config.enableCaching) {
|
|
149
|
-
this.addToCache(cacheKey, thumbnail)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Restore original time
|
|
153
|
-
if (originalTime !== time) {
|
|
154
|
-
await this.seekToTime(originalTime)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return thumbnail
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Generate multiple thumbnails for scrub bar
|
|
162
|
-
*/
|
|
163
|
-
async generateScrubThumbnails(
|
|
164
|
-
count: number,
|
|
165
|
-
options: ThumbnailOptions = {}
|
|
166
|
-
): Promise<GeneratedThumbnail[]> {
|
|
167
|
-
if (!this.videoElement) {
|
|
168
|
-
throw new Error("Video element not set")
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const duration = this.videoElement.duration
|
|
172
|
-
if (!duration || duration === 0) {
|
|
173
|
-
throw new Error("Video duration not available")
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const thumbnails: GeneratedThumbnail[] = []
|
|
177
|
-
const interval = duration / count
|
|
178
|
-
|
|
179
|
-
for (let i = 0; i < count; i++) {
|
|
180
|
-
const time = i * interval
|
|
181
|
-
try {
|
|
182
|
-
const thumbnail = await this.generateThumbnail(time, options)
|
|
183
|
-
thumbnails.push(thumbnail)
|
|
184
|
-
} catch (error) {
|
|
185
|
-
console.warn(`Failed to generate thumbnail at ${time}s:`, error)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return thumbnails
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Generate thumbnail sprite sheet
|
|
194
|
-
*/
|
|
195
|
-
async generateSprite(
|
|
196
|
-
count: number,
|
|
197
|
-
options: ThumbnailOptions & {
|
|
198
|
-
columns?: number
|
|
199
|
-
rows?: number
|
|
200
|
-
} = {}
|
|
201
|
-
): Promise<ThumbnailSprite> {
|
|
202
|
-
if (!this.videoElement) {
|
|
203
|
-
throw new Error("Video element not set")
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const duration = this.videoElement.duration
|
|
207
|
-
if (!duration || duration === 0) {
|
|
208
|
-
throw new Error("Video duration not available")
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const thumbnailWidth = options.width || this.config.defaultWidth
|
|
212
|
-
const thumbnailHeight = options.height || this.config.defaultHeight
|
|
213
|
-
const quality = options.quality || this.config.defaultQuality
|
|
214
|
-
const format = options.format || this.config.defaultFormat
|
|
215
|
-
|
|
216
|
-
// Calculate sprite dimensions
|
|
217
|
-
const columns = options.columns || Math.ceil(Math.sqrt(count))
|
|
218
|
-
const rows = Math.ceil(count / columns)
|
|
219
|
-
|
|
220
|
-
const spriteWidth = columns * thumbnailWidth
|
|
221
|
-
const spriteHeight = rows * thumbnailHeight
|
|
222
|
-
|
|
223
|
-
// Create sprite canvas
|
|
224
|
-
const spriteCanvas = document.createElement("canvas")
|
|
225
|
-
spriteCanvas.width = spriteWidth
|
|
226
|
-
spriteCanvas.height = spriteHeight
|
|
227
|
-
|
|
228
|
-
const spriteContext = spriteCanvas.getContext("2d")
|
|
229
|
-
if (!spriteContext) {
|
|
230
|
-
throw new Error("Canvas 2D context not supported")
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const thumbnails: ThumbnailSprite["thumbnails"] = []
|
|
234
|
-
const interval = duration / count
|
|
235
|
-
|
|
236
|
-
// Generate each thumbnail
|
|
237
|
-
for (let i = 0; i < count; i++) {
|
|
238
|
-
const time = i * interval
|
|
239
|
-
const col = i % columns
|
|
240
|
-
const row = Math.floor(i / columns)
|
|
241
|
-
const x = col * thumbnailWidth
|
|
242
|
-
const y = row * thumbnailHeight
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
// Generate individual thumbnail
|
|
246
|
-
const thumbnail = await this.generateThumbnail(time, {
|
|
247
|
-
width: thumbnailWidth,
|
|
248
|
-
height: thumbnailHeight,
|
|
249
|
-
quality,
|
|
250
|
-
format
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
// Draw thumbnail to sprite
|
|
254
|
-
const img = new Image()
|
|
255
|
-
img.src = thumbnail.dataUrl
|
|
256
|
-
|
|
257
|
-
await new Promise<void>((resolve, reject) => {
|
|
258
|
-
img.onload = () => {
|
|
259
|
-
spriteContext.drawImage(img, x, y, thumbnailWidth, thumbnailHeight)
|
|
260
|
-
resolve()
|
|
261
|
-
}
|
|
262
|
-
img.onerror = reject
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
thumbnails.push({
|
|
266
|
-
x,
|
|
267
|
-
y,
|
|
268
|
-
width: thumbnailWidth,
|
|
269
|
-
height: thumbnailHeight,
|
|
270
|
-
time
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
// Clean up individual thumbnail
|
|
274
|
-
URL.revokeObjectURL(thumbnail.objectUrl)
|
|
275
|
-
} catch (error) {
|
|
276
|
-
console.warn(`Failed to add thumbnail at ${time}s to sprite:`, error)
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Create final sprite image
|
|
281
|
-
const spriteImage = new Image()
|
|
282
|
-
spriteImage.src = spriteCanvas.toDataURL(`image/${format}`, quality)
|
|
283
|
-
|
|
284
|
-
const sprite: ThumbnailSprite = {
|
|
285
|
-
image: spriteImage,
|
|
286
|
-
thumbnails,
|
|
287
|
-
spriteWidth,
|
|
288
|
-
spriteHeight,
|
|
289
|
-
thumbnailWidth,
|
|
290
|
-
thumbnailHeight
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return sprite
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Extract thumbnail from sprite at specific time
|
|
298
|
-
*/
|
|
299
|
-
getThumbnailFromSprite(sprite: ThumbnailSprite, time: number): HTMLCanvasElement | null {
|
|
300
|
-
// Find closest thumbnail
|
|
301
|
-
const thumbnail = sprite.thumbnails.reduce((closest, current) => {
|
|
302
|
-
return Math.abs(current.time - time) < Math.abs(closest.time - time) ? current : closest
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
if (!thumbnail) return null
|
|
306
|
-
|
|
307
|
-
// Create canvas for extracted thumbnail
|
|
308
|
-
const canvas = document.createElement("canvas")
|
|
309
|
-
canvas.width = thumbnail.width
|
|
310
|
-
canvas.height = thumbnail.height
|
|
311
|
-
|
|
312
|
-
const context = canvas.getContext("2d")
|
|
313
|
-
if (!context) return null
|
|
314
|
-
|
|
315
|
-
// Draw portion of sprite to canvas
|
|
316
|
-
context.drawImage(
|
|
317
|
-
sprite.image,
|
|
318
|
-
thumbnail.x,
|
|
319
|
-
thumbnail.y,
|
|
320
|
-
thumbnail.width,
|
|
321
|
-
thumbnail.height,
|
|
322
|
-
0,
|
|
323
|
-
0,
|
|
324
|
-
thumbnail.width,
|
|
325
|
-
thumbnail.height
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
return canvas
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Clear cache
|
|
333
|
-
*/
|
|
334
|
-
clearCache(): void {
|
|
335
|
-
// Revoke object URLs to prevent memory leaks
|
|
336
|
-
this.cache.forEach((thumbnail) => {
|
|
337
|
-
URL.revokeObjectURL(thumbnail.objectUrl)
|
|
338
|
-
})
|
|
339
|
-
this.cache.clear()
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Get cache size
|
|
344
|
-
*/
|
|
345
|
-
getCacheSize(): number {
|
|
346
|
-
return this.cache.size
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Get cache info
|
|
351
|
-
*/
|
|
352
|
-
getCacheInfo(): Array<{ key: string; timestamp: number; size: number }> {
|
|
353
|
-
return Array.from(this.cache.entries()).map(([key, thumbnail]) => ({
|
|
354
|
-
key,
|
|
355
|
-
timestamp: thumbnail.timestamp,
|
|
356
|
-
size: thumbnail.blob.size
|
|
357
|
-
}))
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Set configuration
|
|
362
|
-
*/
|
|
363
|
-
updateConfig(config: Partial<ThumbnailConfig>): void {
|
|
364
|
-
this.config = { ...this.config, ...config }
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Seek video to specific time
|
|
369
|
-
*/
|
|
370
|
-
private async seekToTime(time: number): Promise<void> {
|
|
371
|
-
if (!this.videoElement) return
|
|
372
|
-
|
|
373
|
-
return new Promise((resolve, reject) => {
|
|
374
|
-
let timeoutId: number
|
|
375
|
-
|
|
376
|
-
const onSeeked = () => {
|
|
377
|
-
this.videoElement!.removeEventListener("seeked", onSeeked)
|
|
378
|
-
clearTimeout(timeoutId)
|
|
379
|
-
resolve()
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const onError = () => {
|
|
383
|
-
this.videoElement!.removeEventListener("error", onError)
|
|
384
|
-
this.videoElement!.removeEventListener("seeked", onSeeked)
|
|
385
|
-
clearTimeout(timeoutId)
|
|
386
|
-
reject(new Error("Seek failed"))
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Set timeout to avoid hanging
|
|
390
|
-
timeoutId = window.setTimeout(() => {
|
|
391
|
-
this.videoElement!.removeEventListener("seeked", onSeeked)
|
|
392
|
-
this.videoElement!.removeEventListener("error", onError)
|
|
393
|
-
reject(new Error("Seek timeout"))
|
|
394
|
-
}, 5000)
|
|
395
|
-
|
|
396
|
-
if (!this.videoElement) {
|
|
397
|
-
reject(new Error("Video element not initialized"))
|
|
398
|
-
return
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
this.videoElement.addEventListener("seeked", onSeeked)
|
|
402
|
-
this.videoElement.addEventListener("error", onError)
|
|
403
|
-
this.videoElement.currentTime = Math.max(0, Math.min(time, this.videoElement.duration))
|
|
404
|
-
})
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Convert canvas to blob
|
|
409
|
-
*/
|
|
410
|
-
private canvasToBlob(format: string, quality: number): Promise<Blob> {
|
|
411
|
-
return new Promise((resolve, reject) => {
|
|
412
|
-
this.canvas.toBlob(
|
|
413
|
-
(blob) => {
|
|
414
|
-
if (blob) {
|
|
415
|
-
resolve(blob)
|
|
416
|
-
} else {
|
|
417
|
-
reject(new Error("Failed to create blob from canvas"))
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
`image/${format}`,
|
|
421
|
-
quality
|
|
422
|
-
)
|
|
423
|
-
})
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Generate cache key
|
|
428
|
-
*/
|
|
429
|
-
private getCacheKey(time: number, options: ThumbnailOptions): string {
|
|
430
|
-
const width = options.width || this.config.defaultWidth
|
|
431
|
-
const height = options.height || this.config.defaultHeight
|
|
432
|
-
const quality = options.quality || this.config.defaultQuality
|
|
433
|
-
const format = options.format || this.config.defaultFormat
|
|
434
|
-
|
|
435
|
-
return `${time}-${width}x${height}-${quality}-${format}`
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Add thumbnail to cache
|
|
440
|
-
*/
|
|
441
|
-
private addToCache(key: string, thumbnail: GeneratedThumbnail): void {
|
|
442
|
-
// Remove oldest items if cache is full
|
|
443
|
-
if (this.cache.size >= this.config.maxCacheSize) {
|
|
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)
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
this.cache.set(key, thumbnail)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Cleanup resources
|
|
459
|
-
*/
|
|
460
|
-
destroy(): void {
|
|
461
|
-
this.clearCache()
|
|
462
|
-
this.videoElement = null
|
|
463
|
-
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Utility functions for thumbnail operations
|
|
468
|
-
export const thumbnailUtils = {
|
|
469
|
-
/**
|
|
470
|
-
* Calculate optimal thumbnail dimensions maintaining aspect ratio
|
|
471
|
-
*/
|
|
472
|
-
calculateDimensions(
|
|
473
|
-
sourceWidth: number,
|
|
474
|
-
sourceHeight: number,
|
|
475
|
-
targetWidth: number,
|
|
476
|
-
targetHeight: number
|
|
477
|
-
): { width: number; height: number; offsetX: number; offsetY: number } {
|
|
478
|
-
const sourceAspect = sourceWidth / sourceHeight
|
|
479
|
-
const targetAspect = targetWidth / targetHeight
|
|
480
|
-
|
|
481
|
-
let width = targetWidth
|
|
482
|
-
let height = targetHeight
|
|
483
|
-
let offsetX = 0
|
|
484
|
-
let offsetY = 0
|
|
485
|
-
|
|
486
|
-
if (sourceAspect > targetAspect) {
|
|
487
|
-
height = targetHeight
|
|
488
|
-
width = targetHeight * sourceAspect
|
|
489
|
-
offsetX = (targetWidth - width) / 2
|
|
490
|
-
} else {
|
|
491
|
-
width = targetWidth
|
|
492
|
-
height = targetWidth / sourceAspect
|
|
493
|
-
offsetY = (targetHeight - height) / 2
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return { width, height, offsetX, offsetY }
|
|
497
|
-
},
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Create thumbnail URL from blob
|
|
501
|
-
*/
|
|
502
|
-
createThumbnailUrl(blob: Blob): string {
|
|
503
|
-
return URL.createObjectURL(blob)
|
|
504
|
-
},
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Revoke thumbnail URL
|
|
508
|
-
*/
|
|
509
|
-
revokeThumbnailUrl(url: string): void {
|
|
510
|
-
URL.revokeObjectURL(url)
|
|
511
|
-
},
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Convert time to thumbnail index
|
|
515
|
-
*/
|
|
516
|
-
timeToIndex(time: number, duration: number, count: number): number {
|
|
517
|
-
const normalizedTime = Math.max(0, Math.min(time, duration))
|
|
518
|
-
const index = Math.floor((normalizedTime / duration) * count)
|
|
519
|
-
return Math.min(index, count - 1)
|
|
520
|
-
},
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Convert index to time
|
|
524
|
-
*/
|
|
525
|
-
indexToTime(index: number, duration: number, count: number): number {
|
|
526
|
-
return (index / count) * duration
|
|
527
|
-
}
|
|
528
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
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
|
-
})
|