@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,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File source resolution utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { VideoSourceUnion, ResolveFileSourceOptions } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve file content to VideoSourceUnion
|
|
9
|
+
* Useful for FileWorkspace/file manager integrations
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const source = resolveFileSource({
|
|
13
|
+
* content: file.content,
|
|
14
|
+
* path: file.path,
|
|
15
|
+
* mimeType: file.mimeType,
|
|
16
|
+
* sessionId: sessionId,
|
|
17
|
+
* loadMethod: file.loadMethod,
|
|
18
|
+
* getStreamUrl: terminalClient.terminal_media.streamStreamRetrieveUrl
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* <VideoPlayer source={source} />
|
|
22
|
+
*/
|
|
23
|
+
export function resolveFileSource(options: ResolveFileSourceOptions): VideoSourceUnion | null {
|
|
24
|
+
const {
|
|
25
|
+
content,
|
|
26
|
+
path,
|
|
27
|
+
mimeType,
|
|
28
|
+
sessionId,
|
|
29
|
+
loadMethod,
|
|
30
|
+
getStreamUrl,
|
|
31
|
+
title,
|
|
32
|
+
poster,
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
const contentSize = content
|
|
36
|
+
? typeof content === 'string'
|
|
37
|
+
? content.length
|
|
38
|
+
: content.byteLength
|
|
39
|
+
: 0;
|
|
40
|
+
const hasContent = contentSize > 0;
|
|
41
|
+
|
|
42
|
+
// Priority 1: HTTP Range streaming for large files
|
|
43
|
+
if (loadMethod === 'http_stream' && !hasContent && sessionId && getStreamUrl) {
|
|
44
|
+
return {
|
|
45
|
+
type: 'stream',
|
|
46
|
+
sessionId,
|
|
47
|
+
path,
|
|
48
|
+
getStreamUrl,
|
|
49
|
+
mimeType,
|
|
50
|
+
title,
|
|
51
|
+
poster,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Priority 2: Data URL (base64 string)
|
|
56
|
+
if (typeof content === 'string' && hasContent) {
|
|
57
|
+
return {
|
|
58
|
+
type: 'data-url',
|
|
59
|
+
data: content,
|
|
60
|
+
title,
|
|
61
|
+
poster,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Priority 3: ArrayBuffer → Blob
|
|
66
|
+
if (content instanceof ArrayBuffer && hasContent) {
|
|
67
|
+
return {
|
|
68
|
+
type: 'blob',
|
|
69
|
+
data: content,
|
|
70
|
+
mimeType: mimeType || 'video/mp4',
|
|
71
|
+
title,
|
|
72
|
+
poster,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// No valid content
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video source and player mode resolvers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { VideoSourceUnion, PlayerMode, SimpleStreamSource, StreamSource, VideoPlayerContextValue } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Determine which provider to use based on source type
|
|
9
|
+
*/
|
|
10
|
+
export function resolvePlayerMode(
|
|
11
|
+
source: VideoSourceUnion,
|
|
12
|
+
mode: PlayerMode = 'auto'
|
|
13
|
+
): 'vidstack' | 'native' | 'streaming' {
|
|
14
|
+
if (mode !== 'auto') {
|
|
15
|
+
return mode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
switch (source.type) {
|
|
19
|
+
case 'youtube':
|
|
20
|
+
case 'vimeo':
|
|
21
|
+
case 'hls':
|
|
22
|
+
case 'dash':
|
|
23
|
+
return 'vidstack';
|
|
24
|
+
|
|
25
|
+
case 'stream':
|
|
26
|
+
case 'blob':
|
|
27
|
+
return 'streaming';
|
|
28
|
+
|
|
29
|
+
case 'data-url':
|
|
30
|
+
case 'url':
|
|
31
|
+
default:
|
|
32
|
+
return 'native';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if source is a simplified stream source (without getStreamUrl)
|
|
38
|
+
*/
|
|
39
|
+
export function isSimpleStreamSource(
|
|
40
|
+
source: VideoSourceUnion | SimpleStreamSource
|
|
41
|
+
): source is SimpleStreamSource {
|
|
42
|
+
return source.type === 'stream' && !('getStreamUrl' in source);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve simplified stream source to full stream source using context
|
|
47
|
+
*/
|
|
48
|
+
export function resolveStreamSource(
|
|
49
|
+
source: SimpleStreamSource,
|
|
50
|
+
context: VideoPlayerContextValue | null
|
|
51
|
+
): StreamSource | null {
|
|
52
|
+
if (!context?.getStreamUrl) {
|
|
53
|
+
console.warn(
|
|
54
|
+
'VideoPlayer: Stream source requires getStreamUrl. ' +
|
|
55
|
+
'Either provide it in source or wrap with VideoPlayerProvider.'
|
|
56
|
+
);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sessionId = source.sessionId || context.sessionId;
|
|
61
|
+
if (!sessionId) {
|
|
62
|
+
console.warn('VideoPlayer: Stream source requires sessionId.');
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
type: 'stream',
|
|
68
|
+
sessionId,
|
|
69
|
+
path: source.path,
|
|
70
|
+
getStreamUrl: context.getStreamUrl,
|
|
71
|
+
mimeType: source.mimeType,
|
|
72
|
+
title: source.title,
|
|
73
|
+
poster: source.poster,
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - JsonForm: ~300KB (JSON Schema form generator)
|
|
10
10
|
* - OpenapiViewer: ~400KB (OpenAPI schema viewer & playground)
|
|
11
11
|
* - VideoPlayer: ~150KB (Professional video player with Vidstack)
|
|
12
|
+
* - AudioPlayer: ~200KB (Audio player with WaveSurfer.js)
|
|
13
|
+
* - ImageViewer: ~50KB (Image viewer with zoom/pan/rotate)
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
// Export tools as named exports (all use dynamic imports internally)
|
|
@@ -39,11 +41,97 @@ export { default as OpenapiViewer } from './OpenapiViewer';
|
|
|
39
41
|
export type { PlaygroundConfig, SchemaSource, PlaygroundProps } from './OpenapiViewer';
|
|
40
42
|
|
|
41
43
|
// Export VideoPlayer
|
|
42
|
-
export {
|
|
44
|
+
export {
|
|
45
|
+
VideoPlayer,
|
|
46
|
+
VideoControls,
|
|
47
|
+
VidstackProvider,
|
|
48
|
+
NativeProvider,
|
|
49
|
+
StreamProvider,
|
|
50
|
+
VideoPlayerProvider,
|
|
51
|
+
useVideoPlayerContext,
|
|
52
|
+
VideoErrorFallback,
|
|
53
|
+
createVideoErrorFallback,
|
|
54
|
+
resolvePlayerMode,
|
|
55
|
+
resolveFileSource,
|
|
56
|
+
isSimpleStreamSource,
|
|
57
|
+
resolveStreamSource,
|
|
58
|
+
} from './VideoPlayer';
|
|
43
59
|
export type {
|
|
44
|
-
|
|
60
|
+
VideoSourceUnion,
|
|
61
|
+
UrlSource,
|
|
62
|
+
YouTubeSource,
|
|
63
|
+
VimeoSource,
|
|
64
|
+
HLSSource,
|
|
65
|
+
DASHSource,
|
|
66
|
+
StreamSource,
|
|
67
|
+
BlobSource,
|
|
68
|
+
DataUrlSource,
|
|
69
|
+
PlayerMode,
|
|
70
|
+
AspectRatioValue,
|
|
45
71
|
VideoPlayerProps,
|
|
46
72
|
VideoPlayerRef,
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
ErrorFallbackProps,
|
|
74
|
+
ResolveFileSourceOptions,
|
|
75
|
+
VideoPlayerContextValue,
|
|
76
|
+
VideoPlayerProviderProps,
|
|
77
|
+
SimpleStreamSource,
|
|
78
|
+
VideoErrorFallbackProps,
|
|
79
|
+
CreateVideoErrorFallbackOptions,
|
|
49
80
|
} from './VideoPlayer';
|
|
81
|
+
|
|
82
|
+
// Export AudioPlayer
|
|
83
|
+
export {
|
|
84
|
+
// Simple wrapper (recommended)
|
|
85
|
+
SimpleAudioPlayer,
|
|
86
|
+
// Core components
|
|
87
|
+
AudioPlayer,
|
|
88
|
+
AudioEqualizer,
|
|
89
|
+
AudioReactiveCover,
|
|
90
|
+
AudioShortcutsPopover,
|
|
91
|
+
VisualizationToggle,
|
|
92
|
+
AudioProvider,
|
|
93
|
+
useAudio,
|
|
94
|
+
useAudioControls,
|
|
95
|
+
useAudioState,
|
|
96
|
+
useAudioElement,
|
|
97
|
+
useAudioHotkeys,
|
|
98
|
+
AUDIO_SHORTCUTS,
|
|
99
|
+
useAudioVisualization,
|
|
100
|
+
VisualizationProvider,
|
|
101
|
+
VARIANT_INFO,
|
|
102
|
+
INTENSITY_INFO,
|
|
103
|
+
COLOR_SCHEME_INFO,
|
|
104
|
+
} from './AudioPlayer';
|
|
105
|
+
export type {
|
|
106
|
+
SimpleAudioPlayerProps,
|
|
107
|
+
AudioSource,
|
|
108
|
+
PlaybackStatus,
|
|
109
|
+
WaveformOptions,
|
|
110
|
+
EqualizerOptions,
|
|
111
|
+
AudioContextState,
|
|
112
|
+
AudioPlayerProps,
|
|
113
|
+
AudioEqualizerProps,
|
|
114
|
+
AudioReactiveCoverProps,
|
|
115
|
+
AudioViewerProps,
|
|
116
|
+
VisualizationSettings,
|
|
117
|
+
VisualizationVariant,
|
|
118
|
+
VisualizationIntensity,
|
|
119
|
+
VisualizationColorScheme,
|
|
120
|
+
AudioHotkeyOptions,
|
|
121
|
+
ShortcutItem,
|
|
122
|
+
ShortcutGroup,
|
|
123
|
+
} from './AudioPlayer';
|
|
124
|
+
|
|
125
|
+
// Export ImageViewer
|
|
126
|
+
export { ImageViewer } from './ImageViewer';
|
|
127
|
+
export type { ImageViewerProps, ImageFile } from './ImageViewer';
|
|
128
|
+
|
|
129
|
+
// Export Media Cache Store
|
|
130
|
+
export {
|
|
131
|
+
useMediaCacheStore,
|
|
132
|
+
useImageCache,
|
|
133
|
+
useAudioCache,
|
|
134
|
+
useVideoCache,
|
|
135
|
+
useBlobUrlCleanup,
|
|
136
|
+
generateContentKey,
|
|
137
|
+
} from '../stores/mediaCache';
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NativePlayer - Lightweight native HTML5 video player
|
|
3
|
-
* For demo videos, background videos, autoplay loop muted scenarios
|
|
4
|
-
* Use VideoPlayer (Vidstack) for full-featured player with controls
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use client';
|
|
8
|
-
|
|
9
|
-
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
|
10
|
-
|
|
11
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
|
-
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
13
|
-
|
|
14
|
-
import type { NativePlayerProps, NativePlayerRef } from './types';
|
|
15
|
-
|
|
16
|
-
export const NativePlayer = forwardRef<NativePlayerRef, NativePlayerProps>(
|
|
17
|
-
(
|
|
18
|
-
{
|
|
19
|
-
src,
|
|
20
|
-
poster,
|
|
21
|
-
aspectRatio = 16 / 9,
|
|
22
|
-
autoPlay = true,
|
|
23
|
-
muted = true,
|
|
24
|
-
loop = true,
|
|
25
|
-
playsInline = true,
|
|
26
|
-
preload = 'auto',
|
|
27
|
-
controls = false,
|
|
28
|
-
disableContextMenu = true,
|
|
29
|
-
showPreloader = true,
|
|
30
|
-
preloaderTimeout = 5000,
|
|
31
|
-
className,
|
|
32
|
-
videoClassName,
|
|
33
|
-
preloaderClassName,
|
|
34
|
-
onLoadStart,
|
|
35
|
-
onCanPlay,
|
|
36
|
-
onPlaying,
|
|
37
|
-
onEnded,
|
|
38
|
-
onError,
|
|
39
|
-
},
|
|
40
|
-
ref
|
|
41
|
-
) => {
|
|
42
|
-
const [isLoading, setIsLoading] = useState(showPreloader);
|
|
43
|
-
const videoRef = useRef<HTMLVideoElement>(null);
|
|
44
|
-
|
|
45
|
-
// Expose video element methods via ref
|
|
46
|
-
useImperativeHandle(ref, () => ({
|
|
47
|
-
play: () => videoRef.current?.play(),
|
|
48
|
-
pause: () => videoRef.current?.pause(),
|
|
49
|
-
get currentTime() {
|
|
50
|
-
return videoRef.current?.currentTime ?? 0;
|
|
51
|
-
},
|
|
52
|
-
set currentTime(time: number) {
|
|
53
|
-
if (videoRef.current) videoRef.current.currentTime = time;
|
|
54
|
-
},
|
|
55
|
-
get paused() {
|
|
56
|
-
return videoRef.current?.paused ?? true;
|
|
57
|
-
},
|
|
58
|
-
get element() {
|
|
59
|
-
return videoRef.current;
|
|
60
|
-
},
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (!showPreloader) return;
|
|
65
|
-
|
|
66
|
-
const video = videoRef.current;
|
|
67
|
-
if (!video) return;
|
|
68
|
-
|
|
69
|
-
// Check if video is already loaded (readyState >= HAVE_FUTURE_DATA)
|
|
70
|
-
if (video.readyState >= 3) {
|
|
71
|
-
setIsLoading(false);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const hideLoader = () => setIsLoading(false);
|
|
76
|
-
|
|
77
|
-
// Listen to multiple events for better browser support
|
|
78
|
-
video.addEventListener('canplay', hideLoader);
|
|
79
|
-
video.addEventListener('loadeddata', hideLoader);
|
|
80
|
-
video.addEventListener('playing', hideLoader);
|
|
81
|
-
|
|
82
|
-
// Fallback: hide loader after timeout even if video fails (shows poster instead)
|
|
83
|
-
const timeout = setTimeout(hideLoader, preloaderTimeout);
|
|
84
|
-
|
|
85
|
-
return () => {
|
|
86
|
-
video.removeEventListener('canplay', hideLoader);
|
|
87
|
-
video.removeEventListener('loadeddata', hideLoader);
|
|
88
|
-
video.removeEventListener('playing', hideLoader);
|
|
89
|
-
clearTimeout(timeout);
|
|
90
|
-
};
|
|
91
|
-
}, [showPreloader, preloaderTimeout]);
|
|
92
|
-
|
|
93
|
-
const handleContextMenu = (e: React.MouseEvent) => {
|
|
94
|
-
if (disableContextMenu) {
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<div className={cn('relative overflow-hidden', className)}>
|
|
101
|
-
<AspectRatio ratio={aspectRatio}>
|
|
102
|
-
{/* Preloader */}
|
|
103
|
-
{showPreloader && isLoading && (
|
|
104
|
-
<div
|
|
105
|
-
className={cn(
|
|
106
|
-
'absolute inset-0 flex items-center justify-center bg-muted/30 backdrop-blur-sm z-10',
|
|
107
|
-
preloaderClassName
|
|
108
|
-
)}
|
|
109
|
-
>
|
|
110
|
-
<Preloader size="lg" spinnerClassName="text-white" />
|
|
111
|
-
</div>
|
|
112
|
-
)}
|
|
113
|
-
|
|
114
|
-
{/* Video */}
|
|
115
|
-
<video
|
|
116
|
-
ref={videoRef}
|
|
117
|
-
className={cn('w-full h-full object-cover', videoClassName)}
|
|
118
|
-
autoPlay={autoPlay}
|
|
119
|
-
muted={muted}
|
|
120
|
-
loop={loop}
|
|
121
|
-
playsInline={playsInline}
|
|
122
|
-
preload={preload}
|
|
123
|
-
controls={controls}
|
|
124
|
-
poster={poster}
|
|
125
|
-
onContextMenu={handleContextMenu}
|
|
126
|
-
onLoadStart={onLoadStart}
|
|
127
|
-
onCanPlay={onCanPlay}
|
|
128
|
-
onPlaying={onPlaying}
|
|
129
|
-
onEnded={onEnded}
|
|
130
|
-
onError={onError}
|
|
131
|
-
>
|
|
132
|
-
<source src={src} type="video/mp4" />
|
|
133
|
-
Your browser does not support the video tag.
|
|
134
|
-
</video>
|
|
135
|
-
</AspectRatio>
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
NativePlayer.displayName = 'NativePlayer';
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* Professional VideoPlayer - Vidstack Implementation
|
|
4
|
-
* Supports YouTube, Vimeo, MP4, HLS and more with custom controls
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use client';
|
|
8
|
-
|
|
9
|
-
// Import Vidstack base styles
|
|
10
|
-
import '@vidstack/react/player/styles/base.css';
|
|
11
|
-
// Import default theme
|
|
12
|
-
import '@vidstack/react/player/styles/default/theme.css';
|
|
13
|
-
import '@vidstack/react/player/styles/default/layouts/video.css';
|
|
14
|
-
|
|
15
|
-
import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
|
|
16
|
-
|
|
17
|
-
import { cn, generateOgImageUrl } from '@djangocfg/ui-core/lib';
|
|
18
|
-
import { MediaPlayer, MediaProvider, Poster } from '@vidstack/react';
|
|
19
|
-
import { defaultLayoutIcons, DefaultVideoLayout } from '@vidstack/react/player/layouts/default';
|
|
20
|
-
|
|
21
|
-
import { type, VideoPlayerProps, VideoPlayerRef } from './types';
|
|
22
|
-
|
|
23
|
-
import type { MediaPlayerInstance } from '@vidstack/react';
|
|
24
|
-
/**
|
|
25
|
-
* Custom error class for invalid video URLs
|
|
26
|
-
*/
|
|
27
|
-
export class VideoUrlError extends Error {
|
|
28
|
-
constructor(
|
|
29
|
-
message: string,
|
|
30
|
-
public readonly url: string,
|
|
31
|
-
public readonly suggestion?: string
|
|
32
|
-
) {
|
|
33
|
-
super(message);
|
|
34
|
-
this.name = 'VideoUrlError';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Validates and normalizes video URL for Vidstack
|
|
40
|
-
* @throws {VideoUrlError} If URL format is invalid for the detected provider
|
|
41
|
-
*/
|
|
42
|
-
function normalizeVideoUrl(url: string): string {
|
|
43
|
-
if (!url || typeof url !== 'string') {
|
|
44
|
-
throw new VideoUrlError('Video URL is required', url || '');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const trimmedUrl = url.trim();
|
|
48
|
-
|
|
49
|
-
// Already in correct format (youtube/ID, vimeo/ID, or direct URL)
|
|
50
|
-
if (trimmedUrl.startsWith('youtube/') || trimmedUrl.startsWith('vimeo/')) {
|
|
51
|
-
return trimmedUrl;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// YouTube URL patterns - auto-convert to youtube/ID format
|
|
55
|
-
const youtubePatterns = [
|
|
56
|
-
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
|
|
57
|
-
/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
for (const pattern of youtubePatterns) {
|
|
61
|
-
const match = trimmedUrl.match(pattern);
|
|
62
|
-
if (match) {
|
|
63
|
-
// Auto-convert YouTube URL to youtube/ID format
|
|
64
|
-
const videoId = match[1];
|
|
65
|
-
return `youtube/${videoId}`;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Vimeo URL patterns - auto-convert to vimeo/ID format
|
|
70
|
-
const vimeoPattern = /vimeo\.com\/(\d+)/;
|
|
71
|
-
const vimeoMatch = trimmedUrl.match(vimeoPattern);
|
|
72
|
-
if (vimeoMatch) {
|
|
73
|
-
const videoId = vimeoMatch[1];
|
|
74
|
-
return `vimeo/${videoId}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Direct video URLs (mp4, webm, etc.) - allow as-is
|
|
78
|
-
if (/\.(mp4|webm|ogg|m3u8|mpd)(\?.*)?$/i.test(trimmedUrl)) {
|
|
79
|
-
return trimmedUrl;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// HLS/DASH streams
|
|
83
|
-
if (trimmedUrl.includes('.m3u8') || trimmedUrl.includes('.mpd')) {
|
|
84
|
-
return trimmedUrl;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Unknown format - return as-is
|
|
88
|
-
return trimmedUrl;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
|
92
|
-
source,
|
|
93
|
-
aspectRatio = 16 / 9,
|
|
94
|
-
autoplay = false,
|
|
95
|
-
muted = false,
|
|
96
|
-
playsInline = true,
|
|
97
|
-
controls = true,
|
|
98
|
-
className,
|
|
99
|
-
showInfo = false,
|
|
100
|
-
theme = 'default',
|
|
101
|
-
onPlay,
|
|
102
|
-
onPause,
|
|
103
|
-
onEnded,
|
|
104
|
-
onError,
|
|
105
|
-
}, ref) => {
|
|
106
|
-
const playerRef = useRef<MediaPlayerInstance | null>(null);
|
|
107
|
-
|
|
108
|
-
// Auto-generate poster if not provided (uses OG Image API)
|
|
109
|
-
const posterUrl = useMemo(() => {
|
|
110
|
-
if (source.poster) return source.poster;
|
|
111
|
-
if (!source.title) return undefined;
|
|
112
|
-
return generateOgImageUrl({ title: source.title, description: source.description });
|
|
113
|
-
}, [source.poster, source.title, source.description]);
|
|
114
|
-
|
|
115
|
-
// Validate and normalize URL - throws VideoUrlError if invalid
|
|
116
|
-
const normalizedUrl = useMemo(() => {
|
|
117
|
-
try {
|
|
118
|
-
return normalizeVideoUrl(source.url);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
if (error instanceof VideoUrlError) {
|
|
121
|
-
// Call onError callback and re-throw
|
|
122
|
-
onError?.(error.message + (error.suggestion ? ` Use: "${error.suggestion}"` : ''));
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
}, [source.url, onError]);
|
|
128
|
-
|
|
129
|
-
// Expose player methods via ref
|
|
130
|
-
useImperativeHandle(ref, () => {
|
|
131
|
-
const player = playerRef.current;
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
play: () => player?.play(),
|
|
135
|
-
pause: () => player?.pause(),
|
|
136
|
-
togglePlay: () => {
|
|
137
|
-
if (player) {
|
|
138
|
-
player.paused ? player.play() : player.pause();
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
seekTo: (time: number) => {
|
|
142
|
-
if (player) player.currentTime = time;
|
|
143
|
-
},
|
|
144
|
-
setVolume: (volume: number) => {
|
|
145
|
-
if (player) player.volume = Math.max(0, Math.min(1, volume));
|
|
146
|
-
},
|
|
147
|
-
toggleMute: () => {
|
|
148
|
-
if (player) player.muted = !player.muted;
|
|
149
|
-
},
|
|
150
|
-
enterFullscreen: () => player?.enterFullscreen(),
|
|
151
|
-
exitFullscreen: () => player?.exitFullscreen(),
|
|
152
|
-
};
|
|
153
|
-
}, []);
|
|
154
|
-
|
|
155
|
-
const handlePlay = () => {
|
|
156
|
-
onPlay?.();
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const handlePause = () => {
|
|
160
|
-
onPause?.();
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const handleEnded = () => {
|
|
164
|
-
onEnded?.();
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const handleError = (detail: any) => {
|
|
168
|
-
onError?.(detail?.message || 'Video playback error');
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
<div className={cn("w-full", className)}>
|
|
173
|
-
{/* Video Player */}
|
|
174
|
-
<div
|
|
175
|
-
className={cn(
|
|
176
|
-
"relative w-full rounded-sm bg-black overflow-hidden",
|
|
177
|
-
theme === 'minimal' && "rounded-none",
|
|
178
|
-
theme === 'modern' && "rounded-xl shadow-2xl"
|
|
179
|
-
)}
|
|
180
|
-
style={{ aspectRatio: aspectRatio }}
|
|
181
|
-
>
|
|
182
|
-
<MediaPlayer
|
|
183
|
-
ref={playerRef}
|
|
184
|
-
title={source.title || 'Video'}
|
|
185
|
-
src={normalizedUrl}
|
|
186
|
-
autoPlay={autoplay}
|
|
187
|
-
muted={muted}
|
|
188
|
-
playsInline={playsInline}
|
|
189
|
-
onPlay={handlePlay}
|
|
190
|
-
onPause={handlePause}
|
|
191
|
-
onEnded={handleEnded}
|
|
192
|
-
onError={handleError}
|
|
193
|
-
className="w-full h-full"
|
|
194
|
-
>
|
|
195
|
-
<MediaProvider />
|
|
196
|
-
|
|
197
|
-
{/* Poster with proper aspect ratio handling */}
|
|
198
|
-
{posterUrl && (
|
|
199
|
-
<Poster
|
|
200
|
-
className="vds-poster"
|
|
201
|
-
src={posterUrl}
|
|
202
|
-
alt={source.title || 'Video poster'}
|
|
203
|
-
style={{ objectFit: 'cover' }}
|
|
204
|
-
/>
|
|
205
|
-
)}
|
|
206
|
-
|
|
207
|
-
{/* Use Vidstack's built-in default layout */}
|
|
208
|
-
{controls && (
|
|
209
|
-
<DefaultVideoLayout
|
|
210
|
-
icons={defaultLayoutIcons}
|
|
211
|
-
thumbnails={posterUrl}
|
|
212
|
-
/>
|
|
213
|
-
)}
|
|
214
|
-
</MediaPlayer>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
{/* Video Info */}
|
|
218
|
-
{showInfo && source.title && (
|
|
219
|
-
<div className="mt-4 space-y-2">
|
|
220
|
-
<h3 className="text-xl font-semibold text-foreground">{source.title}</h3>
|
|
221
|
-
{source.description && (
|
|
222
|
-
<p className="text-muted-foreground">{source.description}</p>
|
|
223
|
-
)}
|
|
224
|
-
</div>
|
|
225
|
-
)}
|
|
226
|
-
</div>
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
VideoPlayer.displayName = 'VideoPlayer';
|
|
231
|
-
|