@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,107 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Pause, Play } from "lucide-react"
|
|
4
|
+
import { forwardRef } from "react"
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
import type { PlayButtonProps } from "../../types/index"
|
|
7
|
+
|
|
8
|
+
const sizeVariants = {
|
|
9
|
+
sm: { button: "size-8", icon: 16 },
|
|
10
|
+
md: { button: "size-10", icon: 20 },
|
|
11
|
+
lg: { button: "size-12", icon: 24 },
|
|
12
|
+
xl: { button: "size-16", icon: 32 }
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export const PlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>(
|
|
16
|
+
({ isPlaying, onToggle, disabled = false, size = "lg", className }, ref) => {
|
|
17
|
+
const sizeConfig = sizeVariants[size] || sizeVariants.lg
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
ref={ref}
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={onToggle}
|
|
24
|
+
disabled={disabled}
|
|
25
|
+
data-state={isPlaying ? "playing" : "paused"}
|
|
26
|
+
className={cn(
|
|
27
|
+
// Base styles with new design system
|
|
28
|
+
"group relative inline-flex items-center justify-center",
|
|
29
|
+
"glass-strong media-control rounded-full contain-layout",
|
|
30
|
+
"focus-ring touch-manipulation",
|
|
31
|
+
sizeConfig.button,
|
|
32
|
+
|
|
33
|
+
// Color transitions with OKLCH
|
|
34
|
+
"text-[color:var(--media-text)] transition-all duration-300",
|
|
35
|
+
"bg-[color:var(--media-control-bg)] backdrop-blur-sm",
|
|
36
|
+
"border border-[color:var(--media-border)]/30",
|
|
37
|
+
|
|
38
|
+
// Interactive states
|
|
39
|
+
"hover:bg-[color:var(--media-control-hover)]",
|
|
40
|
+
"hover:border-[color:var(--media-border)]/50",
|
|
41
|
+
"hover:shadow-[color:var(--media-bg)]/20 hover:shadow-lg",
|
|
42
|
+
"hover:-translate-y-0.5 hover:scale-105",
|
|
43
|
+
|
|
44
|
+
"active:bg-[color:var(--media-control-active)]",
|
|
45
|
+
"active:translate-y-0 active:scale-100",
|
|
46
|
+
|
|
47
|
+
// Focus state
|
|
48
|
+
"focus-visible:ring-2 focus-visible:ring-[color:var(--media-focus)]",
|
|
49
|
+
"focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--media-bg)]",
|
|
50
|
+
|
|
51
|
+
// Disabled state
|
|
52
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
53
|
+
"disabled:hover:translate-y-0 disabled:hover:scale-100",
|
|
54
|
+
"disabled:hover:bg-[color:var(--media-control-bg)]",
|
|
55
|
+
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
aria-label={isPlaying ? "Pause media" : "Play media"}
|
|
59
|
+
aria-pressed={isPlaying}>
|
|
60
|
+
{/* Background glow effect */}
|
|
61
|
+
<div
|
|
62
|
+
className={cn(
|
|
63
|
+
"absolute inset-0 rounded-full opacity-0 transition-opacity duration-300",
|
|
64
|
+
"bg-gradient-to-br from-[color:var(--color-media-play)] to-[color:var(--color-media-pause)]",
|
|
65
|
+
"blur-sm group-hover:opacity-20",
|
|
66
|
+
isPlaying && "opacity-10"
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
{/* Icon with enhanced styling */}
|
|
71
|
+
{isPlaying ? (
|
|
72
|
+
<Pause
|
|
73
|
+
size={sizeConfig.icon}
|
|
74
|
+
className={cn(
|
|
75
|
+
"relative z-10 fill-current transition-all duration-200",
|
|
76
|
+
"text-[color:var(--color-media-pause)]",
|
|
77
|
+
"group-hover:scale-110 group-active:scale-95",
|
|
78
|
+
"drop-shadow-sm"
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
) : (
|
|
82
|
+
<Play
|
|
83
|
+
size={sizeConfig.icon}
|
|
84
|
+
className={cn(
|
|
85
|
+
"relative z-10 fill-current transition-all duration-200",
|
|
86
|
+
"ml-0.5 text-[color:var(--color-media-play)]",
|
|
87
|
+
"group-hover:scale-110 group-active:scale-95",
|
|
88
|
+
"drop-shadow-sm"
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Ripple effect */}
|
|
94
|
+
<div
|
|
95
|
+
className={cn(
|
|
96
|
+
"absolute inset-0 rounded-full opacity-0",
|
|
97
|
+
"scale-75 bg-[color:var(--media-text)]/10",
|
|
98
|
+
"group-active:scale-100 group-active:opacity-100",
|
|
99
|
+
"transition-all duration-150 ease-out"
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
</button>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
PlayButton.displayName = "PlayButton"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as Slider from "@radix-ui/react-slider"
|
|
4
|
+
import * as Tooltip from "@radix-ui/react-tooltip"
|
|
5
|
+
import { forwardRef, useCallback, useState, useMemo, useTransition, useDeferredValue, useRef } from "react"
|
|
6
|
+
import { formatTime } from "../../lib/format-time"
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
import type { SeekBarProps } from "../../types/index"
|
|
9
|
+
|
|
10
|
+
export const SeekBar = forwardRef<HTMLDivElement, SeekBarProps>(
|
|
11
|
+
({ currentTime, duration, buffered, onSeek, size = "sm", className }, ref) => {
|
|
12
|
+
// React 19 concurrent features
|
|
13
|
+
const [isPending, startTransition] = useTransition()
|
|
14
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
15
|
+
const [previewTime, setPreviewTime] = useState(0)
|
|
16
|
+
const [isHovering, setIsHovering] = useState(false)
|
|
17
|
+
|
|
18
|
+
// Defer non-critical updates for better performance
|
|
19
|
+
const deferredHovering = useDeferredValue(isHovering)
|
|
20
|
+
const deferredPreviewTime = useDeferredValue(previewTime)
|
|
21
|
+
|
|
22
|
+
// Memoize expensive calculations
|
|
23
|
+
const progress = useMemo(() => duration > 0 ? (currentTime / duration) * 100 : 0, [currentTime, duration])
|
|
24
|
+
|
|
25
|
+
// Calculate buffered ranges with memoization
|
|
26
|
+
const bufferedRanges = useMemo(() => {
|
|
27
|
+
const ranges = []
|
|
28
|
+
if (buffered && duration > 0) {
|
|
29
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
30
|
+
const start = (buffered.start(i) / duration) * 100
|
|
31
|
+
const end = (buffered.end(i) / duration) * 100
|
|
32
|
+
ranges.push({ start, end })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return ranges
|
|
36
|
+
}, [buffered, duration])
|
|
37
|
+
|
|
38
|
+
// Throttled seek handler with React 19 transitions
|
|
39
|
+
const seekTimeoutRef = useRef<NodeJS.Timeout>()
|
|
40
|
+
const lastSeekTime = useRef<number>(0)
|
|
41
|
+
|
|
42
|
+
const handleValueChange = ([value]: number[]) => {
|
|
43
|
+
const time = ((value ?? 0) / 100) * duration
|
|
44
|
+
setPreviewTime(time)
|
|
45
|
+
if (isDragging) {
|
|
46
|
+
onSeek(time)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleValueCommit = ([value]: number[]) => {
|
|
51
|
+
const time = ((value ?? 0) / 100) * duration
|
|
52
|
+
onSeek(time)
|
|
53
|
+
setIsDragging(false)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handlePointerDown = () => {
|
|
57
|
+
setIsDragging(true)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cn(
|
|
64
|
+
"media-container relative w-full text-[color:var(--media-text)]",
|
|
65
|
+
"touch-manipulation contain-layout",
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
onMouseEnter={() => setIsHovering(true)}
|
|
69
|
+
onMouseLeave={() => setIsHovering(false)}>
|
|
70
|
+
<Tooltip.Provider>
|
|
71
|
+
<Tooltip.Root>
|
|
72
|
+
<Tooltip.Trigger asChild>
|
|
73
|
+
<div className="relative animate-enter">
|
|
74
|
+
<Slider.Root
|
|
75
|
+
className={cn(
|
|
76
|
+
"group relative flex w-full touch-none select-none items-center",
|
|
77
|
+
"media-md:h-4 media-sm:h-3 media-xs:h-2",
|
|
78
|
+
"focus-ring cursor-pointer"
|
|
79
|
+
)}
|
|
80
|
+
value={[progress]}
|
|
81
|
+
max={100}
|
|
82
|
+
step={0.1}
|
|
83
|
+
onValueChange={handleValueChange}
|
|
84
|
+
onValueCommit={handleValueCommit}
|
|
85
|
+
onPointerDown={handlePointerDown}
|
|
86
|
+
aria-label="Seek to position">
|
|
87
|
+
<Slider.Track
|
|
88
|
+
className={cn(
|
|
89
|
+
"relative grow overflow-hidden rounded-full",
|
|
90
|
+
"bg-[color:var(--media-control-bg)]/40 backdrop-blur-sm",
|
|
91
|
+
"border border-[color:var(--media-border)]/20",
|
|
92
|
+
"shadow-inner transition-all duration-300 ease-out",
|
|
93
|
+
"h-1 group-focus-within:h-1.5 group-hover:h-1.5",
|
|
94
|
+
"group-hover:bg-[color:var(--media-control-bg)]/60",
|
|
95
|
+
"group-hover:border-[color:var(--media-border)]/40"
|
|
96
|
+
)}>
|
|
97
|
+
{/* Buffered ranges */}
|
|
98
|
+
{bufferedRanges.map((range, index) => (
|
|
99
|
+
<div
|
|
100
|
+
key={index}
|
|
101
|
+
className={cn(
|
|
102
|
+
"absolute h-full rounded-full transition-all duration-500",
|
|
103
|
+
"bg-[color:var(--color-media-buffered)] shadow-inner",
|
|
104
|
+
"opacity-60 group-hover:opacity-80"
|
|
105
|
+
)}
|
|
106
|
+
style={{
|
|
107
|
+
left: `${range.start}%`,
|
|
108
|
+
width: `${range.end - range.start}%`
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
|
|
113
|
+
{/* Progress range with gradient */}
|
|
114
|
+
<Slider.Range
|
|
115
|
+
className={cn(
|
|
116
|
+
"absolute h-full rounded-full transition-all duration-300",
|
|
117
|
+
"bg-gradient-to-r from-[color:var(--color-media-progress)] to-[color:var(--color-brand-accent)]",
|
|
118
|
+
"shadow-[color:var(--color-brand-primary)]/20 shadow-lg",
|
|
119
|
+
"group-hover:shadow-xl"
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
</Slider.Track>
|
|
123
|
+
|
|
124
|
+
<Slider.Thumb
|
|
125
|
+
className={cn(
|
|
126
|
+
"block size-4 rounded-full border-white/20 bg-white",
|
|
127
|
+
"shadow-lg hover:shadow-xl",
|
|
128
|
+
"opacity-0 group-hover:opacity-100",
|
|
129
|
+
"hover:scale-110 hover:bg-white/95 active:scale-95",
|
|
130
|
+
"focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-white/50",
|
|
131
|
+
"focus:ring-offset-2 focus:ring-offset-black/20",
|
|
132
|
+
"transition-all duration-300 ease-out",
|
|
133
|
+
isDragging && "opacity-100"
|
|
134
|
+
)}
|
|
135
|
+
aria-label="Seek"
|
|
136
|
+
/>
|
|
137
|
+
</Slider.Root>
|
|
138
|
+
</div>
|
|
139
|
+
</Tooltip.Trigger>
|
|
140
|
+
|
|
141
|
+
{isDragging && (
|
|
142
|
+
<Tooltip.Portal>
|
|
143
|
+
<Tooltip.Content
|
|
144
|
+
className={cn(
|
|
145
|
+
"rounded bg-black/80 px-2 py-1 text-sm text-white",
|
|
146
|
+
"fade-in-0 zoom-in-95 animate-in"
|
|
147
|
+
)}
|
|
148
|
+
sideOffset={10}>
|
|
149
|
+
{formatTime(previewTime)}
|
|
150
|
+
<Tooltip.Arrow className="fill-black/80" />
|
|
151
|
+
</Tooltip.Content>
|
|
152
|
+
</Tooltip.Portal>
|
|
153
|
+
)}
|
|
154
|
+
</Tooltip.Root>
|
|
155
|
+
</Tooltip.Provider>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
SeekBar.displayName = "SeekBar"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react"
|
|
4
|
+
import { formatTime } from "../../lib/format-time"
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
import type { TimeDisplayProps } from "../../types/index"
|
|
7
|
+
|
|
8
|
+
export const TimeDisplay = forwardRef<HTMLDivElement, TimeDisplayProps>(
|
|
9
|
+
({ currentTime, duration, showRemaining = false, size = "sm", className }, ref) => {
|
|
10
|
+
const current = formatTime(currentTime)
|
|
11
|
+
const total = formatTime(duration)
|
|
12
|
+
const remaining = formatTime(duration - currentTime)
|
|
13
|
+
|
|
14
|
+
const sizeClasses: Record<"sm" | "md" | "lg" | "xl", string> = {
|
|
15
|
+
sm: "text-xs",
|
|
16
|
+
md: "text-sm",
|
|
17
|
+
lg: "text-base",
|
|
18
|
+
xl: "text-lg"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex items-center font-mono text-white text-xs",
|
|
26
|
+
"rounded-lg backdrop-blur-sm",
|
|
27
|
+
"px-3 py-1.5",
|
|
28
|
+
sizeClasses[size],
|
|
29
|
+
"select-none",
|
|
30
|
+
className
|
|
31
|
+
)}>
|
|
32
|
+
<span className="tabular-nums">{current}</span>
|
|
33
|
+
<span className="mx-1 text-white">/</span>
|
|
34
|
+
<span className="tabular-nums">{showRemaining ? `-${remaining}` : total}</span>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
TimeDisplay.displayName = "TimeDisplay"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as Slider from "@radix-ui/react-slider"
|
|
4
|
+
import * as Tooltip from "@radix-ui/react-tooltip"
|
|
5
|
+
import { Volume1, Volume2, VolumeX } from "lucide-react"
|
|
6
|
+
import { forwardRef } from "react"
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
import type { VolumeControlProps } from "../../types/index"
|
|
9
|
+
|
|
10
|
+
const sizeConfig = {
|
|
11
|
+
md: {
|
|
12
|
+
button: "size-10",
|
|
13
|
+
icon: 18,
|
|
14
|
+
slider: "h-6",
|
|
15
|
+
thumb: "size-3",
|
|
16
|
+
track: "h-1"
|
|
17
|
+
},
|
|
18
|
+
lg: {
|
|
19
|
+
button: "size-12",
|
|
20
|
+
icon: 22,
|
|
21
|
+
slider: "h-7",
|
|
22
|
+
thumb: "size-4",
|
|
23
|
+
track: "h-1.5"
|
|
24
|
+
},
|
|
25
|
+
xl: {
|
|
26
|
+
button: "size-14",
|
|
27
|
+
icon: 26,
|
|
28
|
+
slider: "h-8",
|
|
29
|
+
thumb: "size-5",
|
|
30
|
+
track: "h-2"
|
|
31
|
+
}
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
export const VolumeControl = forwardRef<HTMLDivElement, VolumeControlProps>(
|
|
35
|
+
({ volume, isMuted, onVolumeChange, onToggleMute, size = "lg", className }, ref) => {
|
|
36
|
+
const getVolumeIcon = () => {
|
|
37
|
+
if (isMuted || volume === 0) {
|
|
38
|
+
return VolumeX
|
|
39
|
+
} else if (volume < 0.5) {
|
|
40
|
+
return Volume1
|
|
41
|
+
} else {
|
|
42
|
+
return Volume2
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const VolumeIcon = getVolumeIcon()
|
|
47
|
+
const buttonSizes: Record<"md" | "lg" | "xl", string> = {
|
|
48
|
+
md: "h-11 w-11",
|
|
49
|
+
lg: "h-12 w-12",
|
|
50
|
+
xl: "h-14 w-14"
|
|
51
|
+
}
|
|
52
|
+
const iconSizes: Record<"md" | "lg" | "xl", string> = {
|
|
53
|
+
md: "h-5 w-5",
|
|
54
|
+
lg: "h-6 w-6",
|
|
55
|
+
xl: "h-7 w-7"
|
|
56
|
+
}
|
|
57
|
+
const sliderHeight: Record<"md" | "lg" | "xl", string> = {
|
|
58
|
+
md: "h-6",
|
|
59
|
+
lg: "h-7",
|
|
60
|
+
xl: "h-8"
|
|
61
|
+
}
|
|
62
|
+
const thumbSize: Record<"md" | "lg" | "xl", string> = {
|
|
63
|
+
md: "h-4 w-4",
|
|
64
|
+
lg: "h-4 w-4",
|
|
65
|
+
xl: "h-5 w-5"
|
|
66
|
+
}
|
|
67
|
+
const trackHeight: Record<"md" | "lg" | "xl", string> = {
|
|
68
|
+
md: "h-1.5",
|
|
69
|
+
lg: "h-2",
|
|
70
|
+
xl: "h-2.5"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div ref={ref} className={cn("flex items-center space-x-3 text-white", className)}>
|
|
75
|
+
<Tooltip.Provider>
|
|
76
|
+
<Tooltip.Root>
|
|
77
|
+
<Tooltip.Trigger asChild>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={onToggleMute}
|
|
81
|
+
className={cn(
|
|
82
|
+
"inline-flex items-center justify-center rounded-lg bg-transparent p-0 text-white transition-all hover:text-cyan-700 active:scale-95",
|
|
83
|
+
"focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-black/20",
|
|
84
|
+
buttonSizes[size]
|
|
85
|
+
)}
|
|
86
|
+
aria-label={isMuted ? "Unmute" : "Mute"}>
|
|
87
|
+
<VolumeIcon className={cn(iconSizes[size])} />
|
|
88
|
+
</button>
|
|
89
|
+
</Tooltip.Trigger>
|
|
90
|
+
<Tooltip.Portal>
|
|
91
|
+
<Tooltip.Content
|
|
92
|
+
className={cn(
|
|
93
|
+
"rounded bg-black/80 px-2 py-1 text-sm text-white",
|
|
94
|
+
"fade-in-0 zoom-in-95 animate-in"
|
|
95
|
+
)}
|
|
96
|
+
sideOffset={5}>
|
|
97
|
+
{isMuted ? "Unmute" : "Mute"}
|
|
98
|
+
<Tooltip.Arrow className="fill-black/80" />
|
|
99
|
+
</Tooltip.Content>
|
|
100
|
+
</Tooltip.Portal>
|
|
101
|
+
</Tooltip.Root>
|
|
102
|
+
</Tooltip.Provider>
|
|
103
|
+
|
|
104
|
+
<div className="w-32">
|
|
105
|
+
<Slider.Root
|
|
106
|
+
className={cn(
|
|
107
|
+
"group relative flex w-full touch-none select-none items-center",
|
|
108
|
+
sliderHeight[size]
|
|
109
|
+
)}
|
|
110
|
+
value={[isMuted ? 0 : volume]}
|
|
111
|
+
max={1}
|
|
112
|
+
step={0.01}
|
|
113
|
+
onValueChange={([value]) => onVolumeChange(value ?? 0)}
|
|
114
|
+
aria-label="Volume">
|
|
115
|
+
<Slider.Track
|
|
116
|
+
className={cn(
|
|
117
|
+
"relative grow rounded-full bg-white/20 shadow-inner transition-all duration-300 group-hover:bg-white/30",
|
|
118
|
+
"border border-white/10",
|
|
119
|
+
trackHeight[size]
|
|
120
|
+
)}>
|
|
121
|
+
<Slider.Range className="absolute h-full rounded-full bg-white shadow-sm" />
|
|
122
|
+
</Slider.Track>
|
|
123
|
+
<Slider.Thumb
|
|
124
|
+
className={cn(
|
|
125
|
+
"block rounded-full border-2 border-white/20 bg-white shadow-lg",
|
|
126
|
+
"transition-all duration-300 ease-out hover:scale-110 hover:bg-white/95 active:scale-95",
|
|
127
|
+
"focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-black/20",
|
|
128
|
+
thumbSize[size]
|
|
129
|
+
)}
|
|
130
|
+
aria-label="Volume"
|
|
131
|
+
/>
|
|
132
|
+
</Slider.Root>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<span className="w-10 text-right text-sm text-white">
|
|
136
|
+
{Math.round((isMuted ? 0 : volume) * 100)}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
VolumeControl.displayName = "VolumeControl"
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Media Player - Core unified functionality for video and audio
|
|
3
|
+
* This consolidates the logic from MediaPlayer, VideoPlayer, and AudioPlayer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use client"
|
|
7
|
+
|
|
8
|
+
import { forwardRef, useMemo, useTransition, startTransition } from "react"
|
|
9
|
+
import { useKeyboardControls } from "../../hooks/use-keyboard-controls"
|
|
10
|
+
import { useEnhancedMediaPlayer } from "../../hooks/use-enhanced-media-player"
|
|
11
|
+
import { useMemoryOptimization } from "../../hooks/use-memory-optimization"
|
|
12
|
+
import { EnhancedMediaWrapper } from "../media-suspense"
|
|
13
|
+
import { cn } from "../../lib/utils"
|
|
14
|
+
import type { MediaPlayerProps } from "../../types"
|
|
15
|
+
|
|
16
|
+
export interface BaseMediaPlayerProps extends Omit<MediaPlayerProps, 'poster' | 'aspectRatio' | 'width' | 'height'> {
|
|
17
|
+
// Media type detection
|
|
18
|
+
mediaType?: 'video' | 'audio' | 'auto'
|
|
19
|
+
|
|
20
|
+
// Video-specific props (only used for video content)
|
|
21
|
+
poster?: string
|
|
22
|
+
aspectRatio?: string
|
|
23
|
+
width?: string | number
|
|
24
|
+
height?: string | number
|
|
25
|
+
|
|
26
|
+
// Enhanced features
|
|
27
|
+
enablePerformanceMonitoring?: boolean
|
|
28
|
+
bufferingStrategy?: 'adaptive' | 'conservative' | 'aggressive'
|
|
29
|
+
preloadStrategy?: 'progressive' | 'eager' | 'lazy'
|
|
30
|
+
enableOptimizations?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const BaseMediaPlayer = forwardRef<HTMLDivElement, BaseMediaPlayerProps>(
|
|
34
|
+
(
|
|
35
|
+
{
|
|
36
|
+
src,
|
|
37
|
+
mediaType = 'auto',
|
|
38
|
+
className,
|
|
39
|
+
children,
|
|
40
|
+
keyboardControls = true,
|
|
41
|
+
showControls = true,
|
|
42
|
+
customControls = false,
|
|
43
|
+
crossOrigin,
|
|
44
|
+
enablePerformanceMonitoring = true,
|
|
45
|
+
bufferingStrategy = "adaptive",
|
|
46
|
+
preloadStrategy = "progressive",
|
|
47
|
+
enableOptimizations = true,
|
|
48
|
+
// Video-specific props
|
|
49
|
+
poster,
|
|
50
|
+
aspectRatio = "16/9",
|
|
51
|
+
width = "100%",
|
|
52
|
+
height = "auto",
|
|
53
|
+
...options
|
|
54
|
+
},
|
|
55
|
+
ref
|
|
56
|
+
) => {
|
|
57
|
+
// React 19 concurrent features
|
|
58
|
+
const [isPending] = useTransition()
|
|
59
|
+
|
|
60
|
+
// Auto-detect media type if not specified
|
|
61
|
+
const detectedMediaType = useMemo(() => {
|
|
62
|
+
if (mediaType !== 'auto') return mediaType
|
|
63
|
+
|
|
64
|
+
// Simple detection based on file extension
|
|
65
|
+
if (typeof src === 'string') {
|
|
66
|
+
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
|
|
67
|
+
const audioExtensions = ['.mp3', '.wav', '.ogg', '.aac', '.flac', '.m4a']
|
|
68
|
+
|
|
69
|
+
const extension = src.toLowerCase().split('.').pop()
|
|
70
|
+
if (extension && videoExtensions.some(ext => extension.endsWith(ext))) {
|
|
71
|
+
return 'video'
|
|
72
|
+
}
|
|
73
|
+
if (extension && audioExtensions.some(ext => extension.endsWith(ext))) {
|
|
74
|
+
return 'audio'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Default to video for unknown types
|
|
79
|
+
return 'video'
|
|
80
|
+
}, [src, mediaType])
|
|
81
|
+
|
|
82
|
+
// Enhanced media player with optimizations
|
|
83
|
+
const { state, controls, mediaRef, containerRef, performanceMetrics, isEnhanced } = useEnhancedMediaPlayer({
|
|
84
|
+
src,
|
|
85
|
+
crossOrigin,
|
|
86
|
+
enablePerformanceMonitoring,
|
|
87
|
+
bufferingStrategy,
|
|
88
|
+
preloadStrategy,
|
|
89
|
+
enableTimeUpdateThrottling: true,
|
|
90
|
+
timeUpdateThrottleMs: 100,
|
|
91
|
+
enableSeekPreview: true,
|
|
92
|
+
...options
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Memory optimization
|
|
96
|
+
const { memoryStats, addEventListener } = useMemoryOptimization({
|
|
97
|
+
maxMemoryUsage: 50, // 50MB for media player
|
|
98
|
+
cleanupInterval: 15000, // 15 seconds
|
|
99
|
+
enableGC: true,
|
|
100
|
+
monitorMemory: enableOptimizations
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Keyboard controls
|
|
104
|
+
useKeyboardControls({
|
|
105
|
+
enabled: keyboardControls,
|
|
106
|
+
state,
|
|
107
|
+
controls
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Determine the media element based on type
|
|
111
|
+
const MediaElement = useMemo(() => {
|
|
112
|
+
const commonProps = {
|
|
113
|
+
ref: mediaRef,
|
|
114
|
+
src,
|
|
115
|
+
className: cn(
|
|
116
|
+
"w-full h-full",
|
|
117
|
+
state.isLoading && "opacity-0",
|
|
118
|
+
!customControls && showControls && "cursor-pointer"
|
|
119
|
+
),
|
|
120
|
+
onPlay: controls.play,
|
|
121
|
+
onPause: controls.pause,
|
|
122
|
+
onVolumeChange: (e: any) => controls.setVolume(e.target.volume),
|
|
123
|
+
onTimeUpdate: (e: any) => controls.setCurrentTime(e.target.currentTime),
|
|
124
|
+
onSeeked: (e: any) => controls.setCurrentTime(e.target.currentTime),
|
|
125
|
+
crossOrigin
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (detectedMediaType === 'audio') {
|
|
129
|
+
return (
|
|
130
|
+
<audio {...commonProps} controls={!customControls && showControls}>
|
|
131
|
+
{src && <source src={typeof src === 'string' ? src : src.src} />}
|
|
132
|
+
Your browser does not support the audio element.
|
|
133
|
+
</audio>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Video (default)
|
|
138
|
+
return (
|
|
139
|
+
<video
|
|
140
|
+
{...commonProps}
|
|
141
|
+
poster={poster}
|
|
142
|
+
muted={state.isMuted}
|
|
143
|
+
playsInline
|
|
144
|
+
controls={!customControls && showControls}
|
|
145
|
+
>
|
|
146
|
+
{Array.isArray(src) ? (
|
|
147
|
+
src.map((source, index) => (
|
|
148
|
+
<source key={index} src={source.src} type={source.type} />
|
|
149
|
+
))
|
|
150
|
+
) : src ? (
|
|
151
|
+
<source src={typeof src === 'string' ? src : src.src} />
|
|
152
|
+
) : null}
|
|
153
|
+
Your browser does not support the video element.
|
|
154
|
+
</video>
|
|
155
|
+
)
|
|
156
|
+
}, [detectedMediaType, src, mediaRef, state, controls, customControls, showControls, poster])
|
|
157
|
+
|
|
158
|
+
// Container styles based on media type
|
|
159
|
+
const containerStyles = useMemo(() => {
|
|
160
|
+
if (detectedMediaType === 'audio') {
|
|
161
|
+
return cn(
|
|
162
|
+
"relative bg-gradient-to-br from-gray-900 to-gray-800 rounded-lg overflow-hidden",
|
|
163
|
+
"flex items-center justify-center p-8",
|
|
164
|
+
className
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Video styles
|
|
169
|
+
return cn(
|
|
170
|
+
"relative bg-black rounded-lg overflow-hidden",
|
|
171
|
+
className
|
|
172
|
+
)
|
|
173
|
+
}, [detectedMediaType, className])
|
|
174
|
+
|
|
175
|
+
// Video-specific aspect ratio
|
|
176
|
+
const aspectRatioStyles = useMemo(() => {
|
|
177
|
+
if (detectedMediaType === 'video' && aspectRatio) {
|
|
178
|
+
return {
|
|
179
|
+
aspectRatio,
|
|
180
|
+
width: typeof width === 'number' ? `${width}px` : width,
|
|
181
|
+
height: typeof height === 'number' ? `${height}px` : height
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {}
|
|
185
|
+
}, [detectedMediaType, aspectRatio, width, height])
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<EnhancedMediaWrapper>
|
|
189
|
+
<div
|
|
190
|
+
ref={containerRef}
|
|
191
|
+
className={containerStyles}
|
|
192
|
+
style={aspectRatioStyles}
|
|
193
|
+
data-media-type={detectedMediaType}
|
|
194
|
+
data-enhanced={isEnhanced}
|
|
195
|
+
>
|
|
196
|
+
{MediaElement}
|
|
197
|
+
|
|
198
|
+
{/* Loading state */}
|
|
199
|
+
{state.isLoading && (
|
|
200
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
201
|
+
<div className="text-center">
|
|
202
|
+
<div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-white/20 border-t-white"></div>
|
|
203
|
+
<p className="font-medium text-sm text-white">
|
|
204
|
+
{state.isBuffering ? "Buffering..." : "Loading..."}
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Error state */}
|
|
211
|
+
{state.error && (
|
|
212
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
213
|
+
<div className="p-4 text-center text-white">
|
|
214
|
+
<div className="mb-2 text-lg text-red-400">⚠️</div>
|
|
215
|
+
<p className="text-sm">Failed to load media</p>
|
|
216
|
+
<p className="mt-1 text-xs opacity-75">{state.error.message}</p>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{/* Custom controls or children */}
|
|
222
|
+
{children && (
|
|
223
|
+
<div className="pointer-events-none absolute inset-0">
|
|
224
|
+
{typeof children === "function"
|
|
225
|
+
? children({ state, controls, mediaType: detectedMediaType })
|
|
226
|
+
: children
|
|
227
|
+
}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* Performance indicator for development */}
|
|
232
|
+
{process.env.NODE_ENV === 'development' && enablePerformanceMonitoring && (
|
|
233
|
+
<div className="absolute bottom-2 left-2 rounded bg-black/50 px-2 py-1 text-white text-xs">
|
|
234
|
+
<div>FPS: {performanceMetrics.frameRate || 0}</div>
|
|
235
|
+
<div>Memory: {memoryStats.usage.toFixed(1)}MB</div>
|
|
236
|
+
<div>Type: {detectedMediaType}</div>
|
|
237
|
+
{isEnhanced && <div>Enhanced ✓</div>}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
</EnhancedMediaWrapper>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
BaseMediaPlayer.displayName = "BaseMediaPlayer"
|