@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
@@ -1,239 +1,264 @@
1
- # VideoPlayer - Professional Vidstack Implementation
1
+ # VideoPlayer
2
2
 
3
- A professional, accessible video player built with Vidstack React that supports YouTube, Vimeo, MP4, HLS, and more.
3
+ Unified video player component supporting multiple modes and source types.
4
4
 
5
- ## Features
5
+ ## Modes
6
6
 
7
- - **Multi-platform support**: YouTube, Vimeo, MP4, HLS, DASH
8
- - ✅ **Custom controls**: Professional UI with hover effects
9
- - **Accessibility**: Full keyboard navigation and screen reader support
10
- - **Responsive**: Works on all screen sizes
11
- - **TypeScript**: Full type safety
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
- ## Installation
13
+ Mode is auto-detected from source type, or can be forced via `mode` prop.
16
14
 
17
- The VideoPlayer is already included in the UI package with all dependencies:
15
+ ## Installation
18
16
 
19
- ```bash
20
- pnpm add @vidstack/react@next media-icons@next
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
- import { VideoPlayer } from '@djangocfg/ui-nextjs/tools';
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
- ## Advanced Usage
35
+ ### HLS / DASH Streaming
45
36
 
46
37
  ```tsx
47
- import { VideoPlayer, VideoPlayerRef } from '@djangocfg/ui-nextjs/tools';
48
- import { useRef } from 'react';
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
- ## Supported Video Sources
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
- url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
122
- title: 'YouTube Video'
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
- ### Vimeo
67
+ ### Blob / ArrayBuffer
68
+
128
69
  ```tsx
129
70
  <VideoPlayer
130
71
  source={{
131
- url: 'https://vimeo.com/76979871',
132
- title: 'Vimeo Video'
72
+ type: 'blob',
73
+ data: arrayBuffer,
74
+ mimeType: 'video/mp4'
133
75
  }}
134
76
  />
135
77
  ```
136
78
 
137
- ### Direct MP4
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
- url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
142
- title: 'Direct MP4',
143
- poster: 'https://example.com/poster.jpg'
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
- ### HLS Stream
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
- url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
153
- title: 'HLS Stream'
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
- ## API Reference
159
+ ## Fill Mode
159
160
 
160
- ### VideoPlayerProps
161
+ Use `aspectRatio="fill"` to fill the parent container:
161
162
 
162
- | Prop | Type | Default | Description |
163
- |------|------|---------|-------------|
164
- | `source` | `VideoSource` | - | Video source configuration |
165
- | `aspectRatio` | `number` | `16/9` | Video aspect ratio |
166
- | `autoplay` | `boolean` | `false` | Auto-play video |
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
- ## Accessibility
169
+ ## Context Provider
213
170
 
214
- The VideoPlayer includes full accessibility support:
171
+ For apps with multiple streaming videos, use the context to avoid repetition:
215
172
 
216
- - ✅ Keyboard navigation (Space, Arrow keys, F for fullscreen)
217
- - Screen reader announcements
218
- - ✅ Focus indicators
219
- - ✅ ARIA labels and roles
220
- - High contrast support
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
- ## Performance
185
+ ## File Source Helper
223
186
 
224
- - Lazy loading of video content
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
- ## Browser Support
189
+ ```tsx
190
+ import { resolveFileSource } from '@djangocfg/ui-nextjs';
230
191
 
231
- Supports all modern browsers through Vidstack's comprehensive compatibility layer:
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
- - Chrome 63+
234
- - Firefox 67+
235
- - ✅ Safari 12+
236
- - ✅ Edge 79+
237
- - ✅ iOS Safari 12+
238
- - Chrome Android 63+
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
+ }