@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,651 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import "../styles/pixel-art.css"
|
|
4
|
+
import { Button } from "@rp/ui"
|
|
5
|
+
import {
|
|
6
|
+
DownloadIcon,
|
|
7
|
+
ExitFullscreenIcon,
|
|
8
|
+
Forward10Icon,
|
|
9
|
+
FullscreenIcon,
|
|
10
|
+
LoadingIcon,
|
|
11
|
+
MuteIcon,
|
|
12
|
+
PauseIcon,
|
|
13
|
+
PictureInPictureIcon,
|
|
14
|
+
PlayIcon,
|
|
15
|
+
QualityIcon,
|
|
16
|
+
Rewind10Icon,
|
|
17
|
+
SettingsIcon,
|
|
18
|
+
ShareIcon,
|
|
19
|
+
SkipBackwardIcon,
|
|
20
|
+
SkipForwardIcon,
|
|
21
|
+
SpeedIcon,
|
|
22
|
+
SubtitlesIcon,
|
|
23
|
+
VolumeIcon
|
|
24
|
+
} from "@rp/ui"
|
|
25
|
+
import { Slider } from "@rp/ui"
|
|
26
|
+
import {
|
|
27
|
+
DropdownMenu,
|
|
28
|
+
DropdownMenuContent,
|
|
29
|
+
DropdownMenuItem,
|
|
30
|
+
DropdownMenuLabel,
|
|
31
|
+
DropdownMenuSeparator,
|
|
32
|
+
DropdownMenuTrigger
|
|
33
|
+
} from "@rp/ui"
|
|
34
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@rp/ui"
|
|
35
|
+
import React, { useState, useRef, useEffect, useCallback, Suspense, lazy } from "react"
|
|
36
|
+
// Dynamic import for lazy loading
|
|
37
|
+
const ReactPlayer = lazy(() => import("react-player"))
|
|
38
|
+
import { cn } from "../lib/utils"
|
|
39
|
+
|
|
40
|
+
// Minimal ref handle for the methods we call on the underlying player
|
|
41
|
+
interface PlayerHandle {
|
|
42
|
+
seekTo: (amount: number, type?: "seconds" | "fraction") => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface QualityOption {
|
|
46
|
+
label: string
|
|
47
|
+
value: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SubtitleTrack {
|
|
51
|
+
label: string
|
|
52
|
+
value: string
|
|
53
|
+
src: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ProgressState {
|
|
57
|
+
playedSeconds: number
|
|
58
|
+
loaded: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ModernMediaPlayerProps {
|
|
62
|
+
className?: string
|
|
63
|
+
containerClassName?: string
|
|
64
|
+
url?: string | string[]
|
|
65
|
+
playing?: boolean
|
|
66
|
+
volume?: number
|
|
67
|
+
muted?: boolean
|
|
68
|
+
controls?: boolean
|
|
69
|
+
light?: boolean | string
|
|
70
|
+
loop?: boolean
|
|
71
|
+
playbackRate?: number
|
|
72
|
+
width?: string | number
|
|
73
|
+
height?: string | number
|
|
74
|
+
style?: React.CSSProperties
|
|
75
|
+
progressInterval?: number
|
|
76
|
+
playsinline?: boolean
|
|
77
|
+
pip?: boolean
|
|
78
|
+
stopOnUnmount?: boolean
|
|
79
|
+
fallback?: React.ReactNode
|
|
80
|
+
wrapper?: unknown
|
|
81
|
+
config?: unknown
|
|
82
|
+
showDownload?: boolean
|
|
83
|
+
showShare?: boolean
|
|
84
|
+
showSubtitles?: boolean
|
|
85
|
+
showQuality?: boolean
|
|
86
|
+
onDownload?: () => void
|
|
87
|
+
onShare?: () => void
|
|
88
|
+
qualityOptions?: QualityOption[]
|
|
89
|
+
subtitleTracks?: SubtitleTrack[]
|
|
90
|
+
onReady?: () => void
|
|
91
|
+
onStart?: () => void
|
|
92
|
+
onPlay?: () => void
|
|
93
|
+
onProgress?: (state: ProgressState) => void
|
|
94
|
+
onDuration?: (duration: number) => void
|
|
95
|
+
onPause?: () => void
|
|
96
|
+
onBuffer?: () => void
|
|
97
|
+
onBufferEnd?: () => void
|
|
98
|
+
onSeek?: (seconds: number) => void
|
|
99
|
+
onPlaybackRateChange?: (rate: number) => void
|
|
100
|
+
onEnded?: () => void
|
|
101
|
+
onError?: (error: unknown) => void
|
|
102
|
+
onClickPreview?: () => void
|
|
103
|
+
onEnablePIP?: () => void
|
|
104
|
+
onDisablePIP?: () => void
|
|
105
|
+
[key: string]: unknown
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function ModernMediaPlayer({
|
|
109
|
+
className,
|
|
110
|
+
containerClassName,
|
|
111
|
+
url,
|
|
112
|
+
playing = false,
|
|
113
|
+
volume = 1,
|
|
114
|
+
muted = false,
|
|
115
|
+
controls = false,
|
|
116
|
+
light = false,
|
|
117
|
+
loop = false,
|
|
118
|
+
playbackRate = 1,
|
|
119
|
+
width = "100%",
|
|
120
|
+
height = "100%",
|
|
121
|
+
style,
|
|
122
|
+
progressInterval = 1000,
|
|
123
|
+
playsinline = true,
|
|
124
|
+
pip = false,
|
|
125
|
+
stopOnUnmount = true,
|
|
126
|
+
fallback = <LoadingIcon className="size-12 text-white" />,
|
|
127
|
+
wrapper = "div",
|
|
128
|
+
config,
|
|
129
|
+
showDownload = false,
|
|
130
|
+
showShare = false,
|
|
131
|
+
showSubtitles = false,
|
|
132
|
+
showQuality = false,
|
|
133
|
+
onDownload,
|
|
134
|
+
onShare,
|
|
135
|
+
qualityOptions = [
|
|
136
|
+
{ label: "1080p", value: "1080" },
|
|
137
|
+
{ label: "720p", value: "720" },
|
|
138
|
+
{ label: "480p", value: "480" },
|
|
139
|
+
{ label: "360p", value: "360" }
|
|
140
|
+
],
|
|
141
|
+
subtitleTracks = [],
|
|
142
|
+
onReady,
|
|
143
|
+
onStart,
|
|
144
|
+
onPlay,
|
|
145
|
+
onProgress,
|
|
146
|
+
onDuration,
|
|
147
|
+
onPause,
|
|
148
|
+
onBuffer,
|
|
149
|
+
onBufferEnd,
|
|
150
|
+
onSeek,
|
|
151
|
+
onPlaybackRateChange,
|
|
152
|
+
onEnded,
|
|
153
|
+
onError,
|
|
154
|
+
onClickPreview,
|
|
155
|
+
onEnablePIP,
|
|
156
|
+
onDisablePIP,
|
|
157
|
+
...props
|
|
158
|
+
}: ModernMediaPlayerProps) {
|
|
159
|
+
const [isPlaying, setIsPlaying] = useState(playing)
|
|
160
|
+
const [currentVolume, setCurrentVolume] = useState(volume)
|
|
161
|
+
const [isMuted, setIsMuted] = useState(muted)
|
|
162
|
+
const [duration, setDuration] = useState(0)
|
|
163
|
+
const [currentTime, setCurrentTime] = useState(0)
|
|
164
|
+
const [buffered, setBuffered] = useState(0)
|
|
165
|
+
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
166
|
+
const [isPiP, setIsPiP] = useState(pip)
|
|
167
|
+
const [currentRate, setCurrentRate] = useState(playbackRate)
|
|
168
|
+
const [showControls, setShowControls] = useState(true)
|
|
169
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
170
|
+
const [selectedQuality, setSelectedQuality] = useState(qualityOptions[0]?.value || "720")
|
|
171
|
+
const [selectedSubtitle, setSelectedSubtitle] = useState<string | null>(null)
|
|
172
|
+
|
|
173
|
+
const playerRef = useRef<PlayerHandle | null>(null)
|
|
174
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
175
|
+
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
176
|
+
|
|
177
|
+
// Format time helper
|
|
178
|
+
const formatTime = (seconds: number) => {
|
|
179
|
+
const date = new Date(seconds * 1000)
|
|
180
|
+
const hh = date.getUTCHours()
|
|
181
|
+
const mm = date.getUTCMinutes()
|
|
182
|
+
const ss = date.getUTCSeconds().toString().padStart(2, "0")
|
|
183
|
+
if (hh) {
|
|
184
|
+
return `${hh}:${mm.toString().padStart(2, "0")}:${ss}`
|
|
185
|
+
}
|
|
186
|
+
return `${mm}:${ss}`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle mouse movement for auto-hiding controls
|
|
190
|
+
const handleMouseMove = () => {
|
|
191
|
+
setShowControls(true)
|
|
192
|
+
if (controlsTimeoutRef.current) {
|
|
193
|
+
clearTimeout(controlsTimeoutRef.current)
|
|
194
|
+
}
|
|
195
|
+
if (isPlaying) {
|
|
196
|
+
controlsTimeoutRef.current = setTimeout(() => {
|
|
197
|
+
setShowControls(false)
|
|
198
|
+
}, 3000)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
return () => {
|
|
204
|
+
if (controlsTimeoutRef.current) {
|
|
205
|
+
clearTimeout(controlsTimeoutRef.current)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}, [])
|
|
209
|
+
|
|
210
|
+
// Fullscreen handling
|
|
211
|
+
const toggleFullscreen = () => {
|
|
212
|
+
if (!document.fullscreenElement) {
|
|
213
|
+
containerRef.current?.requestFullscreen()
|
|
214
|
+
setIsFullscreen(true)
|
|
215
|
+
} else {
|
|
216
|
+
document.exitFullscreen()
|
|
217
|
+
setIsFullscreen(false)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Keyboard shortcuts
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
const handleKeyPress = (e: KeyboardEvent) => {
|
|
224
|
+
if (!containerRef.current?.contains(document.activeElement)) return
|
|
225
|
+
|
|
226
|
+
switch (e.key.toLowerCase()) {
|
|
227
|
+
case " ":
|
|
228
|
+
case "k":
|
|
229
|
+
e.preventDefault()
|
|
230
|
+
setIsPlaying(!isPlaying)
|
|
231
|
+
break
|
|
232
|
+
case "f":
|
|
233
|
+
e.preventDefault()
|
|
234
|
+
toggleFullscreen()
|
|
235
|
+
break
|
|
236
|
+
case "m":
|
|
237
|
+
e.preventDefault()
|
|
238
|
+
setIsMuted(!isMuted)
|
|
239
|
+
break
|
|
240
|
+
case "arrowleft":
|
|
241
|
+
e.preventDefault()
|
|
242
|
+
playerRef.current?.seekTo(currentTime - 10)
|
|
243
|
+
break
|
|
244
|
+
case "arrowright":
|
|
245
|
+
e.preventDefault()
|
|
246
|
+
playerRef.current?.seekTo(currentTime + 10)
|
|
247
|
+
break
|
|
248
|
+
case "arrowup":
|
|
249
|
+
e.preventDefault()
|
|
250
|
+
setCurrentVolume(Math.min(1, currentVolume + 0.1))
|
|
251
|
+
break
|
|
252
|
+
case "arrowdown":
|
|
253
|
+
e.preventDefault()
|
|
254
|
+
setCurrentVolume(Math.max(0, currentVolume - 0.1))
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
window.addEventListener("keydown", handleKeyPress)
|
|
260
|
+
return () => window.removeEventListener("keydown", handleKeyPress)
|
|
261
|
+
}, [isPlaying, isMuted, currentTime, currentVolume, toggleFullscreen])
|
|
262
|
+
|
|
263
|
+
const playbackRates = [0.5, 0.75, 1, 1.25, 1.5, 2]
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<TooltipProvider>
|
|
267
|
+
<div
|
|
268
|
+
ref={containerRef}
|
|
269
|
+
className={cn(
|
|
270
|
+
"relative overflow-hidden rounded-lg bg-black",
|
|
271
|
+
"focus-within:outline-none",
|
|
272
|
+
containerClassName
|
|
273
|
+
)}
|
|
274
|
+
onMouseMove={handleMouseMove}
|
|
275
|
+
onMouseLeave={() => isPlaying && setShowControls(false)}
|
|
276
|
+
tabIndex={0}>
|
|
277
|
+
{/* React Player */}
|
|
278
|
+
<Suspense fallback={fallback}>
|
|
279
|
+
<ReactPlayer
|
|
280
|
+
ref={playerRef as unknown as React.Ref<any>}
|
|
281
|
+
url={url}
|
|
282
|
+
playing={isPlaying}
|
|
283
|
+
volume={isMuted ? 0 : currentVolume}
|
|
284
|
+
muted={isMuted}
|
|
285
|
+
controls={false}
|
|
286
|
+
light={light}
|
|
287
|
+
loop={loop}
|
|
288
|
+
playbackRate={currentRate}
|
|
289
|
+
width={width}
|
|
290
|
+
height={height}
|
|
291
|
+
style={{ ...style, aspectRatio: "16/9" }}
|
|
292
|
+
progressInterval={progressInterval}
|
|
293
|
+
playsinline={playsinline}
|
|
294
|
+
pip={isPiP}
|
|
295
|
+
stopOnUnmount={stopOnUnmount}
|
|
296
|
+
fallback={fallback}
|
|
297
|
+
wrapper={wrapper as any}
|
|
298
|
+
config={config as any}
|
|
299
|
+
className={cn("aspect-video", className)}
|
|
300
|
+
onReady={() => {
|
|
301
|
+
setIsLoading(false)
|
|
302
|
+
onReady?.()
|
|
303
|
+
}}
|
|
304
|
+
onStart={() => {
|
|
305
|
+
setIsLoading(false)
|
|
306
|
+
onStart?.()
|
|
307
|
+
}}
|
|
308
|
+
onPlay={() => {
|
|
309
|
+
setIsPlaying(true)
|
|
310
|
+
onPlay?.()
|
|
311
|
+
}}
|
|
312
|
+
onPause={() => {
|
|
313
|
+
setIsPlaying(false)
|
|
314
|
+
onPause?.()
|
|
315
|
+
}}
|
|
316
|
+
{...({
|
|
317
|
+
onProgress: (state: ProgressState) => {
|
|
318
|
+
setCurrentTime(state.playedSeconds)
|
|
319
|
+
setBuffered(state.loaded)
|
|
320
|
+
onProgress?.(state)
|
|
321
|
+
}
|
|
322
|
+
} as any)}
|
|
323
|
+
onDuration={(dur: number) => {
|
|
324
|
+
setDuration(dur)
|
|
325
|
+
onDuration?.(dur)
|
|
326
|
+
}}
|
|
327
|
+
onBuffer={() => {
|
|
328
|
+
setIsLoading(true)
|
|
329
|
+
onBuffer?.()
|
|
330
|
+
}}
|
|
331
|
+
onBufferEnd={() => {
|
|
332
|
+
setIsLoading(false)
|
|
333
|
+
onBufferEnd?.()
|
|
334
|
+
}}
|
|
335
|
+
onSeek={onSeek}
|
|
336
|
+
onPlaybackRateChange={(rate: number) => {
|
|
337
|
+
setCurrentRate(rate)
|
|
338
|
+
onPlaybackRateChange?.(rate)
|
|
339
|
+
}}
|
|
340
|
+
onEnded={onEnded}
|
|
341
|
+
onError={onError as any}
|
|
342
|
+
onClickPreview={onClickPreview}
|
|
343
|
+
onEnablePIP={() => {
|
|
344
|
+
setIsPiP(true)
|
|
345
|
+
onEnablePIP?.()
|
|
346
|
+
}}
|
|
347
|
+
onDisablePIP={() => {
|
|
348
|
+
setIsPiP(false)
|
|
349
|
+
onDisablePIP?.()
|
|
350
|
+
}}
|
|
351
|
+
{...props}
|
|
352
|
+
/>
|
|
353
|
+
</Suspense>
|
|
354
|
+
|
|
355
|
+
{/* Loading indicator */}
|
|
356
|
+
{isLoading && (
|
|
357
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
|
|
358
|
+
<LoadingIcon className="size-12 text-white" />
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
{/* Controls overlay */}
|
|
363
|
+
<div
|
|
364
|
+
className={cn(
|
|
365
|
+
"absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-black/20",
|
|
366
|
+
"transition-opacity duration-300",
|
|
367
|
+
showControls ? "opacity-100" : "pointer-events-none opacity-0"
|
|
368
|
+
)}>
|
|
369
|
+
{/* Top controls */}
|
|
370
|
+
<div className="absolute top-0 right-0 left-0 flex items-center justify-between p-4">
|
|
371
|
+
<h3 className="font-medium text-lg text-white">Media Player</h3>
|
|
372
|
+
<div className="flex items-center gap-2">
|
|
373
|
+
{showShare && (
|
|
374
|
+
<Tooltip>
|
|
375
|
+
<TooltipTrigger asChild>
|
|
376
|
+
<Button
|
|
377
|
+
variant="ghost"
|
|
378
|
+
size="icon"
|
|
379
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95"
|
|
380
|
+
onClick={onShare}>
|
|
381
|
+
<ShareIcon className="size-5" />
|
|
382
|
+
</Button>
|
|
383
|
+
</TooltipTrigger>
|
|
384
|
+
<TooltipContent>Share</TooltipContent>
|
|
385
|
+
</Tooltip>
|
|
386
|
+
)}
|
|
387
|
+
{showDownload && (
|
|
388
|
+
<Tooltip>
|
|
389
|
+
<TooltipTrigger asChild>
|
|
390
|
+
<Button
|
|
391
|
+
variant="ghost"
|
|
392
|
+
size="icon"
|
|
393
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95"
|
|
394
|
+
onClick={onDownload}>
|
|
395
|
+
<DownloadIcon className="size-5" />
|
|
396
|
+
</Button>
|
|
397
|
+
</TooltipTrigger>
|
|
398
|
+
<TooltipContent>Download</TooltipContent>
|
|
399
|
+
</Tooltip>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
{/* Center play button */}
|
|
405
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
406
|
+
<Button
|
|
407
|
+
variant="ghost"
|
|
408
|
+
size="icon"
|
|
409
|
+
className="pixel-button size-20 rounded-none border-2 border-gray-600 bg-gray-800/90 text-white shadow-xl transition-all duration-200 hover:scale-105 hover:border-gray-500 hover:bg-gray-700/90 active:scale-95"
|
|
410
|
+
onClick={() => setIsPlaying(!isPlaying)}>
|
|
411
|
+
{isPlaying ? <PauseIcon className="size-10" /> : <PlayIcon className="size-10" />}
|
|
412
|
+
</Button>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
{/* Bottom controls */}
|
|
416
|
+
<div className="absolute right-0 bottom-0 left-0 space-y-2 p-4">
|
|
417
|
+
{/* Progress bar */}
|
|
418
|
+
<div className="relative">
|
|
419
|
+
<Slider
|
|
420
|
+
value={[currentTime]}
|
|
421
|
+
max={duration}
|
|
422
|
+
step={1}
|
|
423
|
+
onValueChange={([value]) => {
|
|
424
|
+
if (typeof value !== "number") return
|
|
425
|
+
playerRef.current?.seekTo(value)
|
|
426
|
+
}}
|
|
427
|
+
className="pixel-progress cursor-pointer [&>span]:rounded-none [&>span]:border [&>span]:border-gray-600 [&>span]:bg-gray-800 [&>span]:shadow-lg [&_[role=slider]]:size-4 [&_[role=slider]]:rounded-none [&_[role=slider]]:border-2 [&_[role=slider]]:border-gray-400 [&_[role=slider]]:bg-white [&_[role=slider]]:shadow-md"
|
|
428
|
+
/>
|
|
429
|
+
{/* Buffered progress */}
|
|
430
|
+
<div
|
|
431
|
+
className="pointer-events-none absolute top-0 left-0 h-full rounded-none border border-gray-500/30 bg-white/20"
|
|
432
|
+
style={{ width: `${buffered * 100}%` }}
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
{/* Control buttons */}
|
|
437
|
+
<div className="flex items-center justify-between">
|
|
438
|
+
<div className="flex items-center gap-2">
|
|
439
|
+
{/* Play/Pause */}
|
|
440
|
+
<Tooltip>
|
|
441
|
+
<TooltipTrigger asChild>
|
|
442
|
+
<Button
|
|
443
|
+
variant="ghost"
|
|
444
|
+
size="icon"
|
|
445
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95"
|
|
446
|
+
onClick={() => setIsPlaying(!isPlaying)}>
|
|
447
|
+
{isPlaying ? (
|
|
448
|
+
<PauseIcon className="size-6" />
|
|
449
|
+
) : (
|
|
450
|
+
<PlayIcon className="size-6" />
|
|
451
|
+
)}
|
|
452
|
+
</Button>
|
|
453
|
+
</TooltipTrigger>
|
|
454
|
+
<TooltipContent>{isPlaying ? "Pause (k)" : "Play (k)"}</TooltipContent>
|
|
455
|
+
</Tooltip>
|
|
456
|
+
|
|
457
|
+
{/* Skip buttons */}
|
|
458
|
+
<Tooltip>
|
|
459
|
+
<TooltipTrigger asChild>
|
|
460
|
+
<Button
|
|
461
|
+
variant="ghost"
|
|
462
|
+
size="icon"
|
|
463
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95"
|
|
464
|
+
onClick={() => playerRef.current?.seekTo(currentTime - 10)}>
|
|
465
|
+
<Rewind10Icon className="size-6" />
|
|
466
|
+
</Button>
|
|
467
|
+
</TooltipTrigger>
|
|
468
|
+
<TooltipContent>Back 10s (←)</TooltipContent>
|
|
469
|
+
</Tooltip>
|
|
470
|
+
|
|
471
|
+
<Tooltip>
|
|
472
|
+
<TooltipTrigger asChild>
|
|
473
|
+
<Button
|
|
474
|
+
variant="ghost"
|
|
475
|
+
size="icon"
|
|
476
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95"
|
|
477
|
+
onClick={() => playerRef.current?.seekTo(currentTime + 10)}>
|
|
478
|
+
<Forward10Icon className="size-6" />
|
|
479
|
+
</Button>
|
|
480
|
+
</TooltipTrigger>
|
|
481
|
+
<TooltipContent>Forward 10s (→)</TooltipContent>
|
|
482
|
+
</Tooltip>
|
|
483
|
+
|
|
484
|
+
{/* Volume */}
|
|
485
|
+
<div className="flex items-center gap-2">
|
|
486
|
+
<Tooltip>
|
|
487
|
+
<TooltipTrigger asChild>
|
|
488
|
+
<Button
|
|
489
|
+
variant="ghost"
|
|
490
|
+
size="icon"
|
|
491
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95"
|
|
492
|
+
onClick={() => setIsMuted(!isMuted)}>
|
|
493
|
+
{isMuted || currentVolume === 0 ? (
|
|
494
|
+
<MuteIcon className="size-6" />
|
|
495
|
+
) : (
|
|
496
|
+
<VolumeIcon className="size-6" />
|
|
497
|
+
)}
|
|
498
|
+
</Button>
|
|
499
|
+
</TooltipTrigger>
|
|
500
|
+
<TooltipContent>{isMuted ? "Unmute (m)" : "Mute (m)"}</TooltipContent>
|
|
501
|
+
</Tooltip>
|
|
502
|
+
<Slider
|
|
503
|
+
value={[isMuted ? 0 : currentVolume]}
|
|
504
|
+
max={1}
|
|
505
|
+
step={0.05}
|
|
506
|
+
onValueChange={([value]) => {
|
|
507
|
+
if (typeof value !== "number") return
|
|
508
|
+
setCurrentVolume(value)
|
|
509
|
+
setIsMuted(value === 0)
|
|
510
|
+
}}
|
|
511
|
+
className="pixel-progress w-20 cursor-pointer [&>span]:rounded-none [&>span]:border [&>span]:border-gray-600 [&>span]:bg-gray-800 [&>span]:shadow-lg [&_[role=slider]]:size-3 [&_[role=slider]]:rounded-none [&_[role=slider]]:border-2 [&_[role=slider]]:border-gray-400 [&_[role=slider]]:bg-white [&_[role=slider]]:shadow-md"
|
|
512
|
+
/>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
{/* Time display */}
|
|
516
|
+
<span className="pixel-time-display rounded-none border border-gray-600/50 bg-gray-900/80 px-2 py-1 font-mono text-sm text-white shadow-lg">
|
|
517
|
+
{formatTime(currentTime)} / {formatTime(duration)}
|
|
518
|
+
</span>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<div className="flex items-center gap-2">
|
|
522
|
+
{/* Subtitles */}
|
|
523
|
+
{showSubtitles && subtitleTracks.length > 0 && (
|
|
524
|
+
<DropdownMenu>
|
|
525
|
+
<Tooltip>
|
|
526
|
+
<TooltipTrigger asChild>
|
|
527
|
+
<DropdownMenuTrigger asChild>
|
|
528
|
+
<Button
|
|
529
|
+
variant="ghost"
|
|
530
|
+
size="icon"
|
|
531
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95">
|
|
532
|
+
<SubtitlesIcon className="size-6" />
|
|
533
|
+
</Button>
|
|
534
|
+
</DropdownMenuTrigger>
|
|
535
|
+
</TooltipTrigger>
|
|
536
|
+
<TooltipContent>Subtitles</TooltipContent>
|
|
537
|
+
</Tooltip>
|
|
538
|
+
<DropdownMenuContent>
|
|
539
|
+
<DropdownMenuLabel>Subtitles</DropdownMenuLabel>
|
|
540
|
+
<DropdownMenuSeparator />
|
|
541
|
+
<DropdownMenuItem onClick={() => setSelectedSubtitle(null)}>
|
|
542
|
+
Off
|
|
543
|
+
</DropdownMenuItem>
|
|
544
|
+
{subtitleTracks.map((track) => (
|
|
545
|
+
<DropdownMenuItem
|
|
546
|
+
key={track.value}
|
|
547
|
+
onClick={() => setSelectedSubtitle(track.value)}>
|
|
548
|
+
{track.label}
|
|
549
|
+
</DropdownMenuItem>
|
|
550
|
+
))}
|
|
551
|
+
</DropdownMenuContent>
|
|
552
|
+
</DropdownMenu>
|
|
553
|
+
)}
|
|
554
|
+
|
|
555
|
+
{/* Quality */}
|
|
556
|
+
{showQuality && (
|
|
557
|
+
<DropdownMenu>
|
|
558
|
+
<Tooltip>
|
|
559
|
+
<TooltipTrigger asChild>
|
|
560
|
+
<DropdownMenuTrigger asChild>
|
|
561
|
+
<Button
|
|
562
|
+
variant="ghost"
|
|
563
|
+
size="icon"
|
|
564
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95">
|
|
565
|
+
<QualityIcon className="size-6" />
|
|
566
|
+
</Button>
|
|
567
|
+
</DropdownMenuTrigger>
|
|
568
|
+
</TooltipTrigger>
|
|
569
|
+
<TooltipContent>Quality</TooltipContent>
|
|
570
|
+
</Tooltip>
|
|
571
|
+
<DropdownMenuContent>
|
|
572
|
+
<DropdownMenuLabel>Quality</DropdownMenuLabel>
|
|
573
|
+
<DropdownMenuSeparator />
|
|
574
|
+
{qualityOptions.map((option) => (
|
|
575
|
+
<DropdownMenuItem
|
|
576
|
+
key={option.value}
|
|
577
|
+
onClick={() => setSelectedQuality(option.value)}>
|
|
578
|
+
{option.label} {selectedQuality === option.value && "✓"}
|
|
579
|
+
</DropdownMenuItem>
|
|
580
|
+
))}
|
|
581
|
+
</DropdownMenuContent>
|
|
582
|
+
</DropdownMenu>
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
{/* Playback speed */}
|
|
586
|
+
<DropdownMenu>
|
|
587
|
+
<Tooltip>
|
|
588
|
+
<TooltipTrigger asChild>
|
|
589
|
+
<DropdownMenuTrigger asChild>
|
|
590
|
+
<Button
|
|
591
|
+
variant="ghost"
|
|
592
|
+
size="icon"
|
|
593
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95">
|
|
594
|
+
<SpeedIcon className="size-6" />
|
|
595
|
+
</Button>
|
|
596
|
+
</DropdownMenuTrigger>
|
|
597
|
+
</TooltipTrigger>
|
|
598
|
+
<TooltipContent>Playback Speed</TooltipContent>
|
|
599
|
+
</Tooltip>
|
|
600
|
+
<DropdownMenuContent>
|
|
601
|
+
<DropdownMenuLabel>Speed</DropdownMenuLabel>
|
|
602
|
+
<DropdownMenuSeparator />
|
|
603
|
+
{playbackRates.map((rate) => (
|
|
604
|
+
<DropdownMenuItem key={rate} onClick={() => setCurrentRate(rate)}>
|
|
605
|
+
{rate}x {currentRate === rate && "✓"}
|
|
606
|
+
</DropdownMenuItem>
|
|
607
|
+
))}
|
|
608
|
+
</DropdownMenuContent>
|
|
609
|
+
</DropdownMenu>
|
|
610
|
+
|
|
611
|
+
{/* Picture in Picture */}
|
|
612
|
+
<Tooltip>
|
|
613
|
+
<TooltipTrigger asChild>
|
|
614
|
+
<Button
|
|
615
|
+
variant="ghost"
|
|
616
|
+
size="icon"
|
|
617
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95"
|
|
618
|
+
onClick={() => setIsPiP(!isPiP)}>
|
|
619
|
+
<PictureInPictureIcon className="size-6" />
|
|
620
|
+
</Button>
|
|
621
|
+
</TooltipTrigger>
|
|
622
|
+
<TooltipContent>Picture in Picture</TooltipContent>
|
|
623
|
+
</Tooltip>
|
|
624
|
+
|
|
625
|
+
{/* Fullscreen */}
|
|
626
|
+
<Tooltip>
|
|
627
|
+
<TooltipTrigger asChild>
|
|
628
|
+
<Button
|
|
629
|
+
variant="ghost"
|
|
630
|
+
size="icon"
|
|
631
|
+
className="pixel-button rounded-none border border-gray-600/50 bg-gray-800/80 text-white shadow-lg transition-all duration-150 hover:border-gray-500 hover:bg-gray-700/80 active:scale-95"
|
|
632
|
+
onClick={toggleFullscreen}>
|
|
633
|
+
{isFullscreen ? (
|
|
634
|
+
<ExitFullscreenIcon className="size-6" />
|
|
635
|
+
) : (
|
|
636
|
+
<FullscreenIcon className="size-6" />
|
|
637
|
+
)}
|
|
638
|
+
</Button>
|
|
639
|
+
</TooltipTrigger>
|
|
640
|
+
<TooltipContent>
|
|
641
|
+
{isFullscreen ? "Exit Fullscreen (f)" : "Fullscreen (f)"}
|
|
642
|
+
</TooltipContent>
|
|
643
|
+
</Tooltip>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
</TooltipProvider>
|
|
650
|
+
)
|
|
651
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef } from "react"
|
|
4
|
+
import "video.js/dist/video-js.css"
|
|
5
|
+
|
|
6
|
+
export function SimpleVideoPlayer({ src, type = "video/mp4" }: { src: string; type?: string }) {
|
|
7
|
+
const videoRef = useRef<HTMLVideoElement>(null)
|
|
8
|
+
const playerRef = useRef<any>(null)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const loadPlayer = async () => {
|
|
12
|
+
if (!videoRef.current || playerRef.current) return
|
|
13
|
+
|
|
14
|
+
// Dynamically import video.js
|
|
15
|
+
const videojs = (await import("video.js")).default
|
|
16
|
+
|
|
17
|
+
// Initialize player
|
|
18
|
+
const player = videojs(videoRef.current, {
|
|
19
|
+
controls: true,
|
|
20
|
+
autoplay: false,
|
|
21
|
+
preload: 'auto',
|
|
22
|
+
fluid: true,
|
|
23
|
+
sources: [{
|
|
24
|
+
src: src,
|
|
25
|
+
type: type
|
|
26
|
+
}]
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
playerRef.current = player
|
|
30
|
+
|
|
31
|
+
player.ready(() => {
|
|
32
|
+
console.log("Player is ready!")
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
loadPlayer()
|
|
37
|
+
|
|
38
|
+
// Cleanup
|
|
39
|
+
return () => {
|
|
40
|
+
if (playerRef.current) {
|
|
41
|
+
playerRef.current.dispose()
|
|
42
|
+
playerRef.current = null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}, [src, type])
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="w-full">
|
|
49
|
+
<div data-vjs-player>
|
|
50
|
+
<video
|
|
51
|
+
ref={videoRef}
|
|
52
|
+
className="video-js vjs-big-play-centered vjs-default-skin w-full"
|
|
53
|
+
playsInline
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
SimpleVideoPlayer.displayName = "SimpleVideoPlayer"
|