@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.
Files changed (92) hide show
  1. package/package.json +13 -8
  2. package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
  3. package/src/stores/index.ts +8 -0
  4. package/src/stores/mediaCache.ts +464 -0
  5. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  6. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  7. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  8. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  9. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  10. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  11. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  12. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  13. package/src/tools/AudioPlayer/README.md +325 -0
  14. package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +200 -0
  15. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +231 -0
  16. package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +99 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  23. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  24. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +280 -0
  25. package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +64 -0
  26. package/src/tools/AudioPlayer/components/index.ts +21 -0
  27. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  28. package/src/tools/AudioPlayer/context/index.ts +11 -0
  29. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  30. package/src/tools/AudioPlayer/effects/index.ts +412 -0
  31. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  32. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  33. package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +149 -0
  34. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  35. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +201 -0
  36. package/src/tools/AudioPlayer/index.ts +139 -0
  37. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  38. package/src/tools/AudioPlayer/types/components.ts +98 -0
  39. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  40. package/src/tools/AudioPlayer/types/index.ts +35 -0
  41. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  42. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  43. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  44. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  45. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  46. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  47. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  48. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  49. package/src/tools/ImageViewer/README.md +174 -0
  50. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  51. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  52. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  53. package/src/tools/ImageViewer/components/index.ts +7 -0
  54. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  55. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  56. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  57. package/src/tools/ImageViewer/index.ts +60 -0
  58. package/src/tools/ImageViewer/types.ts +75 -0
  59. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  60. package/src/tools/ImageViewer/utils/index.ts +16 -0
  61. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  62. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  63. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  64. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  65. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  66. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  67. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  68. package/src/tools/VideoPlayer/README.md +212 -187
  69. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  70. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +174 -0
  71. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
  72. package/src/tools/VideoPlayer/components/index.ts +14 -0
  73. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  74. package/src/tools/VideoPlayer/context/index.ts +8 -0
  75. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  76. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  77. package/src/tools/VideoPlayer/index.ts +70 -9
  78. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
  79. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +401 -0
  80. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +332 -0
  81. package/src/tools/VideoPlayer/providers/index.ts +8 -0
  82. package/src/tools/VideoPlayer/types/index.ts +38 -0
  83. package/src/tools/VideoPlayer/types/player.ts +116 -0
  84. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  85. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  86. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  87. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  88. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  89. package/src/tools/index.ts +92 -4
  90. package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
  91. package/src/tools/VideoPlayer/VideoPlayer.tsx +0 -231
  92. 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,11 @@
1
+ /**
2
+ * VideoPlayer utilities - Public API
3
+ */
4
+
5
+ export {
6
+ resolvePlayerMode,
7
+ isSimpleStreamSource,
8
+ resolveStreamSource,
9
+ } from './resolvers';
10
+
11
+ export { resolveFileSource } from './fileSource';
@@ -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
+ }
@@ -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 { VideoPlayer, VideoUrlError, VideoControls, NativePlayer } from './VideoPlayer';
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
- VideoSource,
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
- NativePlayerProps,
48
- NativePlayerRef,
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
-