@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.
Files changed (52) hide show
  1. package/README.md +2 -1
  2. package/package.json +11 -9
  3. package/src/tools/AudioPlayer/lazy.tsx +13 -27
  4. package/src/tools/ImageViewer/components/ImageViewer.tsx +10 -2
  5. package/src/tools/VideoPlayer/README.md +87 -230
  6. package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
  7. package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
  8. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +38 -0
  9. package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
  10. package/src/tools/VideoPlayer/canvas/index.ts +12 -0
  11. package/src/tools/VideoPlayer/canvas/jsx.d.ts +54 -0
  12. package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
  13. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +39 -0
  14. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +77 -0
  15. package/src/tools/VideoPlayer/index.ts +51 -65
  16. package/src/tools/VideoPlayer/lazy.tsx +11 -54
  17. package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
  18. package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
  19. package/src/tools/VideoPlayer/parts/index.ts +15 -0
  20. package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
  21. package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
  22. package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
  23. package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
  24. package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
  25. package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
  26. package/src/tools/VideoPlayer/styles/video-player.css +141 -0
  27. package/src/tools/VideoPlayer/types.ts +82 -0
  28. package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
  29. package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
  30. package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
  31. package/src/tools/index.ts +35 -28
  32. package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
  33. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
  34. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
  35. package/src/tools/VideoPlayer/components/index.ts +0 -14
  36. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
  37. package/src/tools/VideoPlayer/context/index.ts +0 -8
  38. package/src/tools/VideoPlayer/hooks/index.ts +0 -12
  39. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
  40. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
  41. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
  42. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
  43. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
  44. package/src/tools/VideoPlayer/providers/index.ts +0 -8
  45. package/src/tools/VideoPlayer/types/index.ts +0 -38
  46. package/src/tools/VideoPlayer/types/player.ts +0 -116
  47. package/src/tools/VideoPlayer/types/provider.ts +0 -93
  48. package/src/tools/VideoPlayer/types/sources.ts +0 -97
  49. package/src/tools/VideoPlayer/utils/debug.ts +0 -14
  50. package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
  51. package/src/tools/VideoPlayer/utils/index.ts +0 -12
  52. 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` | ~150KB | Vidstack-based pro player |
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.404",
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.404",
163
- "@djangocfg/ui-core": "^2.1.404",
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
- "vidstack": "next",
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.404",
218
- "@djangocfg/typescript-config": "^2.1.404",
219
- "@djangocfg/ui-core": "^2.1.404",
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 the only heavy export it dynamically imports the full
7
- * `Player` tree (PlayerShell + Layout + Cover + Waveform + Controls + audio
8
- * decoding helpers). Everything else listed here is light:
9
- * - types are erased,
10
- * - the Zustand store, context hooks, and selectors carry no UI,
11
- * - the slot components (`Cover`, `Title`, `PlayButton`, `Waveform`, …)
12
- * are presentational React components that read from PlayerContext
13
- * they only become meaningful inside a `<PlayerProvider>` (which only
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
- * Consumers building a fully custom layout: render `<LazyPlayer>` once to
17
- * get the provider, then arrange slot components inside via render-children
18
- * pattern, or use the bare `Player` re-exported from the root barrel.
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
- // Lazy heavy component
21
+ // Player component (synchronous; previously lazy — see note above)
26
22
  // ============================================================================
27
23
 
28
- export const LazyPlayer = createLazyComponent<PlayerProps>(
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
- <div className="relative">
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 inset-0 max-w-full max-h-full object-contain select-none"
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
- Unified video player component supporting multiple modes and source types.
4
-
5
- ## Modes
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
- ## Basic Usage
11
+ // Structured source
12
+ <VideoPlayer source={{ type: 'youtube', videoId: 'sGbxmsDFVnE' }} />
27
13
 
28
- ### YouTube / Vimeo
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
- ### HLS / DASH Streaming
18
+ ## Why media-chrome
36
19
 
37
- ```tsx
38
- <VideoPlayer source={{ type: 'hls', url: 'https://example.com/video.m3u8' }} />
39
- <VideoPlayer source={{ type: 'dash', url: 'https://example.com/video.mpd' }} />
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
- ### Direct URL
27
+ ## Sources
43
28
 
44
- ```tsx
45
- <VideoPlayer source={{ type: 'url', url: 'https://example.com/video.mp4' }} />
46
- ```
29
+ `VideoSource` is a discriminated union — pass an object, or a raw URL
30
+ string that `parseEmbedUrl` classifies for you.
47
31
 
48
- ### HTTP Range Streaming (with auth)
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
- // Full source (standalone)
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
- // Simplified source (with context)
62
- <VideoPlayerProvider sessionId={sessionId} getStreamUrl={getStreamUrl}>
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
- ### Blob / ArrayBuffer
46
+ parseEmbedUrl('https://vimeo.com/76979871');
47
+ // → { type: 'vimeo', videoId: '76979871' }
68
48
 
69
- ```tsx
70
- <VideoPlayer
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 | 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
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
- ```tsx
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
- ## Context Provider
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 { resolveFileSource } from '@djangocfg/ui-nextjs';
191
-
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
- });
197
-
198
- if (source) {
199
- return <VideoPlayer source={source} />;
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
- ## Source Types Reference
90
+ ## Parts
204
91
 
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
- ```
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
- ## Architecture
97
+ ## Theming
218
98
 
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
- ```
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
- ## Accessibility
104
+ ## Notes
250
105
 
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
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
- ## Browser Support
116
+ ## Storybook
258
117
 
259
- - Chrome 63+
260
- - Firefox 67+
261
- - Safari 12+
262
- - Edge 79+
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
+ }