@djangocfg/ui-tools 2.1.402 → 2.1.407
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/README.md +16 -1
- package/package.json +11 -9
- package/src/tools/AudioPlayer/lazy.tsx +13 -27
- package/src/tools/ImageViewer/components/ImageViewer.tsx +10 -2
- package/src/tools/JsonForm/JsonSchemaForm.tsx +3 -1
- package/src/tools/JsonForm/README.md +12 -2
- package/src/tools/JsonForm/widgets/TextareaWidget.tsx +25 -0
- package/src/tools/JsonForm/widgets/index.ts +1 -0
- package/src/tools/JsonTree/README.md +12 -0
- package/src/tools/PrettyCode/README.md +81 -0
- package/src/tools/VideoPlayer/README.md +87 -230
- package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
- package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
- package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +38 -0
- package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
- package/src/tools/VideoPlayer/canvas/index.ts +12 -0
- package/src/tools/VideoPlayer/canvas/jsx.d.ts +54 -0
- package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
- package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +39 -0
- package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +77 -0
- package/src/tools/VideoPlayer/index.ts +51 -65
- package/src/tools/VideoPlayer/lazy.tsx +11 -54
- package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
- package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
- package/src/tools/VideoPlayer/parts/index.ts +15 -0
- package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
- package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
- package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
- package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
- package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
- package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
- package/src/tools/VideoPlayer/styles/video-player.css +141 -0
- package/src/tools/VideoPlayer/types.ts +82 -0
- package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
- package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
- package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
- package/src/tools/index.ts +35 -28
- package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
- package/src/tools/VideoPlayer/components/index.ts +0 -14
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
- package/src/tools/VideoPlayer/context/index.ts +0 -8
- package/src/tools/VideoPlayer/hooks/index.ts +0 -12
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
- package/src/tools/VideoPlayer/providers/index.ts +0 -8
- package/src/tools/VideoPlayer/types/index.ts +0 -38
- package/src/tools/VideoPlayer/types/player.ts +0 -116
- package/src/tools/VideoPlayer/types/provider.ts +0 -93
- package/src/tools/VideoPlayer/types/sources.ts +0 -97
- package/src/tools/VideoPlayer/utils/debug.ts +0 -14
- package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
- package/src/tools/VideoPlayer/utils/index.ts +0 -12
- package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
|
@@ -1,264 +1,121 @@
|
|
|
1
1
|
# VideoPlayer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
3
|
+
Composable video player built on [media-chrome](https://media-chrome.org)
|
|
4
|
+
(Mux, MIT, ~12 KB). One `<MediaController>` shell drives **YouTube**,
|
|
5
|
+
**Vimeo**, **HLS**, native **MP4/WebM**, and arbitrary **iframe**
|
|
6
|
+
embeds — the source element swaps, the UI stays the same.
|
|
16
7
|
|
|
17
8
|
```tsx
|
|
18
|
-
import {
|
|
19
|
-
VideoPlayer,
|
|
20
|
-
VideoPlayerProvider,
|
|
21
|
-
VideoErrorFallback,
|
|
22
|
-
resolveFileSource
|
|
23
|
-
} from '@djangocfg/ui-nextjs';
|
|
24
|
-
```
|
|
9
|
+
import { VideoPlayer } from '@djangocfg/ui-tools/video-player';
|
|
25
10
|
|
|
26
|
-
|
|
11
|
+
// Structured source
|
|
12
|
+
<VideoPlayer source={{ type: 'youtube', videoId: 'sGbxmsDFVnE' }} />
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```tsx
|
|
31
|
-
<VideoPlayer source={{ type: 'youtube', id: 'dQw4w9WgXcQ' }} />
|
|
32
|
-
<VideoPlayer source={{ type: 'vimeo', id: '123456789' }} />
|
|
14
|
+
// Raw URL — auto-classified (YouTube / Vimeo / HLS / MP4 / iframe)
|
|
15
|
+
<VideoPlayer source="https://youtu.be/sGbxmsDFVnE?t=90" />
|
|
33
16
|
```
|
|
34
17
|
|
|
35
|
-
|
|
18
|
+
## Why media-chrome
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
20
|
+
Native HTML5 `<video>` can't play YouTube. Hand-rolling the YouTube
|
|
21
|
+
IFrame API and keeping it in sync with a custom control bar is fragile.
|
|
22
|
+
media-chrome solves both: its `<media-controller>` speaks one protocol,
|
|
23
|
+
and provider elements (`<youtube-video>`, `<vimeo-video>`,
|
|
24
|
+
`<hls-video>`) plug into the same slot. Our control parts are thin
|
|
25
|
+
React wrappers themed through CSS custom properties.
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
## Sources
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```
|
|
29
|
+
`VideoSource` is a discriminated union — pass an object, or a raw URL
|
|
30
|
+
string that `parseEmbedUrl` classifies for you.
|
|
47
31
|
|
|
48
|
-
|
|
32
|
+
| `type` | Shape | Engine |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `url` | `{ type:'url', url, mimeType?, poster?, title? }` | native `<video>` |
|
|
35
|
+
| `youtube` | `{ type:'youtube', videoId, startTime?, playlistId?, poster?, title? }` | `youtube-video-element` |
|
|
36
|
+
| `vimeo` | `{ type:'vimeo', videoId, startTime?, poster?, title? }` | `vimeo-video-element` |
|
|
37
|
+
| `hls` | `{ type:'hls', url, poster?, title? }` | `hls-video-element` (native in Safari, `hls.js` elsewhere) |
|
|
38
|
+
| `iframe` | `{ type:'iframe', url, allow?, poster?, title? }` | plain `<iframe>` — control bar auto-hidden |
|
|
49
39
|
|
|
50
40
|
```tsx
|
|
51
|
-
|
|
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
|
-
/>
|
|
41
|
+
import { parseEmbedUrl } from '@djangocfg/ui-tools/video-player';
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<VideoPlayer source={{ type: 'stream', path: '/videos/movie.mp4' }} />
|
|
64
|
-
</VideoPlayerProvider>
|
|
65
|
-
```
|
|
43
|
+
parseEmbedUrl('https://www.youtube.com/watch?v=ID&t=42s');
|
|
44
|
+
// → { type: 'youtube', videoId: 'ID', startTime: 42 }
|
|
66
45
|
|
|
67
|
-
|
|
46
|
+
parseEmbedUrl('https://vimeo.com/76979871');
|
|
47
|
+
// → { type: 'vimeo', videoId: '76979871' }
|
|
68
48
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
source={{
|
|
72
|
-
type: 'blob',
|
|
73
|
-
data: arrayBuffer,
|
|
74
|
-
mimeType: 'video/mp4'
|
|
75
|
-
}}
|
|
76
|
-
/>
|
|
49
|
+
parseEmbedUrl('https://stream.mux.com/abc.m3u8');
|
|
50
|
+
// → { type: 'hls', url: '…' }
|
|
77
51
|
```
|
|
78
52
|
|
|
79
53
|
## Props
|
|
80
54
|
|
|
81
|
-
| Prop | Type | Default |
|
|
82
|
-
|
|
83
|
-
| `source` | `
|
|
84
|
-
| `
|
|
85
|
-
| `aspectRatio` | `number \| 'auto' \| 'fill'` | `16/9` |
|
|
86
|
-
| `autoPlay` | `boolean` | `false` |
|
|
87
|
-
| `muted`
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
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
|
|
55
|
+
| Prop | Type | Default | Notes |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `source` | `VideoSource \| string` | — | Object or raw URL (auto-parsed). |
|
|
58
|
+
| `controls` | `boolean` | `true` | `false` → no built-in control bar. |
|
|
59
|
+
| `aspectRatio` | `number \| 'auto' \| 'fill'` | `16/9` | `'fill'` stretches to parent height; `'auto'` keeps intrinsic. |
|
|
60
|
+
| `autoPlay` | `boolean` | `false` | Browsers require `muted` for autoplay to start. |
|
|
61
|
+
| `muted` / `loop` / `playsInline` | `boolean` | — | Forwarded to the engine. |
|
|
62
|
+
| `preload` | `'none' \| 'metadata' \| 'auto'` | — | Native sources only. |
|
|
63
|
+
| `crossOrigin` | `'' \| 'anonymous' \| 'use-credentials'` | `'anonymous'` | Native sources only. |
|
|
64
|
+
| `className` | `string` | — | On the `<MediaController>` wrapper. |
|
|
65
|
+
| `children` | `ReactNode` | — | Replaces the default control bar entirely. |
|
|
128
66
|
|
|
129
|
-
|
|
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
|
-
```
|
|
67
|
+
## Composable layout
|
|
168
68
|
|
|
169
|
-
|
|
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:
|
|
69
|
+
Bring your own control bar by passing `children`. Compose any
|
|
70
|
+
media-chrome element or our restyled parts:
|
|
188
71
|
|
|
189
72
|
```tsx
|
|
190
|
-
import {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
73
|
+
import {
|
|
74
|
+
VideoPlayer, ControlsBar, PlayButton, SeekBar,
|
|
75
|
+
Volume, PlaybackRate, Pip, Fullscreen,
|
|
76
|
+
} from '@djangocfg/ui-tools/video-player';
|
|
77
|
+
|
|
78
|
+
<VideoPlayer source={{ type: 'url', url: '/clip.mp4' }}>
|
|
79
|
+
<ControlsBar>
|
|
80
|
+
<PlayButton />
|
|
81
|
+
<SeekBar />
|
|
82
|
+
<Volume />
|
|
83
|
+
<PlaybackRate />
|
|
84
|
+
<Pip />
|
|
85
|
+
<Fullscreen />
|
|
86
|
+
</ControlsBar>
|
|
87
|
+
</VideoPlayer>
|
|
201
88
|
```
|
|
202
89
|
|
|
203
|
-
##
|
|
90
|
+
## Parts
|
|
204
91
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
```
|
|
92
|
+
`PlayButton` · `SeekBar` · `Volume` · `PlaybackRate` · `Pip` ·
|
|
93
|
+
`Fullscreen` · `ControlsBar` · `Poster` — thin wrappers over
|
|
94
|
+
media-chrome components, restyled through semantic tokens. Each accepts
|
|
95
|
+
the props of the media-chrome element it wraps.
|
|
216
96
|
|
|
217
|
-
##
|
|
97
|
+
## Theming
|
|
218
98
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
```
|
|
99
|
+
media-chrome reads CSS custom properties; `styles/video-player.css`
|
|
100
|
+
binds them to ui-core semantic tokens (`--primary`, `--popover`, …) via
|
|
101
|
+
`color-mix`, so the player follows light/dark and any active preset.
|
|
102
|
+
The video surface itself stays dark (`bg-black`) by convention.
|
|
248
103
|
|
|
249
|
-
##
|
|
104
|
+
## Notes
|
|
250
105
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
-
|
|
254
|
-
|
|
255
|
-
-
|
|
106
|
+
- **YouTube/Vimeo branding** — embed providers enforce a minimal logo;
|
|
107
|
+
`modestbranding` / `rel=0` reduce it but it cannot be fully removed.
|
|
108
|
+
- **PiP over YouTube** — depends on the embed; the button hides itself
|
|
109
|
+
when the engine reports no support.
|
|
110
|
+
- **HLS** — native in Safari; elsewhere `hls-video-element` lazy-loads
|
|
111
|
+
`hls.js` (~110 KB) on first HLS mount only.
|
|
112
|
+
- **`LazyVideoPlayer`** — kept as a synchronous alias of `VideoPlayer`
|
|
113
|
+
for back-compat. No lazy boundary is needed; engines tree-shake per
|
|
114
|
+
source type.
|
|
256
115
|
|
|
257
|
-
##
|
|
116
|
+
## Storybook
|
|
258
117
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
- iOS Safari 12+
|
|
264
|
-
- Chrome Android 63+
|
|
118
|
+
`UI Tools / Video Player / Player` — `NativeMp4`, `YouTubeStarWars`,
|
|
119
|
+
`YouTubeStarTrek`, `YouTubeAutoParseUrl`, `YouTubeWithTimestamp`,
|
|
120
|
+
`Vimeo`, `Sintel`, `HlsStream`, `ComposableLayout`, `Fill`,
|
|
121
|
+
`NoControls`, `LazyAlias`.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VideoPlayer — composable shell over `media-chrome` web components.
|
|
5
|
+
*
|
|
6
|
+
* The shell is just a `<MediaController>` wrapper + a `<CanvasDispatcher>`
|
|
7
|
+
* that mounts the right media element (`<video>`, `<youtube-video>`,
|
|
8
|
+
* `<vimeo-video>`, `<hls-video>`, `<iframe>`) into the `media` slot.
|
|
9
|
+
*
|
|
10
|
+
* Custom layouts: pass `children` to replace the default control bar.
|
|
11
|
+
* `controls={false}` opts out of any control surface (useful for
|
|
12
|
+
* iframe sources where the embed renders its own UI).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useMemo, type CSSProperties } from 'react';
|
|
16
|
+
import { MediaController } from 'media-chrome/react';
|
|
17
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
|
+
import './styles/video-player.css';
|
|
19
|
+
|
|
20
|
+
import type { VideoPlayerProps, AspectRatioValue } from './types';
|
|
21
|
+
import { parseEmbedUrl } from './utils/parse-embed-url';
|
|
22
|
+
import { CanvasDispatcher } from './canvas/canvas-dispatcher';
|
|
23
|
+
import {
|
|
24
|
+
ControlsBar,
|
|
25
|
+
PlayButton,
|
|
26
|
+
SeekBar,
|
|
27
|
+
Volume,
|
|
28
|
+
PlaybackRate,
|
|
29
|
+
Pip,
|
|
30
|
+
Fullscreen,
|
|
31
|
+
} from './parts';
|
|
32
|
+
|
|
33
|
+
function aspectRatioStyle(ar: AspectRatioValue): CSSProperties | undefined {
|
|
34
|
+
if (ar === 'fill') return { height: '100%' };
|
|
35
|
+
if (ar === 'auto') return undefined;
|
|
36
|
+
return { aspectRatio: String(ar) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function VideoPlayer({
|
|
40
|
+
source,
|
|
41
|
+
controls = true,
|
|
42
|
+
aspectRatio = 16 / 9,
|
|
43
|
+
className,
|
|
44
|
+
children,
|
|
45
|
+
...settings
|
|
46
|
+
}: VideoPlayerProps) {
|
|
47
|
+
const normalized = useMemo(
|
|
48
|
+
() => (typeof source === 'string' ? parseEmbedUrl(source) : source),
|
|
49
|
+
[source],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const isIframe = normalized.type === 'iframe';
|
|
53
|
+
// For iframe embeds media-chrome cannot drive the inner player — hide the
|
|
54
|
+
// control bar to avoid a non-functional UI.
|
|
55
|
+
const showControls = controls && !isIframe;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<MediaController
|
|
59
|
+
// Fade controls + scrim after 2.5s of inactivity while playing;
|
|
60
|
+
// they reappear on mousemove / pause / focus (media-chrome built-in).
|
|
61
|
+
autohide="2.5"
|
|
62
|
+
className={cn(
|
|
63
|
+
'video-player relative block w-full overflow-hidden rounded-lg bg-black',
|
|
64
|
+
className,
|
|
65
|
+
)}
|
|
66
|
+
style={aspectRatioStyle(aspectRatio)}
|
|
67
|
+
>
|
|
68
|
+
<CanvasDispatcher source={normalized} {...settings} />
|
|
69
|
+
{children ??
|
|
70
|
+
(showControls && (
|
|
71
|
+
<ControlsBar>
|
|
72
|
+
<PlayButton />
|
|
73
|
+
<SeekBar />
|
|
74
|
+
<Volume iconOnly />
|
|
75
|
+
<PlaybackRate />
|
|
76
|
+
<Pip />
|
|
77
|
+
<Fullscreen />
|
|
78
|
+
</ControlsBar>
|
|
79
|
+
))}
|
|
80
|
+
</MediaController>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dispatches `source.type` to the right canvas. Single-switch indirection
|
|
5
|
+
* keeps `<VideoPlayer>` simple and isolates engine-specific imports inside
|
|
6
|
+
* the canvas files so tree-shakers can drop unused providers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { VideoSource, VideoPlayerSettings } from '../types';
|
|
10
|
+
import { NativeCanvas } from './native-canvas';
|
|
11
|
+
import { YouTubeCanvas } from './youtube-canvas';
|
|
12
|
+
import { VimeoCanvas } from './vimeo-canvas';
|
|
13
|
+
import { HlsCanvas } from './hls-canvas';
|
|
14
|
+
import { IframeCanvas } from './iframe-canvas';
|
|
15
|
+
|
|
16
|
+
export interface CanvasDispatcherProps extends VideoPlayerSettings {
|
|
17
|
+
readonly source: VideoSource;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CanvasDispatcher({ source, ...settings }: CanvasDispatcherProps) {
|
|
21
|
+
switch (source.type) {
|
|
22
|
+
case 'youtube':
|
|
23
|
+
return <YouTubeCanvas source={source} {...settings} />;
|
|
24
|
+
case 'vimeo':
|
|
25
|
+
return <VimeoCanvas source={source} {...settings} />;
|
|
26
|
+
case 'hls':
|
|
27
|
+
return <HlsCanvas source={source} {...settings} />;
|
|
28
|
+
case 'iframe':
|
|
29
|
+
return <IframeCanvas source={source} />;
|
|
30
|
+
case 'url':
|
|
31
|
+
default:
|
|
32
|
+
return <NativeCanvas source={source} {...settings} />;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<hls-video slot="media">` — HLS (`.m3u8`) playback via
|
|
5
|
+
* `hls-video-element`, which lazy-loads `hls.js` only on browsers that
|
|
6
|
+
* don't ship native HLS (everything except Safari).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import 'hls-video-element';
|
|
10
|
+
import { MediaPosterImage } from 'media-chrome/react';
|
|
11
|
+
import type { HlsSource, VideoPlayerSettings } from '../types';
|
|
12
|
+
|
|
13
|
+
export interface HlsCanvasProps extends VideoPlayerSettings {
|
|
14
|
+
readonly source: HlsSource;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function HlsCanvas({
|
|
18
|
+
source,
|
|
19
|
+
autoPlay,
|
|
20
|
+
muted,
|
|
21
|
+
loop,
|
|
22
|
+
crossOrigin = 'anonymous',
|
|
23
|
+
}: HlsCanvasProps) {
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<hls-video
|
|
27
|
+
slot="media"
|
|
28
|
+
src={source.url}
|
|
29
|
+
crossOrigin={crossOrigin}
|
|
30
|
+
{...(muted && { muted: true })}
|
|
31
|
+
{...(autoPlay && { autoplay: true })}
|
|
32
|
+
{...(loop && { loop: true })}
|
|
33
|
+
playsInline
|
|
34
|
+
/>
|
|
35
|
+
{source.poster && <MediaPosterImage slot="poster" src={source.poster} />}
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fallback `<iframe>` for arbitrary embed URLs that don't fit a known
|
|
5
|
+
* provider. Rendered *outside* the media-chrome controls (no
|
|
6
|
+
* `slot="media"`) because there is no JS bridge between the iframe
|
|
7
|
+
* contents and `<MediaController>`. Consumers should pass
|
|
8
|
+
* `controls={false}` for iframe sources, or live with a non-functional
|
|
9
|
+
* control bar — we hide it via CSS in `VideoPlayer.tsx`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { IframeSource } from '../types';
|
|
13
|
+
|
|
14
|
+
export interface IframeCanvasProps {
|
|
15
|
+
readonly source: IframeSource;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_ALLOW =
|
|
19
|
+
'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
|
|
20
|
+
|
|
21
|
+
export function IframeCanvas({ source }: IframeCanvasProps) {
|
|
22
|
+
return (
|
|
23
|
+
<iframe
|
|
24
|
+
slot="media"
|
|
25
|
+
src={source.url}
|
|
26
|
+
title={source.title ?? 'Embedded video'}
|
|
27
|
+
allow={source.allow ?? DEFAULT_ALLOW}
|
|
28
|
+
allowFullScreen
|
|
29
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
30
|
+
className="absolute inset-0 h-full w-full border-0"
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { CanvasDispatcher } from './canvas-dispatcher';
|
|
2
|
+
export type { CanvasDispatcherProps } from './canvas-dispatcher';
|
|
3
|
+
export { NativeCanvas } from './native-canvas';
|
|
4
|
+
export type { NativeCanvasProps } from './native-canvas';
|
|
5
|
+
export { YouTubeCanvas } from './youtube-canvas';
|
|
6
|
+
export type { YouTubeCanvasProps } from './youtube-canvas';
|
|
7
|
+
export { VimeoCanvas } from './vimeo-canvas';
|
|
8
|
+
export type { VimeoCanvasProps } from './vimeo-canvas';
|
|
9
|
+
export { HlsCanvas } from './hls-canvas';
|
|
10
|
+
export type { HlsCanvasProps } from './hls-canvas';
|
|
11
|
+
export { IframeCanvas } from './iframe-canvas';
|
|
12
|
+
export type { IframeCanvasProps } from './iframe-canvas';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX intrinsic-element declarations for the custom-element wrappers
|
|
3
|
+
* shipped by `youtube-video-element`, `vimeo-video-element`,
|
|
4
|
+
* `hls-video-element`.
|
|
5
|
+
*
|
|
6
|
+
* media-chrome relies on `slot="media"` to attach a media element to
|
|
7
|
+
* its `<MediaController>`. We declare the bare minimum prop surface
|
|
8
|
+
* we use — `src`, `slot`, `autoplay`, `muted`, `loop`, `playsinline`,
|
|
9
|
+
* `crossorigin`, `preload`, `poster`.
|
|
10
|
+
*
|
|
11
|
+
* All custom elements are HTMLElement subclasses with `HTMLVideoElement`
|
|
12
|
+
* -shaped APIs, so we type their JSX props as a partial of
|
|
13
|
+
* `HTMLVideoElementAttributes` + `slot`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { DetailedHTMLProps, HTMLAttributes, VideoHTMLAttributes } from 'react';
|
|
17
|
+
|
|
18
|
+
type VideoLikeElement = DetailedHTMLProps<
|
|
19
|
+
VideoHTMLAttributes<HTMLElement> & HTMLAttributes<HTMLElement>,
|
|
20
|
+
HTMLElement
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `youtube-video-element` accepts a `config` property — an object merged
|
|
25
|
+
* into the YouTube IFrame `playerVars` (it is JSON-serialized into a
|
|
26
|
+
* `data-config` attribute internally). Used to suppress native chrome.
|
|
27
|
+
*/
|
|
28
|
+
type YouTubeVideoElement = VideoLikeElement & {
|
|
29
|
+
config?: Record<string, string | number | boolean>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
declare module 'react' {
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
34
|
+
namespace JSX {
|
|
35
|
+
interface IntrinsicElements {
|
|
36
|
+
'youtube-video': YouTubeVideoElement;
|
|
37
|
+
'vimeo-video': VideoLikeElement;
|
|
38
|
+
'hls-video': VideoLikeElement;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare global {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
45
|
+
namespace JSX {
|
|
46
|
+
interface IntrinsicElements {
|
|
47
|
+
'youtube-video': YouTubeVideoElement;
|
|
48
|
+
'vimeo-video': VideoLikeElement;
|
|
49
|
+
'hls-video': VideoLikeElement;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native `<video slot="media">` for direct MP4 / WebM / blob / data URLs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { MediaPosterImage } from 'media-chrome/react';
|
|
8
|
+
import type { UrlSource, VideoPlayerSettings } from '../types';
|
|
9
|
+
|
|
10
|
+
export interface NativeCanvasProps extends VideoPlayerSettings {
|
|
11
|
+
readonly source: UrlSource;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function NativeCanvas({
|
|
15
|
+
source,
|
|
16
|
+
autoPlay,
|
|
17
|
+
muted,
|
|
18
|
+
loop,
|
|
19
|
+
playsInline = true,
|
|
20
|
+
crossOrigin = 'anonymous',
|
|
21
|
+
preload = 'metadata',
|
|
22
|
+
}: NativeCanvasProps) {
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<video
|
|
26
|
+
slot="media"
|
|
27
|
+
src={source.url}
|
|
28
|
+
muted={muted}
|
|
29
|
+
loop={loop}
|
|
30
|
+
autoPlay={autoPlay}
|
|
31
|
+
playsInline={playsInline}
|
|
32
|
+
crossOrigin={crossOrigin}
|
|
33
|
+
preload={preload}
|
|
34
|
+
/>
|
|
35
|
+
{source.poster && <MediaPosterImage slot="poster" src={source.poster} />}
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|