@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.
Files changed (58) hide show
  1. package/README.md +16 -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/JsonForm/JsonSchemaForm.tsx +3 -1
  6. package/src/tools/JsonForm/README.md +12 -2
  7. package/src/tools/JsonForm/widgets/TextareaWidget.tsx +25 -0
  8. package/src/tools/JsonForm/widgets/index.ts +1 -0
  9. package/src/tools/JsonTree/README.md +12 -0
  10. package/src/tools/PrettyCode/README.md +81 -0
  11. package/src/tools/VideoPlayer/README.md +87 -230
  12. package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
  13. package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
  14. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +38 -0
  15. package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
  16. package/src/tools/VideoPlayer/canvas/index.ts +12 -0
  17. package/src/tools/VideoPlayer/canvas/jsx.d.ts +54 -0
  18. package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
  19. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +39 -0
  20. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +77 -0
  21. package/src/tools/VideoPlayer/index.ts +51 -65
  22. package/src/tools/VideoPlayer/lazy.tsx +11 -54
  23. package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
  24. package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
  25. package/src/tools/VideoPlayer/parts/index.ts +15 -0
  26. package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
  27. package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
  28. package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
  29. package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
  30. package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
  31. package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
  32. package/src/tools/VideoPlayer/styles/video-player.css +141 -0
  33. package/src/tools/VideoPlayer/types.ts +82 -0
  34. package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
  35. package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
  36. package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
  37. package/src/tools/index.ts +35 -28
  38. package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
  39. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
  40. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
  41. package/src/tools/VideoPlayer/components/index.ts +0 -14
  42. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
  43. package/src/tools/VideoPlayer/context/index.ts +0 -8
  44. package/src/tools/VideoPlayer/hooks/index.ts +0 -12
  45. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
  46. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
  47. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
  48. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
  49. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
  50. package/src/tools/VideoPlayer/providers/index.ts +0 -8
  51. package/src/tools/VideoPlayer/types/index.ts +0 -38
  52. package/src/tools/VideoPlayer/types/player.ts +0 -116
  53. package/src/tools/VideoPlayer/types/provider.ts +0 -93
  54. package/src/tools/VideoPlayer/types/sources.ts +0 -97
  55. package/src/tools/VideoPlayer/utils/debug.ts +0 -14
  56. package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
  57. package/src/tools/VideoPlayer/utils/index.ts +0 -12
  58. package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
@@ -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
+ }
@@ -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
+ }