@djangocfg/ui-nextjs 2.1.65 → 2.1.66
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 +9 -6
- package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
- package/src/tools/AudioPlayer/AudioEqualizer.tsx +235 -0
- package/src/tools/AudioPlayer/AudioPlayer.tsx +223 -0
- package/src/tools/AudioPlayer/AudioReactiveCover.tsx +389 -0
- package/src/tools/AudioPlayer/AudioShortcutsPopover.tsx +95 -0
- package/src/tools/AudioPlayer/README.md +301 -0
- package/src/tools/AudioPlayer/SimpleAudioPlayer.tsx +275 -0
- package/src/tools/AudioPlayer/VisualizationToggle.tsx +68 -0
- package/src/tools/AudioPlayer/context.tsx +426 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/index.ts +84 -0
- package/src/tools/AudioPlayer/types.ts +162 -0
- package/src/tools/AudioPlayer/useAudioHotkeys.ts +142 -0
- package/src/tools/AudioPlayer/useAudioVisualization.tsx +195 -0
- package/src/tools/ImageViewer/ImageViewer.tsx +416 -0
- package/src/tools/ImageViewer/README.md +161 -0
- package/src/tools/ImageViewer/index.ts +16 -0
- package/src/tools/VideoPlayer/README.md +196 -187
- package/src/tools/VideoPlayer/VideoErrorFallback.tsx +174 -0
- package/src/tools/VideoPlayer/VideoPlayer.tsx +189 -218
- package/src/tools/VideoPlayer/VideoPlayerContext.tsx +125 -0
- package/src/tools/VideoPlayer/index.ts +59 -7
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +311 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +254 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types.ts +320 -71
- package/src/tools/index.ts +82 -4
- package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
|
@@ -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';
|
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
|
|
18
|
+
import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
|
|
19
|
+
|
|
20
|
+
/** Default error fallback UI */
|
|
21
|
+
function DefaultErrorFallback({ error }: ErrorFallbackProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4 text-white">
|
|
24
|
+
<svg
|
|
25
|
+
className="w-16 h-16 text-muted-foreground"
|
|
26
|
+
fill="none"
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
>
|
|
30
|
+
<path
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
strokeWidth={2}
|
|
34
|
+
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"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
<p className="text-lg">{error || 'Video cannot be previewed'}</p>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
43
|
+
(
|
|
44
|
+
{
|
|
45
|
+
source,
|
|
46
|
+
aspectRatio = 16 / 9,
|
|
47
|
+
autoPlay = false,
|
|
48
|
+
muted = false,
|
|
49
|
+
loop = false,
|
|
50
|
+
playsInline = true,
|
|
51
|
+
preload = 'metadata',
|
|
52
|
+
controls = true,
|
|
53
|
+
disableContextMenu = false,
|
|
54
|
+
showPreloader = true,
|
|
55
|
+
preloaderTimeout = 10000,
|
|
56
|
+
className,
|
|
57
|
+
videoClassName,
|
|
58
|
+
errorFallback,
|
|
59
|
+
onPlay,
|
|
60
|
+
onPause,
|
|
61
|
+
onEnded,
|
|
62
|
+
onError,
|
|
63
|
+
onLoadStart,
|
|
64
|
+
onCanPlay,
|
|
65
|
+
onTimeUpdate,
|
|
66
|
+
},
|
|
67
|
+
ref
|
|
68
|
+
) => {
|
|
69
|
+
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
|
70
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
71
|
+
const [hasError, setHasError] = useState(false);
|
|
72
|
+
const [errorMessage, setErrorMessage] = useState<string>('Video cannot be previewed');
|
|
73
|
+
const blobUrlRef = useRef<string | null>(null);
|
|
74
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
75
|
+
|
|
76
|
+
// Retry function for error fallback
|
|
77
|
+
const retry = useCallback(() => {
|
|
78
|
+
setHasError(false);
|
|
79
|
+
setIsLoading(true);
|
|
80
|
+
// Re-trigger source effect by forcing state update
|
|
81
|
+
const video = videoRef.current;
|
|
82
|
+
if (video && videoUrl) {
|
|
83
|
+
video.load();
|
|
84
|
+
}
|
|
85
|
+
}, [videoUrl]);
|
|
86
|
+
|
|
87
|
+
// Expose video element methods via ref
|
|
88
|
+
useImperativeHandle(
|
|
89
|
+
ref,
|
|
90
|
+
() => ({
|
|
91
|
+
play: () => videoRef.current?.play(),
|
|
92
|
+
pause: () => videoRef.current?.pause(),
|
|
93
|
+
togglePlay: () => {
|
|
94
|
+
const video = videoRef.current;
|
|
95
|
+
if (video) {
|
|
96
|
+
video.paused ? video.play() : video.pause();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
seekTo: (time: number) => {
|
|
100
|
+
if (videoRef.current) videoRef.current.currentTime = time;
|
|
101
|
+
},
|
|
102
|
+
setVolume: (volume: number) => {
|
|
103
|
+
if (videoRef.current) videoRef.current.volume = Math.max(0, Math.min(1, volume));
|
|
104
|
+
},
|
|
105
|
+
toggleMute: () => {
|
|
106
|
+
if (videoRef.current) videoRef.current.muted = !videoRef.current.muted;
|
|
107
|
+
},
|
|
108
|
+
enterFullscreen: () => videoRef.current?.requestFullscreen(),
|
|
109
|
+
exitFullscreen: () => document.exitFullscreen(),
|
|
110
|
+
get currentTime() {
|
|
111
|
+
return videoRef.current?.currentTime ?? 0;
|
|
112
|
+
},
|
|
113
|
+
get duration() {
|
|
114
|
+
return videoRef.current?.duration ?? 0;
|
|
115
|
+
},
|
|
116
|
+
get paused() {
|
|
117
|
+
return videoRef.current?.paused ?? true;
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
[]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Create video URL based on source type
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
// Cleanup previous blob URL
|
|
126
|
+
if (blobUrlRef.current) {
|
|
127
|
+
URL.revokeObjectURL(blobUrlRef.current);
|
|
128
|
+
blobUrlRef.current = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setHasError(false);
|
|
132
|
+
setIsLoading(true);
|
|
133
|
+
|
|
134
|
+
switch (source.type) {
|
|
135
|
+
case 'stream': {
|
|
136
|
+
const streamSource = source as StreamSource;
|
|
137
|
+
const url = streamSource.getStreamUrl(streamSource.sessionId, streamSource.path);
|
|
138
|
+
setVideoUrl(url);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'blob': {
|
|
143
|
+
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;
|
|
149
|
+
setVideoUrl(url);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'data-url': {
|
|
154
|
+
const dataUrlSource = source as DataUrlSource;
|
|
155
|
+
setVideoUrl(dataUrlSource.data);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
default:
|
|
160
|
+
setVideoUrl(null);
|
|
161
|
+
setHasError(true);
|
|
162
|
+
setErrorMessage('Invalid video source');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
if (blobUrlRef.current) {
|
|
167
|
+
URL.revokeObjectURL(blobUrlRef.current);
|
|
168
|
+
blobUrlRef.current = null;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}, [source]);
|
|
172
|
+
|
|
173
|
+
// Preloader timeout
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (!showPreloader || !isLoading) return;
|
|
176
|
+
|
|
177
|
+
const timeout = setTimeout(() => {
|
|
178
|
+
setIsLoading(false);
|
|
179
|
+
}, preloaderTimeout);
|
|
180
|
+
|
|
181
|
+
return () => clearTimeout(timeout);
|
|
182
|
+
}, [showPreloader, isLoading, preloaderTimeout]);
|
|
183
|
+
|
|
184
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
185
|
+
if (disableContextMenu) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleCanPlay = () => {
|
|
191
|
+
setIsLoading(false);
|
|
192
|
+
onCanPlay?.();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleLoadedData = () => {
|
|
196
|
+
setIsLoading(false);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const handleError = () => {
|
|
200
|
+
setIsLoading(false);
|
|
201
|
+
setHasError(true);
|
|
202
|
+
setErrorMessage('Failed to load video');
|
|
203
|
+
onError?.('Video playback error');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleTimeUpdate = () => {
|
|
207
|
+
const video = videoRef.current;
|
|
208
|
+
if (video && onTimeUpdate) {
|
|
209
|
+
onTimeUpdate(video.currentTime, video.duration);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Determine if we should use AspectRatio wrapper or fill mode
|
|
214
|
+
const isFillMode = aspectRatio === 'fill';
|
|
215
|
+
const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
|
|
216
|
+
|
|
217
|
+
// Render error fallback
|
|
218
|
+
const renderErrorFallback = () => {
|
|
219
|
+
const fallbackProps: ErrorFallbackProps = { error: errorMessage, retry };
|
|
220
|
+
|
|
221
|
+
if (typeof errorFallback === 'function') {
|
|
222
|
+
return errorFallback(fallbackProps);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (errorFallback) {
|
|
226
|
+
return errorFallback;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return <DefaultErrorFallback {...fallbackProps} />;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Error state
|
|
233
|
+
if (!videoUrl || hasError) {
|
|
234
|
+
if (isFillMode) {
|
|
235
|
+
return (
|
|
236
|
+
<div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
|
|
237
|
+
{renderErrorFallback()}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className={cn('relative overflow-hidden bg-black', className)}>
|
|
244
|
+
<AspectRatio ratio={computedAspectRatio}>
|
|
245
|
+
{renderErrorFallback()}
|
|
246
|
+
</AspectRatio>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Video content
|
|
252
|
+
const videoContent = (
|
|
253
|
+
<>
|
|
254
|
+
{/* Loading indicator */}
|
|
255
|
+
{showPreloader && isLoading && (
|
|
256
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
|
|
257
|
+
<Preloader size="lg" spinnerClassName="text-white" />
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{/* Video element */}
|
|
262
|
+
<video
|
|
263
|
+
ref={videoRef}
|
|
264
|
+
src={videoUrl}
|
|
265
|
+
className={cn(
|
|
266
|
+
'w-full h-full object-contain',
|
|
267
|
+
isLoading && 'opacity-0',
|
|
268
|
+
videoClassName
|
|
269
|
+
)}
|
|
270
|
+
autoPlay={autoPlay}
|
|
271
|
+
muted={muted}
|
|
272
|
+
loop={loop}
|
|
273
|
+
playsInline={playsInline}
|
|
274
|
+
preload={preload}
|
|
275
|
+
controls={controls}
|
|
276
|
+
crossOrigin="anonymous"
|
|
277
|
+
poster={source.poster}
|
|
278
|
+
onContextMenu={handleContextMenu}
|
|
279
|
+
onLoadStart={onLoadStart}
|
|
280
|
+
onCanPlay={handleCanPlay}
|
|
281
|
+
onLoadedData={handleLoadedData}
|
|
282
|
+
onPlay={onPlay}
|
|
283
|
+
onPause={onPause}
|
|
284
|
+
onEnded={onEnded}
|
|
285
|
+
onError={handleError}
|
|
286
|
+
onTimeUpdate={handleTimeUpdate}
|
|
287
|
+
/>
|
|
288
|
+
</>
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Fill mode - no AspectRatio wrapper
|
|
292
|
+
if (isFillMode) {
|
|
293
|
+
return (
|
|
294
|
+
<div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
|
|
295
|
+
{videoContent}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Normal mode with AspectRatio
|
|
301
|
+
return (
|
|
302
|
+
<div className={cn('relative overflow-hidden bg-black', className)}>
|
|
303
|
+
<AspectRatio ratio={computedAspectRatio}>
|
|
304
|
+
{videoContent}
|
|
305
|
+
</AspectRatio>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
StreamProvider.displayName = 'StreamProvider';
|