@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,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VidstackProvider - Full-featured video player using Vidstack
|
|
3
|
+
* Supports YouTube, Vimeo, HLS, DASH, and direct URLs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
// Import Vidstack base styles
|
|
9
|
+
import '@vidstack/react/player/styles/base.css';
|
|
10
|
+
import '@vidstack/react/player/styles/default/theme.css';
|
|
11
|
+
import '@vidstack/react/player/styles/default/layouts/video.css';
|
|
12
|
+
|
|
13
|
+
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import { cn, generateOgImageUrl } from '@djangocfg/ui-core/lib';
|
|
16
|
+
import { MediaPlayer, MediaProvider, Poster } from '@vidstack/react';
|
|
17
|
+
import { defaultLayoutIcons, DefaultVideoLayout } from '@vidstack/react/player/layouts/default';
|
|
18
|
+
|
|
19
|
+
import type { MediaPlayerInstance } from '@vidstack/react';
|
|
20
|
+
import type { VidstackProviderProps, VideoPlayerRef, ErrorFallbackProps } from '../types';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert source to Vidstack-compatible format
|
|
24
|
+
*/
|
|
25
|
+
function getVidstackSrc(source: VidstackProviderProps['source']): string {
|
|
26
|
+
switch (source.type) {
|
|
27
|
+
case 'youtube':
|
|
28
|
+
return `youtube/${source.id}`;
|
|
29
|
+
case 'vimeo':
|
|
30
|
+
return `vimeo/${source.id}`;
|
|
31
|
+
case 'hls':
|
|
32
|
+
case 'dash':
|
|
33
|
+
case 'url':
|
|
34
|
+
return source.url;
|
|
35
|
+
default:
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Default error fallback UI */
|
|
41
|
+
function DefaultErrorFallback({ error }: ErrorFallbackProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4 text-white bg-black">
|
|
44
|
+
<svg
|
|
45
|
+
className="w-16 h-16 text-muted-foreground"
|
|
46
|
+
fill="none"
|
|
47
|
+
stroke="currentColor"
|
|
48
|
+
viewBox="0 0 24 24"
|
|
49
|
+
>
|
|
50
|
+
<path
|
|
51
|
+
strokeLinecap="round"
|
|
52
|
+
strokeLinejoin="round"
|
|
53
|
+
strokeWidth={2}
|
|
54
|
+
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"
|
|
55
|
+
/>
|
|
56
|
+
</svg>
|
|
57
|
+
<p className="text-lg">{error || 'Video cannot be played'}</p>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const VidstackProvider = forwardRef<VideoPlayerRef, VidstackProviderProps>(
|
|
63
|
+
(
|
|
64
|
+
{
|
|
65
|
+
source,
|
|
66
|
+
aspectRatio = 16 / 9,
|
|
67
|
+
autoPlay = false,
|
|
68
|
+
muted = false,
|
|
69
|
+
loop = false,
|
|
70
|
+
playsInline = true,
|
|
71
|
+
controls = true,
|
|
72
|
+
className,
|
|
73
|
+
showInfo = false,
|
|
74
|
+
theme = 'default',
|
|
75
|
+
errorFallback,
|
|
76
|
+
onPlay,
|
|
77
|
+
onPause,
|
|
78
|
+
onEnded,
|
|
79
|
+
onError,
|
|
80
|
+
onLoadStart,
|
|
81
|
+
onCanPlay,
|
|
82
|
+
onTimeUpdate,
|
|
83
|
+
},
|
|
84
|
+
ref
|
|
85
|
+
) => {
|
|
86
|
+
const playerRef = useRef<MediaPlayerInstance | null>(null);
|
|
87
|
+
const [hasError, setHasError] = useState(false);
|
|
88
|
+
const [errorMessage, setErrorMessage] = useState<string>('Video cannot be played');
|
|
89
|
+
|
|
90
|
+
// Generate poster if not provided
|
|
91
|
+
const posterUrl = useMemo(() => {
|
|
92
|
+
if (source.poster) return source.poster;
|
|
93
|
+
if (!source.title) return undefined;
|
|
94
|
+
return generateOgImageUrl({ title: source.title });
|
|
95
|
+
}, [source.poster, source.title]);
|
|
96
|
+
|
|
97
|
+
// Get Vidstack-compatible source URL
|
|
98
|
+
const vidstackSrc = useMemo(() => getVidstackSrc(source), [source]);
|
|
99
|
+
|
|
100
|
+
// Retry function
|
|
101
|
+
const retry = useCallback(() => {
|
|
102
|
+
setHasError(false);
|
|
103
|
+
setErrorMessage('Video cannot be played');
|
|
104
|
+
// Force reload by updating key would be needed, but for now just reset state
|
|
105
|
+
const player = playerRef.current;
|
|
106
|
+
if (player) {
|
|
107
|
+
player.currentTime = 0;
|
|
108
|
+
player.play();
|
|
109
|
+
}
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
// Expose player methods via ref
|
|
113
|
+
useImperativeHandle(
|
|
114
|
+
ref,
|
|
115
|
+
() => {
|
|
116
|
+
const player = playerRef.current;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
play: () => player?.play(),
|
|
120
|
+
pause: () => player?.pause(),
|
|
121
|
+
togglePlay: () => {
|
|
122
|
+
if (player) {
|
|
123
|
+
player.paused ? player.play() : player.pause();
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
seekTo: (time: number) => {
|
|
127
|
+
if (player) player.currentTime = time;
|
|
128
|
+
},
|
|
129
|
+
setVolume: (volume: number) => {
|
|
130
|
+
if (player) player.volume = Math.max(0, Math.min(1, volume));
|
|
131
|
+
},
|
|
132
|
+
toggleMute: () => {
|
|
133
|
+
if (player) player.muted = !player.muted;
|
|
134
|
+
},
|
|
135
|
+
enterFullscreen: () => player?.enterFullscreen(),
|
|
136
|
+
exitFullscreen: () => player?.exitFullscreen(),
|
|
137
|
+
get currentTime() {
|
|
138
|
+
return player?.currentTime ?? 0;
|
|
139
|
+
},
|
|
140
|
+
get duration() {
|
|
141
|
+
return player?.duration ?? 0;
|
|
142
|
+
},
|
|
143
|
+
get paused() {
|
|
144
|
+
return player?.paused ?? true;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
[]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const handlePlay = () => onPlay?.();
|
|
152
|
+
const handlePause = () => onPause?.();
|
|
153
|
+
const handleEnded = () => onEnded?.();
|
|
154
|
+
const handleError = (detail: unknown) => {
|
|
155
|
+
const error = detail as { message?: string };
|
|
156
|
+
const msg = error?.message || 'Video playback error';
|
|
157
|
+
setHasError(true);
|
|
158
|
+
setErrorMessage(msg);
|
|
159
|
+
onError?.(msg);
|
|
160
|
+
};
|
|
161
|
+
const handleLoadStart = () => onLoadStart?.();
|
|
162
|
+
const handleCanPlay = () => {
|
|
163
|
+
setHasError(false);
|
|
164
|
+
onCanPlay?.();
|
|
165
|
+
};
|
|
166
|
+
const handleTimeUpdate = () => {
|
|
167
|
+
const player = playerRef.current;
|
|
168
|
+
if (player && onTimeUpdate) {
|
|
169
|
+
onTimeUpdate(player.currentTime, player.duration);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Determine layout mode
|
|
174
|
+
const isFillMode = aspectRatio === 'fill';
|
|
175
|
+
const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
|
|
176
|
+
|
|
177
|
+
// Render error fallback
|
|
178
|
+
const renderErrorFallback = () => {
|
|
179
|
+
const fallbackProps: ErrorFallbackProps = { error: errorMessage, retry };
|
|
180
|
+
|
|
181
|
+
if (typeof errorFallback === 'function') {
|
|
182
|
+
return errorFallback(fallbackProps);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (errorFallback) {
|
|
186
|
+
return errorFallback;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return <DefaultErrorFallback {...fallbackProps} />;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Container styles based on mode
|
|
193
|
+
const containerStyles = isFillMode
|
|
194
|
+
? { width: '100%', height: '100%' }
|
|
195
|
+
: { aspectRatio: computedAspectRatio };
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className={cn(isFillMode ? 'w-full h-full' : 'w-full', className)}>
|
|
199
|
+
<div
|
|
200
|
+
className={cn(
|
|
201
|
+
'relative w-full rounded-sm bg-black overflow-hidden',
|
|
202
|
+
isFillMode && 'h-full',
|
|
203
|
+
theme === 'minimal' && 'rounded-none',
|
|
204
|
+
theme === 'modern' && 'rounded-xl shadow-2xl'
|
|
205
|
+
)}
|
|
206
|
+
style={containerStyles}
|
|
207
|
+
>
|
|
208
|
+
{hasError ? (
|
|
209
|
+
renderErrorFallback()
|
|
210
|
+
) : (
|
|
211
|
+
<MediaPlayer
|
|
212
|
+
ref={playerRef}
|
|
213
|
+
title={source.title || 'Video'}
|
|
214
|
+
src={vidstackSrc}
|
|
215
|
+
autoPlay={autoPlay}
|
|
216
|
+
muted={muted}
|
|
217
|
+
loop={loop}
|
|
218
|
+
playsInline={playsInline}
|
|
219
|
+
onPlay={handlePlay}
|
|
220
|
+
onPause={handlePause}
|
|
221
|
+
onEnded={handleEnded}
|
|
222
|
+
onError={handleError}
|
|
223
|
+
onLoadStart={handleLoadStart}
|
|
224
|
+
onCanPlay={handleCanPlay}
|
|
225
|
+
onTimeUpdate={handleTimeUpdate}
|
|
226
|
+
className="w-full h-full"
|
|
227
|
+
>
|
|
228
|
+
<MediaProvider />
|
|
229
|
+
|
|
230
|
+
{posterUrl && (
|
|
231
|
+
<Poster
|
|
232
|
+
className="vds-poster"
|
|
233
|
+
src={posterUrl}
|
|
234
|
+
alt={source.title || 'Video poster'}
|
|
235
|
+
style={{ objectFit: 'cover' }}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{controls && <DefaultVideoLayout icons={defaultLayoutIcons} thumbnails={posterUrl} />}
|
|
240
|
+
</MediaPlayer>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{showInfo && source.title && (
|
|
245
|
+
<div className="mt-4 space-y-2">
|
|
246
|
+
<h3 className="text-xl font-semibold text-foreground">{source.title}</h3>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
VidstackProvider.displayName = 'VidstackProvider';
|
|
@@ -1,54 +1,195 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VideoPlayer Types -
|
|
2
|
+
* VideoPlayer Types - Unified Video Player
|
|
3
|
+
* Supports Vidstack, Native HTML5, and HTTP Streaming
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
import type React from 'react';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Source Types - Different ways to provide video content
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/** Simple URL source (mp4, webm, etc.) */
|
|
13
|
+
export interface UrlSource {
|
|
14
|
+
type: 'url';
|
|
7
15
|
url: string;
|
|
8
|
-
/** Video title */
|
|
9
16
|
title?: string;
|
|
10
|
-
/** Video description */
|
|
11
|
-
description?: string;
|
|
12
|
-
/** Custom poster/thumbnail URL */
|
|
13
17
|
poster?: string;
|
|
14
|
-
/** Video duration in seconds */
|
|
15
|
-
duration?: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
|
|
20
|
+
/** YouTube embed source */
|
|
21
|
+
export interface YouTubeSource {
|
|
22
|
+
type: 'youtube';
|
|
23
|
+
/** YouTube video ID (11 characters) */
|
|
24
|
+
id: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
poster?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Vimeo embed source */
|
|
30
|
+
export interface VimeoSource {
|
|
31
|
+
type: 'vimeo';
|
|
32
|
+
/** Vimeo video ID */
|
|
33
|
+
id: string;
|
|
34
|
+
title?: string;
|
|
35
|
+
poster?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** HLS streaming source */
|
|
39
|
+
export interface HLSSource {
|
|
40
|
+
type: 'hls';
|
|
41
|
+
/** URL to .m3u8 manifest */
|
|
42
|
+
url: string;
|
|
43
|
+
title?: string;
|
|
44
|
+
poster?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** DASH streaming source */
|
|
48
|
+
export interface DASHSource {
|
|
49
|
+
type: 'dash';
|
|
50
|
+
/** URL to .mpd manifest */
|
|
51
|
+
url: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
poster?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** HTTP Range streaming source (for large files with auth) */
|
|
57
|
+
export interface StreamSource {
|
|
58
|
+
type: 'stream';
|
|
59
|
+
/** Session ID for authorization */
|
|
60
|
+
sessionId: string;
|
|
61
|
+
/** File path on server */
|
|
62
|
+
path: string;
|
|
63
|
+
/** Function to generate stream URL */
|
|
64
|
+
getStreamUrl: (sessionId: string, path: string) => string;
|
|
65
|
+
/** MIME type for the video */
|
|
66
|
+
mimeType?: string;
|
|
67
|
+
title?: string;
|
|
68
|
+
poster?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Binary data source (ArrayBuffer) */
|
|
72
|
+
export interface BlobSource {
|
|
73
|
+
type: 'blob';
|
|
74
|
+
/** Video data as ArrayBuffer */
|
|
75
|
+
data: ArrayBuffer;
|
|
76
|
+
/** MIME type (default: video/mp4) */
|
|
77
|
+
mimeType?: string;
|
|
78
|
+
title?: string;
|
|
79
|
+
poster?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Base64 data URL source */
|
|
83
|
+
export interface DataUrlSource {
|
|
84
|
+
type: 'data-url';
|
|
85
|
+
/** Base64 encoded data URL */
|
|
86
|
+
data: string;
|
|
87
|
+
title?: string;
|
|
88
|
+
poster?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Union of all source types */
|
|
92
|
+
export type VideoSourceUnion =
|
|
93
|
+
| UrlSource
|
|
94
|
+
| YouTubeSource
|
|
95
|
+
| VimeoSource
|
|
96
|
+
| HLSSource
|
|
97
|
+
| DASHSource
|
|
98
|
+
| StreamSource
|
|
99
|
+
| BlobSource
|
|
100
|
+
| DataUrlSource;
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Player Mode
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/** Player mode - determines which provider to use */
|
|
107
|
+
export type PlayerMode =
|
|
108
|
+
| 'auto' // Auto-select based on source type
|
|
109
|
+
| 'vidstack' // Force Vidstack (full-featured)
|
|
110
|
+
| 'native' // Force native HTML5 <video>
|
|
111
|
+
| 'streaming'; // Force streaming provider
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Player Props
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/** Aspect ratio options */
|
|
118
|
+
export type AspectRatioValue = number | 'auto' | 'fill';
|
|
119
|
+
|
|
120
|
+
/** Common player settings */
|
|
121
|
+
export interface CommonPlayerSettings {
|
|
23
122
|
/** Auto-play video */
|
|
24
|
-
|
|
123
|
+
autoPlay?: boolean;
|
|
25
124
|
/** Mute video by default */
|
|
26
125
|
muted?: boolean;
|
|
126
|
+
/** Loop video */
|
|
127
|
+
loop?: boolean;
|
|
27
128
|
/** Play video inline on mobile */
|
|
28
129
|
playsInline?: boolean;
|
|
29
|
-
/** Show
|
|
130
|
+
/** Show player controls */
|
|
30
131
|
controls?: boolean;
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Aspect ratio:
|
|
134
|
+
* - number (e.g. 16/9): Fixed aspect ratio
|
|
135
|
+
* - 'auto': Natural video aspect ratio
|
|
136
|
+
* - 'fill': Fill parent container (100% width & height)
|
|
137
|
+
*/
|
|
138
|
+
aspectRatio?: AspectRatioValue;
|
|
139
|
+
/** Preload strategy */
|
|
140
|
+
preload?: 'auto' | 'metadata' | 'none';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Common player events */
|
|
144
|
+
export interface CommonPlayerEvents {
|
|
38
145
|
onPlay?: () => void;
|
|
39
146
|
onPause?: () => void;
|
|
40
147
|
onEnded?: () => void;
|
|
41
148
|
onError?: (error: string) => void;
|
|
149
|
+
onLoadStart?: () => void;
|
|
150
|
+
onCanPlay?: () => void;
|
|
151
|
+
onTimeUpdate?: (currentTime: number, duration: number) => void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Error fallback render props */
|
|
155
|
+
export interface ErrorFallbackProps {
|
|
156
|
+
error: string;
|
|
157
|
+
retry?: () => void;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Main VideoPlayer props */
|
|
161
|
+
export interface VideoPlayerProps extends CommonPlayerSettings, CommonPlayerEvents {
|
|
162
|
+
/** Video source configuration */
|
|
163
|
+
source: VideoSourceUnion;
|
|
164
|
+
/** Player mode (default: 'auto') */
|
|
165
|
+
mode?: PlayerMode;
|
|
166
|
+
/** Player theme (Vidstack only) */
|
|
167
|
+
theme?: 'default' | 'minimal' | 'modern';
|
|
168
|
+
/** Show video info below player */
|
|
169
|
+
showInfo?: boolean;
|
|
170
|
+
/** Container className */
|
|
171
|
+
className?: string;
|
|
172
|
+
/** Video element className (native/streaming only) */
|
|
173
|
+
videoClassName?: string;
|
|
174
|
+
/** Disable right-click context menu */
|
|
175
|
+
disableContextMenu?: boolean;
|
|
176
|
+
/** Show loading spinner */
|
|
177
|
+
showPreloader?: boolean;
|
|
178
|
+
/** Preloader timeout in ms */
|
|
179
|
+
preloaderTimeout?: number;
|
|
180
|
+
/** Custom error fallback UI */
|
|
181
|
+
errorFallback?: React.ReactNode | ((props: ErrorFallbackProps) => React.ReactNode);
|
|
42
182
|
}
|
|
43
183
|
|
|
184
|
+
/** VideoPlayer ref methods */
|
|
44
185
|
export interface VideoPlayerRef {
|
|
45
186
|
/** Play video */
|
|
46
|
-
play: () => void;
|
|
187
|
+
play: () => Promise<void> | void;
|
|
47
188
|
/** Pause video */
|
|
48
189
|
pause: () => void;
|
|
49
190
|
/** Toggle play/pause */
|
|
50
191
|
togglePlay: () => void;
|
|
51
|
-
/** Seek to time */
|
|
192
|
+
/** Seek to time in seconds */
|
|
52
193
|
seekTo: (time: number) => void;
|
|
53
194
|
/** Set volume (0-1) */
|
|
54
195
|
setVolume: (volume: number) => void;
|
|
@@ -58,61 +199,169 @@ export interface VideoPlayerRef {
|
|
|
58
199
|
enterFullscreen: () => void;
|
|
59
200
|
/** Exit fullscreen */
|
|
60
201
|
exitFullscreen: () => void;
|
|
202
|
+
/** Current playback time in seconds */
|
|
203
|
+
readonly currentTime: number;
|
|
204
|
+
/** Video duration in seconds */
|
|
205
|
+
readonly duration: number;
|
|
206
|
+
/** Whether video is paused */
|
|
207
|
+
readonly paused: boolean;
|
|
61
208
|
}
|
|
62
209
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Provider-specific Props (internal use)
|
|
212
|
+
// ============================================================================
|
|
66
213
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
playsInline?: boolean;
|
|
82
|
-
/** Preload strategy (default: 'auto') */
|
|
83
|
-
preload?: 'auto' | 'metadata' | 'none';
|
|
84
|
-
/** Show native browser controls (default: false) */
|
|
85
|
-
controls?: boolean;
|
|
86
|
-
/** Disable right-click context menu (default: true) */
|
|
214
|
+
/** Props passed to Vidstack provider */
|
|
215
|
+
export interface VidstackProviderProps extends CommonPlayerSettings, CommonPlayerEvents {
|
|
216
|
+
source: UrlSource | YouTubeSource | VimeoSource | HLSSource | DASHSource;
|
|
217
|
+
theme?: 'default' | 'minimal' | 'modern';
|
|
218
|
+
showInfo?: boolean;
|
|
219
|
+
className?: string;
|
|
220
|
+
errorFallback?: React.ReactNode | ((props: ErrorFallbackProps) => React.ReactNode);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Props passed to Native provider */
|
|
224
|
+
export interface NativeProviderProps extends CommonPlayerSettings, CommonPlayerEvents {
|
|
225
|
+
source: UrlSource | DataUrlSource;
|
|
226
|
+
className?: string;
|
|
227
|
+
videoClassName?: string;
|
|
87
228
|
disableContextMenu?: boolean;
|
|
88
|
-
/** Show preloader while video loads (default: true) */
|
|
89
229
|
showPreloader?: boolean;
|
|
90
|
-
/** Preloader timeout in ms - hide even if not loaded (default: 5000) */
|
|
91
230
|
preloaderTimeout?: number;
|
|
92
|
-
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Props passed to Stream provider */
|
|
234
|
+
export interface StreamProviderProps extends CommonPlayerSettings, CommonPlayerEvents {
|
|
235
|
+
source: StreamSource | BlobSource | DataUrlSource;
|
|
93
236
|
className?: string;
|
|
94
|
-
/** Video element className */
|
|
95
237
|
videoClassName?: string;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
onCanPlay?: () => void;
|
|
101
|
-
onPlaying?: () => void;
|
|
102
|
-
onEnded?: () => void;
|
|
103
|
-
onError?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
|
|
238
|
+
disableContextMenu?: boolean;
|
|
239
|
+
showPreloader?: boolean;
|
|
240
|
+
preloaderTimeout?: number;
|
|
241
|
+
errorFallback?: React.ReactNode | ((props: ErrorFallbackProps) => React.ReactNode);
|
|
104
242
|
}
|
|
105
243
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Helper function to resolve mode
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
/** Determine which provider to use based on source type */
|
|
249
|
+
export function resolvePlayerMode(source: VideoSourceUnion, mode: PlayerMode = 'auto'): 'vidstack' | 'native' | 'streaming' {
|
|
250
|
+
if (mode !== 'auto') {
|
|
251
|
+
return mode;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
switch (source.type) {
|
|
255
|
+
case 'youtube':
|
|
256
|
+
case 'vimeo':
|
|
257
|
+
case 'hls':
|
|
258
|
+
case 'dash':
|
|
259
|
+
return 'vidstack';
|
|
260
|
+
|
|
261
|
+
case 'stream':
|
|
262
|
+
case 'blob':
|
|
263
|
+
return 'streaming';
|
|
264
|
+
|
|
265
|
+
case 'data-url':
|
|
266
|
+
case 'url':
|
|
267
|
+
default:
|
|
268
|
+
return 'native';
|
|
269
|
+
}
|
|
117
270
|
}
|
|
118
271
|
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// File Source Helper (for FileWorkspace integration)
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
276
|
+
/** Options for resolving file source */
|
|
277
|
+
export interface ResolveFileSourceOptions {
|
|
278
|
+
/** File content - can be data URL string or binary ArrayBuffer */
|
|
279
|
+
content: string | ArrayBuffer | null | undefined;
|
|
280
|
+
/** File path for streaming */
|
|
281
|
+
path: string;
|
|
282
|
+
/** MIME type of the file */
|
|
283
|
+
mimeType?: string;
|
|
284
|
+
/** Session ID for authenticated streaming */
|
|
285
|
+
sessionId?: string | null;
|
|
286
|
+
/** Load method hint - 'http_stream' enables streaming mode */
|
|
287
|
+
loadMethod?: 'http_stream' | 'rpc' | 'unspecified' | 'skip' | string;
|
|
288
|
+
/** Function to generate stream URL (required for streaming) */
|
|
289
|
+
getStreamUrl?: (sessionId: string, path: string) => string;
|
|
290
|
+
/** Optional title */
|
|
291
|
+
title?: string;
|
|
292
|
+
/** Optional poster */
|
|
293
|
+
poster?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Resolve file content to VideoSourceUnion
|
|
298
|
+
* Useful for FileWorkspace/file manager integrations
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* const source = resolveFileSource({
|
|
302
|
+
* content: file.content,
|
|
303
|
+
* path: file.path,
|
|
304
|
+
* mimeType: file.mimeType,
|
|
305
|
+
* sessionId: sessionId,
|
|
306
|
+
* loadMethod: file.loadMethod,
|
|
307
|
+
* getStreamUrl: terminalClient.terminal_media.streamStreamRetrieveUrl
|
|
308
|
+
* });
|
|
309
|
+
*
|
|
310
|
+
* <VideoPlayer source={source} />
|
|
311
|
+
*/
|
|
312
|
+
export function resolveFileSource(options: ResolveFileSourceOptions): VideoSourceUnion | null {
|
|
313
|
+
const {
|
|
314
|
+
content,
|
|
315
|
+
path,
|
|
316
|
+
mimeType,
|
|
317
|
+
sessionId,
|
|
318
|
+
loadMethod,
|
|
319
|
+
getStreamUrl,
|
|
320
|
+
title,
|
|
321
|
+
poster,
|
|
322
|
+
} = options;
|
|
323
|
+
|
|
324
|
+
const contentSize = content
|
|
325
|
+
? typeof content === 'string'
|
|
326
|
+
? content.length
|
|
327
|
+
: content.byteLength
|
|
328
|
+
: 0;
|
|
329
|
+
const hasContent = contentSize > 0;
|
|
330
|
+
|
|
331
|
+
// Priority 1: HTTP Range streaming for large files
|
|
332
|
+
if (loadMethod === 'http_stream' && !hasContent && sessionId && getStreamUrl) {
|
|
333
|
+
return {
|
|
334
|
+
type: 'stream',
|
|
335
|
+
sessionId,
|
|
336
|
+
path,
|
|
337
|
+
getStreamUrl,
|
|
338
|
+
mimeType,
|
|
339
|
+
title,
|
|
340
|
+
poster,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Priority 2: Data URL (base64 string)
|
|
345
|
+
if (typeof content === 'string' && hasContent) {
|
|
346
|
+
return {
|
|
347
|
+
type: 'data-url',
|
|
348
|
+
data: content,
|
|
349
|
+
title,
|
|
350
|
+
poster,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Priority 3: ArrayBuffer → Blob
|
|
355
|
+
if (content instanceof ArrayBuffer && hasContent) {
|
|
356
|
+
return {
|
|
357
|
+
type: 'blob',
|
|
358
|
+
data: content,
|
|
359
|
+
mimeType: mimeType || 'video/mp4',
|
|
360
|
+
title,
|
|
361
|
+
poster,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// No valid content
|
|
366
|
+
return null;
|
|
367
|
+
}
|