@djangocfg/ui-nextjs 2.1.66 → 2.1.68
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 +8 -6
- package/src/stores/index.ts +8 -0
- package/src/stores/mediaCache.ts +474 -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 +35 -11
- package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
- package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
- package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
- 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/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
- package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
- 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/hooks/index.ts +29 -0
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
- package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
- package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
- package/src/tools/AudioPlayer/index.ts +104 -49
- package/src/tools/AudioPlayer/types/audio.ts +107 -0
- package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
- 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 +16 -3
- 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 +47 -3
- 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 +26 -10
- package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
- package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
- package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
- 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 +29 -20
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
- 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 +10 -0
- package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
- package/src/tools/AudioPlayer/context.tsx +0 -426
- package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
- package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
- package/src/tools/VideoPlayer/types.ts +0 -367
|
@@ -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
|
+
}
|
|
@@ -4,10 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Main component
|
|
7
|
-
export { VideoPlayer } from './
|
|
7
|
+
export { VideoPlayer } from './components';
|
|
8
8
|
|
|
9
9
|
// Controls (can be used standalone with Vidstack)
|
|
10
|
-
export { VideoControls } from './
|
|
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';
|
|
11
21
|
|
|
12
22
|
// Providers (for advanced usage)
|
|
13
23
|
export { VidstackProvider, NativeProvider, StreamProvider } from './providers';
|
|
@@ -16,24 +26,22 @@ export { VidstackProvider, NativeProvider, StreamProvider } from './providers';
|
|
|
16
26
|
export {
|
|
17
27
|
VideoPlayerProvider,
|
|
18
28
|
useVideoPlayerContext,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
} from './context';
|
|
30
|
+
|
|
31
|
+
// Hooks
|
|
32
|
+
export { useVideoPositionCache } from './hooks';
|
|
22
33
|
export type {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from './VideoPlayerContext';
|
|
34
|
+
UseVideoPositionCacheOptions,
|
|
35
|
+
UseVideoPositionCacheReturn,
|
|
36
|
+
} from './hooks';
|
|
27
37
|
|
|
28
|
-
//
|
|
38
|
+
// Utils
|
|
29
39
|
export {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
CreateVideoErrorFallbackOptions,
|
|
36
|
-
} from './VideoErrorFallback';
|
|
40
|
+
resolvePlayerMode,
|
|
41
|
+
resolveFileSource,
|
|
42
|
+
isSimpleStreamSource,
|
|
43
|
+
resolveStreamSource,
|
|
44
|
+
} from './utils';
|
|
37
45
|
|
|
38
46
|
// Types
|
|
39
47
|
export type {
|
|
@@ -62,7 +70,8 @@ export type {
|
|
|
62
70
|
CommonPlayerEvents,
|
|
63
71
|
// File source helper types
|
|
64
72
|
ResolveFileSourceOptions,
|
|
73
|
+
// Context types
|
|
74
|
+
VideoPlayerContextValue,
|
|
75
|
+
VideoPlayerProviderProps,
|
|
76
|
+
SimpleStreamSource,
|
|
65
77
|
} from './types';
|
|
66
|
-
|
|
67
|
-
// Helpers
|
|
68
|
-
export { resolvePlayerMode, resolveFileSource } from './types';
|
|
@@ -14,6 +14,7 @@ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef,
|
|
|
14
14
|
|
|
15
15
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
16
16
|
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
17
|
+
import { useVideoCache, generateContentKey } from '../../../stores/mediaCache';
|
|
17
18
|
|
|
18
19
|
import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
|
|
19
20
|
|
|
@@ -63,6 +64,7 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
63
64
|
onLoadStart,
|
|
64
65
|
onCanPlay,
|
|
65
66
|
onTimeUpdate,
|
|
67
|
+
onBufferProgress,
|
|
66
68
|
},
|
|
67
69
|
ref
|
|
68
70
|
) => {
|
|
@@ -70,8 +72,18 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
70
72
|
const [isLoading, setIsLoading] = useState(true);
|
|
71
73
|
const [hasError, setHasError] = useState(false);
|
|
72
74
|
const [errorMessage, setErrorMessage] = useState<string>('Video cannot be previewed');
|
|
73
|
-
const blobUrlRef = useRef<string | null>(null);
|
|
74
75
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
76
|
+
const contentKeyRef = useRef<string | null>(null);
|
|
77
|
+
const lastSavedTimeRef = useRef<number>(0);
|
|
78
|
+
|
|
79
|
+
// Cache hooks
|
|
80
|
+
const {
|
|
81
|
+
getOrCreateBlobUrl,
|
|
82
|
+
releaseBlobUrl,
|
|
83
|
+
getOrCreateStreamUrl,
|
|
84
|
+
saveVideoPosition,
|
|
85
|
+
getVideoPosition,
|
|
86
|
+
} = useVideoCache();
|
|
75
87
|
|
|
76
88
|
// Retry function for error fallback
|
|
77
89
|
const retry = useCallback(() => {
|
|
@@ -120,12 +132,12 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
120
132
|
[]
|
|
121
133
|
);
|
|
122
134
|
|
|
123
|
-
// Create video URL based on source type
|
|
135
|
+
// Create video URL based on source type with caching
|
|
124
136
|
useEffect(() => {
|
|
125
|
-
// Cleanup previous blob URL
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
// Cleanup previous blob URL from cache
|
|
138
|
+
if (contentKeyRef.current) {
|
|
139
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
140
|
+
contentKeyRef.current = null;
|
|
129
141
|
}
|
|
130
142
|
|
|
131
143
|
setHasError(false);
|
|
@@ -134,18 +146,27 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
134
146
|
switch (source.type) {
|
|
135
147
|
case 'stream': {
|
|
136
148
|
const streamSource = source as StreamSource;
|
|
137
|
-
|
|
149
|
+
// Use cached stream URL
|
|
150
|
+
const url = getOrCreateStreamUrl(
|
|
151
|
+
streamSource.sessionId,
|
|
152
|
+
streamSource.path,
|
|
153
|
+
streamSource.getStreamUrl
|
|
154
|
+
);
|
|
138
155
|
setVideoUrl(url);
|
|
139
156
|
break;
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
case 'blob': {
|
|
143
160
|
const blobSource = source as BlobSource;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
// Generate content key for caching
|
|
162
|
+
const contentKey = generateContentKey(blobSource.data);
|
|
163
|
+
contentKeyRef.current = contentKey;
|
|
164
|
+
// Use cached blob URL
|
|
165
|
+
const url = getOrCreateBlobUrl(
|
|
166
|
+
contentKey,
|
|
167
|
+
blobSource.data,
|
|
168
|
+
blobSource.mimeType || 'video/mp4'
|
|
169
|
+
);
|
|
149
170
|
setVideoUrl(url);
|
|
150
171
|
break;
|
|
151
172
|
}
|
|
@@ -163,13 +184,93 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
163
184
|
}
|
|
164
185
|
|
|
165
186
|
return () => {
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
if (contentKeyRef.current) {
|
|
188
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
189
|
+
contentKeyRef.current = null;
|
|
169
190
|
}
|
|
170
191
|
};
|
|
192
|
+
}, [source, getOrCreateBlobUrl, getOrCreateStreamUrl, releaseBlobUrl]);
|
|
193
|
+
|
|
194
|
+
// Get source key for position caching
|
|
195
|
+
const getSourceKey = useCallback(() => {
|
|
196
|
+
switch (source.type) {
|
|
197
|
+
case 'stream':
|
|
198
|
+
return `stream:${(source as StreamSource).sessionId}:${(source as StreamSource).path}`;
|
|
199
|
+
case 'blob':
|
|
200
|
+
return contentKeyRef.current ? `blob:${contentKeyRef.current}` : null;
|
|
201
|
+
case 'data-url':
|
|
202
|
+
return `data:${(source as DataUrlSource).data.slice(0, 50)}`;
|
|
203
|
+
default:
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
171
206
|
}, [source]);
|
|
172
207
|
|
|
208
|
+
// Restore cached playback position when video is ready
|
|
209
|
+
const handleCanPlay = useCallback(() => {
|
|
210
|
+
setIsLoading(false);
|
|
211
|
+
|
|
212
|
+
// Restore position from cache
|
|
213
|
+
const sourceKey = getSourceKey();
|
|
214
|
+
if (sourceKey && videoRef.current) {
|
|
215
|
+
const cachedPosition = getVideoPosition(sourceKey);
|
|
216
|
+
if (cachedPosition && cachedPosition > 0) {
|
|
217
|
+
const duration = videoRef.current.duration;
|
|
218
|
+
// Only restore if position is valid (not at the end)
|
|
219
|
+
if (cachedPosition < duration - 1) {
|
|
220
|
+
videoRef.current.currentTime = cachedPosition;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
onCanPlay?.();
|
|
226
|
+
}, [getSourceKey, getVideoPosition, onCanPlay]);
|
|
227
|
+
|
|
228
|
+
// Save playback position periodically
|
|
229
|
+
const handleTimeUpdate = useCallback(() => {
|
|
230
|
+
const video = videoRef.current;
|
|
231
|
+
if (!video) return;
|
|
232
|
+
|
|
233
|
+
// Save position every 5 seconds
|
|
234
|
+
const sourceKey = getSourceKey();
|
|
235
|
+
if (sourceKey && video.currentTime > 0) {
|
|
236
|
+
const timeSinceLastSave = video.currentTime - lastSavedTimeRef.current;
|
|
237
|
+
if (timeSinceLastSave >= 5 || timeSinceLastSave < 0) {
|
|
238
|
+
saveVideoPosition(sourceKey, video.currentTime);
|
|
239
|
+
lastSavedTimeRef.current = video.currentTime;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
onTimeUpdate?.(video.currentTime, video.duration);
|
|
244
|
+
}, [getSourceKey, saveVideoPosition, onTimeUpdate]);
|
|
245
|
+
|
|
246
|
+
// Save position on pause
|
|
247
|
+
const handlePause = useCallback(() => {
|
|
248
|
+
const video = videoRef.current;
|
|
249
|
+
const sourceKey = getSourceKey();
|
|
250
|
+
if (sourceKey && video && video.currentTime > 0) {
|
|
251
|
+
saveVideoPosition(sourceKey, video.currentTime);
|
|
252
|
+
lastSavedTimeRef.current = video.currentTime;
|
|
253
|
+
}
|
|
254
|
+
onPause?.();
|
|
255
|
+
}, [getSourceKey, saveVideoPosition, onPause]);
|
|
256
|
+
|
|
257
|
+
// Handle buffer progress
|
|
258
|
+
const handleProgress = useCallback(() => {
|
|
259
|
+
const video = videoRef.current;
|
|
260
|
+
if (!video || !onBufferProgress) return;
|
|
261
|
+
|
|
262
|
+
// Get the buffered time ranges
|
|
263
|
+
if (video.buffered.length > 0) {
|
|
264
|
+
// Get the end of the last buffered range
|
|
265
|
+
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
|
|
266
|
+
const duration = video.duration;
|
|
267
|
+
|
|
268
|
+
if (duration > 0 && !isNaN(bufferedEnd)) {
|
|
269
|
+
onBufferProgress(bufferedEnd, duration);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}, [onBufferProgress]);
|
|
273
|
+
|
|
173
274
|
// Preloader timeout
|
|
174
275
|
useEffect(() => {
|
|
175
276
|
if (!showPreloader || !isLoading) return;
|
|
@@ -187,11 +288,6 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
187
288
|
}
|
|
188
289
|
};
|
|
189
290
|
|
|
190
|
-
const handleCanPlay = () => {
|
|
191
|
-
setIsLoading(false);
|
|
192
|
-
onCanPlay?.();
|
|
193
|
-
};
|
|
194
|
-
|
|
195
291
|
const handleLoadedData = () => {
|
|
196
292
|
setIsLoading(false);
|
|
197
293
|
};
|
|
@@ -203,13 +299,6 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
203
299
|
onError?.('Video playback error');
|
|
204
300
|
};
|
|
205
301
|
|
|
206
|
-
const handleTimeUpdate = () => {
|
|
207
|
-
const video = videoRef.current;
|
|
208
|
-
if (video && onTimeUpdate) {
|
|
209
|
-
onTimeUpdate(video.currentTime, video.duration);
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
302
|
// Determine if we should use AspectRatio wrapper or fill mode
|
|
214
303
|
const isFillMode = aspectRatio === 'fill';
|
|
215
304
|
const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
|
|
@@ -280,10 +369,11 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
280
369
|
onCanPlay={handleCanPlay}
|
|
281
370
|
onLoadedData={handleLoadedData}
|
|
282
371
|
onPlay={onPlay}
|
|
283
|
-
onPause={
|
|
372
|
+
onPause={handlePause}
|
|
284
373
|
onEnded={onEnded}
|
|
285
374
|
onError={handleError}
|
|
286
375
|
onTimeUpdate={handleTimeUpdate}
|
|
376
|
+
onProgress={handleProgress}
|
|
287
377
|
/>
|
|
288
378
|
</>
|
|
289
379
|
);
|
|
@@ -10,11 +10,12 @@ import '@vidstack/react/player/styles/base.css';
|
|
|
10
10
|
import '@vidstack/react/player/styles/default/theme.css';
|
|
11
11
|
import '@vidstack/react/player/styles/default/layouts/video.css';
|
|
12
12
|
|
|
13
|
-
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
14
14
|
|
|
15
15
|
import { cn, generateOgImageUrl } from '@djangocfg/ui-core/lib';
|
|
16
16
|
import { MediaPlayer, MediaProvider, Poster } from '@vidstack/react';
|
|
17
17
|
import { defaultLayoutIcons, DefaultVideoLayout } from '@vidstack/react/player/layouts/default';
|
|
18
|
+
import { useVideoCache } from '../../../stores/mediaCache';
|
|
18
19
|
|
|
19
20
|
import type { MediaPlayerInstance } from '@vidstack/react';
|
|
20
21
|
import type { VidstackProviderProps, VideoPlayerRef, ErrorFallbackProps } from '../types';
|
|
@@ -86,13 +87,47 @@ export const VidstackProvider = forwardRef<VideoPlayerRef, VidstackProviderProps
|
|
|
86
87
|
const playerRef = useRef<MediaPlayerInstance | null>(null);
|
|
87
88
|
const [hasError, setHasError] = useState(false);
|
|
88
89
|
const [errorMessage, setErrorMessage] = useState<string>('Video cannot be played');
|
|
90
|
+
const lastSavedTimeRef = useRef<number>(0);
|
|
91
|
+
const hasRestoredPositionRef = useRef(false);
|
|
89
92
|
|
|
90
|
-
//
|
|
93
|
+
// Cache hooks
|
|
94
|
+
const {
|
|
95
|
+
getPosterUrl,
|
|
96
|
+
cachePosterUrl,
|
|
97
|
+
saveVideoPosition,
|
|
98
|
+
getVideoPosition,
|
|
99
|
+
} = useVideoCache();
|
|
100
|
+
|
|
101
|
+
// Get source key for position caching
|
|
102
|
+
const sourceKey = useMemo(() => {
|
|
103
|
+
switch (source.type) {
|
|
104
|
+
case 'youtube':
|
|
105
|
+
return `youtube:${source.id}`;
|
|
106
|
+
case 'vimeo':
|
|
107
|
+
return `vimeo:${source.id}`;
|
|
108
|
+
case 'hls':
|
|
109
|
+
case 'dash':
|
|
110
|
+
case 'url':
|
|
111
|
+
return `url:${source.url}`;
|
|
112
|
+
default:
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}, [source]);
|
|
116
|
+
|
|
117
|
+
// Generate poster if not provided, with caching
|
|
91
118
|
const posterUrl = useMemo(() => {
|
|
92
119
|
if (source.poster) return source.poster;
|
|
93
120
|
if (!source.title) return undefined;
|
|
94
|
-
|
|
95
|
-
|
|
121
|
+
|
|
122
|
+
// Check cache first
|
|
123
|
+
const cached = getPosterUrl(source.title);
|
|
124
|
+
if (cached) return cached;
|
|
125
|
+
|
|
126
|
+
// Generate and cache
|
|
127
|
+
const url = generateOgImageUrl({ title: source.title });
|
|
128
|
+
cachePosterUrl(source.title, url);
|
|
129
|
+
return url;
|
|
130
|
+
}, [source.poster, source.title, getPosterUrl, cachePosterUrl]);
|
|
96
131
|
|
|
97
132
|
// Get Vidstack-compatible source URL
|
|
98
133
|
const vidstackSrc = useMemo(() => getVidstackSrc(source), [source]);
|
|
@@ -149,8 +184,19 @@ export const VidstackProvider = forwardRef<VideoPlayerRef, VidstackProviderProps
|
|
|
149
184
|
);
|
|
150
185
|
|
|
151
186
|
const handlePlay = () => onPlay?.();
|
|
152
|
-
|
|
187
|
+
|
|
188
|
+
const handlePause = useCallback(() => {
|
|
189
|
+
// Save position on pause
|
|
190
|
+
const player = playerRef.current;
|
|
191
|
+
if (sourceKey && player && player.currentTime > 0) {
|
|
192
|
+
saveVideoPosition(sourceKey, player.currentTime);
|
|
193
|
+
lastSavedTimeRef.current = player.currentTime;
|
|
194
|
+
}
|
|
195
|
+
onPause?.();
|
|
196
|
+
}, [sourceKey, saveVideoPosition, onPause]);
|
|
197
|
+
|
|
153
198
|
const handleEnded = () => onEnded?.();
|
|
199
|
+
|
|
154
200
|
const handleError = (detail: unknown) => {
|
|
155
201
|
const error = detail as { message?: string };
|
|
156
202
|
const msg = error?.message || 'Video playback error';
|
|
@@ -158,17 +204,49 @@ export const VidstackProvider = forwardRef<VideoPlayerRef, VidstackProviderProps
|
|
|
158
204
|
setErrorMessage(msg);
|
|
159
205
|
onError?.(msg);
|
|
160
206
|
};
|
|
207
|
+
|
|
161
208
|
const handleLoadStart = () => onLoadStart?.();
|
|
162
|
-
|
|
209
|
+
|
|
210
|
+
const handleCanPlay = useCallback(() => {
|
|
163
211
|
setHasError(false);
|
|
212
|
+
|
|
213
|
+
// Restore position from cache (only once per source)
|
|
214
|
+
if (sourceKey && playerRef.current && !hasRestoredPositionRef.current) {
|
|
215
|
+
const cachedPosition = getVideoPosition(sourceKey);
|
|
216
|
+
if (cachedPosition && cachedPosition > 0) {
|
|
217
|
+
const duration = playerRef.current.duration;
|
|
218
|
+
// Only restore if position is valid (not at the end)
|
|
219
|
+
if (cachedPosition < duration - 1) {
|
|
220
|
+
playerRef.current.currentTime = cachedPosition;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
hasRestoredPositionRef.current = true;
|
|
224
|
+
}
|
|
225
|
+
|
|
164
226
|
onCanPlay?.();
|
|
165
|
-
};
|
|
166
|
-
|
|
227
|
+
}, [sourceKey, getVideoPosition, onCanPlay]);
|
|
228
|
+
|
|
229
|
+
const handleTimeUpdate = useCallback(() => {
|
|
167
230
|
const player = playerRef.current;
|
|
168
|
-
if (player
|
|
169
|
-
|
|
231
|
+
if (!player) return;
|
|
232
|
+
|
|
233
|
+
// Save position every 5 seconds
|
|
234
|
+
if (sourceKey && player.currentTime > 0) {
|
|
235
|
+
const timeSinceLastSave = player.currentTime - lastSavedTimeRef.current;
|
|
236
|
+
if (timeSinceLastSave >= 5 || timeSinceLastSave < 0) {
|
|
237
|
+
saveVideoPosition(sourceKey, player.currentTime);
|
|
238
|
+
lastSavedTimeRef.current = player.currentTime;
|
|
239
|
+
}
|
|
170
240
|
}
|
|
171
|
-
|
|
241
|
+
|
|
242
|
+
onTimeUpdate?.(player.currentTime, player.duration);
|
|
243
|
+
}, [sourceKey, saveVideoPosition, onTimeUpdate]);
|
|
244
|
+
|
|
245
|
+
// Reset position restoration flag when source changes
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
hasRestoredPositionRef.current = false;
|
|
248
|
+
lastSavedTimeRef.current = 0;
|
|
249
|
+
}, [sourceKey]);
|
|
172
250
|
|
|
173
251
|
// Determine layout mode
|
|
174
252
|
const isFillMode = aspectRatio === 'fill';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoPlayer types - Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Source types
|
|
6
|
+
export type {
|
|
7
|
+
UrlSource,
|
|
8
|
+
YouTubeSource,
|
|
9
|
+
VimeoSource,
|
|
10
|
+
HLSSource,
|
|
11
|
+
DASHSource,
|
|
12
|
+
StreamSource,
|
|
13
|
+
BlobSource,
|
|
14
|
+
DataUrlSource,
|
|
15
|
+
VideoSourceUnion,
|
|
16
|
+
} from './sources';
|
|
17
|
+
|
|
18
|
+
// Player types
|
|
19
|
+
export type {
|
|
20
|
+
PlayerMode,
|
|
21
|
+
AspectRatioValue,
|
|
22
|
+
CommonPlayerSettings,
|
|
23
|
+
CommonPlayerEvents,
|
|
24
|
+
ErrorFallbackProps,
|
|
25
|
+
VideoPlayerProps,
|
|
26
|
+
VideoPlayerRef,
|
|
27
|
+
} from './player';
|
|
28
|
+
|
|
29
|
+
// Provider types
|
|
30
|
+
export type {
|
|
31
|
+
VidstackProviderProps,
|
|
32
|
+
NativeProviderProps,
|
|
33
|
+
StreamProviderProps,
|
|
34
|
+
ResolveFileSourceOptions,
|
|
35
|
+
VideoPlayerContextValue,
|
|
36
|
+
VideoPlayerProviderProps,
|
|
37
|
+
SimpleStreamSource,
|
|
38
|
+
} from './provider';
|