@djangocfg/ui-nextjs 2.1.65 → 2.1.67
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/package.json +13 -8
- package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
- package/src/stores/index.ts +8 -0
- package/src/stores/mediaCache.ts +464 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
- package/src/tools/AudioPlayer/README.md +325 -0
- package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +200 -0
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +231 -0
- package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +99 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +64 -0
- package/src/tools/AudioPlayer/components/index.ts +21 -0
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
- package/src/tools/AudioPlayer/context/index.ts +11 -0
- package/src/tools/AudioPlayer/context/selectors.ts +96 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +29 -0
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
- package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +149 -0
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +201 -0
- package/src/tools/AudioPlayer/index.ts +139 -0
- package/src/tools/AudioPlayer/types/audio.ts +107 -0
- package/src/tools/AudioPlayer/types/components.ts +98 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +35 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +5 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +174 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +75 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/index.ts +16 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +212 -187
- package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +174 -0
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +9 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
- package/src/tools/VideoPlayer/index.ts +70 -9
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +401 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +332 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +11 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/index.ts +92 -4
- package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
- package/src/tools/VideoPlayer/VideoPlayer.tsx +0 -231
- package/src/tools/VideoPlayer/types.ts +0 -118
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoPlayer - Unified Video Player Component
|
|
3
|
+
*
|
|
4
|
+
* Supports multiple modes:
|
|
5
|
+
* - vidstack: Full-featured player (YouTube, Vimeo, HLS, DASH)
|
|
6
|
+
* - native: Lightweight HTML5 player
|
|
7
|
+
* - streaming: HTTP Range streaming with auth / Blob sources
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // YouTube video
|
|
11
|
+
* <VideoPlayer source={{ type: 'youtube', id: 'dQw4w9WgXcQ' }} />
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // HLS stream
|
|
15
|
+
* <VideoPlayer source={{ type: 'hls', url: 'https://example.com/video.m3u8' }} />
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // HTTP Range streaming with auth (full source)
|
|
19
|
+
* <VideoPlayer
|
|
20
|
+
* source={{
|
|
21
|
+
* type: 'stream',
|
|
22
|
+
* sessionId: 'abc123',
|
|
23
|
+
* path: '/videos/movie.mp4',
|
|
24
|
+
* getStreamUrl: (id, path) => `/api/stream/${id}?path=${path}&token=${token}`
|
|
25
|
+
* }}
|
|
26
|
+
* />
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // HTTP Range streaming (simplified, using VideoPlayerProvider context)
|
|
30
|
+
* <VideoPlayerProvider sessionId={sessionId} getStreamUrl={getStreamUrl}>
|
|
31
|
+
* <VideoPlayer source={{ type: 'stream', path: '/videos/movie.mp4' }} />
|
|
32
|
+
* </VideoPlayerProvider>
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Blob/ArrayBuffer
|
|
36
|
+
* <VideoPlayer source={{ type: 'blob', data: arrayBuffer, mimeType: 'video/mp4' }} />
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
'use client';
|
|
40
|
+
|
|
41
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
42
|
+
|
|
43
|
+
import { VidstackProvider, NativeProvider, StreamProvider } from '../providers';
|
|
44
|
+
import { useVideoPlayerContext } from '../context';
|
|
45
|
+
import { resolvePlayerMode, isSimpleStreamSource, resolveStreamSource } from '../utils';
|
|
46
|
+
|
|
47
|
+
import type { VideoPlayerProps, VideoPlayerRef, VideoSourceUnion, VidstackProviderProps, NativeProviderProps, StreamProviderProps, SimpleStreamSource } from '../types';
|
|
48
|
+
|
|
49
|
+
export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps & { source: VideoSourceUnion | SimpleStreamSource }>(
|
|
50
|
+
(
|
|
51
|
+
{
|
|
52
|
+
source: rawSource,
|
|
53
|
+
mode = 'auto',
|
|
54
|
+
aspectRatio = 16 / 9,
|
|
55
|
+
autoPlay = false,
|
|
56
|
+
muted = false,
|
|
57
|
+
loop = false,
|
|
58
|
+
playsInline = true,
|
|
59
|
+
controls = true,
|
|
60
|
+
preload = 'metadata',
|
|
61
|
+
theme = 'default',
|
|
62
|
+
showInfo = false,
|
|
63
|
+
className,
|
|
64
|
+
videoClassName,
|
|
65
|
+
disableContextMenu = false,
|
|
66
|
+
showPreloader = true,
|
|
67
|
+
preloaderTimeout = 5000,
|
|
68
|
+
errorFallback,
|
|
69
|
+
onPlay,
|
|
70
|
+
onPause,
|
|
71
|
+
onEnded,
|
|
72
|
+
onError,
|
|
73
|
+
onLoadStart,
|
|
74
|
+
onCanPlay,
|
|
75
|
+
onTimeUpdate,
|
|
76
|
+
},
|
|
77
|
+
ref
|
|
78
|
+
) => {
|
|
79
|
+
// Get context for simplified stream sources
|
|
80
|
+
const context = useVideoPlayerContext();
|
|
81
|
+
|
|
82
|
+
// Resolve simplified stream source to full source using context
|
|
83
|
+
const source = useMemo(() => {
|
|
84
|
+
if (isSimpleStreamSource(rawSource)) {
|
|
85
|
+
const resolved = resolveStreamSource(rawSource, context);
|
|
86
|
+
if (!resolved) {
|
|
87
|
+
// Return a special error source that will trigger error fallback
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return resolved;
|
|
91
|
+
}
|
|
92
|
+
return rawSource;
|
|
93
|
+
}, [rawSource, context]);
|
|
94
|
+
|
|
95
|
+
// Handle unresolved source
|
|
96
|
+
if (!source) {
|
|
97
|
+
// Render error state
|
|
98
|
+
const errorMessage = 'Stream source requires VideoPlayerProvider with getStreamUrl and sessionId';
|
|
99
|
+
|
|
100
|
+
if (typeof errorFallback === 'function') {
|
|
101
|
+
return (
|
|
102
|
+
<div className={className} style={{ aspectRatio: aspectRatio === 'fill' ? undefined : aspectRatio }}>
|
|
103
|
+
{errorFallback({ error: errorMessage })}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (errorFallback) {
|
|
109
|
+
return (
|
|
110
|
+
<div className={className} style={{ aspectRatio: aspectRatio === 'fill' ? undefined : aspectRatio }}>
|
|
111
|
+
{errorFallback}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default error UI
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
className={className}
|
|
120
|
+
style={{
|
|
121
|
+
aspectRatio: aspectRatio === 'fill' ? undefined : aspectRatio,
|
|
122
|
+
display: 'flex',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
justifyContent: 'center',
|
|
125
|
+
backgroundColor: 'black',
|
|
126
|
+
color: 'white',
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<p>{errorMessage}</p>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Determine which provider to use
|
|
135
|
+
const resolvedMode = resolvePlayerMode(source, mode);
|
|
136
|
+
|
|
137
|
+
// Common props for all providers
|
|
138
|
+
const commonProps = {
|
|
139
|
+
aspectRatio,
|
|
140
|
+
autoPlay,
|
|
141
|
+
muted,
|
|
142
|
+
loop,
|
|
143
|
+
playsInline,
|
|
144
|
+
controls,
|
|
145
|
+
preload,
|
|
146
|
+
className,
|
|
147
|
+
onPlay,
|
|
148
|
+
onPause,
|
|
149
|
+
onEnded,
|
|
150
|
+
onError,
|
|
151
|
+
onLoadStart,
|
|
152
|
+
onCanPlay,
|
|
153
|
+
onTimeUpdate,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Render appropriate provider
|
|
157
|
+
switch (resolvedMode) {
|
|
158
|
+
case 'vidstack':
|
|
159
|
+
return (
|
|
160
|
+
<VidstackProvider
|
|
161
|
+
ref={ref}
|
|
162
|
+
source={source as VidstackProviderProps['source']}
|
|
163
|
+
theme={theme}
|
|
164
|
+
showInfo={showInfo}
|
|
165
|
+
errorFallback={errorFallback}
|
|
166
|
+
{...commonProps}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
case 'streaming':
|
|
171
|
+
return (
|
|
172
|
+
<StreamProvider
|
|
173
|
+
ref={ref}
|
|
174
|
+
source={source as StreamProviderProps['source']}
|
|
175
|
+
videoClassName={videoClassName}
|
|
176
|
+
disableContextMenu={disableContextMenu}
|
|
177
|
+
showPreloader={showPreloader}
|
|
178
|
+
preloaderTimeout={preloaderTimeout}
|
|
179
|
+
errorFallback={errorFallback}
|
|
180
|
+
{...commonProps}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
case 'native':
|
|
185
|
+
default:
|
|
186
|
+
return (
|
|
187
|
+
<NativeProvider
|
|
188
|
+
ref={ref}
|
|
189
|
+
source={source as NativeProviderProps['source']}
|
|
190
|
+
videoClassName={videoClassName}
|
|
191
|
+
disableContextMenu={disableContextMenu}
|
|
192
|
+
showPreloader={showPreloader}
|
|
193
|
+
preloaderTimeout={preloaderTimeout}
|
|
194
|
+
{...commonProps}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
VideoPlayer.displayName = 'VideoPlayer';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoPlayer components - Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { VideoPlayer } from './VideoPlayer';
|
|
6
|
+
export { VideoControls } from './VideoControls';
|
|
7
|
+
export {
|
|
8
|
+
VideoErrorFallback,
|
|
9
|
+
createVideoErrorFallback,
|
|
10
|
+
} from './VideoErrorFallback';
|
|
11
|
+
export type {
|
|
12
|
+
VideoErrorFallbackProps,
|
|
13
|
+
CreateVideoErrorFallbackOptions,
|
|
14
|
+
} from './VideoErrorFallback';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoPlayerContext - Context for streaming configuration
|
|
3
|
+
* Simplifies streaming API by providing getStreamUrl globally
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { VideoPlayerContextValue, VideoPlayerProviderProps } from '../types';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Context
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
const VideoPlayerContext = createContext<VideoPlayerContextValue | null>(null);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Provider for VideoPlayer streaming configuration
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // In your app layout or FileWorkspace
|
|
23
|
+
* <VideoPlayerProvider
|
|
24
|
+
* sessionId={sessionId}
|
|
25
|
+
* getStreamUrl={terminalClient.terminal_media.streamStreamRetrieveUrl}
|
|
26
|
+
* >
|
|
27
|
+
* <VideoPlayer source={{ type: 'stream', path: '/video.mp4' }} />
|
|
28
|
+
* </VideoPlayerProvider>
|
|
29
|
+
*/
|
|
30
|
+
export function VideoPlayerProvider({
|
|
31
|
+
children,
|
|
32
|
+
getStreamUrl,
|
|
33
|
+
sessionId,
|
|
34
|
+
}: VideoPlayerProviderProps) {
|
|
35
|
+
const value = useMemo(
|
|
36
|
+
() => ({ getStreamUrl, sessionId }),
|
|
37
|
+
[getStreamUrl, sessionId]
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<VideoPlayerContext.Provider value={value}>
|
|
42
|
+
{children}
|
|
43
|
+
</VideoPlayerContext.Provider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hook to access VideoPlayer context
|
|
49
|
+
*/
|
|
50
|
+
export function useVideoPlayerContext(): VideoPlayerContextValue | null {
|
|
51
|
+
return useContext(VideoPlayerContext);
|
|
52
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useVideoPositionCache - Manages video playback position caching
|
|
5
|
+
*
|
|
6
|
+
* Saves position periodically during playback and restores on load.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useRef, useEffect, useCallback } from 'react';
|
|
10
|
+
import { useVideoCache } from '../../../stores/mediaCache';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// TYPES
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export interface UseVideoPositionCacheOptions {
|
|
17
|
+
/** Unique key for caching (e.g., video URL or stream key) */
|
|
18
|
+
cacheKey: string | null;
|
|
19
|
+
/** Current playback time in seconds */
|
|
20
|
+
currentTime: number;
|
|
21
|
+
/** Video duration in seconds */
|
|
22
|
+
duration: number;
|
|
23
|
+
/** Whether video is currently playing */
|
|
24
|
+
isPlaying: boolean;
|
|
25
|
+
/** Whether video is ready to play */
|
|
26
|
+
isReady: boolean;
|
|
27
|
+
/** Callback to seek to a specific time */
|
|
28
|
+
onSeek: (time: number) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UseVideoPositionCacheReturn {
|
|
32
|
+
/** Manually save current position */
|
|
33
|
+
savePosition: () => void;
|
|
34
|
+
/** Clear saved position */
|
|
35
|
+
clearPosition: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// CONSTANTS
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/** Save interval in seconds */
|
|
43
|
+
const SAVE_INTERVAL = 5;
|
|
44
|
+
|
|
45
|
+
/** Minimum offset from end to restore (avoid restoring at the very end) */
|
|
46
|
+
const END_BUFFER = 1;
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// HOOK
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
export function useVideoPositionCache(
|
|
53
|
+
options: UseVideoPositionCacheOptions
|
|
54
|
+
): UseVideoPositionCacheReturn {
|
|
55
|
+
const { cacheKey, currentTime, duration, isPlaying, isReady, onSeek } = options;
|
|
56
|
+
|
|
57
|
+
const { saveVideoPosition, getVideoPosition } = useVideoCache();
|
|
58
|
+
const lastSavedTimeRef = useRef<number>(0);
|
|
59
|
+
const hasRestoredRef = useRef<boolean>(false);
|
|
60
|
+
|
|
61
|
+
// Restore position when ready
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!isReady || !cacheKey || hasRestoredRef.current) return;
|
|
64
|
+
|
|
65
|
+
const savedPosition = getVideoPosition(cacheKey);
|
|
66
|
+
if (savedPosition && savedPosition > 0 && duration > 0) {
|
|
67
|
+
// Only restore if position is valid (not at the end)
|
|
68
|
+
if (savedPosition < duration - END_BUFFER) {
|
|
69
|
+
onSeek(savedPosition);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
hasRestoredRef.current = true;
|
|
73
|
+
}, [isReady, cacheKey, duration, getVideoPosition, onSeek]);
|
|
74
|
+
|
|
75
|
+
// Reset restored flag when cache key changes
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
hasRestoredRef.current = false;
|
|
78
|
+
lastSavedTimeRef.current = 0;
|
|
79
|
+
}, [cacheKey]);
|
|
80
|
+
|
|
81
|
+
// Save position periodically during playback
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!cacheKey || !isPlaying || currentTime <= 0) return;
|
|
84
|
+
|
|
85
|
+
const timeSinceLastSave = currentTime - lastSavedTimeRef.current;
|
|
86
|
+
if (timeSinceLastSave >= SAVE_INTERVAL || timeSinceLastSave < 0) {
|
|
87
|
+
saveVideoPosition(cacheKey, currentTime);
|
|
88
|
+
lastSavedTimeRef.current = currentTime;
|
|
89
|
+
}
|
|
90
|
+
}, [cacheKey, isPlaying, currentTime, saveVideoPosition]);
|
|
91
|
+
|
|
92
|
+
const savePosition = useCallback(() => {
|
|
93
|
+
if (cacheKey && currentTime > 0) {
|
|
94
|
+
saveVideoPosition(cacheKey, currentTime);
|
|
95
|
+
lastSavedTimeRef.current = currentTime;
|
|
96
|
+
}
|
|
97
|
+
}, [cacheKey, currentTime, saveVideoPosition]);
|
|
98
|
+
|
|
99
|
+
const clearPosition = useCallback(() => {
|
|
100
|
+
if (cacheKey) {
|
|
101
|
+
saveVideoPosition(cacheKey, 0);
|
|
102
|
+
}
|
|
103
|
+
}, [cacheKey, saveVideoPosition]);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
savePosition,
|
|
107
|
+
clearPosition,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -1,16 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* VideoPlayer - Unified Video Player
|
|
3
|
+
* Supports Vidstack (YouTube, Vimeo, HLS, DASH), Native HTML5, and HTTP Streaming
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
export {
|
|
8
|
-
|
|
6
|
+
// Main component
|
|
7
|
+
export { VideoPlayer } from './components';
|
|
8
|
+
|
|
9
|
+
// Controls (can be used standalone with Vidstack)
|
|
10
|
+
export { VideoControls } from './components';
|
|
11
|
+
|
|
12
|
+
// Error Fallback
|
|
13
|
+
export {
|
|
14
|
+
VideoErrorFallback,
|
|
15
|
+
createVideoErrorFallback,
|
|
16
|
+
} from './components';
|
|
17
|
+
export type {
|
|
18
|
+
VideoErrorFallbackProps,
|
|
19
|
+
CreateVideoErrorFallbackOptions,
|
|
20
|
+
} from './components';
|
|
21
|
+
|
|
22
|
+
// Providers (for advanced usage)
|
|
23
|
+
export { VidstackProvider, NativeProvider, StreamProvider } from './providers';
|
|
24
|
+
|
|
25
|
+
// Context (for streaming configuration)
|
|
26
|
+
export {
|
|
27
|
+
VideoPlayerProvider,
|
|
28
|
+
useVideoPlayerContext,
|
|
29
|
+
} from './context';
|
|
30
|
+
|
|
31
|
+
// Hooks
|
|
32
|
+
export { useVideoPositionCache } from './hooks';
|
|
9
33
|
export type {
|
|
10
|
-
|
|
34
|
+
UseVideoPositionCacheOptions,
|
|
35
|
+
UseVideoPositionCacheReturn,
|
|
36
|
+
} from './hooks';
|
|
37
|
+
|
|
38
|
+
// Utils
|
|
39
|
+
export {
|
|
40
|
+
resolvePlayerMode,
|
|
41
|
+
resolveFileSource,
|
|
42
|
+
isSimpleStreamSource,
|
|
43
|
+
resolveStreamSource,
|
|
44
|
+
} from './utils';
|
|
45
|
+
|
|
46
|
+
// Types
|
|
47
|
+
export type {
|
|
48
|
+
// Source types
|
|
49
|
+
VideoSourceUnion,
|
|
50
|
+
UrlSource,
|
|
51
|
+
YouTubeSource,
|
|
52
|
+
VimeoSource,
|
|
53
|
+
HLSSource,
|
|
54
|
+
DASHSource,
|
|
55
|
+
StreamSource,
|
|
56
|
+
BlobSource,
|
|
57
|
+
DataUrlSource,
|
|
58
|
+
// Player types
|
|
59
|
+
PlayerMode,
|
|
60
|
+
AspectRatioValue,
|
|
11
61
|
VideoPlayerProps,
|
|
12
62
|
VideoPlayerRef,
|
|
13
|
-
|
|
14
|
-
|
|
63
|
+
ErrorFallbackProps,
|
|
64
|
+
// Provider props (internal)
|
|
65
|
+
VidstackProviderProps,
|
|
66
|
+
NativeProviderProps,
|
|
67
|
+
StreamProviderProps,
|
|
68
|
+
// Common types
|
|
69
|
+
CommonPlayerSettings,
|
|
70
|
+
CommonPlayerEvents,
|
|
71
|
+
// File source helper types
|
|
72
|
+
ResolveFileSourceOptions,
|
|
73
|
+
// Context types
|
|
74
|
+
VideoPlayerContextValue,
|
|
75
|
+
VideoPlayerProviderProps,
|
|
76
|
+
SimpleStreamSource,
|
|
15
77
|
} from './types';
|
|
16
|
-
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeProvider - Lightweight native HTML5 video player
|
|
3
|
+
* For demo videos, background videos, autoplay loop muted scenarios
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
11
|
+
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
12
|
+
|
|
13
|
+
import type { NativeProviderProps, VideoPlayerRef } from '../types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get video URL from source
|
|
17
|
+
*/
|
|
18
|
+
function getVideoUrl(source: NativeProviderProps['source']): string {
|
|
19
|
+
switch (source.type) {
|
|
20
|
+
case 'url':
|
|
21
|
+
return source.url;
|
|
22
|
+
case 'data-url':
|
|
23
|
+
return source.data;
|
|
24
|
+
default:
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const NativeProvider = forwardRef<VideoPlayerRef, NativeProviderProps>(
|
|
30
|
+
(
|
|
31
|
+
{
|
|
32
|
+
source,
|
|
33
|
+
aspectRatio = 16 / 9,
|
|
34
|
+
autoPlay = true,
|
|
35
|
+
muted = true,
|
|
36
|
+
loop = true,
|
|
37
|
+
playsInline = true,
|
|
38
|
+
preload = 'auto',
|
|
39
|
+
controls = false,
|
|
40
|
+
disableContextMenu = true,
|
|
41
|
+
showPreloader = true,
|
|
42
|
+
preloaderTimeout = 5000,
|
|
43
|
+
className,
|
|
44
|
+
videoClassName,
|
|
45
|
+
onPlay,
|
|
46
|
+
onPause,
|
|
47
|
+
onEnded,
|
|
48
|
+
onError,
|
|
49
|
+
onLoadStart,
|
|
50
|
+
onCanPlay,
|
|
51
|
+
onTimeUpdate,
|
|
52
|
+
},
|
|
53
|
+
ref
|
|
54
|
+
) => {
|
|
55
|
+
const [isLoading, setIsLoading] = useState(showPreloader);
|
|
56
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
57
|
+
|
|
58
|
+
const videoUrl = getVideoUrl(source);
|
|
59
|
+
|
|
60
|
+
// Expose video element methods via ref
|
|
61
|
+
useImperativeHandle(
|
|
62
|
+
ref,
|
|
63
|
+
() => ({
|
|
64
|
+
play: () => videoRef.current?.play(),
|
|
65
|
+
pause: () => videoRef.current?.pause(),
|
|
66
|
+
togglePlay: () => {
|
|
67
|
+
const video = videoRef.current;
|
|
68
|
+
if (video) {
|
|
69
|
+
video.paused ? video.play() : video.pause();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
seekTo: (time: number) => {
|
|
73
|
+
if (videoRef.current) videoRef.current.currentTime = time;
|
|
74
|
+
},
|
|
75
|
+
setVolume: (volume: number) => {
|
|
76
|
+
if (videoRef.current) videoRef.current.volume = Math.max(0, Math.min(1, volume));
|
|
77
|
+
},
|
|
78
|
+
toggleMute: () => {
|
|
79
|
+
if (videoRef.current) videoRef.current.muted = !videoRef.current.muted;
|
|
80
|
+
},
|
|
81
|
+
enterFullscreen: () => videoRef.current?.requestFullscreen(),
|
|
82
|
+
exitFullscreen: () => document.exitFullscreen(),
|
|
83
|
+
get currentTime() {
|
|
84
|
+
return videoRef.current?.currentTime ?? 0;
|
|
85
|
+
},
|
|
86
|
+
get duration() {
|
|
87
|
+
return videoRef.current?.duration ?? 0;
|
|
88
|
+
},
|
|
89
|
+
get paused() {
|
|
90
|
+
return videoRef.current?.paused ?? true;
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
[]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!showPreloader) return;
|
|
98
|
+
|
|
99
|
+
const video = videoRef.current;
|
|
100
|
+
if (!video) return;
|
|
101
|
+
|
|
102
|
+
// Check if video is already loaded
|
|
103
|
+
if (video.readyState >= 3) {
|
|
104
|
+
setIsLoading(false);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hideLoader = () => setIsLoading(false);
|
|
109
|
+
|
|
110
|
+
video.addEventListener('canplay', hideLoader);
|
|
111
|
+
video.addEventListener('loadeddata', hideLoader);
|
|
112
|
+
video.addEventListener('playing', hideLoader);
|
|
113
|
+
|
|
114
|
+
// Fallback: hide loader after timeout
|
|
115
|
+
const timeout = setTimeout(hideLoader, preloaderTimeout);
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
video.removeEventListener('canplay', hideLoader);
|
|
119
|
+
video.removeEventListener('loadeddata', hideLoader);
|
|
120
|
+
video.removeEventListener('playing', hideLoader);
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
};
|
|
123
|
+
}, [showPreloader, preloaderTimeout]);
|
|
124
|
+
|
|
125
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
126
|
+
if (disableContextMenu) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleError = (e: React.SyntheticEvent<HTMLVideoElement>) => {
|
|
132
|
+
setIsLoading(false);
|
|
133
|
+
onError?.(e.currentTarget.error?.message || 'Video playback error');
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleTimeUpdate = () => {
|
|
137
|
+
const video = videoRef.current;
|
|
138
|
+
if (video && onTimeUpdate) {
|
|
139
|
+
onTimeUpdate(video.currentTime, video.duration);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Determine if we should use AspectRatio wrapper or fill mode
|
|
144
|
+
const isFillMode = aspectRatio === 'fill';
|
|
145
|
+
const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
|
|
146
|
+
|
|
147
|
+
// Video content
|
|
148
|
+
const videoContent = (
|
|
149
|
+
<>
|
|
150
|
+
{/* Preloader */}
|
|
151
|
+
{showPreloader && isLoading && (
|
|
152
|
+
<div
|
|
153
|
+
className={cn(
|
|
154
|
+
'absolute inset-0 flex items-center justify-center bg-muted/30 backdrop-blur-sm z-10'
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
<Preloader size="lg" spinnerClassName="text-white" />
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Video */}
|
|
162
|
+
<video
|
|
163
|
+
ref={videoRef}
|
|
164
|
+
className={cn('w-full h-full object-cover', videoClassName)}
|
|
165
|
+
src={videoUrl}
|
|
166
|
+
autoPlay={autoPlay}
|
|
167
|
+
muted={muted}
|
|
168
|
+
loop={loop}
|
|
169
|
+
playsInline={playsInline}
|
|
170
|
+
preload={preload}
|
|
171
|
+
controls={controls}
|
|
172
|
+
poster={source.poster}
|
|
173
|
+
onContextMenu={handleContextMenu}
|
|
174
|
+
onLoadStart={onLoadStart}
|
|
175
|
+
onCanPlay={onCanPlay}
|
|
176
|
+
onPlay={onPlay}
|
|
177
|
+
onPause={onPause}
|
|
178
|
+
onPlaying={() => setIsLoading(false)}
|
|
179
|
+
onEnded={onEnded}
|
|
180
|
+
onError={handleError}
|
|
181
|
+
onTimeUpdate={handleTimeUpdate}
|
|
182
|
+
/>
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Fill mode - no AspectRatio wrapper
|
|
187
|
+
if (isFillMode) {
|
|
188
|
+
return (
|
|
189
|
+
<div className={cn('relative w-full h-full overflow-hidden', className)}>
|
|
190
|
+
{videoContent}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Normal mode with AspectRatio
|
|
196
|
+
return (
|
|
197
|
+
<div className={cn('relative overflow-hidden', className)}>
|
|
198
|
+
<AspectRatio ratio={computedAspectRatio}>
|
|
199
|
+
{videoContent}
|
|
200
|
+
</AspectRatio>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
NativeProvider.displayName = 'NativeProvider';
|