@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,258 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo, useEffect } from "react"
|
|
2
|
+
|
|
3
|
+
interface SubtitleCue {
|
|
4
|
+
start: number
|
|
5
|
+
end: number
|
|
6
|
+
text: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SubtitleTrack {
|
|
10
|
+
id: string
|
|
11
|
+
label: string
|
|
12
|
+
language?: string
|
|
13
|
+
src?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseEnhancedSubtitlesOptions {
|
|
17
|
+
subtitleSrc?: string
|
|
18
|
+
subtitleTracks?: SubtitleTrack[]
|
|
19
|
+
defaultFontSize?: "small" | "medium" | "large"
|
|
20
|
+
onLoadError?: (error: Error) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useEnhancedSubtitles({
|
|
24
|
+
subtitleSrc,
|
|
25
|
+
subtitleTracks = [],
|
|
26
|
+
defaultFontSize = "medium",
|
|
27
|
+
onLoadError
|
|
28
|
+
}: UseEnhancedSubtitlesOptions = {}) {
|
|
29
|
+
const [subtitlesEnabled, setSubtitlesEnabled] = useState(false)
|
|
30
|
+
const [subtitleCues, setSubtitleCues] = useState<SubtitleCue[]>([])
|
|
31
|
+
const [currentSubtitle, setCurrentSubtitle] = useState("")
|
|
32
|
+
const [currentTrackId, setCurrentTrackId] = useState<string | null>(null)
|
|
33
|
+
const [fontSize, setFontSize] = useState<"small" | "medium" | "large">(defaultFontSize)
|
|
34
|
+
const [loadError, setLoadError] = useState<Error | null>(null)
|
|
35
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
36
|
+
|
|
37
|
+
// Size mapping for subtitle font sizes
|
|
38
|
+
const sizeMap = {
|
|
39
|
+
small: "text-sm",
|
|
40
|
+
medium: "text-base",
|
|
41
|
+
large: "text-lg"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Determine if subtitles are available
|
|
45
|
+
const hasSubtitles = !!(subtitleSrc || (subtitleTracks && subtitleTracks.length > 0))
|
|
46
|
+
|
|
47
|
+
// Parse VTT/WebVTT timestamp
|
|
48
|
+
const parseVTTTime = (timeStr: string): number => {
|
|
49
|
+
const parts = timeStr.split(":")
|
|
50
|
+
const seconds = parts[2] ? parseFloat(parts[2]) : 0
|
|
51
|
+
const minutes = parts[1] ? parseInt(parts[1], 10) * 60 : 0
|
|
52
|
+
const hours = parts[0] ? parseInt(parts[0], 10) * 3600 : 0
|
|
53
|
+
return hours + minutes + seconds
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse SRT timestamp
|
|
57
|
+
const parseSRTTime = (timeStr: string): number => {
|
|
58
|
+
// SRT format: 00:00:00,000 (hours:minutes:seconds,milliseconds)
|
|
59
|
+
const [time, ms] = timeStr.split(",")
|
|
60
|
+
const parts = (time || "").split(":")
|
|
61
|
+
const milliseconds = ms ? parseInt(ms, 10) / 1000 : 0
|
|
62
|
+
const seconds = parts[2] ? parseInt(parts[2], 10) : 0
|
|
63
|
+
const minutes = parts[1] ? parseInt(parts[1], 10) * 60 : 0
|
|
64
|
+
const hours = parts[0] ? parseInt(parts[0], 10) * 3600 : 0
|
|
65
|
+
return hours + minutes + seconds + milliseconds
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Load and parse subtitle file
|
|
69
|
+
const loadSubtitles = async (url: string) => {
|
|
70
|
+
setIsLoading(true)
|
|
71
|
+
setLoadError(null)
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(url)
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Failed to load subtitles: ${response.status} ${response.statusText}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const text = await response.text()
|
|
80
|
+
const cues: SubtitleCue[] = []
|
|
81
|
+
const lines = text.split("\n")
|
|
82
|
+
let i = 0
|
|
83
|
+
|
|
84
|
+
// Detect format (VTT or SRT)
|
|
85
|
+
const isVTT = text.includes("WEBVTT") || url.endsWith(".vtt")
|
|
86
|
+
const isSRT = url.endsWith(".srt") || /^\d+$/.test(lines[0]?.trim() || "")
|
|
87
|
+
|
|
88
|
+
if (isVTT) {
|
|
89
|
+
// Parse VTT format
|
|
90
|
+
while (i < lines.length) {
|
|
91
|
+
if (lines[i]?.includes("-->")) {
|
|
92
|
+
const [startStr, endStr] = (lines[i] || "").split("-->").map((s) => s.trim())
|
|
93
|
+
const start = parseVTTTime(startStr || "")
|
|
94
|
+
const end = parseVTTTime(endStr || "")
|
|
95
|
+
|
|
96
|
+
i++
|
|
97
|
+
let subtitleText = ""
|
|
98
|
+
while (i < lines.length && lines[i]?.trim() !== "") {
|
|
99
|
+
subtitleText += (subtitleText ? " " : "") + lines[i]?.trim()
|
|
100
|
+
i++
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (subtitleText) {
|
|
104
|
+
cues.push({ start, end, text: subtitleText })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
i++
|
|
108
|
+
}
|
|
109
|
+
} else if (isSRT) {
|
|
110
|
+
// Parse SRT format
|
|
111
|
+
while (i < lines.length) {
|
|
112
|
+
// Skip the sequence number
|
|
113
|
+
if (/^\d+$/.test(lines[i]?.trim() || "")) {
|
|
114
|
+
i++
|
|
115
|
+
// Parse timestamp line
|
|
116
|
+
if (lines[i]?.includes("-->")) {
|
|
117
|
+
const [startStr, endStr] = (lines[i] || "").split("-->").map((s) => s.trim())
|
|
118
|
+
const start = parseSRTTime(startStr || "")
|
|
119
|
+
const end = parseSRTTime(endStr || "")
|
|
120
|
+
|
|
121
|
+
i++
|
|
122
|
+
let subtitleText = ""
|
|
123
|
+
while (i < lines.length && lines[i]?.trim() !== "") {
|
|
124
|
+
subtitleText += (subtitleText ? " " : "") + lines[i]?.trim()
|
|
125
|
+
i++
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (subtitleText) {
|
|
129
|
+
cues.push({ start, end, text: subtitleText })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
i++
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
throw new Error("Unsupported subtitle format. Only VTT and SRT are supported.")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (cues.length === 0) {
|
|
140
|
+
throw new Error("No subtitle cues found in file")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setSubtitleCues(cues)
|
|
144
|
+
setIsLoading(false)
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const err = error instanceof Error ? error : new Error("Failed to load subtitles")
|
|
147
|
+
console.error("Failed to load subtitles:", err)
|
|
148
|
+
setLoadError(err)
|
|
149
|
+
setIsLoading(false)
|
|
150
|
+
setSubtitleCues([])
|
|
151
|
+
onLoadError?.(err)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get current subtitle text based on time
|
|
156
|
+
const getCurrentSubtitle = (currentTime: number) => {
|
|
157
|
+
if (!subtitlesEnabled || subtitleCues.length === 0) return ""
|
|
158
|
+
|
|
159
|
+
const currentCue = subtitleCues.find(
|
|
160
|
+
(cue) => currentTime >= cue.start && currentTime <= cue.end
|
|
161
|
+
)
|
|
162
|
+
return currentCue?.text || ""
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Toggle subtitles on/off
|
|
166
|
+
const toggleSubtitles = () => {
|
|
167
|
+
setSubtitlesEnabled(prev => {
|
|
168
|
+
const newEnabled = !prev
|
|
169
|
+
|
|
170
|
+
if (newEnabled && !loadError) {
|
|
171
|
+
// Try to load subtitles if not already loaded
|
|
172
|
+
if (subtitleCues.length === 0 && !isLoading) {
|
|
173
|
+
// Load default subtitle source or first track
|
|
174
|
+
const srcToLoad = subtitleSrc || subtitleTracks[0]?.src
|
|
175
|
+
if (srcToLoad) {
|
|
176
|
+
loadSubtitles(srcToLoad)
|
|
177
|
+
if (subtitleTracks[0]) {
|
|
178
|
+
setCurrentTrackId(subtitleTracks[0].id)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else if (!newEnabled) {
|
|
183
|
+
setCurrentSubtitle("")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return newEnabled
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Select a specific subtitle track
|
|
191
|
+
const selectSubtitleTrack = (trackId: string | null) => {
|
|
192
|
+
if (!trackId) {
|
|
193
|
+
// Turn off subtitles
|
|
194
|
+
setSubtitlesEnabled(false)
|
|
195
|
+
setCurrentTrackId(null)
|
|
196
|
+
setCurrentSubtitle("")
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const track = subtitleTracks.find(t => t.id === trackId)
|
|
201
|
+
if (track?.src) {
|
|
202
|
+
setCurrentTrackId(trackId)
|
|
203
|
+
loadSubtitles(track.src)
|
|
204
|
+
setSubtitlesEnabled(true)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Change subtitle font size
|
|
209
|
+
const changeSubtitleFontSize = (size: "small" | "medium" | "large") => {
|
|
210
|
+
setFontSize(size)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Get CSS classes for subtitle display
|
|
214
|
+
const getSubtitleStyles = {
|
|
215
|
+
fontSize: sizeMap[fontSize],
|
|
216
|
+
className: `${sizeMap[fontSize]} font-medium`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Clear subtitle on disable
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!subtitlesEnabled) {
|
|
222
|
+
setCurrentSubtitle("")
|
|
223
|
+
}
|
|
224
|
+
}, [subtitlesEnabled])
|
|
225
|
+
|
|
226
|
+
// Auto-load default subtitles on mount if available
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (subtitleSrc && !subtitleCues.length && !loadError && !isLoading) {
|
|
229
|
+
// Don't auto-load, wait for user to enable
|
|
230
|
+
// This prevents errors on initial load
|
|
231
|
+
}
|
|
232
|
+
}, [])
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
// State
|
|
236
|
+
subtitlesEnabled,
|
|
237
|
+
currentSubtitle,
|
|
238
|
+
subtitleCues,
|
|
239
|
+
currentTrackId,
|
|
240
|
+
fontSize,
|
|
241
|
+
loadError,
|
|
242
|
+
isLoading,
|
|
243
|
+
hasSubtitles,
|
|
244
|
+
|
|
245
|
+
// Actions
|
|
246
|
+
toggleSubtitles,
|
|
247
|
+
selectSubtitleTrack,
|
|
248
|
+
changeSubtitleFontSize,
|
|
249
|
+
setCurrentSubtitle,
|
|
250
|
+
|
|
251
|
+
// Utilities
|
|
252
|
+
getCurrentSubtitle,
|
|
253
|
+
getSubtitleStyles,
|
|
254
|
+
|
|
255
|
+
// Track info
|
|
256
|
+
availableTracks: subtitleTracks
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useCallback } from "react"
|
|
2
|
+
import type { VideoControls } from "./useVideoPlayer"
|
|
3
|
+
|
|
4
|
+
interface UseKeyboardShortcutsProps {
|
|
5
|
+
videoControls: VideoControls
|
|
6
|
+
toggleSubtitles: () => void
|
|
7
|
+
takeScreenshot: (ctrlKey?: boolean) => void
|
|
8
|
+
duration: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useKeyboardShortcuts({
|
|
12
|
+
videoControls,
|
|
13
|
+
toggleSubtitles,
|
|
14
|
+
takeScreenshot,
|
|
15
|
+
duration
|
|
16
|
+
}: UseKeyboardShortcutsProps) {
|
|
17
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
18
|
+
const video = document.querySelector('video')
|
|
19
|
+
if (!video) return
|
|
20
|
+
|
|
21
|
+
switch (e.key.toLowerCase()) {
|
|
22
|
+
case " ":
|
|
23
|
+
case "k":
|
|
24
|
+
e.preventDefault()
|
|
25
|
+
videoControls.togglePlay()
|
|
26
|
+
break
|
|
27
|
+
case "j":
|
|
28
|
+
e.preventDefault()
|
|
29
|
+
videoControls.seekBackward()
|
|
30
|
+
break
|
|
31
|
+
case "l":
|
|
32
|
+
e.preventDefault()
|
|
33
|
+
videoControls.seekForward()
|
|
34
|
+
break
|
|
35
|
+
case "arrowleft":
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
video.currentTime = Math.max(0, video.currentTime - 5)
|
|
38
|
+
break
|
|
39
|
+
case "arrowright":
|
|
40
|
+
e.preventDefault()
|
|
41
|
+
video.currentTime = Math.min(duration, video.currentTime + 5)
|
|
42
|
+
break
|
|
43
|
+
case "arrowup":
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
video.volume = Math.min(1, video.volume + 0.1)
|
|
46
|
+
break
|
|
47
|
+
case "arrowdown":
|
|
48
|
+
e.preventDefault()
|
|
49
|
+
video.volume = Math.max(0, video.volume - 0.1)
|
|
50
|
+
break
|
|
51
|
+
case "m":
|
|
52
|
+
e.preventDefault()
|
|
53
|
+
videoControls.toggleMute()
|
|
54
|
+
break
|
|
55
|
+
case "f":
|
|
56
|
+
e.preventDefault()
|
|
57
|
+
videoControls.toggleFullscreen()
|
|
58
|
+
break
|
|
59
|
+
case "c":
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
toggleSubtitles()
|
|
62
|
+
break
|
|
63
|
+
case "s":
|
|
64
|
+
e.preventDefault()
|
|
65
|
+
takeScreenshot(e.ctrlKey || e.metaKey)
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
document.addEventListener("keydown", handleKeyDown)
|
|
72
|
+
return () => document.removeEventListener("keydown", handleKeyDown)
|
|
73
|
+
}, [handleKeyDown])
|
|
74
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from "react"
|
|
2
|
+
|
|
3
|
+
export function useMediaControls(isPlaying: boolean, showSettings: boolean) {
|
|
4
|
+
const [controlsVisible, setControlsVisible] = useState(true)
|
|
5
|
+
const [showVolumeSlider, setShowVolumeSlider] = useState(false)
|
|
6
|
+
const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
7
|
+
|
|
8
|
+
const showControls = () => {
|
|
9
|
+
setControlsVisible(true)
|
|
10
|
+
if (hideTimeoutRef.current) {
|
|
11
|
+
clearTimeout(hideTimeoutRef.current)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isPlaying && !showSettings) {
|
|
15
|
+
hideTimeoutRef.current = setTimeout(() => {
|
|
16
|
+
setControlsVisible(false)
|
|
17
|
+
}, 3000)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const handleMouseMove = () => {
|
|
22
|
+
showControls()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const handleMouseEnter = () => {
|
|
26
|
+
setControlsVisible(true)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleMouseLeave = () => {
|
|
30
|
+
if (isPlaying) {
|
|
31
|
+
setControlsVisible(false)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
showControls()
|
|
37
|
+
return () => {
|
|
38
|
+
if (hideTimeoutRef.current) {
|
|
39
|
+
clearTimeout(hideTimeoutRef.current)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, [showControls])
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
controlsVisible,
|
|
46
|
+
showVolumeSlider,
|
|
47
|
+
setShowVolumeSlider,
|
|
48
|
+
handleMouseMove,
|
|
49
|
+
handleMouseEnter,
|
|
50
|
+
handleMouseLeave
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback } from "react"
|
|
2
|
+
|
|
3
|
+
export function useScreenshot(videoRef: React.RefObject<HTMLVideoElement | null>) {
|
|
4
|
+
const takeScreenshot = (ctrlKey: boolean = false) => {
|
|
5
|
+
if (!videoRef.current) return
|
|
6
|
+
|
|
7
|
+
const video = videoRef.current
|
|
8
|
+
const canvas = document.createElement("canvas")
|
|
9
|
+
canvas.width = video.videoWidth
|
|
10
|
+
canvas.height = video.videoHeight
|
|
11
|
+
const ctx = canvas.getContext("2d")
|
|
12
|
+
if (!ctx) return
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Draw the video frame to canvas
|
|
16
|
+
ctx.drawImage(video, 0, 0)
|
|
17
|
+
|
|
18
|
+
// Try to export the canvas
|
|
19
|
+
canvas.toBlob((blob) => {
|
|
20
|
+
if (!blob) {
|
|
21
|
+
console.error("Failed to create blob from canvas")
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (ctrlKey) {
|
|
26
|
+
// Copy to clipboard
|
|
27
|
+
if (navigator.clipboard && window.ClipboardItem) {
|
|
28
|
+
const item = new ClipboardItem({ "image/png": blob })
|
|
29
|
+
navigator.clipboard.write([item]).catch(err => {
|
|
30
|
+
console.error("Failed to copy to clipboard:", err)
|
|
31
|
+
})
|
|
32
|
+
} else {
|
|
33
|
+
console.warn("Clipboard API not supported")
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
// Download
|
|
37
|
+
const url = URL.createObjectURL(blob)
|
|
38
|
+
const a = document.createElement("a")
|
|
39
|
+
a.href = url
|
|
40
|
+
a.download = `screenshot-${Date.now()}.png`
|
|
41
|
+
a.click()
|
|
42
|
+
URL.revokeObjectURL(url)
|
|
43
|
+
}
|
|
44
|
+
}, "image/png")
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof Error && error.message.includes("tainted")) {
|
|
47
|
+
console.error("Canvas is tainted - video source may not have proper CORS headers. Try setting crossOrigin='anonymous' on the video element.")
|
|
48
|
+
alert("Screenshot failed: The video source doesn't allow screenshots due to CORS policy. Please ensure the video server includes proper CORS headers or set crossOrigin='anonymous' on the video player.")
|
|
49
|
+
} else {
|
|
50
|
+
console.error("Screenshot failed:", error)
|
|
51
|
+
alert("Screenshot failed: " + (error instanceof Error ? error.message : "Unknown error"))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { takeScreenshot }
|
|
57
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo, useEffect } from "react"
|
|
2
|
+
|
|
3
|
+
interface SubtitleCue {
|
|
4
|
+
start: number
|
|
5
|
+
end: number
|
|
6
|
+
text: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useSubtitles(subtitleSrc?: string) {
|
|
10
|
+
const [subtitlesEnabled, setSubtitlesEnabled] = useState(false)
|
|
11
|
+
const [subtitleCues, setSubtitleCues] = useState<SubtitleCue[]>([])
|
|
12
|
+
const [currentSubtitle, setCurrentSubtitle] = useState("")
|
|
13
|
+
|
|
14
|
+
const parseVTTTime = (timeStr: string): number => {
|
|
15
|
+
const parts = timeStr.split(":")
|
|
16
|
+
const seconds = parts[2] ? parseFloat(parts[2]) : 0
|
|
17
|
+
const minutes = parts[1] ? parseInt(parts[1], 10) * 60 : 0
|
|
18
|
+
const hours = parts[0] ? parseInt(parts[0], 10) * 3600 : 0
|
|
19
|
+
return hours + minutes + seconds
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const loadSubtitles = async (url: string) => {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(url)
|
|
25
|
+
const text = await response.text()
|
|
26
|
+
|
|
27
|
+
const cues: SubtitleCue[] = []
|
|
28
|
+
const lines = text.split("\n")
|
|
29
|
+
let i = 0
|
|
30
|
+
|
|
31
|
+
while (i < lines.length) {
|
|
32
|
+
if (lines[i]?.includes("-->")) {
|
|
33
|
+
const [startStr, endStr] = (lines[i] || "").split("-->").map((s) => s.trim())
|
|
34
|
+
const start = parseVTTTime(startStr || "")
|
|
35
|
+
const end = parseVTTTime(endStr || "")
|
|
36
|
+
|
|
37
|
+
i++
|
|
38
|
+
let subtitleText = ""
|
|
39
|
+
while (i < lines.length && lines[i]?.trim() !== "") {
|
|
40
|
+
subtitleText += (subtitleText ? " " : "") + lines[i]?.trim()
|
|
41
|
+
i++
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (subtitleText) {
|
|
45
|
+
cues.push({ start, end, text: subtitleText })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
i++
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setSubtitleCues(cues)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error("Failed to load subtitles:", error)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const getCurrentSubtitle = (currentTime: number) => {
|
|
58
|
+
if (!subtitlesEnabled || subtitleCues.length === 0) return ""
|
|
59
|
+
|
|
60
|
+
const currentCue = subtitleCues.find(
|
|
61
|
+
(cue) => currentTime >= cue.start && currentTime <= cue.end
|
|
62
|
+
)
|
|
63
|
+
return currentCue?.text || ""
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const toggleSubtitles = () => {
|
|
67
|
+
setSubtitlesEnabled(prev => {
|
|
68
|
+
const newEnabled = !prev
|
|
69
|
+
if (newEnabled && subtitleSrc && subtitleCues.length === 0) {
|
|
70
|
+
loadSubtitles(subtitleSrc)
|
|
71
|
+
} else if (!newEnabled) {
|
|
72
|
+
setCurrentSubtitle("")
|
|
73
|
+
}
|
|
74
|
+
return newEnabled
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!subtitlesEnabled) {
|
|
80
|
+
setCurrentSubtitle("")
|
|
81
|
+
}
|
|
82
|
+
}, [subtitlesEnabled])
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
subtitlesEnabled,
|
|
86
|
+
currentSubtitle,
|
|
87
|
+
toggleSubtitles,
|
|
88
|
+
getCurrentSubtitle,
|
|
89
|
+
setCurrentSubtitle
|
|
90
|
+
}
|
|
91
|
+
}
|