@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
|
@@ -1,239 +1,264 @@
|
|
|
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
|
+
├── 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
|
|
239
258
|
|
|
259
|
+
- Chrome 63+
|
|
260
|
+
- Firefox 67+
|
|
261
|
+
- Safari 12+
|
|
262
|
+
- Edge 79+
|
|
263
|
+
- iOS Safari 12+
|
|
264
|
+
- Chrome Android 63+
|
|
@@ -19,7 +19,7 @@ interface VideoControlsProps {
|
|
|
19
19
|
export function VideoControls({ player, className }: VideoControlsProps) {
|
|
20
20
|
const store = useMediaStore(player);
|
|
21
21
|
const remote = useMediaRemote();
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
const isPaused = store.paused;
|
|
24
24
|
const isMuted = store.muted;
|
|
25
25
|
const isFullscreen = store.fullscreen;
|
|
@@ -56,7 +56,7 @@ export function VideoControls({ player, className }: VideoControlsProps) {
|
|
|
56
56
|
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
|
-
<div
|
|
59
|
+
<div
|
|
60
60
|
className={cn(
|
|
61
61
|
"absolute inset-0 flex flex-col justify-end transition-opacity duration-300",
|
|
62
62
|
"bg-gradient-to-t from-black/80 via-black/20 to-transparent",
|
|
@@ -67,11 +67,11 @@ export function VideoControls({ player, className }: VideoControlsProps) {
|
|
|
67
67
|
>
|
|
68
68
|
{/* Progress Bar */}
|
|
69
69
|
<div className="px-4 pb-2 pointer-events-auto">
|
|
70
|
-
<div
|
|
70
|
+
<div
|
|
71
71
|
className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all group"
|
|
72
72
|
onClick={handleProgressClick}
|
|
73
73
|
>
|
|
74
|
-
<div
|
|
74
|
+
<div
|
|
75
75
|
className="h-full bg-primary rounded-full transition-all relative group-hover:bg-primary/90"
|
|
76
76
|
style={{ width: `${progress}%` }}
|
|
77
77
|
>
|
|
@@ -109,15 +109,15 @@ export function VideoControls({ player, className }: VideoControlsProps) {
|
|
|
109
109
|
<Volume2 className="h-5 w-5" />
|
|
110
110
|
)}
|
|
111
111
|
</button>
|
|
112
|
-
|
|
113
|
-
<div
|
|
112
|
+
|
|
113
|
+
<div
|
|
114
114
|
className="w-0 group-hover/volume:w-20 transition-all overflow-hidden"
|
|
115
115
|
>
|
|
116
|
-
<div
|
|
116
|
+
<div
|
|
117
117
|
className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all"
|
|
118
118
|
onClick={handleVolumeChange}
|
|
119
119
|
>
|
|
120
|
-
<div
|
|
120
|
+
<div
|
|
121
121
|
className="h-full bg-white rounded-full transition-all"
|
|
122
122
|
style={{ width: `${volume * 100}%` }}
|
|
123
123
|
/>
|
|
@@ -136,4 +136,3 @@ export function VideoControls({ player, className }: VideoControlsProps) {
|
|
|
136
136
|
</div>
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
|
-
|
|
@@ -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
|
+
}
|