@djangocfg/ui-nextjs 2.1.64 → 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
|
@@ -1,239 +1,248 @@
|
|
|
1
|
-
# VideoPlayer
|
|
1
|
+
# VideoPlayer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Unified video player component supporting multiple modes and source types.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Modes
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- ✅ **Themes**: Default, minimal, and modern themes
|
|
13
|
-
- ✅ **No recommendations**: Clean playback without YouTube distractions
|
|
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 |
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
Mode is auto-detected from source type, or can be forced via `mode` prop.
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
## Installation
|
|
18
16
|
|
|
19
|
-
```
|
|
20
|
-
|
|
17
|
+
```tsx
|
|
18
|
+
import {
|
|
19
|
+
VideoPlayer,
|
|
20
|
+
VideoPlayerProvider,
|
|
21
|
+
VideoErrorFallback,
|
|
22
|
+
resolveFileSource
|
|
23
|
+
} from '@djangocfg/ui-nextjs';
|
|
21
24
|
```
|
|
22
25
|
|
|
23
26
|
## Basic Usage
|
|
24
27
|
|
|
28
|
+
### YouTube / Vimeo
|
|
29
|
+
|
|
25
30
|
```tsx
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
function MyComponent() {
|
|
29
|
-
return (
|
|
30
|
-
<VideoPlayer
|
|
31
|
-
source={{
|
|
32
|
-
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
33
|
-
title: 'Never Gonna Give You Up',
|
|
34
|
-
description: 'Rick Astley - Never Gonna Give You Up (Official Video)'
|
|
35
|
-
}}
|
|
36
|
-
autoplay={false}
|
|
37
|
-
controls={true}
|
|
38
|
-
className="max-w-4xl mx-auto"
|
|
39
|
-
/>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
31
|
+
<VideoPlayer source={{ type: 'youtube', id: 'dQw4w9WgXcQ' }} />
|
|
32
|
+
<VideoPlayer source={{ type: 'vimeo', id: '123456789' }} />
|
|
42
33
|
```
|
|
43
34
|
|
|
44
|
-
|
|
35
|
+
### HLS / DASH Streaming
|
|
45
36
|
|
|
46
37
|
```tsx
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
function AdvancedPlayer() {
|
|
51
|
-
const playerRef = useRef<VideoPlayerRef>(null);
|
|
52
|
-
|
|
53
|
-
const handleCustomPlay = () => {
|
|
54
|
-
playerRef.current?.play();
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<VideoPlayer
|
|
59
|
-
ref={playerRef}
|
|
60
|
-
source={{
|
|
61
|
-
url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
|
62
|
-
title: 'Big Buck Bunny',
|
|
63
|
-
poster: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg',
|
|
64
|
-
duration: 596
|
|
65
|
-
}}
|
|
66
|
-
theme="modern"
|
|
67
|
-
autoplay={false}
|
|
68
|
-
muted={false}
|
|
69
|
-
playsInline={true}
|
|
70
|
-
showInfo={true}
|
|
71
|
-
onPlay={() => console.log('Video started')}
|
|
72
|
-
onPause={() => console.log('Video paused')}
|
|
73
|
-
onEnded={() => console.log('Video ended')}
|
|
74
|
-
onError={(error) => console.error('Video error:', error)}
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
38
|
+
<VideoPlayer source={{ type: 'hls', url: 'https://example.com/video.m3u8' }} />
|
|
39
|
+
<VideoPlayer source={{ type: 'dash', url: 'https://example.com/video.mpd' }} />
|
|
78
40
|
```
|
|
79
41
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
### YouTube
|
|
83
|
-
- **URL Format**: `https://www.youtube.com/watch?v=VIDEO_ID` or `youtube/VIDEO_ID`
|
|
84
|
-
- **Auto-conversion**: Full YouTube URLs are automatically converted to `youtube/ID` format
|
|
85
|
-
- **Poster**: ⚠️ YouTube iframe ignores custom poster images and always shows YouTube's thumbnail
|
|
86
|
-
- **Examples**:
|
|
87
|
-
```tsx
|
|
88
|
-
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
|
89
|
-
url: 'https://youtu.be/dQw4w9WgXcQ'
|
|
90
|
-
url: 'youtube/dQw4w9WgXcQ'
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Vimeo
|
|
94
|
-
- **URL Format**: `https://vimeo.com/VIDEO_ID` or `vimeo/VIDEO_ID`
|
|
95
|
-
- **Auto-conversion**: Full Vimeo URLs are automatically converted to `vimeo/ID` format
|
|
96
|
-
- **Poster**: ⚠️ Vimeo may ignore custom poster and use their own thumbnail
|
|
97
|
-
- **Example**: `url: 'vimeo/76979871'`
|
|
98
|
-
|
|
99
|
-
### Direct Video Files (MP4, WebM, OGG)
|
|
100
|
-
- **Poster**: ✅ **Works perfectly!** Custom poster images are fully supported
|
|
101
|
-
- **Examples**:
|
|
102
|
-
```tsx
|
|
103
|
-
url: 'https://example.com/video.mp4',
|
|
104
|
-
poster: '/images/video-poster.jpg' // This works!
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### HLS Streams
|
|
108
|
-
- **Poster**: ✅ Custom poster supported
|
|
109
|
-
- **Example**: `url: 'https://example.com/stream.m3u8'`
|
|
110
|
-
|
|
111
|
-
### DASH Streams
|
|
112
|
-
- **Poster**: ✅ Custom poster supported
|
|
113
|
-
- **Example**: `url: 'https://example.com/stream.mpd'`
|
|
114
|
-
|
|
115
|
-
> **Note**: The `poster` prop works for direct video files, HLS, and DASH streams. For YouTube and Vimeo, the platform's own thumbnail is displayed regardless of the `poster` prop due to iframe limitations.
|
|
116
|
-
|
|
117
|
-
### YouTube
|
|
42
|
+
### Direct URL
|
|
43
|
+
|
|
118
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)
|
|
119
52
|
<VideoPlayer
|
|
120
53
|
source={{
|
|
121
|
-
|
|
122
|
-
|
|
54
|
+
type: 'stream',
|
|
55
|
+
sessionId: 'abc123',
|
|
56
|
+
path: '/videos/movie.mp4',
|
|
57
|
+
getStreamUrl: (id, path) => `/api/stream/${id}?path=${path}&token=${token}`
|
|
123
58
|
}}
|
|
124
59
|
/>
|
|
60
|
+
|
|
61
|
+
// Simplified source (with context)
|
|
62
|
+
<VideoPlayerProvider sessionId={sessionId} getStreamUrl={getStreamUrl}>
|
|
63
|
+
<VideoPlayer source={{ type: 'stream', path: '/videos/movie.mp4' }} />
|
|
64
|
+
</VideoPlayerProvider>
|
|
125
65
|
```
|
|
126
66
|
|
|
127
|
-
###
|
|
67
|
+
### Blob / ArrayBuffer
|
|
68
|
+
|
|
128
69
|
```tsx
|
|
129
70
|
<VideoPlayer
|
|
130
71
|
source={{
|
|
131
|
-
|
|
132
|
-
|
|
72
|
+
type: 'blob',
|
|
73
|
+
data: arrayBuffer,
|
|
74
|
+
mimeType: 'video/mp4'
|
|
133
75
|
}}
|
|
134
76
|
/>
|
|
135
77
|
```
|
|
136
78
|
|
|
137
|
-
|
|
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
|
+
|
|
138
129
|
```tsx
|
|
139
130
|
<VideoPlayer
|
|
140
|
-
source={
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
131
|
+
source={source}
|
|
132
|
+
errorFallback={(props) => (
|
|
133
|
+
<div>
|
|
134
|
+
<p>Error: {props.error}</p>
|
|
135
|
+
<button onClick={props.retry}>Retry</button>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
145
138
|
/>
|
|
146
139
|
```
|
|
147
140
|
|
|
148
|
-
###
|
|
141
|
+
### Pre-built Error Fallback with Download
|
|
142
|
+
|
|
149
143
|
```tsx
|
|
144
|
+
import { VideoErrorFallback } from '@djangocfg/ui-nextjs';
|
|
145
|
+
|
|
150
146
|
<VideoPlayer
|
|
151
|
-
source={
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
source={source}
|
|
148
|
+
errorFallback={(props) => (
|
|
149
|
+
<VideoErrorFallback
|
|
150
|
+
{...props}
|
|
151
|
+
downloadUrl={getDownloadUrl()}
|
|
152
|
+
downloadFilename="video.mp4"
|
|
153
|
+
fileSize="125 MB"
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
155
156
|
/>
|
|
156
157
|
```
|
|
157
158
|
|
|
158
|
-
##
|
|
159
|
+
## Fill Mode
|
|
159
160
|
|
|
160
|
-
|
|
161
|
+
Use `aspectRatio="fill"` to fill the parent container:
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
| `muted` | `boolean` | `false` | Mute video by default |
|
|
168
|
-
| `playsInline` | `boolean` | `true` | Play inline on mobile |
|
|
169
|
-
| `controls` | `boolean` | `true` | Show custom controls |
|
|
170
|
-
| `showInfo` | `boolean` | `false` | Show video info below player |
|
|
171
|
-
| `theme` | `'default' \| 'minimal' \| 'modern'` | `'default'` | Player theme |
|
|
172
|
-
| `className` | `string` | - | Custom CSS class |
|
|
173
|
-
| `onPlay` | `() => void` | - | Play event callback |
|
|
174
|
-
| `onPause` | `() => void` | - | Pause event callback |
|
|
175
|
-
| `onEnded` | `() => void` | - | End event callback |
|
|
176
|
-
| `onError` | `(error: string) => void` | - | Error event callback |
|
|
177
|
-
|
|
178
|
-
### VideoSource
|
|
179
|
-
|
|
180
|
-
| Property | Type | Description |
|
|
181
|
-
|----------|------|-------------|
|
|
182
|
-
| `url` | `string` | Video URL (YouTube, Vimeo, MP4, HLS, etc.) |
|
|
183
|
-
| `title` | `string?` | Video title |
|
|
184
|
-
| `description` | `string?` | Video description |
|
|
185
|
-
| `poster` | `string?` | Custom poster/thumbnail URL |
|
|
186
|
-
| `duration` | `number?` | Video duration in seconds |
|
|
187
|
-
|
|
188
|
-
### VideoPlayerRef Methods
|
|
189
|
-
|
|
190
|
-
| Method | Description |
|
|
191
|
-
|--------|-------------|
|
|
192
|
-
| `play()` | Play the video |
|
|
193
|
-
| `pause()` | Pause the video |
|
|
194
|
-
| `togglePlay()` | Toggle play/pause |
|
|
195
|
-
| `seekTo(time: number)` | Seek to specific time |
|
|
196
|
-
| `setVolume(volume: number)` | Set volume (0-1) |
|
|
197
|
-
| `toggleMute()` | Toggle mute |
|
|
198
|
-
| `enterFullscreen()` | Enter fullscreen |
|
|
199
|
-
| `exitFullscreen()` | Exit fullscreen |
|
|
200
|
-
|
|
201
|
-
## Themes
|
|
202
|
-
|
|
203
|
-
### Default Theme
|
|
204
|
-
Clean, professional look with rounded corners and subtle shadows.
|
|
205
|
-
|
|
206
|
-
### Minimal Theme
|
|
207
|
-
No rounded corners, minimal styling for embedding in tight spaces.
|
|
208
|
-
|
|
209
|
-
### Modern Theme
|
|
210
|
-
Enhanced shadows and larger border radius for a contemporary look.
|
|
163
|
+
```tsx
|
|
164
|
+
<div className="absolute inset-0">
|
|
165
|
+
<VideoPlayer source={source} aspectRatio="fill" />
|
|
166
|
+
</div>
|
|
167
|
+
```
|
|
211
168
|
|
|
212
|
-
##
|
|
169
|
+
## Context Provider
|
|
213
170
|
|
|
214
|
-
|
|
171
|
+
For apps with multiple streaming videos, use the context to avoid repetition:
|
|
215
172
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
```
|
|
221
184
|
|
|
222
|
-
##
|
|
185
|
+
## File Source Helper
|
|
223
186
|
|
|
224
|
-
|
|
225
|
-
- ✅ Efficient re-renders with Vidstack's optimized state management
|
|
226
|
-
- ✅ Minimal bundle size impact
|
|
227
|
-
- ✅ Hardware-accelerated playback when available
|
|
187
|
+
For file browser integration:
|
|
228
188
|
|
|
229
|
-
|
|
189
|
+
```tsx
|
|
190
|
+
import { resolveFileSource } from '@djangocfg/ui-nextjs';
|
|
230
191
|
|
|
231
|
-
|
|
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
|
+
});
|
|
232
197
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
├── VideoPlayer.tsx # Main orchestrator
|
|
222
|
+
├── VideoPlayerContext.tsx # Context for streaming config
|
|
223
|
+
├── VideoErrorFallback.tsx # Pre-built error UI
|
|
224
|
+
├── VideoControls.tsx # Standalone controls (Vidstack)
|
|
225
|
+
├── types.ts # Type definitions
|
|
226
|
+
├── providers/
|
|
227
|
+
│ ├── VidstackProvider.tsx # YouTube, Vimeo, HLS, DASH
|
|
228
|
+
│ ├── NativeProvider.tsx # HTML5 <video>
|
|
229
|
+
│ └── StreamProvider.tsx # HTTP Range, Blob
|
|
230
|
+
└── index.ts # Exports
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Accessibility
|
|
234
|
+
|
|
235
|
+
Full accessibility support:
|
|
236
|
+
- Keyboard navigation (Space, Arrow keys, F for fullscreen)
|
|
237
|
+
- Screen reader announcements
|
|
238
|
+
- Focus indicators
|
|
239
|
+
- ARIA labels and roles
|
|
240
|
+
|
|
241
|
+
## Browser Support
|
|
239
242
|
|
|
243
|
+
- Chrome 63+
|
|
244
|
+
- Firefox 67+
|
|
245
|
+
- Safari 12+
|
|
246
|
+
- Edge 79+
|
|
247
|
+
- iOS Safari 12+
|
|
248
|
+
- Chrome Android 63+
|
|
@@ -0,0 +1,174 @@
|
|
|
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 } from '@djangocfg/ui-core/lib';
|
|
12
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
13
|
+
import { DownloadButton } from '../../components/button-download';
|
|
14
|
+
|
|
15
|
+
import type { ErrorFallbackProps } from './types';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export interface VideoErrorFallbackProps extends ErrorFallbackProps {
|
|
22
|
+
/** URL for download button (if provided, shows download button) */
|
|
23
|
+
downloadUrl?: string;
|
|
24
|
+
/** Filename for download */
|
|
25
|
+
downloadFilename?: string;
|
|
26
|
+
/** File size to display */
|
|
27
|
+
fileSize?: string;
|
|
28
|
+
/** Show retry button */
|
|
29
|
+
showRetry?: boolean;
|
|
30
|
+
/** Custom className */
|
|
31
|
+
className?: string;
|
|
32
|
+
/** Custom icon */
|
|
33
|
+
icon?: React.ReactNode;
|
|
34
|
+
/** Custom title (defaults to error message) */
|
|
35
|
+
title?: string;
|
|
36
|
+
/** Custom description */
|
|
37
|
+
description?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Component
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pre-built error fallback component for VideoPlayer
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Basic usage
|
|
49
|
+
* <VideoPlayer
|
|
50
|
+
* source={source}
|
|
51
|
+
* errorFallback={(props) => (
|
|
52
|
+
* <VideoErrorFallback
|
|
53
|
+
* {...props}
|
|
54
|
+
* downloadUrl={getDownloadUrl()}
|
|
55
|
+
* downloadFilename="video.mp4"
|
|
56
|
+
* />
|
|
57
|
+
* )}
|
|
58
|
+
* />
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // With file size
|
|
62
|
+
* <VideoErrorFallback
|
|
63
|
+
* error="Failed to load video"
|
|
64
|
+
* downloadUrl="/api/download/video.mp4"
|
|
65
|
+
* fileSize="125 MB"
|
|
66
|
+
* showRetry
|
|
67
|
+
* retry={() => reloadVideo()}
|
|
68
|
+
* />
|
|
69
|
+
*/
|
|
70
|
+
export function VideoErrorFallback({
|
|
71
|
+
error,
|
|
72
|
+
retry,
|
|
73
|
+
downloadUrl,
|
|
74
|
+
downloadFilename,
|
|
75
|
+
fileSize,
|
|
76
|
+
showRetry = true,
|
|
77
|
+
className,
|
|
78
|
+
icon,
|
|
79
|
+
title,
|
|
80
|
+
description,
|
|
81
|
+
}: VideoErrorFallbackProps) {
|
|
82
|
+
const displayTitle = title || error || 'Video cannot be previewed';
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className={cn(
|
|
87
|
+
'absolute inset-0 flex flex-col items-center justify-center gap-4 bg-black/90 text-white p-6',
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
{/* Icon */}
|
|
92
|
+
{icon || <FileVideo className="w-16 h-16 text-muted-foreground" />}
|
|
93
|
+
|
|
94
|
+
{/* Title */}
|
|
95
|
+
<p className="text-lg font-medium text-center">{displayTitle}</p>
|
|
96
|
+
|
|
97
|
+
{/* Description / File size */}
|
|
98
|
+
{(description || fileSize) && (
|
|
99
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
100
|
+
{description || fileSize}
|
|
101
|
+
</p>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{/* Actions */}
|
|
105
|
+
<div className="flex items-center gap-3 mt-2">
|
|
106
|
+
{/* Retry button */}
|
|
107
|
+
{showRetry && retry && (
|
|
108
|
+
<Button
|
|
109
|
+
variant="outline"
|
|
110
|
+
size="sm"
|
|
111
|
+
onClick={retry}
|
|
112
|
+
className="gap-2"
|
|
113
|
+
>
|
|
114
|
+
<RefreshCw className="w-4 h-4" />
|
|
115
|
+
Retry
|
|
116
|
+
</Button>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{/* Download button */}
|
|
120
|
+
{downloadUrl && (
|
|
121
|
+
<DownloadButton
|
|
122
|
+
url={downloadUrl}
|
|
123
|
+
filename={downloadFilename}
|
|
124
|
+
variant="default"
|
|
125
|
+
size="sm"
|
|
126
|
+
>
|
|
127
|
+
Download to view
|
|
128
|
+
</DownloadButton>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Factory for common use cases
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
export interface CreateVideoErrorFallbackOptions {
|
|
140
|
+
/** Function to get download URL from source */
|
|
141
|
+
getDownloadUrl?: (source: unknown) => string | undefined;
|
|
142
|
+
/** Function to get filename from source */
|
|
143
|
+
getFilename?: (source: unknown) => string | undefined;
|
|
144
|
+
/** Function to get file size from source */
|
|
145
|
+
getFileSize?: (source: unknown) => string | undefined;
|
|
146
|
+
/** Show retry button */
|
|
147
|
+
showRetry?: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Factory to create error fallback function for VideoPlayer
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* const errorFallback = createVideoErrorFallback({
|
|
155
|
+
* getDownloadUrl: (source) => source.downloadUrl,
|
|
156
|
+
* getFilename: (source) => source.filename,
|
|
157
|
+
* showRetry: true,
|
|
158
|
+
* });
|
|
159
|
+
*
|
|
160
|
+
* <VideoPlayer source={source} errorFallback={errorFallback} />
|
|
161
|
+
*/
|
|
162
|
+
export function createVideoErrorFallback(
|
|
163
|
+
options: CreateVideoErrorFallbackOptions
|
|
164
|
+
): (props: ErrorFallbackProps, source?: unknown) => React.ReactNode {
|
|
165
|
+
return (props: ErrorFallbackProps, source?: unknown) => (
|
|
166
|
+
<VideoErrorFallback
|
|
167
|
+
{...props}
|
|
168
|
+
downloadUrl={options.getDownloadUrl?.(source)}
|
|
169
|
+
downloadFilename={options.getFilename?.(source)}
|
|
170
|
+
fileSize={options.getFileSize?.(source)}
|
|
171
|
+
showRetry={options.showRetry}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|