@djangocfg/ui-tools 2.1.404 → 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 +2 -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/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
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Sixteen tools, each one lazy-loaded so it doesn't ship until used. Bundle size i
|
|
|
32
32
|
| `LottiePlayer` | ~200KB | Lottie animation player |
|
|
33
33
|
| `Chat` | ~150KB | Streaming chat (SSE + tool calls + attachments). [README](src/tools/Chat/README.md) |
|
|
34
34
|
| `SpeechRecognition` | ~40KB | Mic capture + STT with pluggable engines (Web Speech / HTTP / WS). [README](src/tools/SpeechRecognition/README.md) |
|
|
35
|
-
| `VideoPlayer` | ~
|
|
35
|
+
| `VideoPlayer` | ~12KB core | media-chrome player — YouTube / Vimeo / HLS / MP4 in one composable shell. [README](src/tools/VideoPlayer/README.md) |
|
|
36
36
|
| `MarkdownMessage` | ~120KB | Read-only chat-tuned markdown. **SSR-safe** — use as a Client Component, the result is server-rendered. [README](src/components/markdown/MarkdownMessage/README.md) |
|
|
37
37
|
| `JsonTree` | ~100KB | JSON visualization (full/compact/inline modes) |
|
|
38
38
|
| `AudioPlayer` | ~80KB | WebView-safe waveform player |
|
|
@@ -78,6 +78,7 @@ Subpaths come in three flavors:
|
|
|
78
78
|
| `@djangocfg/ui-tools/markdown-editor` | `LazyMarkdownEditor`, `mentionPresets`, types | TipTap + ProseMirror (~200 KB) only loads via the lazy wrapper. |
|
|
79
79
|
| `@djangocfg/ui-tools/map` | `LazyMapContainer`, `LazyMapView`, plus light primitives (`MapMarker`, `MapPopup`, `MapCluster`, `MapSource`, `MapLayer`, `MapControls`, `MapProvider`, types) | The heavy MapLibre GL chunk (~800 KB) only loads when `LazyMapContainer` actually mounts. Markers and popups are thin `react-map-gl` wrappers — exported synchronously. |
|
|
80
80
|
| `@djangocfg/ui-tools/markdown-message` | `MarkdownMessage`, `ChatMessageRow`, `ActionRow`, `extractTextFromChildren`, types | **SSR-safe.** The component itself is `'use client'`, but rendering produces plain HTML — Next.js will pre-render it on the server when imported from a Client Component. Use this when you want the markdown renderer without dragging in the full chat. |
|
|
81
|
+
| `@djangocfg/ui-tools/video-player` | `VideoPlayer` (+ `LazyVideoPlayer` alias), `parseEmbedUrl`, composable parts (`PlayButton`, `SeekBar`, `Volume`, `ControlsBar`, …), per-engine canvases, types | media-chrome shell — YouTube / Vimeo / HLS / MP4 / iframe behind one API. Provider engines (`youtube-video-element`, `hls-video-element`, …) are imported only by the matching canvas, so unused engines tree-shake. [README](src/tools/VideoPlayer/README.md) |
|
|
81
82
|
| `@djangocfg/ui-tools/<tool-name>` | One tool | `mermaid`, `speech-recognition`, `json-tree`, `pretty-code`, `openapi-viewer`, `json-form`, `lottie-player`, `video-player`, `image-viewer`, `cron-scheduler`, `gallery`, `tour`, `tree`, `file-icon`, `upload` |
|
|
82
83
|
| `@djangocfg/ui-tools/json-form/full` | `JsonSchemaForm`, `ObjectFieldTemplate`, `evaluateDisabledWhen`, all widgets / templates / utils | Eager bundle. Use only for storybook / internal tooling that needs the template + util APIs at module scope. Production code should import `LazyJsonSchemaForm` from `/json-form` instead. |
|
|
83
84
|
| `@djangocfg/ui-tools/styles` | Tailwind source CSS | |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.407",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -159,8 +159,8 @@
|
|
|
159
159
|
"test:watch": "vitest"
|
|
160
160
|
},
|
|
161
161
|
"peerDependencies": {
|
|
162
|
-
"@djangocfg/i18n": "^2.1.
|
|
163
|
-
"@djangocfg/ui-core": "^2.1.
|
|
162
|
+
"@djangocfg/i18n": "^2.1.407",
|
|
163
|
+
"@djangocfg/ui-core": "^2.1.407",
|
|
164
164
|
"consola": "^3.4.2",
|
|
165
165
|
"lodash-es": "^4.18.1",
|
|
166
166
|
"lucide-react": "^0.545.0",
|
|
@@ -183,9 +183,10 @@
|
|
|
183
183
|
"@tiptap/react": "^3.20.1",
|
|
184
184
|
"@tiptap/starter-kit": "^3.20.1",
|
|
185
185
|
"@tiptap/suggestion": "^3.20.1",
|
|
186
|
-
"@vidstack/react": "next",
|
|
187
186
|
"@wavesurfer/react": "^1.0.12",
|
|
187
|
+
"hls-video-element": "^1.5.11",
|
|
188
188
|
"maplibre-gl": "^4.7.1",
|
|
189
|
+
"media-chrome": "^4.19.0",
|
|
189
190
|
"media-icons": "next",
|
|
190
191
|
"mermaid": "^11.12.0",
|
|
191
192
|
"monaco-editor": "^0.55.1",
|
|
@@ -205,8 +206,9 @@
|
|
|
205
206
|
"remark-emoji": "^5.0.2",
|
|
206
207
|
"remark-gfm": "4.0.1",
|
|
207
208
|
"remark-smartypants": "^3.0.2",
|
|
208
|
-
"
|
|
209
|
-
"wavesurfer.js": "^7.12.1"
|
|
209
|
+
"vimeo-video-element": "^1.7.2",
|
|
210
|
+
"wavesurfer.js": "^7.12.1",
|
|
211
|
+
"youtube-video-element": "^1.9.0"
|
|
210
212
|
},
|
|
211
213
|
"optionalDependencies": {
|
|
212
214
|
"@mapbox/mapbox-gl-draw": "^1.4.3",
|
|
@@ -214,9 +216,9 @@
|
|
|
214
216
|
"material-file-icons": "^2.4.0"
|
|
215
217
|
},
|
|
216
218
|
"devDependencies": {
|
|
217
|
-
"@djangocfg/i18n": "^2.1.
|
|
218
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
219
|
-
"@djangocfg/ui-core": "^2.1.
|
|
219
|
+
"@djangocfg/i18n": "^2.1.407",
|
|
220
|
+
"@djangocfg/typescript-config": "^2.1.407",
|
|
221
|
+
"@djangocfg/ui-core": "^2.1.407",
|
|
220
222
|
"@types/lodash-es": "^4.17.12",
|
|
221
223
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
222
224
|
"@types/node": "^24.7.2",
|
|
@@ -3,39 +3,25 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* `@djangocfg/ui-tools/audio-player` subpath entrypoint.
|
|
5
5
|
*
|
|
6
|
-
* `LazyPlayer` is
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* exists inside `LazyPlayer` once loaded).
|
|
6
|
+
* `LazyPlayer` is exported as a direct (synchronous) alias of `Player`. We
|
|
7
|
+
* intentionally avoid `React.lazy` + `import('./Player')` here: under bundlers
|
|
8
|
+
* that pre-bundle subpath entries (Vite optimizeDeps in Next.js/Vite/SB), the
|
|
9
|
+
* dynamic import creates a second chunk that re-instantiates the React
|
|
10
|
+
* Contexts (AudioRefCtx/ControlsCtx/MetaCtx/StateCtx/LevelsCtx). The slot
|
|
11
|
+
* components and selector hooks re-exported below would then read from a
|
|
12
|
+
* different context instance than `<PlayerProvider>` writes to, which made
|
|
13
|
+
* `usePlayerAudio` throw "must be used inside <PlayerProvider>".
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Heavy audio-decoding work (peaks, AudioContext) already happens lazily at
|
|
16
|
+
* runtime via effects inside `PlayerProvider`/`PlayerShell` — there is no
|
|
17
|
+
* benefit to splitting the React shell behind a second chunk.
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
|
-
import { createLazyComponent } from '../../components';
|
|
22
|
-
import type { PlayerProps } from './types';
|
|
23
|
-
|
|
24
20
|
// ============================================================================
|
|
25
|
-
//
|
|
21
|
+
// Player component (synchronous; previously lazy — see note above)
|
|
26
22
|
// ============================================================================
|
|
27
23
|
|
|
28
|
-
export
|
|
29
|
-
() => import('./Player').then((mod) => ({ default: mod.Player })),
|
|
30
|
-
{
|
|
31
|
-
displayName: 'LazyAudioPlayer',
|
|
32
|
-
fallback: (
|
|
33
|
-
<div className="rounded-lg border border-border/60 bg-card px-4 py-6 text-sm text-muted-foreground">
|
|
34
|
-
Loading audio player…
|
|
35
|
-
</div>
|
|
36
|
-
),
|
|
37
|
-
},
|
|
38
|
-
);
|
|
24
|
+
export { Player, Player as LazyPlayer } from './Player';
|
|
39
25
|
|
|
40
26
|
// ============================================================================
|
|
41
27
|
// Light surface — types, store, context, slot components, hooks
|
|
@@ -196,13 +196,21 @@ export function ImageViewer({
|
|
|
196
196
|
wrapperClass="!w-full !h-full cursor-grab active:cursor-grabbing"
|
|
197
197
|
contentClass="!w-full !h-full flex items-center justify-center"
|
|
198
198
|
>
|
|
199
|
-
|
|
199
|
+
{/*
|
|
200
|
+
Fill the TransformComponent content box so the children's
|
|
201
|
+
`max-w-full / max-h-full` resolve against the actual viewport
|
|
202
|
+
instead of the image's natural box. Without `w-full h-full`
|
|
203
|
+
this wrapper shrink-fits the image, which collapses the
|
|
204
|
+
max-* constraints and renders the image at intrinsic size —
|
|
205
|
+
visible as cropping / half-height in tall containers.
|
|
206
|
+
*/}
|
|
207
|
+
<div className="relative w-full h-full flex items-center justify-center">
|
|
200
208
|
{useProgressiveLoading && lqip && !isFullyLoaded && (
|
|
201
209
|
<img
|
|
202
210
|
src={lqip}
|
|
203
211
|
alt=""
|
|
204
212
|
aria-hidden="true"
|
|
205
|
-
className="absolute
|
|
213
|
+
className="absolute max-w-full max-h-full object-contain select-none"
|
|
206
214
|
style={{ transform: transformStyle, filter: 'blur(20px)', transition: 'opacity 0.3s ease-out', opacity: isFullyLoaded ? 0 : 1 }}
|
|
207
215
|
draggable={false}
|
|
208
216
|
/>
|
|
@@ -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
|
+
}
|