@djangocfg/ui-tools 2.1.91
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/dist/LottiePlayer.client-LBEC2JKY.mjs +161 -0
- package/dist/LottiePlayer.client-LBEC2JKY.mjs.map +1 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs +168 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs.map +1 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs +477 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs.map +1 -0
- package/dist/Mermaid.client-SBYY364Q.cjs +483 -0
- package/dist/Mermaid.client-SBYY364Q.cjs.map +1 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs +1003 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs.map +1 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs +996 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs.map +1 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs +152 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs.map +1 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs +154 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs.map +1 -0
- package/dist/chunk-37ZI6VD4.mjs +12 -0
- package/dist/chunk-37ZI6VD4.mjs.map +1 -0
- package/dist/chunk-3HK2OE62.cjs +81 -0
- package/dist/chunk-3HK2OE62.cjs.map +1 -0
- package/dist/chunk-7DGDQVQW.cjs +591 -0
- package/dist/chunk-7DGDQVQW.cjs.map +1 -0
- package/dist/chunk-M6P2FU7L.mjs +572 -0
- package/dist/chunk-M6P2FU7L.mjs.map +1 -0
- package/dist/chunk-UQ3XI5MY.cjs +15 -0
- package/dist/chunk-UQ3XI5MY.cjs.map +1 -0
- package/dist/chunk-YFRNE2IR.mjs +79 -0
- package/dist/chunk-YFRNE2IR.mjs.map +1 -0
- package/dist/index.cjs +5042 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1591 -0
- package/dist/index.d.ts +1591 -0
- package/dist/index.mjs +4941 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +340 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/index.ts +26 -0
- package/src/stores/index.ts +9 -0
- package/src/stores/mediaCache.ts +534 -0
- package/src/tools/AudioPlayer/README.md +206 -0
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +149 -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/index.ts +22 -0
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +158 -0
- package/src/tools/AudioPlayer/context/index.ts +16 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +35 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +207 -0
- package/src/tools/AudioPlayer/index.ts +133 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +27 -0
- package/src/tools/AudioPlayer/utils/debug.ts +14 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +6 -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 +200 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +145 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +241 -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 +204 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +81 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/debug.ts +14 -0
- package/src/tools/ImageViewer/utils/index.ts +17 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +197 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +249 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +161 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +47 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +74 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +107 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +35 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +62 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +116 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +213 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +37 -0
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +219 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +89 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +97 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +148 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +35 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +96 -0
- package/src/tools/JsonForm/widgets/index.ts +14 -0
- package/src/tools/JsonTree/index.tsx +243 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +213 -0
- package/src/tools/LottiePlayer/index.tsx +56 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +164 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +82 -0
- package/src/tools/Mermaid/components/MermaidCodeViewer.tsx +95 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +103 -0
- package/src/tools/Mermaid/hooks/index.ts +4 -0
- package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +73 -0
- package/src/tools/Mermaid/hooks/useMermaidFullscreen.ts +46 -0
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +226 -0
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +29 -0
- package/src/tools/Mermaid/index.tsx +44 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +33 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +149 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +263 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +125 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +100 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +157 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +173 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +68 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +337 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +199 -0
- package/src/tools/OpenapiViewer/index.tsx +37 -0
- package/src/tools/OpenapiViewer/types.ts +151 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +208 -0
- package/src/tools/PrettyCode/index.tsx +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 +264 -0
- package/src/tools/VideoPlayer/components/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +172 -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 +12 -0
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +70 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +116 -0
- package/src/tools/VideoPlayer/index.ts +77 -0
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +284 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +505 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +400 -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/debug.ts +14 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +12 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/_shared.ts +29 -0
- package/src/tools/index.ts +172 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# VideoPlayer
|
|
2
|
+
|
|
3
|
+
Unified video player component supporting multiple modes and source types.
|
|
4
|
+
|
|
5
|
+
## Modes
|
|
6
|
+
|
|
7
|
+
| Mode | Source Types | Use Case |
|
|
8
|
+
|------|-------------|----------|
|
|
9
|
+
| `vidstack` | YouTube, Vimeo, HLS, DASH | Full-featured player with themes and controls |
|
|
10
|
+
| `native` | URL, data-url | Lightweight HTML5 player |
|
|
11
|
+
| `streaming` | stream, blob | HTTP Range streaming with auth, binary data |
|
|
12
|
+
|
|
13
|
+
Mode is auto-detected from source type, or can be forced via `mode` prop.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import {
|
|
19
|
+
VideoPlayer,
|
|
20
|
+
VideoPlayerProvider,
|
|
21
|
+
VideoErrorFallback,
|
|
22
|
+
resolveFileSource
|
|
23
|
+
} from '@djangocfg/ui-nextjs';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Basic Usage
|
|
27
|
+
|
|
28
|
+
### YouTube / Vimeo
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<VideoPlayer source={{ type: 'youtube', id: 'dQw4w9WgXcQ' }} />
|
|
32
|
+
<VideoPlayer source={{ type: 'vimeo', id: '123456789' }} />
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### HLS / DASH Streaming
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<VideoPlayer source={{ type: 'hls', url: 'https://example.com/video.m3u8' }} />
|
|
39
|
+
<VideoPlayer source={{ type: 'dash', url: 'https://example.com/video.mpd' }} />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Direct URL
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
<VideoPlayer source={{ type: 'url', url: 'https://example.com/video.mp4' }} />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### HTTP Range Streaming (with auth)
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Full source (standalone)
|
|
52
|
+
<VideoPlayer
|
|
53
|
+
source={{
|
|
54
|
+
type: 'stream',
|
|
55
|
+
sessionId: 'abc123',
|
|
56
|
+
path: '/videos/movie.mp4',
|
|
57
|
+
getStreamUrl: (id, path) => `/api/stream/${id}?path=${path}&token=${token}`
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
// Simplified source (with context)
|
|
62
|
+
<VideoPlayerProvider sessionId={sessionId} getStreamUrl={getStreamUrl}>
|
|
63
|
+
<VideoPlayer source={{ type: 'stream', path: '/videos/movie.mp4' }} />
|
|
64
|
+
</VideoPlayerProvider>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Blob / ArrayBuffer
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<VideoPlayer
|
|
71
|
+
source={{
|
|
72
|
+
type: 'blob',
|
|
73
|
+
data: arrayBuffer,
|
|
74
|
+
mimeType: 'video/mp4'
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Props
|
|
80
|
+
|
|
81
|
+
| Prop | Type | Default | Description |
|
|
82
|
+
|------|------|---------|-------------|
|
|
83
|
+
| `source` | `VideoSourceUnion` | required | Video source configuration |
|
|
84
|
+
| `mode` | `'auto' \| 'vidstack' \| 'native' \| 'streaming'` | `'auto'` | Force specific player mode |
|
|
85
|
+
| `aspectRatio` | `number \| 'auto' \| 'fill'` | `16/9` | Aspect ratio or fill parent |
|
|
86
|
+
| `autoPlay` | `boolean` | `false` | Auto-play on load |
|
|
87
|
+
| `muted` | `boolean` | `false` | Muted by default |
|
|
88
|
+
| `loop` | `boolean` | `false` | Loop playback |
|
|
89
|
+
| `controls` | `boolean` | `true` | Show player controls |
|
|
90
|
+
| `playsInline` | `boolean` | `true` | Inline playback on mobile |
|
|
91
|
+
| `preload` | `'none' \| 'metadata' \| 'auto'` | `'metadata'` | Preload strategy |
|
|
92
|
+
| `theme` | `'default' \| 'minimal' \| 'modern'` | `'default'` | Vidstack theme |
|
|
93
|
+
| `errorFallback` | `ReactNode \| (props) => ReactNode` | - | Custom error UI |
|
|
94
|
+
| `className` | `string` | - | Container className |
|
|
95
|
+
| `videoClassName` | `string` | - | Video element className |
|
|
96
|
+
|
|
97
|
+
## Events
|
|
98
|
+
|
|
99
|
+
| Event | Type | Description |
|
|
100
|
+
|-------|------|-------------|
|
|
101
|
+
| `onPlay` | `() => void` | Playback started |
|
|
102
|
+
| `onPause` | `() => void` | Playback paused |
|
|
103
|
+
| `onEnded` | `() => void` | Playback ended |
|
|
104
|
+
| `onError` | `(error: string) => void` | Error occurred |
|
|
105
|
+
| `onLoadStart` | `() => void` | Loading started |
|
|
106
|
+
| `onCanPlay` | `() => void` | Ready to play |
|
|
107
|
+
| `onTimeUpdate` | `(time: number, duration: number) => void` | Time updated |
|
|
108
|
+
|
|
109
|
+
## Ref API
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
const playerRef = useRef<VideoPlayerRef>(null);
|
|
113
|
+
|
|
114
|
+
<VideoPlayer ref={playerRef} source={source} />
|
|
115
|
+
|
|
116
|
+
// Control methods
|
|
117
|
+
playerRef.current?.play();
|
|
118
|
+
playerRef.current?.pause();
|
|
119
|
+
playerRef.current?.seek(30); // Seek to 30 seconds
|
|
120
|
+
playerRef.current?.setVolume(0.5); // 0-1
|
|
121
|
+
playerRef.current?.setMuted(true);
|
|
122
|
+
playerRef.current?.reload();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Error Handling
|
|
126
|
+
|
|
127
|
+
### Custom Error Fallback
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<VideoPlayer
|
|
131
|
+
source={source}
|
|
132
|
+
errorFallback={(props) => (
|
|
133
|
+
<div>
|
|
134
|
+
<p>Error: {props.error}</p>
|
|
135
|
+
<button onClick={props.retry}>Retry</button>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Pre-built Error Fallback with Download
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { VideoErrorFallback } from '@djangocfg/ui-nextjs';
|
|
145
|
+
|
|
146
|
+
<VideoPlayer
|
|
147
|
+
source={source}
|
|
148
|
+
errorFallback={(props) => (
|
|
149
|
+
<VideoErrorFallback
|
|
150
|
+
{...props}
|
|
151
|
+
downloadUrl={getDownloadUrl()}
|
|
152
|
+
downloadFilename="video.mp4"
|
|
153
|
+
fileSize="125 MB"
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
/>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Fill Mode
|
|
160
|
+
|
|
161
|
+
Use `aspectRatio="fill"` to fill the parent container:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
<div className="absolute inset-0">
|
|
165
|
+
<VideoPlayer source={source} aspectRatio="fill" />
|
|
166
|
+
</div>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Context Provider
|
|
170
|
+
|
|
171
|
+
For apps with multiple streaming videos, use the context to avoid repetition:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
// In layout or parent component
|
|
175
|
+
<VideoPlayerProvider
|
|
176
|
+
sessionId={sessionId}
|
|
177
|
+
getStreamUrl={(id, path) => `/api/stream/${id}?path=${path}`}
|
|
178
|
+
>
|
|
179
|
+
{/* All nested VideoPlayers use this config */}
|
|
180
|
+
<VideoPlayer source={{ type: 'stream', path: '/video1.mp4' }} />
|
|
181
|
+
<VideoPlayer source={{ type: 'stream', path: '/video2.mp4' }} />
|
|
182
|
+
</VideoPlayerProvider>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## File Source Helper
|
|
186
|
+
|
|
187
|
+
For file browser integration:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { resolveFileSource } from '@djangocfg/ui-nextjs';
|
|
191
|
+
|
|
192
|
+
const source = resolveFileSource({
|
|
193
|
+
file: { name: 'movie.mp4', path: '/videos/movie.mp4' },
|
|
194
|
+
sessionId: 'abc123',
|
|
195
|
+
getStreamUrl: (id, path) => `/api/stream/${id}?path=${path}`,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (source) {
|
|
199
|
+
return <VideoPlayer source={source} />;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Source Types Reference
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
type VideoSourceUnion =
|
|
207
|
+
| { type: 'url'; url: string; title?: string; poster?: string }
|
|
208
|
+
| { type: 'youtube'; id: string; title?: string; poster?: string }
|
|
209
|
+
| { type: 'vimeo'; id: string; title?: string; poster?: string }
|
|
210
|
+
| { type: 'hls'; url: string; title?: string; poster?: string }
|
|
211
|
+
| { type: 'dash'; url: string; title?: string; poster?: string }
|
|
212
|
+
| { type: 'stream'; sessionId: string; path: string; getStreamUrl: fn; mimeType?: string }
|
|
213
|
+
| { type: 'blob'; data: ArrayBuffer | Blob; mimeType?: string }
|
|
214
|
+
| { type: 'data-url'; data: string; mimeType?: string }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Architecture
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
VideoPlayer/
|
|
221
|
+
├── index.ts # Public API exports
|
|
222
|
+
├── types/
|
|
223
|
+
│ ├── index.ts # Type re-exports
|
|
224
|
+
│ ├── sources.ts # Source types (Url, YouTube, HLS, etc.)
|
|
225
|
+
│ ├── player.ts # Player props, ref, events
|
|
226
|
+
│ └── provider.ts # Provider & context types
|
|
227
|
+
├── hooks/
|
|
228
|
+
│ ├── index.ts
|
|
229
|
+
│ └── useVideoPositionCache.ts # Playback position caching
|
|
230
|
+
├── utils/
|
|
231
|
+
│ ├── index.ts
|
|
232
|
+
│ ├── resolvers.ts # resolvePlayerMode, isSimpleStreamSource
|
|
233
|
+
│ └── fileSource.ts # resolveFileSource helper
|
|
234
|
+
├── components/
|
|
235
|
+
│ ├── index.ts
|
|
236
|
+
│ ├── VideoPlayer.tsx # Main orchestrator
|
|
237
|
+
│ ├── VideoControls.tsx # Standalone Vidstack controls
|
|
238
|
+
│ └── VideoErrorFallback.tsx # Pre-built error UI
|
|
239
|
+
├── context/
|
|
240
|
+
│ ├── index.ts
|
|
241
|
+
│ └── VideoPlayerContext.tsx # Streaming config provider
|
|
242
|
+
└── providers/
|
|
243
|
+
├── index.ts
|
|
244
|
+
├── VidstackProvider.tsx # YouTube, Vimeo, HLS, DASH
|
|
245
|
+
├── NativeProvider.tsx # HTML5 <video>
|
|
246
|
+
└── StreamProvider.tsx # HTTP Range, Blob
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Accessibility
|
|
250
|
+
|
|
251
|
+
Full accessibility support:
|
|
252
|
+
- Keyboard navigation (Space, Arrow keys, F for fullscreen)
|
|
253
|
+
- Screen reader announcements
|
|
254
|
+
- Focus indicators
|
|
255
|
+
- ARIA labels and roles
|
|
256
|
+
|
|
257
|
+
## Browser Support
|
|
258
|
+
|
|
259
|
+
- Chrome 63+
|
|
260
|
+
- Firefox 67+
|
|
261
|
+
- Safari 12+
|
|
262
|
+
- Edge 79+
|
|
263
|
+
- iOS Safari 12+
|
|
264
|
+
- Chrome Android 63+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Video Controls for Vidstack Player
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use client';
|
|
6
|
+
|
|
7
|
+
import { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from 'lucide-react';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
11
|
+
import { useMediaRemote, useMediaStore } from '@vidstack/react';
|
|
12
|
+
|
|
13
|
+
import type { MediaPlayerInstance } from '@vidstack/react';
|
|
14
|
+
interface VideoControlsProps {
|
|
15
|
+
player: React.RefObject<MediaPlayerInstance | null>;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function VideoControls({ player, className }: VideoControlsProps) {
|
|
20
|
+
const store = useMediaStore(player);
|
|
21
|
+
const remote = useMediaRemote();
|
|
22
|
+
|
|
23
|
+
const isPaused = store.paused;
|
|
24
|
+
const isMuted = store.muted;
|
|
25
|
+
const isFullscreen = store.fullscreen;
|
|
26
|
+
const currentTime = store.currentTime;
|
|
27
|
+
const duration = store.duration;
|
|
28
|
+
const volume = store.volume;
|
|
29
|
+
|
|
30
|
+
const formatTime = (seconds: number): string => {
|
|
31
|
+
if (!seconds || seconds < 0) return '0:00';
|
|
32
|
+
const minutes = Math.floor(seconds / 60);
|
|
33
|
+
const secs = Math.floor(seconds % 60);
|
|
34
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
38
|
+
if (!duration) return;
|
|
39
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
40
|
+
const clickX = e.clientX - rect.left;
|
|
41
|
+
const percentage = clickX / rect.width;
|
|
42
|
+
const newTime = percentage * duration;
|
|
43
|
+
remote.seek(newTime);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleVolumeChange = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
47
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
48
|
+
const clickX = e.clientX - rect.left;
|
|
49
|
+
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
|
|
50
|
+
remote.changeVolume(percentage);
|
|
51
|
+
if (percentage > 0 && isMuted) {
|
|
52
|
+
remote.toggleMuted();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
"absolute inset-0 flex flex-col justify-end transition-opacity duration-300",
|
|
62
|
+
"bg-gradient-to-t from-black/80 via-black/20 to-transparent",
|
|
63
|
+
"opacity-0 group-hover:opacity-100 focus-within:opacity-100",
|
|
64
|
+
"pointer-events-none group-hover:pointer-events-auto",
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{/* Progress Bar */}
|
|
69
|
+
<div className="px-4 pb-2 pointer-events-auto">
|
|
70
|
+
<div
|
|
71
|
+
className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all group"
|
|
72
|
+
onClick={handleProgressClick}
|
|
73
|
+
>
|
|
74
|
+
<div
|
|
75
|
+
className="h-full bg-primary rounded-full transition-all relative group-hover:bg-primary/90"
|
|
76
|
+
style={{ width: `${progress}%` }}
|
|
77
|
+
>
|
|
78
|
+
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Control Bar */}
|
|
84
|
+
<div className="flex items-center gap-4 px-4 pb-4 pointer-events-auto">
|
|
85
|
+
{/* Play/Pause */}
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => remote.togglePaused()}
|
|
88
|
+
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
89
|
+
>
|
|
90
|
+
{isPaused ? <Play className="h-6 w-6" /> : <Pause className="h-6 w-6" />}
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
{/* Time */}
|
|
94
|
+
<div className="text-white text-sm font-medium">
|
|
95
|
+
{formatTime(currentTime)} / {formatTime(duration)}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="flex-1" />
|
|
99
|
+
|
|
100
|
+
{/* Volume Control */}
|
|
101
|
+
<div className="flex items-center gap-2 group/volume">
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => remote.toggleMuted()}
|
|
104
|
+
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
105
|
+
>
|
|
106
|
+
{isMuted || volume === 0 ? (
|
|
107
|
+
<VolumeX className="h-5 w-5" />
|
|
108
|
+
) : (
|
|
109
|
+
<Volume2 className="h-5 w-5" />
|
|
110
|
+
)}
|
|
111
|
+
</button>
|
|
112
|
+
|
|
113
|
+
<div
|
|
114
|
+
className="w-0 group-hover/volume:w-20 transition-all overflow-hidden"
|
|
115
|
+
>
|
|
116
|
+
<div
|
|
117
|
+
className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all"
|
|
118
|
+
onClick={handleVolumeChange}
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
className="h-full bg-white rounded-full transition-all"
|
|
122
|
+
style={{ width: `${volume * 100}%` }}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Fullscreen */}
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => isFullscreen ? remote.exitFullscreen() : remote.enterFullscreen()}
|
|
131
|
+
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
132
|
+
>
|
|
133
|
+
{isFullscreen ? <Minimize className="h-5 w-5" /> : <Maximize className="h-5 w-5" />}
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoErrorFallback - Pre-built error fallback with download button
|
|
3
|
+
* For use with VideoPlayer errorFallback prop
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { FileVideo, RefreshCw } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { cn, Button, DownloadButton } from '../../_shared';
|
|
12
|
+
|
|
13
|
+
import type { ErrorFallbackProps } from '../types';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface VideoErrorFallbackProps extends ErrorFallbackProps {
|
|
20
|
+
/** URL for download button (if provided, shows download button) */
|
|
21
|
+
downloadUrl?: string;
|
|
22
|
+
/** Filename for download */
|
|
23
|
+
downloadFilename?: string;
|
|
24
|
+
/** File size to display */
|
|
25
|
+
fileSize?: string;
|
|
26
|
+
/** Show retry button */
|
|
27
|
+
showRetry?: boolean;
|
|
28
|
+
/** Custom className */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Custom icon */
|
|
31
|
+
icon?: React.ReactNode;
|
|
32
|
+
/** Custom title (defaults to error message) */
|
|
33
|
+
title?: string;
|
|
34
|
+
/** Custom description */
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Component
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pre-built error fallback component for VideoPlayer
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Basic usage
|
|
47
|
+
* <VideoPlayer
|
|
48
|
+
* source={source}
|
|
49
|
+
* errorFallback={(props) => (
|
|
50
|
+
* <VideoErrorFallback
|
|
51
|
+
* {...props}
|
|
52
|
+
* downloadUrl={getDownloadUrl()}
|
|
53
|
+
* downloadFilename="video.mp4"
|
|
54
|
+
* />
|
|
55
|
+
* )}
|
|
56
|
+
* />
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // With file size
|
|
60
|
+
* <VideoErrorFallback
|
|
61
|
+
* error="Failed to load video"
|
|
62
|
+
* downloadUrl="/api/download/video.mp4"
|
|
63
|
+
* fileSize="125 MB"
|
|
64
|
+
* showRetry
|
|
65
|
+
* retry={() => reloadVideo()}
|
|
66
|
+
* />
|
|
67
|
+
*/
|
|
68
|
+
export function VideoErrorFallback({
|
|
69
|
+
error,
|
|
70
|
+
retry,
|
|
71
|
+
downloadUrl,
|
|
72
|
+
downloadFilename,
|
|
73
|
+
fileSize,
|
|
74
|
+
showRetry = true,
|
|
75
|
+
className,
|
|
76
|
+
icon,
|
|
77
|
+
title,
|
|
78
|
+
description,
|
|
79
|
+
}: VideoErrorFallbackProps) {
|
|
80
|
+
const displayTitle = title || error || 'Video cannot be previewed';
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
className={cn(
|
|
85
|
+
'absolute inset-0 flex flex-col items-center justify-center gap-4 bg-black/90 text-white p-6',
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{/* Icon */}
|
|
90
|
+
{icon || <FileVideo className="w-16 h-16 text-muted-foreground" />}
|
|
91
|
+
|
|
92
|
+
{/* Title */}
|
|
93
|
+
<p className="text-lg font-medium text-center">{displayTitle}</p>
|
|
94
|
+
|
|
95
|
+
{/* Description / File size */}
|
|
96
|
+
{(description || fileSize) && (
|
|
97
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
98
|
+
{description || fileSize}
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{/* Actions */}
|
|
103
|
+
<div className="flex items-center gap-3 mt-2">
|
|
104
|
+
{/* Retry button */}
|
|
105
|
+
{showRetry && retry && (
|
|
106
|
+
<Button
|
|
107
|
+
variant="outline"
|
|
108
|
+
size="sm"
|
|
109
|
+
onClick={retry}
|
|
110
|
+
className="gap-2"
|
|
111
|
+
>
|
|
112
|
+
<RefreshCw className="w-4 h-4" />
|
|
113
|
+
Retry
|
|
114
|
+
</Button>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* Download button */}
|
|
118
|
+
{downloadUrl && (
|
|
119
|
+
<DownloadButton
|
|
120
|
+
url={downloadUrl}
|
|
121
|
+
filename={downloadFilename}
|
|
122
|
+
variant="default"
|
|
123
|
+
size="sm"
|
|
124
|
+
>
|
|
125
|
+
Download to view
|
|
126
|
+
</DownloadButton>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Factory for common use cases
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
export interface CreateVideoErrorFallbackOptions {
|
|
138
|
+
/** Function to get download URL from source */
|
|
139
|
+
getDownloadUrl?: (source: unknown) => string | undefined;
|
|
140
|
+
/** Function to get filename from source */
|
|
141
|
+
getFilename?: (source: unknown) => string | undefined;
|
|
142
|
+
/** Function to get file size from source */
|
|
143
|
+
getFileSize?: (source: unknown) => string | undefined;
|
|
144
|
+
/** Show retry button */
|
|
145
|
+
showRetry?: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Factory to create error fallback function for VideoPlayer
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* const errorFallback = createVideoErrorFallback({
|
|
153
|
+
* getDownloadUrl: (source) => source.downloadUrl,
|
|
154
|
+
* getFilename: (source) => source.filename,
|
|
155
|
+
* showRetry: true,
|
|
156
|
+
* });
|
|
157
|
+
*
|
|
158
|
+
* <VideoPlayer source={source} errorFallback={errorFallback} />
|
|
159
|
+
*/
|
|
160
|
+
export function createVideoErrorFallback(
|
|
161
|
+
options: CreateVideoErrorFallbackOptions
|
|
162
|
+
): (props: ErrorFallbackProps, source?: unknown) => React.ReactNode {
|
|
163
|
+
return (props: ErrorFallbackProps, source?: unknown) => (
|
|
164
|
+
<VideoErrorFallback
|
|
165
|
+
{...props}
|
|
166
|
+
downloadUrl={options.getDownloadUrl?.(source)}
|
|
167
|
+
downloadFilename={options.getFilename?.(source)}
|
|
168
|
+
fileSize={options.getFileSize?.(source)}
|
|
169
|
+
showRetry={options.showRetry}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
}
|