@djangocfg/ui-nextjs 2.1.66 → 2.1.68

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 (90) hide show
  1. package/package.json +8 -6
  2. package/src/stores/index.ts +8 -0
  3. package/src/stores/mediaCache.ts +474 -0
  4. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  5. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  6. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  7. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  8. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  9. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  10. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  11. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  12. package/src/tools/AudioPlayer/README.md +35 -11
  13. package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
  14. package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
  15. package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
  16. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  23. package/src/tools/AudioPlayer/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
  24. package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
  25. package/src/tools/AudioPlayer/components/index.ts +21 -0
  26. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  27. package/src/tools/AudioPlayer/context/index.ts +11 -0
  28. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  29. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  30. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  31. package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
  32. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  33. package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
  34. package/src/tools/AudioPlayer/index.ts +104 -49
  35. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  36. package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
  37. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  38. package/src/tools/AudioPlayer/types/index.ts +35 -0
  39. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  40. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  41. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  42. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  43. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  44. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  45. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  46. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  47. package/src/tools/ImageViewer/README.md +16 -3
  48. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  49. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  50. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  51. package/src/tools/ImageViewer/components/index.ts +7 -0
  52. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  53. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  54. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  55. package/src/tools/ImageViewer/index.ts +47 -3
  56. package/src/tools/ImageViewer/types.ts +75 -0
  57. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  58. package/src/tools/ImageViewer/utils/index.ts +16 -0
  59. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  60. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  61. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  62. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  63. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  64. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  65. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  66. package/src/tools/VideoPlayer/README.md +26 -10
  67. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  68. package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
  69. package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
  70. package/src/tools/VideoPlayer/components/index.ts +14 -0
  71. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  72. package/src/tools/VideoPlayer/context/index.ts +8 -0
  73. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  74. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  75. package/src/tools/VideoPlayer/index.ts +29 -20
  76. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
  77. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
  78. package/src/tools/VideoPlayer/types/index.ts +38 -0
  79. package/src/tools/VideoPlayer/types/player.ts +116 -0
  80. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  81. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  82. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  83. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  84. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  85. package/src/tools/index.ts +10 -0
  86. package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
  87. package/src/tools/AudioPlayer/context.tsx +0 -426
  88. package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
  89. package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
  90. package/src/tools/VideoPlayer/types.ts +0 -367
