@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,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamProvider - HTTP Range streaming and Blob video player
|
|
3
|
+
* Supports:
|
|
4
|
+
* - HTTP Range requests with authorization (for large files)
|
|
5
|
+
* - Blob/ArrayBuffer sources
|
|
6
|
+
* - Data URL sources
|
|
7
|
+
* - Fill parent container mode
|
|
8
|
+
* - Custom error fallback
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use client';
|
|
12
|
+
|
|
13
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
16
|
+
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
17
|
+
import { useVideoCache, generateContentKey } from '../../../stores/mediaCache';
|
|
18
|
+
|
|
19
|
+
import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
|
|
20
|
+
|
|
21
|
+
/** Default error fallback UI */
|
|
22
|
+
function DefaultErrorFallback({ error }: ErrorFallbackProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4 text-white">
|
|
25
|
+
<svg
|
|
26
|
+
className="w-16 h-16 text-muted-foreground"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
>
|
|
31
|
+
<path
|
|
32
|
+
strokeLinecap="round"
|
|
33
|
+
strokeLinejoin="round"
|
|
34
|
+
strokeWidth={2}
|
|
35
|
+
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
<p className="text-lg">{error || 'Video cannot be previewed'}</p>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
44
|
+
(
|
|
45
|
+
{
|
|
46
|
+
source,
|
|
47
|
+
aspectRatio = 16 / 9,
|
|
48
|
+
autoPlay = false,
|
|
49
|
+
muted = false,
|
|
50
|
+
loop = false,
|
|
51
|
+
playsInline = true,
|
|
52
|
+
preload = 'metadata',
|
|
53
|
+
controls = true,
|
|
54
|
+
disableContextMenu = false,
|
|
55
|
+
showPreloader = true,
|
|
56
|
+
preloaderTimeout = 10000,
|
|
57
|
+
className,
|
|
58
|
+
videoClassName,
|
|
59
|
+
errorFallback,
|
|
60
|
+
onPlay,
|
|
61
|
+
onPause,
|
|
62
|
+
onEnded,
|
|
63
|
+
onError,
|
|
64
|
+
onLoadStart,
|
|
65
|
+
onCanPlay,
|
|
66
|
+
onTimeUpdate,
|
|
67
|
+
onBufferProgress,
|
|
68
|
+
},
|
|
69
|
+
ref
|
|
70
|
+
) => {
|
|
71
|
+
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
|
72
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
73
|
+
const [hasError, setHasError] = useState(false);
|
|
74
|
+
const [errorMessage, setErrorMessage] = useState<string>('Video cannot be previewed');
|
|
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();
|
|
87
|
+
|
|
88
|
+
// Retry function for error fallback
|
|
89
|
+
const retry = useCallback(() => {
|
|
90
|
+
setHasError(false);
|
|
91
|
+
setIsLoading(true);
|
|
92
|
+
// Re-trigger source effect by forcing state update
|
|
93
|
+
const video = videoRef.current;
|
|
94
|
+
if (video && videoUrl) {
|
|
95
|
+
video.load();
|
|
96
|
+
}
|
|
97
|
+
}, [videoUrl]);
|
|
98
|
+
|
|
99
|
+
// Expose video element methods via ref
|
|
100
|
+
useImperativeHandle(
|
|
101
|
+
ref,
|
|
102
|
+
() => ({
|
|
103
|
+
play: () => videoRef.current?.play(),
|
|
104
|
+
pause: () => videoRef.current?.pause(),
|
|
105
|
+
togglePlay: () => {
|
|
106
|
+
const video = videoRef.current;
|
|
107
|
+
if (video) {
|
|
108
|
+
video.paused ? video.play() : video.pause();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
seekTo: (time: number) => {
|
|
112
|
+
if (videoRef.current) videoRef.current.currentTime = time;
|
|
113
|
+
},
|
|
114
|
+
setVolume: (volume: number) => {
|
|
115
|
+
if (videoRef.current) videoRef.current.volume = Math.max(0, Math.min(1, volume));
|
|
116
|
+
},
|
|
117
|
+
toggleMute: () => {
|
|
118
|
+
if (videoRef.current) videoRef.current.muted = !videoRef.current.muted;
|
|
119
|
+
},
|
|
120
|
+
enterFullscreen: () => videoRef.current?.requestFullscreen(),
|
|
121
|
+
exitFullscreen: () => document.exitFullscreen(),
|
|
122
|
+
get currentTime() {
|
|
123
|
+
return videoRef.current?.currentTime ?? 0;
|
|
124
|
+
},
|
|
125
|
+
get duration() {
|
|
126
|
+
return videoRef.current?.duration ?? 0;
|
|
127
|
+
},
|
|
128
|
+
get paused() {
|
|
129
|
+
return videoRef.current?.paused ?? true;
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
[]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Create video URL based on source type with caching
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
// Cleanup previous blob URL from cache
|
|
138
|
+
if (contentKeyRef.current) {
|
|
139
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
140
|
+
contentKeyRef.current = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setHasError(false);
|
|
144
|
+
setIsLoading(true);
|
|
145
|
+
|
|
146
|
+
switch (source.type) {
|
|
147
|
+
case 'stream': {
|
|
148
|
+
const streamSource = source as StreamSource;
|
|
149
|
+
// Use cached stream URL
|
|
150
|
+
const url = getOrCreateStreamUrl(
|
|
151
|
+
streamSource.sessionId,
|
|
152
|
+
streamSource.path,
|
|
153
|
+
streamSource.getStreamUrl
|
|
154
|
+
);
|
|
155
|
+
setVideoUrl(url);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'blob': {
|
|
160
|
+
const blobSource = source as BlobSource;
|
|
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
|
+
);
|
|
170
|
+
setVideoUrl(url);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case 'data-url': {
|
|
175
|
+
const dataUrlSource = source as DataUrlSource;
|
|
176
|
+
setVideoUrl(dataUrlSource.data);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
default:
|
|
181
|
+
setVideoUrl(null);
|
|
182
|
+
setHasError(true);
|
|
183
|
+
setErrorMessage('Invalid video source');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return () => {
|
|
187
|
+
if (contentKeyRef.current) {
|
|
188
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
189
|
+
contentKeyRef.current = null;
|
|
190
|
+
}
|
|
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
|
+
}
|
|
206
|
+
}, [source]);
|
|
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
|
+
|
|
274
|
+
// Preloader timeout
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (!showPreloader || !isLoading) return;
|
|
277
|
+
|
|
278
|
+
const timeout = setTimeout(() => {
|
|
279
|
+
setIsLoading(false);
|
|
280
|
+
}, preloaderTimeout);
|
|
281
|
+
|
|
282
|
+
return () => clearTimeout(timeout);
|
|
283
|
+
}, [showPreloader, isLoading, preloaderTimeout]);
|
|
284
|
+
|
|
285
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
286
|
+
if (disableContextMenu) {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const handleLoadedData = () => {
|
|
292
|
+
setIsLoading(false);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const handleError = () => {
|
|
296
|
+
setIsLoading(false);
|
|
297
|
+
setHasError(true);
|
|
298
|
+
setErrorMessage('Failed to load video');
|
|
299
|
+
onError?.('Video playback error');
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Determine if we should use AspectRatio wrapper or fill mode
|
|
303
|
+
const isFillMode = aspectRatio === 'fill';
|
|
304
|
+
const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
|
|
305
|
+
|
|
306
|
+
// Render error fallback
|
|
307
|
+
const renderErrorFallback = () => {
|
|
308
|
+
const fallbackProps: ErrorFallbackProps = { error: errorMessage, retry };
|
|
309
|
+
|
|
310
|
+
if (typeof errorFallback === 'function') {
|
|
311
|
+
return errorFallback(fallbackProps);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (errorFallback) {
|
|
315
|
+
return errorFallback;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return <DefaultErrorFallback {...fallbackProps} />;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Error state
|
|
322
|
+
if (!videoUrl || hasError) {
|
|
323
|
+
if (isFillMode) {
|
|
324
|
+
return (
|
|
325
|
+
<div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
|
|
326
|
+
{renderErrorFallback()}
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<div className={cn('relative overflow-hidden bg-black', className)}>
|
|
333
|
+
<AspectRatio ratio={computedAspectRatio}>
|
|
334
|
+
{renderErrorFallback()}
|
|
335
|
+
</AspectRatio>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Video content
|
|
341
|
+
const videoContent = (
|
|
342
|
+
<>
|
|
343
|
+
{/* Loading indicator */}
|
|
344
|
+
{showPreloader && isLoading && (
|
|
345
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
|
|
346
|
+
<Preloader size="lg" spinnerClassName="text-white" />
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{/* Video element */}
|
|
351
|
+
<video
|
|
352
|
+
ref={videoRef}
|
|
353
|
+
src={videoUrl}
|
|
354
|
+
className={cn(
|
|
355
|
+
'w-full h-full object-contain',
|
|
356
|
+
isLoading && 'opacity-0',
|
|
357
|
+
videoClassName
|
|
358
|
+
)}
|
|
359
|
+
autoPlay={autoPlay}
|
|
360
|
+
muted={muted}
|
|
361
|
+
loop={loop}
|
|
362
|
+
playsInline={playsInline}
|
|
363
|
+
preload={preload}
|
|
364
|
+
controls={controls}
|
|
365
|
+
crossOrigin="anonymous"
|
|
366
|
+
poster={source.poster}
|
|
367
|
+
onContextMenu={handleContextMenu}
|
|
368
|
+
onLoadStart={onLoadStart}
|
|
369
|
+
onCanPlay={handleCanPlay}
|
|
370
|
+
onLoadedData={handleLoadedData}
|
|
371
|
+
onPlay={onPlay}
|
|
372
|
+
onPause={handlePause}
|
|
373
|
+
onEnded={onEnded}
|
|
374
|
+
onError={handleError}
|
|
375
|
+
onTimeUpdate={handleTimeUpdate}
|
|
376
|
+
onProgress={handleProgress}
|
|
377
|
+
/>
|
|
378
|
+
</>
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Fill mode - no AspectRatio wrapper
|
|
382
|
+
if (isFillMode) {
|
|
383
|
+
return (
|
|
384
|
+
<div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
|
|
385
|
+
{videoContent}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Normal mode with AspectRatio
|
|
391
|
+
return (
|
|
392
|
+
<div className={cn('relative overflow-hidden bg-black', className)}>
|
|
393
|
+
<AspectRatio ratio={computedAspectRatio}>
|
|
394
|
+
{videoContent}
|
|
395
|
+
</AspectRatio>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
StreamProvider.displayName = 'StreamProvider';
|