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