@djangocfg/ui-nextjs 2.1.66 → 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.
Files changed (90) hide show
  1. package/package.json +8 -6
  2. package/src/stores/index.ts +8 -0
  3. package/src/stores/mediaCache.ts +464 -0
  4. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  5. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  6. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  7. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  8. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  9. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  10. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  11. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  12. package/src/tools/AudioPlayer/README.md +35 -11
  13. package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
  14. package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
  15. package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
  16. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  23. package/src/tools/AudioPlayer/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
  24. package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
  25. package/src/tools/AudioPlayer/components/index.ts +21 -0
  26. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  27. package/src/tools/AudioPlayer/context/index.ts +11 -0
  28. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  29. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  30. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  31. package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
  32. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  33. package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
  34. package/src/tools/AudioPlayer/index.ts +104 -49
  35. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  36. package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
  37. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  38. package/src/tools/AudioPlayer/types/index.ts +35 -0
  39. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  40. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  41. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  42. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  43. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  44. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  45. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  46. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  47. package/src/tools/ImageViewer/README.md +16 -3
  48. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  49. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  50. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  51. package/src/tools/ImageViewer/components/index.ts +7 -0
  52. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  53. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  54. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  55. package/src/tools/ImageViewer/index.ts +47 -3
  56. package/src/tools/ImageViewer/types.ts +75 -0
  57. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  58. package/src/tools/ImageViewer/utils/index.ts +16 -0
  59. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  60. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  61. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  62. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  63. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  64. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  65. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  66. package/src/tools/VideoPlayer/README.md +26 -10
  67. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  68. package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
  69. package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
  70. package/src/tools/VideoPlayer/components/index.ts +14 -0
  71. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  72. package/src/tools/VideoPlayer/context/index.ts +8 -0
  73. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  74. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  75. package/src/tools/VideoPlayer/index.ts +29 -20
  76. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
  77. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
  78. package/src/tools/VideoPlayer/types/index.ts +38 -0
  79. package/src/tools/VideoPlayer/types/player.ts +116 -0
  80. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  81. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  82. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  83. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  84. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  85. package/src/tools/index.ts +10 -0
  86. package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
  87. package/src/tools/AudioPlayer/context.tsx +0 -426
  88. package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
  89. package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
  90. 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,8 @@
1
+ /**
2
+ * VideoPlayer context - Public API
3
+ */
4
+
5
+ export {
6
+ VideoPlayerProvider,
7
+ useVideoPlayerContext,
8
+ } from './VideoPlayerContext';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * VideoPlayer hooks - Public API
3
+ */
4
+
5
+ export { useVideoPositionCache } from './useVideoPositionCache';
6
+ export type {
7
+ UseVideoPositionCacheOptions,
8
+ UseVideoPositionCacheReturn,
9
+ } from './useVideoPositionCache';
@@ -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 './VideoPlayer';
7
+ export { VideoPlayer } from './components';
8
8
 
9
9
  // Controls (can be used standalone with Vidstack)
10
- export { VideoControls } from './VideoControls';
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
- isSimpleStreamSource,
20
- resolveStreamSource,
21
- } from './VideoPlayerContext';
29
+ } from './context';
30
+
31
+ // Hooks
32
+ export { useVideoPositionCache } from './hooks';
22
33
  export type {
23
- VideoPlayerContextValue,
24
- VideoPlayerProviderProps,
25
- SimpleStreamSource,
26
- } from './VideoPlayerContext';
34
+ UseVideoPositionCacheOptions,
35
+ UseVideoPositionCacheReturn,
36
+ } from './hooks';
27
37
 
28
- // Error Fallback
38
+ // Utils
29
39
  export {
30
- VideoErrorFallback,
31
- createVideoErrorFallback,
32
- } from './VideoErrorFallback';
33
- export type {
34
- VideoErrorFallbackProps,
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 (blobUrlRef.current) {
127
- URL.revokeObjectURL(blobUrlRef.current);
128
- blobUrlRef.current = null;
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
- const url = streamSource.getStreamUrl(streamSource.sessionId, streamSource.path);
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
- const blob = new Blob([blobSource.data], {
145
- type: blobSource.mimeType || 'video/mp4'
146
- });
147
- const url = URL.createObjectURL(blob);
148
- blobUrlRef.current = url;
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 (blobUrlRef.current) {
167
- URL.revokeObjectURL(blobUrlRef.current);
168
- blobUrlRef.current = null;
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={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
- // Generate poster if not provided
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
- return generateOgImageUrl({ title: source.title });
95
- }, [source.poster, source.title]);
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
- const handlePause = () => onPause?.();
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
- const handleCanPlay = () => {
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
- const handleTimeUpdate = () => {
227
+ }, [sourceKey, getVideoPosition, onCanPlay]);
228
+
229
+ const handleTimeUpdate = useCallback(() => {
167
230
  const player = playerRef.current;
168
- if (player && onTimeUpdate) {
169
- onTimeUpdate(player.currentTime, player.duration);
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';