@@ -1,416 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * ImageViewer - Image viewer with zoom, pan, rotate, flip
5
- *
6
- * Features:
7
- * - Zoom with mouse wheel and presets
8
- * - Pan with drag
9
- * - Rotate 90°
10
- * - Flip horizontal/vertical
11
- * - Fullscreen dialog
12
- * - Keyboard shortcuts (+/-, 0, r)
13
- */
14
-
15
- import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
16
- import { ImageIcon, ZoomIn, ZoomOut, RotateCw, FlipHorizontal, FlipVertical, Maximize2, Expand } from 'lucide-react';
17
- import { TransformWrapper, TransformComponent, useControls } from 'react-zoom-pan-pinch';
18
- import { Button, cn, Dialog, DialogContent } from '@djangocfg/ui-core';
19
- import {
20
- DropdownMenu,
21
- DropdownMenuContent,
22
- DropdownMenuItem,
23
- DropdownMenuTrigger,
24
- } from '../../components/dropdown-menu';
25
-
26
- // =============================================================================
27
- // TYPES
28
- // =============================================================================
29
-
30
- export interface ImageFile {
31
- name: string;
32
- path: string;
33
- mimeType?: string;
34
- }
35
-
36
- // Preset zoom levels
37
- const ZOOM_PRESETS = [
38
- { label: 'Fit', value: 'fit' },
39
- { label: '25%', value: 0.25 },
40
- { label: '50%', value: 0.5 },
41
- { label: '100%', value: 1 },
42
- { label: '200%', value: 2 },
43
- { label: '400%', value: 4 },
44
- ] as const;
45
-
46
- export interface ImageViewerProps {
47
- /** File info (name, path, mimeType) */
48
- file: ImageFile;
49
- /** Image content as string (data URL or base64) or ArrayBuffer */
50
- content: string | ArrayBuffer;
51
- /** Hide expand button when already in dialog */
52
- inDialog?: boolean;
53
- }
54
-
55
- interface ImageTransform {
56
- rotation: number;
57
- flipH: boolean;
58
- flipV: boolean;
59
- }
60
-
61
- // Toolbar component
62
- function ImageToolbar({
63
- scale,
64
- transform,
65
- onRotate,
66
- onFlipH,
67
- onFlipV,
68
- onZoomPreset,
69
- onExpand,
70
- }: {
71
- scale: number;
72
- transform: ImageTransform;
73
- onRotate: () => void;
74
- onFlipH: () => void;
75
- onFlipV: () => void;
76
- onZoomPreset: (value: number | 'fit') => void;
77
- onExpand?: () => void;
78
- }) {
79
- const { zoomIn, zoomOut, resetTransform, centerView } = useControls();
80
-
81
- const zoomLabel = useMemo(() => {
82
- const percent = Math.round(scale * 100);
83
- return `${percent}%`;
84
- }, [scale]);
85
-
86
- return (
87
- <div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-0.5 bg-background/90 backdrop-blur-sm border rounded-lg p-1 shadow-lg">
88
- {/* Zoom controls */}
89
- <Button
90
- variant="ghost"
91
- size="icon"
92
- className="h-7 w-7"
93
- onClick={() => zoomOut()}
94
- >
95
- <ZoomOut className="h-3.5 w-3.5" />
96
- </Button>
97
-
98
- <DropdownMenu>
99
- <DropdownMenuTrigger asChild>
100
- <Button variant="ghost" size="sm" className="h-7 px-2 min-w-[52px] font-mono text-xs">
101
- {zoomLabel}
102
- </Button>
103
- </DropdownMenuTrigger>
104
- <DropdownMenuContent align="center" className="min-w-[80px]">
105
- {ZOOM_PRESETS.map((preset) => (
106
- <DropdownMenuItem
107
- key={preset.label}
108
- onClick={() => onZoomPreset(preset.value)}
109
- className="text-xs justify-center"
110
- >
111
- {preset.label}
112
- </DropdownMenuItem>
113
- ))}
114
- </DropdownMenuContent>
115
- </DropdownMenu>
116
-
117
- <Button
118
- variant="ghost"
119
- size="icon"
120
- className="h-7 w-7"
121
- onClick={() => zoomIn()}
122
- >
123
- <ZoomIn className="h-3.5 w-3.5" />
124
- </Button>
125
-
126
- <div className="w-px h-4 bg-border mx-1" />
127
-
128
- {/* Fit to view */}
129
- <Button
130
- variant="ghost"
131
- size="icon"
132
- className="h-7 w-7"
133
- onClick={() => resetTransform()}
134
- title="Fit to view"
135
- >
136
- <Maximize2 className="h-3.5 w-3.5" />
137
- </Button>
138
-
139
- <div className="w-px h-4 bg-border mx-1" />
140
-
141
- {/* Transform controls */}
142
- <Button
143
- variant="ghost"
144
- size="icon"
145
- className={cn("h-7 w-7", transform.flipH && "bg-accent")}
146
- onClick={onFlipH}
147
- title="Flip horizontal"
148
- >
149
- <FlipHorizontal className="h-3.5 w-3.5" />
150
- </Button>
151
-
152
- <Button
153
- variant="ghost"
154
- size="icon"
155
- className={cn("h-7 w-7", transform.flipV && "bg-accent")}
156
- onClick={onFlipV}
157
- title="Flip vertical"
158
- >
159
- <FlipVertical className="h-3.5 w-3.5" />
160
- </Button>
161
-
162
- <Button
163
- variant="ghost"
164
- size="icon"
165
- className="h-7 w-7"
166
- onClick={onRotate}
167
- title="Rotate 90°"
168
- >
169
- <RotateCw className="h-3.5 w-3.5" />
170
- </Button>
171
-
172
- {transform.rotation !== 0 && (
173
- <span className="text-[10px] text-muted-foreground font-mono pl-1">
174
- {transform.rotation}°
175
- </span>
176
- )}
177
-
178
- {onExpand && (
179
- <>
180
- <div className="w-px h-4 bg-border mx-1" />
181
- <Button
182
- variant="ghost"
183
- size="icon"
184
- className="h-7 w-7"
185
- onClick={onExpand}
186
- title="Open in fullscreen"
187
- >
188
- <Expand className="h-3.5 w-3.5" />
189
- </Button>
190
- </>
191
- )}
192
- </div>
193
- );
194
- }
195
-
196
- // Image dimensions badge
197
- function ImageInfo({ src }: { src: string }) {
198
- const [dimensions, setDimensions] = useState<{ w: number; h: number } | null>(null);
199
-
200
- useEffect(() => {
201
- const img = new Image();
202
- img.onload = () => setDimensions({ w: img.naturalWidth, h: img.naturalHeight });
203
- img.src = src;
204
- }, [src]);
205
-
206
- if (!dimensions) return null;
207
-
208
- return (
209
- <div className="absolute top-3 right-3 z-10 px-2 py-1 bg-background/80 backdrop-blur-sm border rounded text-[10px] text-muted-foreground font-mono">
210
- {dimensions.w} × {dimensions.h}
211
- </div>
212
- );
213
- }
214
-
215
- export function ImageViewer({ file, content, inDialog = false }: ImageViewerProps) {
216
- const contentSize = content ? (typeof content === 'string' ? content.length : content.byteLength) : 0;
217
- const hasContent = contentSize > 0;
218
-
219
- const [transform, setTransform] = useState<ImageTransform>({
220
- rotation: 0,
221
- flipH: false,
222
- flipV: false,
223
- });
224
- const [scale, setScale] = useState(1);
225
- const [dialogOpen, setDialogOpen] = useState(false);
226
- const blobUrlRef = useRef<string | null>(null);
227
- const [src, setSrc] = useState<string | null>(null);
228
- const containerRef = useRef<HTMLDivElement>(null);
229
- const controlsRef = useRef<ReturnType<typeof useControls> | null>(null);
230
-
231
- // Create blob URL
232
- useEffect(() => {
233
- if (!hasContent) {
234
- setSrc(null);
235
- return;
236
- }
237
-
238
- if (typeof content === 'string') {
239
- const url = content.startsWith('data:') ? content : `data:${file.mimeType};base64,${btoa(content)}`;
240
- setSrc(url);
241
- return;
242
- }
243
-
244
- const blob = new Blob([content], { type: file.mimeType || 'image/png' });
245
- const url = URL.createObjectURL(blob);
246
- blobUrlRef.current = url;
247
- setSrc(url);
248
-
249
- return () => {
250
- if (blobUrlRef.current) {
251
- URL.revokeObjectURL(blobUrlRef.current);
252
- blobUrlRef.current = null;
253
- }
254
- };
255
- }, [content, file.mimeType, hasContent]);
256
-
257
- // Reset transform when file changes
258
- useEffect(() => {
259
- setTransform({ rotation: 0, flipH: false, flipV: false });
260
- }, [file.path]);
261
-
262
- const handleRotate = useCallback(() => {
263
- setTransform((prev) => ({ ...prev, rotation: (prev.rotation + 90) % 360 }));
264
- }, []);
265
-
266
- const handleFlipH = useCallback(() => {
267
- setTransform((prev) => ({ ...prev, flipH: !prev.flipH }));
268
- }, []);
269
-
270
- const handleFlipV = useCallback(() => {
271
- setTransform((prev) => ({ ...prev, flipV: !prev.flipV }));
272
- }, []);
273
-
274
- const handleZoomPreset = useCallback((value: number | 'fit') => {
275
- if (!controlsRef.current) return;
276
- if (value === 'fit') {
277
- controlsRef.current.resetTransform();
278
- } else {
279
- controlsRef.current.setTransform(0, 0, value);
280
- }
281
- }, []);
282
-
283
- const handleExpand = useCallback(() => {
284
- setDialogOpen(true);
285
- }, []);
286
-
287
- // CSS transform for rotation and flip
288
- const imageTransform = useMemo(() => {
289
- const transforms: string[] = [];
290
- if (transform.rotation) transforms.push(`rotate(${transform.rotation}deg)`);
291
- if (transform.flipH) transforms.push('scaleX(-1)');
292
- if (transform.flipV) transforms.push('scaleY(-1)');
293
- return transforms.join(' ') || 'none';
294
- }, [transform]);
295
-
296
- // Keyboard shortcuts
297
- useEffect(() => {
298
- const handleKeyDown = (e: KeyboardEvent) => {
299
- if (!containerRef.current?.contains(document.activeElement) &&
300
- document.activeElement !== containerRef.current) {
301
- return;
302
- }
303
-
304
- const controls = controlsRef.current;
305
- if (!controls) return;
306
-
307
- switch (e.key) {
308
- case '+':
309
- case '=':
310
- e.preventDefault();
311
- controls.zoomIn();
312
- break;
313
- case '-':
314
- e.preventDefault();
315
- controls.zoomOut();
316
- break;
317
- case '0':
318
- e.preventDefault();
319
- controls.resetTransform();
320
- break;
321
- case 'r':
322
- if (!e.metaKey && !e.ctrlKey) {
323
- e.preventDefault();
324
- handleRotate();
325
- }
326
- break;
327
- }
328
- };
329
-
330
- window.addEventListener('keydown', handleKeyDown);
331
- return () => window.removeEventListener('keydown', handleKeyDown);
332
- }, [handleRotate]);
333
-
334
- if (!hasContent) {
335
- return (
336
- <div className="flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30">
337
- <ImageIcon className="w-12 h-12 text-muted-foreground/50" />
338
- <p className="text-sm text-muted-foreground">Failed to load image</p>
339
- </div>
340
- );
341
- }
342
-
343
- return (
344
- <div
345
- ref={containerRef}
346
- tabIndex={0}
347
- className={cn(
348
- "flex-1 h-full relative overflow-hidden outline-none",
349
- "bg-[length:16px_16px]",
350
- "[background-color:hsl(var(--muted)/0.2)]",
351
- "[background-image:linear-gradient(45deg,hsl(var(--muted)/0.4)_25%,transparent_25%),linear-gradient(-45deg,hsl(var(--muted)/0.4)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,hsl(var(--muted)/0.4)_75%),linear-gradient(-45deg,transparent_75%,hsl(var(--muted)/0.4)_75%)]",
352
- "[background-position:0_0,0_8px,8px_-8px,-8px_0px]"
353
- )}
354
- >
355
- {src && <ImageInfo src={src} />}
356
-
357
- <TransformWrapper
358
- initialScale={1}
359
- minScale={0.1}
360
- maxScale={8}
361
- centerOnInit
362
- onTransformed={(ref, state) => {
363
- setScale(state.scale);
364
- controlsRef.current = ref;
365
- }}
366
- onInit={(ref) => {
367
- controlsRef.current = ref;
368
- }}
369
- wheel={{ step: 0.1 }}
370
- doubleClick={{ mode: 'toggle', step: 2 }}
371
- panning={{ velocityDisabled: false }}
372
- >
373
- <ImageToolbar
374
- scale={scale}
375
- transform={transform}
376
- onRotate={handleRotate}
377
- onFlipH={handleFlipH}
378
- onFlipV={handleFlipV}
379
- onZoomPreset={handleZoomPreset}
380
- onExpand={!inDialog ? handleExpand : undefined}
381
- />
382
-
383
- <TransformComponent
384
- wrapperClass="!w-full !h-full cursor-grab active:cursor-grabbing"
385
- contentClass="!w-full !h-full flex items-center justify-center"
386
- >
387
- <img
388
- src={src!}
389
- alt={file.name}
390
- className="max-w-full max-h-full object-contain select-none"
391
- style={{ transform: imageTransform, transition: 'transform 0.15s ease-out' }}
392
- draggable={false}
393
- />
394
- </TransformComponent>
395
- </TransformWrapper>
396
-
397
- {/* Fullscreen dialog */}
398
- {!inDialog && (
399
- <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
400
- <DialogContent className="max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden">
401
- <div className="h-full flex flex-col">
402
- <div className="flex items-center justify-between px-4 py-2 border-b shrink-0">
403
- <span className="text-sm font-medium truncate">{file.name}</span>
404
- </div>
405
- <div className="flex-1 min-h-0 relative">
406
- <div className="absolute inset-0">
407
- <ImageViewer file={file} content={content} inDialog />
408
- </div>
409
- </div>
410
- </div>
411
- </DialogContent>
412
- </Dialog>
413
- )}
414
- </div>
415
- );
416
- }
@@ -1,125 +0,0 @@
1
- /**
2
- * VideoPlayerContext - Context for streaming configuration
3
- * Simplifies streaming API by providing getStreamUrl globally
4
- */
5
-
6
- 'use client';
7
-
8
- import React, { createContext, useContext, useMemo } from 'react';
9
-
10
- import type { VideoSourceUnion, StreamSource } from './types';
11
-
12
- // ============================================================================
13
- // Context Types
14
- // ============================================================================
15
-
16
- export interface VideoPlayerContextValue {
17
- /** Function to generate stream URL (for HTTP Range streaming) */
18
- getStreamUrl?: (sessionId: string, path: string) => string;
19
- /** Current session ID */
20
- sessionId?: string | null;
21
- }
22
-
23
- export interface VideoPlayerProviderProps extends VideoPlayerContextValue {
24
- children: React.ReactNode;
25
- }
26
-
27
- // ============================================================================
28
- // Context
29
- // ============================================================================
30
-
31
- const VideoPlayerContext = createContext<VideoPlayerContextValue | null>(null);
32
-
33
- /**
34
- * Provider for VideoPlayer streaming configuration
35
- *
36
- * @example
37
- * // In your app layout or FileWorkspace
38
- * <VideoPlayerProvider
39
- * sessionId={sessionId}
40
- * getStreamUrl={terminalClient.terminal_media.streamStreamRetrieveUrl}
41
- * >
42
- * <VideoPlayer source={{ type: 'stream', path: '/video.mp4' }} />
43
- * </VideoPlayerProvider>
44
- */
45
- export function VideoPlayerProvider({
46
- children,
47
- getStreamUrl,
48
- sessionId,
49
- }: VideoPlayerProviderProps) {
50
- const value = useMemo(
51
- () => ({ getStreamUrl, sessionId }),
52
- [getStreamUrl, sessionId]
53
- );
54
-
55
- return (
56
- <VideoPlayerContext.Provider value={value}>
57
- {children}
58
- </VideoPlayerContext.Provider>
59
- );
60
- }
61
-
62
- /**
63
- * Hook to access VideoPlayer context
64
- */
65
- export function useVideoPlayerContext(): VideoPlayerContextValue | null {
66
- return useContext(VideoPlayerContext);
67
- }
68
-
69
- // ============================================================================
70
- // Simplified Stream Source
71
- // ============================================================================
72
-
73
- /** Simplified stream source (uses context for getStreamUrl) */
74
- export interface SimpleStreamSource {
75
- type: 'stream';
76
- /** File path on server */
77
- path: string;
78
- /** Session ID (optional, uses context if not provided) */
79
- sessionId?: string;
80
- /** MIME type for the video */
81
- mimeType?: string;
82
- title?: string;
83
- poster?: string;
84
- }
85
-
86
- /**
87
- * Check if source is a simplified stream source (without getStreamUrl)
88
- */
89
- export function isSimpleStreamSource(
90
- source: VideoSourceUnion | SimpleStreamSource
91
- ): source is SimpleStreamSource {
92
- return source.type === 'stream' && !('getStreamUrl' in source);
93
- }
94
-
95
- /**
96
- * Resolve simplified stream source to full stream source using context
97
- */
98
- export function resolveStreamSource(
99
- source: SimpleStreamSource,
100
- context: VideoPlayerContextValue | null
101
- ): StreamSource | null {
102
- if (!context?.getStreamUrl) {
103
- console.warn(
104
- 'VideoPlayer: Stream source requires getStreamUrl. ' +
105
- 'Either provide it in source or wrap with VideoPlayerProvider.'
106
- );
107
- return null;
108
- }
109
-
110
- const sessionId = source.sessionId || context.sessionId;
111
- if (!sessionId) {
112
- console.warn('VideoPlayer: Stream source requires sessionId.');
113
- return null;
114
- }
115
-
116
- return {
117
- type: 'stream',
118
- sessionId,
119
- path: source.path,
120
- getStreamUrl: context.getStreamUrl,
121
- mimeType: source.mimeType,
122
- title: source.title,
123
- poster: source.poster,
124
- };
125
- }