@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,235 @@
|
|
|
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 EnhancedSeekBar = 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
|
+
// Throttling refs
|
|
23
|
+
const seekTimeoutRef = useRef<NodeJS.Timeout>()
|
|
24
|
+
const lastSeekTime = useRef<number>(0)
|
|
25
|
+
const rafId = useRef<number>()
|
|
26
|
+
|
|
27
|
+
// Memoize expensive calculations
|
|
28
|
+
const progress = useMemo(() => duration > 0 ? (currentTime / duration) * 100 : 0, [currentTime, duration])
|
|
29
|
+
|
|
30
|
+
// Calculate buffered ranges with memoization
|
|
31
|
+
const bufferedRanges = useMemo(() => {
|
|
32
|
+
const ranges = []
|
|
33
|
+
if (buffered && duration > 0) {
|
|
34
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
35
|
+
const start = (buffered.start(i) / duration) * 100
|
|
36
|
+
const end = (buffered.end(i) / duration) * 100
|
|
37
|
+
ranges.push({ start, end })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return ranges
|
|
41
|
+
}, [buffered, duration])
|
|
42
|
+
|
|
43
|
+
// Optimized value change handler with throttling and RAF
|
|
44
|
+
const handleValueChange = useCallback(([value]: number[]) => {
|
|
45
|
+
const time = ((value ?? 0) / 100) * duration
|
|
46
|
+
|
|
47
|
+
setPreviewTime(time)
|
|
48
|
+
|
|
49
|
+
// Cancel any pending RAF
|
|
50
|
+
if (rafId.current) {
|
|
51
|
+
cancelAnimationFrame(rafId.current)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isDragging) {
|
|
55
|
+
// Clear previous timeout
|
|
56
|
+
if (seekTimeoutRef.current) {
|
|
57
|
+
clearTimeout(seekTimeoutRef.current)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Use RAF for smooth updates during dragging
|
|
61
|
+
rafId.current = requestAnimationFrame(() => {
|
|
62
|
+
const now = performance.now()
|
|
63
|
+
const timeDiff = now - lastSeekTime.current
|
|
64
|
+
const timeJump = Math.abs(time - currentTime)
|
|
65
|
+
|
|
66
|
+
// Only seek if enough time has passed or if this is a large jump
|
|
67
|
+
if (timeDiff > 16 || timeJump > 0.5) { // 60fps = 16ms per frame
|
|
68
|
+
startTransition(() => {
|
|
69
|
+
onSeek(time)
|
|
70
|
+
})
|
|
71
|
+
lastSeekTime.current = now
|
|
72
|
+
} else {
|
|
73
|
+
// Defer the seek
|
|
74
|
+
seekTimeoutRef.current = setTimeout(() => {
|
|
75
|
+
startTransition(() => {
|
|
76
|
+
onSeek(time)
|
|
77
|
+
})
|
|
78
|
+
lastSeekTime.current = performance.now()
|
|
79
|
+
}, 16)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}, [duration, isDragging, onSeek, currentTime, startTransition])
|
|
84
|
+
|
|
85
|
+
const handleValueCommit = useCallback(([value]: number[]) => {
|
|
86
|
+
const time = ((value ?? 0) / 100) * duration
|
|
87
|
+
|
|
88
|
+
// Cancel any pending RAF
|
|
89
|
+
if (rafId.current) {
|
|
90
|
+
cancelAnimationFrame(rafId.current)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Clear any pending seeks
|
|
94
|
+
if (seekTimeoutRef.current) {
|
|
95
|
+
clearTimeout(seekTimeoutRef.current)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
startTransition(() => {
|
|
99
|
+
onSeek(time)
|
|
100
|
+
})
|
|
101
|
+
setIsDragging(false)
|
|
102
|
+
}, [duration, onSeek, startTransition])
|
|
103
|
+
|
|
104
|
+
const handlePointerDown = useCallback(() => {
|
|
105
|
+
setIsDragging(true)
|
|
106
|
+
lastSeekTime.current = performance.now()
|
|
107
|
+
}, [])
|
|
108
|
+
|
|
109
|
+
// Cleanup on unmount
|
|
110
|
+
useMemo(() => {
|
|
111
|
+
return () => {
|
|
112
|
+
if (rafId.current) {
|
|
113
|
+
cancelAnimationFrame(rafId.current)
|
|
114
|
+
}
|
|
115
|
+
if (seekTimeoutRef.current) {
|
|
116
|
+
clearTimeout(seekTimeoutRef.current)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
// Memoize tooltip content to prevent re-renders
|
|
122
|
+
const tooltipContent = useMemo(() => {
|
|
123
|
+
return (
|
|
124
|
+
<div className={cn(
|
|
125
|
+
"rounded bg-black/80 px-2 py-1 text-sm text-white",
|
|
126
|
+
"fade-in-0 zoom-in-95 animate-in"
|
|
127
|
+
)}>
|
|
128
|
+
{formatTime(deferredPreviewTime)}
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}, [deferredPreviewTime])
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div
|
|
135
|
+
ref={ref}
|
|
136
|
+
className={cn(
|
|
137
|
+
"media-container relative w-full text-[color:var(--media-text)]",
|
|
138
|
+
"touch-manipulation contain-layout",
|
|
139
|
+
className
|
|
140
|
+
)}
|
|
141
|
+
onMouseEnter={() => setIsHovering(true)}
|
|
142
|
+
onMouseLeave={() => setIsHovering(false)}>
|
|
143
|
+
<Tooltip.Provider>
|
|
144
|
+
<Tooltip.Root>
|
|
145
|
+
<Tooltip.Trigger asChild>
|
|
146
|
+
<div className="relative animate-enter">
|
|
147
|
+
<Slider.Root
|
|
148
|
+
className={cn(
|
|
149
|
+
"group relative flex w-full touch-none select-none items-center",
|
|
150
|
+
"media-md:h-4 media-sm:h-3 media-xs:h-2",
|
|
151
|
+
"focus-ring cursor-pointer",
|
|
152
|
+
isPending && "opacity-75"
|
|
153
|
+
)}
|
|
154
|
+
value={[progress]}
|
|
155
|
+
max={100}
|
|
156
|
+
step={0.1}
|
|
157
|
+
onValueChange={handleValueChange}
|
|
158
|
+
onValueCommit={handleValueCommit}
|
|
159
|
+
onPointerDown={handlePointerDown}
|
|
160
|
+
aria-label="Seek to position">
|
|
161
|
+
<Slider.Track
|
|
162
|
+
className={cn(
|
|
163
|
+
"relative grow overflow-hidden rounded-full",
|
|
164
|
+
"bg-[color:var(--media-control-bg)]/40 backdrop-blur-sm",
|
|
165
|
+
"border border-[color:var(--media-border)]/20",
|
|
166
|
+
"shadow-inner transition-all duration-300 ease-out",
|
|
167
|
+
"h-1 group-focus-within:h-1.5 group-hover:h-1.5",
|
|
168
|
+
"group-hover:bg-[color:var(--media-control-bg)]/60",
|
|
169
|
+
"group-hover:border-[color:var(--media-border)]/40"
|
|
170
|
+
)}>
|
|
171
|
+
{/* Buffered ranges - optimized rendering */}
|
|
172
|
+
{bufferedRanges.map((range, index) => (
|
|
173
|
+
<div
|
|
174
|
+
key={`${range.start}-${range.end}-${index}`}
|
|
175
|
+
className={cn(
|
|
176
|
+
"absolute h-full rounded-full transition-all duration-500",
|
|
177
|
+
"bg-[color:var(--color-media-buffered)] shadow-inner",
|
|
178
|
+
"opacity-60 group-hover:opacity-80"
|
|
179
|
+
)}
|
|
180
|
+
style={{
|
|
181
|
+
left: `${range.start}%`,
|
|
182
|
+
width: `${range.end - range.start}%`,
|
|
183
|
+
willChange: 'width, left'
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
))}
|
|
187
|
+
|
|
188
|
+
{/* Progress range with gradient */}
|
|
189
|
+
<Slider.Range
|
|
190
|
+
className={cn(
|
|
191
|
+
"absolute h-full rounded-full transition-all duration-300",
|
|
192
|
+
"bg-gradient-to-r from-[color:var(--color-media-progress)] to-[color:var(--color-brand-accent)]",
|
|
193
|
+
"shadow-[color:var(--color-brand-primary)]/20 shadow-lg",
|
|
194
|
+
"group-hover:shadow-xl",
|
|
195
|
+
isDragging && "transition-none"
|
|
196
|
+
)}
|
|
197
|
+
/>
|
|
198
|
+
</Slider.Track>
|
|
199
|
+
|
|
200
|
+
<Slider.Thumb
|
|
201
|
+
className={cn(
|
|
202
|
+
"block size-4 rounded-full border-white/20 bg-white",
|
|
203
|
+
"shadow-lg hover:shadow-xl",
|
|
204
|
+
"opacity-0 group-hover:opacity-100",
|
|
205
|
+
"hover:scale-110 hover:bg-white/95 active:scale-95",
|
|
206
|
+
"focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-white/50",
|
|
207
|
+
"focus:ring-offset-2 focus:ring-offset-black/20",
|
|
208
|
+
"transition-all duration-300 ease-out",
|
|
209
|
+
isDragging && "scale-110 opacity-100",
|
|
210
|
+
isPending && "animate-pulse"
|
|
211
|
+
)}
|
|
212
|
+
aria-label="Seek"
|
|
213
|
+
/>
|
|
214
|
+
</Slider.Root>
|
|
215
|
+
</div>
|
|
216
|
+
</Tooltip.Trigger>
|
|
217
|
+
|
|
218
|
+
{(isDragging || deferredHovering) && (
|
|
219
|
+
<Tooltip.Portal>
|
|
220
|
+
<Tooltip.Content
|
|
221
|
+
sideOffset={10}
|
|
222
|
+
className="data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
|
223
|
+
{tooltipContent}
|
|
224
|
+
<Tooltip.Arrow className="fill-black/80" />
|
|
225
|
+
</Tooltip.Content>
|
|
226
|
+
</Tooltip.Portal>
|
|
227
|
+
)}
|
|
228
|
+
</Tooltip.Root>
|
|
229
|
+
</Tooltip.Provider>
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
EnhancedSeekBar.displayName = "EnhancedSeekBar"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as Tooltip from "@radix-ui/react-tooltip"
|
|
4
|
+
import { Maximize, Minimize } from "lucide-react"
|
|
5
|
+
import { forwardRef } from "react"
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
import type { FullscreenButtonProps } from "../../types/index"
|
|
8
|
+
|
|
9
|
+
export const FullscreenButton = forwardRef<HTMLButtonElement, FullscreenButtonProps>(
|
|
10
|
+
({ isFullscreen, onToggle, disabled = false, size = "lg", className }, ref) => {
|
|
11
|
+
const Icon = isFullscreen ? Minimize : Maximize
|
|
12
|
+
const buttonSizes: Record<"md" | "lg" | "xl", string> = {
|
|
13
|
+
md: "h-11 w-11",
|
|
14
|
+
lg: "h-12 w-12",
|
|
15
|
+
xl: "h-14 w-14"
|
|
16
|
+
}
|
|
17
|
+
const iconSizes: Record<"md" | "lg" | "xl", string> = {
|
|
18
|
+
md: "h-5 w-5",
|
|
19
|
+
lg: "h-6 w-6",
|
|
20
|
+
xl: "h-7 w-7"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Tooltip.Provider>
|
|
25
|
+
<Tooltip.Root>
|
|
26
|
+
<Tooltip.Trigger asChild>
|
|
27
|
+
<button
|
|
28
|
+
ref={ref}
|
|
29
|
+
type="button"
|
|
30
|
+
onClick={onToggle}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
className={cn(
|
|
33
|
+
"inline-flex items-center justify-center rounded-lg bg-transparent p-0 text-white transition-all hover:text-cyan-700 active:scale-95",
|
|
34
|
+
"focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-black/20",
|
|
35
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
36
|
+
buttonSizes[size],
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}>
|
|
40
|
+
<Icon className={cn(iconSizes[size])} />
|
|
41
|
+
</button>
|
|
42
|
+
</Tooltip.Trigger>
|
|
43
|
+
<Tooltip.Portal>
|
|
44
|
+
<Tooltip.Content
|
|
45
|
+
className={cn(
|
|
46
|
+
"rounded bg-black/80 px-2 py-1 text-sm text-white",
|
|
47
|
+
"fade-in-0 zoom-in-95 animate-in"
|
|
48
|
+
)}
|
|
49
|
+
sideOffset={5}>
|
|
50
|
+
{isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
51
|
+
<Tooltip.Arrow className="fill-black/80" />
|
|
52
|
+
</Tooltip.Content>
|
|
53
|
+
</Tooltip.Portal>
|
|
54
|
+
</Tooltip.Root>
|
|
55
|
+
</Tooltip.Provider>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
FullscreenButton.displayName = "FullscreenButton"
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Resource Preloader using React 19's resource preloading APIs
|
|
3
|
+
* Implements intelligent preloading strategies based on user behavior and network conditions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useRef, useMemo } from 'react';
|
|
7
|
+
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
|
|
8
|
+
import { MediaSource } from '../../types';
|
|
9
|
+
|
|
10
|
+
interface MediaResourcePreloaderProps {
|
|
11
|
+
/** Media sources to preload */
|
|
12
|
+
sources: MediaSource[];
|
|
13
|
+
/** Enable intelligent preloading based on user behavior */
|
|
14
|
+
enableIntelligentPreloading?: boolean;
|
|
15
|
+
/** Network-aware preloading */
|
|
16
|
+
adaptToNetworkConditions?: boolean;
|
|
17
|
+
/** Preload priority level */
|
|
18
|
+
priority?: 'high' | 'low' | 'auto';
|
|
19
|
+
/** Custom preloading strategy */
|
|
20
|
+
strategy?: 'conservative' | 'aggressive' | 'adaptive';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PreloadingConfig {
|
|
24
|
+
dnsPrefetch: string[];
|
|
25
|
+
preconnect: string[];
|
|
26
|
+
preload: Array<{ url: string; type: string }>;
|
|
27
|
+
preinit: Array<{ url: string; type: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const MediaResourcePreloader: React.FC<MediaResourcePreloaderProps> = ({
|
|
31
|
+
sources,
|
|
32
|
+
enableIntelligentPreloading = true,
|
|
33
|
+
adaptToNetworkConditions = true,
|
|
34
|
+
priority = 'auto',
|
|
35
|
+
strategy = 'adaptive',
|
|
36
|
+
}) => {
|
|
37
|
+
const networkInfoRef = useRef<NetworkInformation | null>(null);
|
|
38
|
+
const userBehaviorRef = useRef<UserBehaviorTracker | null>(null);
|
|
39
|
+
const preloadingHistoryRef = useRef<Map<string, number>>(new Map());
|
|
40
|
+
|
|
41
|
+
// Analyze sources and determine preloading configuration
|
|
42
|
+
const preloadingConfig = useMemo((): PreloadingConfig => {
|
|
43
|
+
const config: PreloadingConfig = {
|
|
44
|
+
dnsPrefetch: [],
|
|
45
|
+
preconnect: [],
|
|
46
|
+
preload: [],
|
|
47
|
+
preinit: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
sources.forEach((source) => {
|
|
51
|
+
const url = new URL(source.src);
|
|
52
|
+
const domain = url.origin;
|
|
53
|
+
|
|
54
|
+
// DNS prefetch for CDN domains
|
|
55
|
+
if (isCDNDomain(domain)) {
|
|
56
|
+
config.dnsPrefetch.push(domain);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Preconnect to media domains
|
|
60
|
+
if (shouldPreconnect(source.type, strategy)) {
|
|
61
|
+
config.preconnect.push(domain);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Preload media files based on strategy
|
|
65
|
+
if (shouldPreload(source, priority, strategy)) {
|
|
66
|
+
config.preload.push({
|
|
67
|
+
url: source.src,
|
|
68
|
+
type: getPreloadType(source.type),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Preinit critical resources
|
|
73
|
+
if (shouldPreinit(source, strategy)) {
|
|
74
|
+
config.preinit.push({
|
|
75
|
+
url: source.src,
|
|
76
|
+
type: 'script',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return config;
|
|
82
|
+
}, [sources, priority, strategy]);
|
|
83
|
+
|
|
84
|
+
// Initialize network monitoring
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (adaptToNetworkConditions && 'connection' in navigator) {
|
|
87
|
+
networkInfoRef.current = (navigator as any).connection;
|
|
88
|
+
|
|
89
|
+
const handleNetworkChange = () => {
|
|
90
|
+
adjustPreloadingStrategy();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
networkInfoRef.current.addEventListener('change', handleNetworkChange);
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
networkInfoRef.current?.removeEventListener('change', handleNetworkChange);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}, [adaptToNetworkConditions]);
|
|
100
|
+
|
|
101
|
+
// Initialize user behavior tracking
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (enableIntelligentPreloading) {
|
|
104
|
+
userBehaviorRef.current = new UserBehaviorTracker();
|
|
105
|
+
userBehaviorRef.current.startTracking();
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
userBehaviorRef.current?.stopTracking();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}, [enableIntelligentPreloading]);
|
|
112
|
+
|
|
113
|
+
// Apply DNS prefetching
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
preloadingConfig.dnsPrefetch.forEach((domain) => {
|
|
116
|
+
prefetchDNS(domain);
|
|
117
|
+
});
|
|
118
|
+
}, [preloadingConfig.dnsPrefetch]);
|
|
119
|
+
|
|
120
|
+
// Apply preconnections
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
preloadingConfig.preconnect.forEach((origin) => {
|
|
123
|
+
preconnect(origin, { crossOrigin: 'anonymous' });
|
|
124
|
+
});
|
|
125
|
+
}, [preloadingConfig.preconnect]);
|
|
126
|
+
|
|
127
|
+
// Apply preloading
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
preloadingConfig.preload.forEach(({ url, type }) => {
|
|
130
|
+
const crossOrigin = shouldUseCrossOrigin(url) ? 'anonymous' : undefined;
|
|
131
|
+
|
|
132
|
+
preload(url, {
|
|
133
|
+
as: type,
|
|
134
|
+
crossOrigin,
|
|
135
|
+
integrity: getAssetIntegrity(url),
|
|
136
|
+
priority,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}, [preloadingConfig.preload, priority]);
|
|
140
|
+
|
|
141
|
+
// Apply preinit for critical resources
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
preloadingConfig.preinit.forEach(({ url, type }) => {
|
|
144
|
+
const crossOrigin = shouldUseCrossOrigin(url) ? 'anonymous' : undefined;
|
|
145
|
+
|
|
146
|
+
preinit(url, {
|
|
147
|
+
as: type,
|
|
148
|
+
crossOrigin,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}, [preloadingConfig.preinit]);
|
|
152
|
+
|
|
153
|
+
// Intelligent preloading based on user behavior
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!enableIntelligentPreloading || !userBehaviorRef.current) return;
|
|
156
|
+
|
|
157
|
+
const handleUserBehavior = (behavior: UserBehaviorData) => {
|
|
158
|
+
if (behavior.isLikelyToInteract) {
|
|
159
|
+
preloadLikelyContent(behavior.predictedSources);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
userBehaviorRef.current.onBehaviorChange(handleUserBehavior);
|
|
164
|
+
}, [enableIntelligentPreloading]);
|
|
165
|
+
|
|
166
|
+
// Helper functions
|
|
167
|
+
function isCDNDomain(domain: string): boolean {
|
|
168
|
+
const cdnDomains = [
|
|
169
|
+
'cdn.jsdelivr.net',
|
|
170
|
+
'cdnjs.cloudflare.com',
|
|
171
|
+
'unpkg.com',
|
|
172
|
+
'cloudflare.com',
|
|
173
|
+
'fastly.net',
|
|
174
|
+
'akamaized.net',
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
return cdnDomains.some((cdn) => domain.includes(cdn));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function shouldPreconnect(mediaType: string, strategy: string): boolean {
|
|
181
|
+
if (strategy === 'aggressive') return true;
|
|
182
|
+
if (strategy === 'conservative') return ['hls', 'm3u8'].includes(mediaType);
|
|
183
|
+
return ['hls', 'm3u8', 'mp4'].includes(mediaType);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function shouldPreload(source: MediaSource, priority: string, strategy: string): boolean {
|
|
187
|
+
// Check if already preloaded recently
|
|
188
|
+
const lastPreload = preloadingHistoryRef.current.get(source.src);
|
|
189
|
+
if (lastPreload && Date.now() - lastPreload < 300000) return false; // 5 minutes
|
|
190
|
+
|
|
191
|
+
// Strategy-based decision
|
|
192
|
+
if (strategy === 'conservative') {
|
|
193
|
+
return priority === 'high' && source.type === 'hls';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (strategy === 'aggressive') {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Adaptive strategy
|
|
201
|
+
return shouldAdaptivePreload(source, priority);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function shouldAdaptivePreload(source: MediaSource, priority: string): boolean {
|
|
205
|
+
// Consider network conditions
|
|
206
|
+
if (networkInfoRef.current) {
|
|
207
|
+
const { effectiveType, saveData } = networkInfoRef.current;
|
|
208
|
+
|
|
209
|
+
if (saveData) return false;
|
|
210
|
+
if (effectiveType === 'slow-2g' || effectiveType === '2g') {
|
|
211
|
+
return priority === 'high' && source.type === 'hls';
|
|
212
|
+
}
|
|
213
|
+
if (effectiveType === '3g') {
|
|
214
|
+
return ['hls', 'm3u8'].includes(source.type);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Consider user behavior
|
|
219
|
+
if (userBehaviorRef.current) {
|
|
220
|
+
const behavior = userBehaviorRef.current.getCurrentBehavior();
|
|
221
|
+
if (behavior.engagementScore > 0.7) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return priority !== 'low';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function shouldPreinit(source: MediaSource, strategy: string): boolean {
|
|
230
|
+
return strategy === 'aggressive' && source.type === 'hls';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getPreloadType(mediaType: string): string {
|
|
234
|
+
switch (mediaType) {
|
|
235
|
+
case 'hls':
|
|
236
|
+
case 'm3u8':
|
|
237
|
+
return 'fetch';
|
|
238
|
+
case 'mp4':
|
|
239
|
+
case 'webm':
|
|
240
|
+
return 'video';
|
|
241
|
+
case 'mp3':
|
|
242
|
+
case 'wav':
|
|
243
|
+
return 'audio';
|
|
244
|
+
default:
|
|
245
|
+
return 'fetch';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function shouldUseCrossOrigin(url: string): boolean {
|
|
250
|
+
return !url.includes(window.location.origin);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getAssetIntegrity(url: string): string | undefined {
|
|
254
|
+
// Return integrity hash if available for the asset
|
|
255
|
+
// This would typically come from a build process or asset manifest
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function adjustPreloadingStrategy(): void {
|
|
260
|
+
// Adjust preloading based on network conditions
|
|
261
|
+
if (!networkInfoRef.current) return;
|
|
262
|
+
|
|
263
|
+
const { effectiveType, downlink } = networkInfoRef.current;
|
|
264
|
+
|
|
265
|
+
// This would update the preloading configuration
|
|
266
|
+
// Implementation depends on specific requirements
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function preloadLikelyContent(sources: string[]): void {
|
|
270
|
+
sources.forEach((url) => {
|
|
271
|
+
// Mark as preloaded
|
|
272
|
+
preloadingHistoryRef.current.set(url, Date.now());
|
|
273
|
+
|
|
274
|
+
// Preload the content
|
|
275
|
+
preload(url, {
|
|
276
|
+
as: 'fetch',
|
|
277
|
+
crossOrigin: 'anonymous',
|
|
278
|
+
priority: 'high',
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// This component doesn't render anything visible
|
|
284
|
+
return null;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* User behavior tracker for intelligent preloading
|
|
289
|
+
*/
|
|
290
|
+
class UserBehaviorTracker {
|
|
291
|
+
private behaviorData: UserBehaviorData = {
|
|
292
|
+
isLikelyToInteract: false,
|
|
293
|
+
engagementScore: 0,
|
|
294
|
+
predictedSources: [],
|
|
295
|
+
interactionHistory: [],
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
private listeners: ((behavior: UserBehaviorData) => void)[] = [];
|
|
299
|
+
private mouseMoveTimer: number | null = null;
|
|
300
|
+
|
|
301
|
+
public startTracking(): void {
|
|
302
|
+
// Track mouse movements as interaction indicators
|
|
303
|
+
document.addEventListener('mousemove', this.handleMouseMove);
|
|
304
|
+
document.addEventListener('scroll', this.handleScroll);
|
|
305
|
+
document.addEventListener('keydown', this.handleKeydown);
|
|
306
|
+
|
|
307
|
+
// Track focus events
|
|
308
|
+
document.addEventListener('focusin', this.handleFocus);
|
|
309
|
+
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public stopTracking(): void {
|
|
313
|
+
document.removeEventListener('mousemove', this.handleMouseMove);
|
|
314
|
+
document.removeEventListener('scroll', this.handleScroll);
|
|
315
|
+
document.removeEventListener('keydown', this.handleKeydown);
|
|
316
|
+
document.removeEventListener('focusin', this.handleFocus);
|
|
317
|
+
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
318
|
+
|
|
319
|
+
if (this.mouseMoveTimer) {
|
|
320
|
+
clearTimeout(this.mouseMoveTimer);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
public onBehaviorChange(listener: (behavior: UserBehaviorData) => void): void {
|
|
325
|
+
this.listeners.push(listener);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
public getCurrentBehavior(): UserBehaviorData {
|
|
329
|
+
return this.behaviorData;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private handleMouseMove = (): void => {
|
|
333
|
+
if (this.mouseMoveTimer) return;
|
|
334
|
+
|
|
335
|
+
this.mouseMoveTimer = window.setTimeout(() => {
|
|
336
|
+
this.updateEngagementScore(0.1);
|
|
337
|
+
this.mouseMoveTimer = null;
|
|
338
|
+
}, 100);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
private handleScroll = (): void => {
|
|
342
|
+
this.updateEngagementScore(0.2);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
private handleKeydown = (): void => {
|
|
346
|
+
this.updateEngagementScore(0.3);
|
|
347
|
+
this.behaviorData.isLikelyToInteract = true;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
private handleFocus = (): void => {
|
|
351
|
+
this.updateEngagementScore(0.4);
|
|
352
|
+
this.behaviorData.isLikelyToInteract = true;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
private handleVisibilityChange = (): void => {
|
|
356
|
+
if (!document.hidden) {
|
|
357
|
+
this.updateEngagementScore(0.5);
|
|
358
|
+
this.behaviorData.isLikelyToInteract = true;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
private updateEngagementScore(increment: number): void {
|
|
363
|
+
this.behaviorData.engagementScore = Math.min(
|
|
364
|
+
1,
|
|
365
|
+
this.behaviorData.engagementScore + increment
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Decay engagement score over time
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
this.behaviorData.engagementScore = Math.max(
|
|
371
|
+
0,
|
|
372
|
+
this.behaviorData.engagementScore - increment * 0.5
|
|
373
|
+
);
|
|
374
|
+
this.notifyListeners();
|
|
375
|
+
}, 30000); // 30 seconds decay
|
|
376
|
+
|
|
377
|
+
this.notifyListeners();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private notifyListeners(): void {
|
|
381
|
+
this.listeners.forEach((listener) => listener(this.behaviorData));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
interface UserBehaviorData {
|
|
386
|
+
isLikelyToInteract: boolean;
|
|
387
|
+
engagementScore: number;
|
|
388
|
+
predictedSources: string[];
|
|
389
|
+
interactionHistory: Array<{
|
|
390
|
+
type: string;
|
|
391
|
+
timestamp: number;
|
|
392
|
+
}>;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export default MediaResourcePreloader;
|