@djangocfg/ui-tools 2.1.91
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/dist/LottiePlayer.client-LBEC2JKY.mjs +161 -0
- package/dist/LottiePlayer.client-LBEC2JKY.mjs.map +1 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs +168 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs.map +1 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs +477 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs.map +1 -0
- package/dist/Mermaid.client-SBYY364Q.cjs +483 -0
- package/dist/Mermaid.client-SBYY364Q.cjs.map +1 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs +1003 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs.map +1 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs +996 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs.map +1 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs +152 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs.map +1 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs +154 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs.map +1 -0
- package/dist/chunk-37ZI6VD4.mjs +12 -0
- package/dist/chunk-37ZI6VD4.mjs.map +1 -0
- package/dist/chunk-3HK2OE62.cjs +81 -0
- package/dist/chunk-3HK2OE62.cjs.map +1 -0
- package/dist/chunk-7DGDQVQW.cjs +591 -0
- package/dist/chunk-7DGDQVQW.cjs.map +1 -0
- package/dist/chunk-M6P2FU7L.mjs +572 -0
- package/dist/chunk-M6P2FU7L.mjs.map +1 -0
- package/dist/chunk-UQ3XI5MY.cjs +15 -0
- package/dist/chunk-UQ3XI5MY.cjs.map +1 -0
- package/dist/chunk-YFRNE2IR.mjs +79 -0
- package/dist/chunk-YFRNE2IR.mjs.map +1 -0
- package/dist/index.cjs +5042 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1591 -0
- package/dist/index.d.ts +1591 -0
- package/dist/index.mjs +4941 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +340 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/index.ts +26 -0
- package/src/stores/index.ts +9 -0
- package/src/stores/mediaCache.ts +534 -0
- package/src/tools/AudioPlayer/README.md +206 -0
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +149 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/components/index.ts +22 -0
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +158 -0
- package/src/tools/AudioPlayer/context/index.ts +16 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +35 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +207 -0
- package/src/tools/AudioPlayer/index.ts +133 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +27 -0
- package/src/tools/AudioPlayer/utils/debug.ts +14 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +6 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +200 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +145 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +241 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +204 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +81 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/debug.ts +14 -0
- package/src/tools/ImageViewer/utils/index.ts +17 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +197 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +249 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +161 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +47 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +74 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +107 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +35 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +62 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +116 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +213 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +37 -0
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +219 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +89 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +97 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +148 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +35 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +96 -0
- package/src/tools/JsonForm/widgets/index.ts +14 -0
- package/src/tools/JsonTree/index.tsx +243 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +213 -0
- package/src/tools/LottiePlayer/index.tsx +56 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +164 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +82 -0
- package/src/tools/Mermaid/components/MermaidCodeViewer.tsx +95 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +103 -0
- package/src/tools/Mermaid/hooks/index.ts +4 -0
- package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +73 -0
- package/src/tools/Mermaid/hooks/useMermaidFullscreen.ts +46 -0
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +226 -0
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +29 -0
- package/src/tools/Mermaid/index.tsx +44 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +33 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +149 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +263 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +125 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +100 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +157 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +173 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +68 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +337 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +199 -0
- package/src/tools/OpenapiViewer/index.tsx +37 -0
- package/src/tools/OpenapiViewer/types.ts +151 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +208 -0
- package/src/tools/PrettyCode/index.tsx +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +264 -0
- package/src/tools/VideoPlayer/components/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +172 -0
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +12 -0
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +70 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +116 -0
- package/src/tools/VideoPlayer/index.ts +77 -0
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +284 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +505 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +400 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/debug.ts +14 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +12 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/_shared.ts +29 -0
- package/src/tools/index.ts +172 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamProvider - HTTP Range streaming and Blob video player
|
|
3
|
+
* Supports:
|
|
4
|
+
* - HTTP Range requests with authorization (for large files)
|
|
5
|
+
* - Blob/ArrayBuffer sources
|
|
6
|
+
* - Data URL sources
|
|
7
|
+
* - Fill parent container mode
|
|
8
|
+
* - Custom error fallback
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use client';
|
|
12
|
+
|
|
13
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
16
|
+
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
17
|
+
import { useMediaCacheStore, generateContentKey } from '../../../stores/mediaCache';
|
|
18
|
+
import { useVideoPlayerSettings } from '../hooks/useVideoPlayerSettings';
|
|
19
|
+
|
|
20
|
+
import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
|
|
21
|
+
import { videoDebug } from '../utils/debug';
|
|
22
|
+
|
|
23
|
+
/** Default error fallback UI */
|
|
24
|
+
function DefaultErrorFallback({ error }: ErrorFallbackProps) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4 text-white">
|
|
27
|
+
<svg
|
|
28
|
+
className="w-16 h-16 text-muted-foreground"
|
|
29
|
+
fill="none"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
viewBox="0 0 24 24"
|
|
32
|
+
>
|
|
33
|
+
<path
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
strokeWidth={2}
|
|
37
|
+
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
<p className="text-lg">{error || 'Video cannot be previewed'}</p>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
46
|
+
(
|
|
47
|
+
{
|
|
48
|
+
source,
|
|
49
|
+
aspectRatio = 16 / 9,
|
|
50
|
+
autoPlay = false,
|
|
51
|
+
muted = false,
|
|
52
|
+
loop = false,
|
|
53
|
+
playsInline = true,
|
|
54
|
+
preload = 'metadata',
|
|
55
|
+
controls = true,
|
|
56
|
+
disableContextMenu = false,
|
|
57
|
+
showPreloader = true,
|
|
58
|
+
preloaderTimeout = 10000,
|
|
59
|
+
className,
|
|
60
|
+
videoClassName,
|
|
61
|
+
errorFallback,
|
|
62
|
+
onPlay,
|
|
63
|
+
onPause,
|
|
64
|
+
onEnded,
|
|
65
|
+
onError,
|
|
66
|
+
onLoadStart,
|
|
67
|
+
onCanPlay,
|
|
68
|
+
onTimeUpdate,
|
|
69
|
+
onBufferProgress,
|
|
70
|
+
},
|
|
71
|
+
ref
|
|
72
|
+
) => {
|
|
73
|
+
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
|
74
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
75
|
+
const [hasError, setHasError] = useState(false);
|
|
76
|
+
const [errorMessage, setErrorMessage] = useState<string>('Video cannot be previewed');
|
|
77
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
78
|
+
const contentKeyRef = useRef<string | null>(null);
|
|
79
|
+
const lastSavedTimeRef = useRef<number>(0);
|
|
80
|
+
|
|
81
|
+
// Get stable function references from store (not from hook to avoid re-renders)
|
|
82
|
+
const getOrCreateBlobUrl = useMediaCacheStore.getState().getOrCreateBlobUrl;
|
|
83
|
+
const releaseBlobUrl = useMediaCacheStore.getState().releaseBlobUrl;
|
|
84
|
+
const getOrCreateStreamUrl = useMediaCacheStore.getState().getOrCreateStreamUrl;
|
|
85
|
+
const saveVideoPosition = useMediaCacheStore.getState().saveVideoPosition;
|
|
86
|
+
const getVideoPosition = useMediaCacheStore.getState().getVideoPosition;
|
|
87
|
+
|
|
88
|
+
// Persisted player settings
|
|
89
|
+
const { settings: savedSettings, updateVolume } = useVideoPlayerSettings();
|
|
90
|
+
|
|
91
|
+
// Retry function for error fallback
|
|
92
|
+
// Regenerates URL for stream sources to get fresh token/session
|
|
93
|
+
const retry = useCallback(() => {
|
|
94
|
+
setHasError(false);
|
|
95
|
+
setIsLoading(true);
|
|
96
|
+
|
|
97
|
+
// For stream sources, regenerate URL bypassing cache
|
|
98
|
+
if (source.type === 'stream') {
|
|
99
|
+
const streamSource = source as StreamSource;
|
|
100
|
+
const freshUrl = streamSource.getStreamUrl(streamSource.sessionId, streamSource.path);
|
|
101
|
+
setVideoUrl(freshUrl);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// For other sources, just reload
|
|
106
|
+
const video = videoRef.current;
|
|
107
|
+
if (video && videoUrl) {
|
|
108
|
+
video.load();
|
|
109
|
+
}
|
|
110
|
+
}, [source, videoUrl]);
|
|
111
|
+
|
|
112
|
+
// Expose video element methods via ref
|
|
113
|
+
useImperativeHandle(
|
|
114
|
+
ref,
|
|
115
|
+
() => ({
|
|
116
|
+
play: () => videoRef.current?.play(),
|
|
117
|
+
pause: () => videoRef.current?.pause(),
|
|
118
|
+
togglePlay: () => {
|
|
119
|
+
const video = videoRef.current;
|
|
120
|
+
if (video) {
|
|
121
|
+
video.paused ? video.play() : video.pause();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
seekTo: (time: number) => {
|
|
125
|
+
if (videoRef.current) videoRef.current.currentTime = time;
|
|
126
|
+
},
|
|
127
|
+
setVolume: (volume: number) => {
|
|
128
|
+
if (videoRef.current) videoRef.current.volume = Math.max(0, Math.min(1, volume));
|
|
129
|
+
},
|
|
130
|
+
toggleMute: () => {
|
|
131
|
+
if (videoRef.current) videoRef.current.muted = !videoRef.current.muted;
|
|
132
|
+
},
|
|
133
|
+
enterFullscreen: () => videoRef.current?.requestFullscreen(),
|
|
134
|
+
exitFullscreen: () => document.exitFullscreen(),
|
|
135
|
+
get currentTime() {
|
|
136
|
+
return videoRef.current?.currentTime ?? 0;
|
|
137
|
+
},
|
|
138
|
+
get duration() {
|
|
139
|
+
return videoRef.current?.duration ?? 0;
|
|
140
|
+
},
|
|
141
|
+
get paused() {
|
|
142
|
+
return videoRef.current?.paused ?? true;
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
[]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Track unmount for cleanup
|
|
149
|
+
const isMountedRef = useRef(true);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
isMountedRef.current = true;
|
|
152
|
+
return () => {
|
|
153
|
+
isMountedRef.current = false;
|
|
154
|
+
// Release blob URL only on actual unmount
|
|
155
|
+
if (contentKeyRef.current) {
|
|
156
|
+
useMediaCacheStore.getState().releaseBlobUrl(contentKeyRef.current);
|
|
157
|
+
contentKeyRef.current = null;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
// Create video URL based on source type with caching
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
// Release previous blob URL if source changed
|
|
165
|
+
if (contentKeyRef.current) {
|
|
166
|
+
const newKey = source.type === 'blob'
|
|
167
|
+
? generateContentKey((source as BlobSource).data)
|
|
168
|
+
: null;
|
|
169
|
+
if (newKey !== contentKeyRef.current) {
|
|
170
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
171
|
+
contentKeyRef.current = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setHasError(false);
|
|
176
|
+
setIsLoading(true);
|
|
177
|
+
|
|
178
|
+
switch (source.type) {
|
|
179
|
+
case 'stream': {
|
|
180
|
+
const streamSource = source as StreamSource;
|
|
181
|
+
// Use cached stream URL
|
|
182
|
+
const url = getOrCreateStreamUrl(
|
|
183
|
+
streamSource.sessionId,
|
|
184
|
+
streamSource.path,
|
|
185
|
+
streamSource.getStreamUrl
|
|
186
|
+
);
|
|
187
|
+
videoDebug.load(url, 'stream');
|
|
188
|
+
setVideoUrl(url);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'blob': {
|
|
193
|
+
const blobSource = source as BlobSource;
|
|
194
|
+
// Generate content key for caching
|
|
195
|
+
const contentKey = generateContentKey(blobSource.data);
|
|
196
|
+
contentKeyRef.current = contentKey;
|
|
197
|
+
// Use cached blob URL
|
|
198
|
+
const url = getOrCreateBlobUrl(
|
|
199
|
+
contentKey,
|
|
200
|
+
blobSource.data,
|
|
201
|
+
blobSource.mimeType || 'video/mp4'
|
|
202
|
+
);
|
|
203
|
+
videoDebug.load(url, 'blob');
|
|
204
|
+
setVideoUrl(url);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'data-url': {
|
|
209
|
+
const dataUrlSource = source as DataUrlSource;
|
|
210
|
+
videoDebug.load(dataUrlSource.data.slice(0, 50) + '...', 'data-url');
|
|
211
|
+
setVideoUrl(dataUrlSource.data);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
default:
|
|
216
|
+
videoDebug.error('Invalid video source type', { type: (source as { type: string }).type });
|
|
217
|
+
setVideoUrl(null);
|
|
218
|
+
setHasError(true);
|
|
219
|
+
setErrorMessage('Invalid video source');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// No cleanup here - cleanup happens in unmount effect above
|
|
223
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
224
|
+
}, [source]);
|
|
225
|
+
|
|
226
|
+
// Get source key for position caching
|
|
227
|
+
const getSourceKey = useCallback(() => {
|
|
228
|
+
switch (source.type) {
|
|
229
|
+
case 'stream':
|
|
230
|
+
return `stream:${(source as StreamSource).sessionId}:${(source as StreamSource).path}`;
|
|
231
|
+
case 'blob':
|
|
232
|
+
return contentKeyRef.current ? `blob:${contentKeyRef.current}` : null;
|
|
233
|
+
case 'data-url':
|
|
234
|
+
return `data:${(source as DataUrlSource).data.slice(0, 50)}`;
|
|
235
|
+
default:
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}, [source]);
|
|
239
|
+
|
|
240
|
+
// Restore cached playback position and settings when video is ready
|
|
241
|
+
const handleCanPlay = useCallback(() => {
|
|
242
|
+
const video = videoRef.current;
|
|
243
|
+
if (video) {
|
|
244
|
+
videoDebug.state('canplay', { duration: video.duration, buffered: video.buffered.length });
|
|
245
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
246
|
+
|
|
247
|
+
// Apply saved volume (user preference)
|
|
248
|
+
video.volume = savedSettings.volume;
|
|
249
|
+
}
|
|
250
|
+
setIsLoading(false);
|
|
251
|
+
|
|
252
|
+
// Restore position from cache
|
|
253
|
+
const sourceKey = getSourceKey();
|
|
254
|
+
if (sourceKey && video) {
|
|
255
|
+
const cachedPosition = getVideoPosition(sourceKey);
|
|
256
|
+
if (cachedPosition && cachedPosition > 0) {
|
|
257
|
+
const duration = video.duration;
|
|
258
|
+
// Only restore if position is valid (not at the end)
|
|
259
|
+
if (cachedPosition < duration - 1) {
|
|
260
|
+
videoDebug.debug(`Restoring position: ${cachedPosition}s`);
|
|
261
|
+
video.currentTime = cachedPosition;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
onCanPlay?.();
|
|
267
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
268
|
+
}, [getSourceKey, onCanPlay, savedSettings.volume]);
|
|
269
|
+
|
|
270
|
+
// Save playback position periodically
|
|
271
|
+
const handleTimeUpdate = useCallback(() => {
|
|
272
|
+
const video = videoRef.current;
|
|
273
|
+
if (!video) return;
|
|
274
|
+
|
|
275
|
+
// Save position every 5 seconds
|
|
276
|
+
const sourceKey = getSourceKey();
|
|
277
|
+
if (sourceKey && video.currentTime > 0) {
|
|
278
|
+
const timeSinceLastSave = video.currentTime - lastSavedTimeRef.current;
|
|
279
|
+
if (timeSinceLastSave >= 5 || timeSinceLastSave < 0) {
|
|
280
|
+
saveVideoPosition(sourceKey, video.currentTime);
|
|
281
|
+
lastSavedTimeRef.current = video.currentTime;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
onTimeUpdate?.(video.currentTime, video.duration);
|
|
286
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
287
|
+
}, [getSourceKey, onTimeUpdate]);
|
|
288
|
+
|
|
289
|
+
// Save position on pause
|
|
290
|
+
const handlePause = useCallback(() => {
|
|
291
|
+
const video = videoRef.current;
|
|
292
|
+
const sourceKey = getSourceKey();
|
|
293
|
+
if (sourceKey && video && video.currentTime > 0) {
|
|
294
|
+
saveVideoPosition(sourceKey, video.currentTime);
|
|
295
|
+
lastSavedTimeRef.current = video.currentTime;
|
|
296
|
+
}
|
|
297
|
+
onPause?.();
|
|
298
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
299
|
+
}, [getSourceKey, onPause]);
|
|
300
|
+
|
|
301
|
+
// Handle buffer progress
|
|
302
|
+
const handleProgress = useCallback(() => {
|
|
303
|
+
const video = videoRef.current;
|
|
304
|
+
if (!video || !onBufferProgress) return;
|
|
305
|
+
|
|
306
|
+
// Get the buffered time ranges
|
|
307
|
+
if (video.buffered.length > 0) {
|
|
308
|
+
// Get the end of the last buffered range
|
|
309
|
+
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
|
|
310
|
+
const duration = video.duration;
|
|
311
|
+
|
|
312
|
+
if (duration > 0 && !isNaN(bufferedEnd)) {
|
|
313
|
+
onBufferProgress(bufferedEnd, duration);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}, [onBufferProgress]);
|
|
317
|
+
|
|
318
|
+
// Preloader timeout
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (!showPreloader || !isLoading) return;
|
|
321
|
+
|
|
322
|
+
const timeout = setTimeout(() => {
|
|
323
|
+
setIsLoading(false);
|
|
324
|
+
}, preloaderTimeout);
|
|
325
|
+
|
|
326
|
+
return () => clearTimeout(timeout);
|
|
327
|
+
}, [showPreloader, isLoading, preloaderTimeout]);
|
|
328
|
+
|
|
329
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
330
|
+
if (disableContextMenu) {
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const handleLoadedData = () => {
|
|
336
|
+
setIsLoading(false);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const handleError = () => {
|
|
340
|
+
const video = videoRef.current;
|
|
341
|
+
if (video) {
|
|
342
|
+
videoDebug.error('Video error', { code: video.error?.code, message: video.error?.message });
|
|
343
|
+
}
|
|
344
|
+
setIsLoading(false);
|
|
345
|
+
setHasError(true);
|
|
346
|
+
setErrorMessage('Failed to load video');
|
|
347
|
+
onError?.('Video playback error');
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Debug: Log video events
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
const video = videoRef.current;
|
|
353
|
+
if (!video) return;
|
|
354
|
+
|
|
355
|
+
const handleLoadedMetadata = () => {
|
|
356
|
+
videoDebug.state('loadedmetadata', { duration: video.duration });
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const handleSeeking = () => {
|
|
360
|
+
videoDebug.event('seeking', { currentTime: video.currentTime });
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const handleSeeked = () => {
|
|
364
|
+
videoDebug.event('seeked', { currentTime: video.currentTime });
|
|
365
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const handleWaiting = () => {
|
|
369
|
+
videoDebug.warn('WAITING - buffering...');
|
|
370
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const handleStalled = () => {
|
|
374
|
+
videoDebug.warn('STALLED - network issue');
|
|
375
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
379
|
+
video.addEventListener('seeking', handleSeeking);
|
|
380
|
+
video.addEventListener('seeked', handleSeeked);
|
|
381
|
+
video.addEventListener('waiting', handleWaiting);
|
|
382
|
+
video.addEventListener('stalled', handleStalled);
|
|
383
|
+
|
|
384
|
+
return () => {
|
|
385
|
+
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
386
|
+
video.removeEventListener('seeking', handleSeeking);
|
|
387
|
+
video.removeEventListener('seeked', handleSeeked);
|
|
388
|
+
video.removeEventListener('waiting', handleWaiting);
|
|
389
|
+
video.removeEventListener('stalled', handleStalled);
|
|
390
|
+
};
|
|
391
|
+
}, [videoUrl]);
|
|
392
|
+
|
|
393
|
+
// Persist volume when user changes it via native controls
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
const video = videoRef.current;
|
|
396
|
+
if (!video) return;
|
|
397
|
+
|
|
398
|
+
const handleVolumeChange = () => {
|
|
399
|
+
updateVolume(video.volume);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
video.addEventListener('volumechange', handleVolumeChange);
|
|
403
|
+
return () => video.removeEventListener('volumechange', handleVolumeChange);
|
|
404
|
+
}, [videoUrl, updateVolume]);
|
|
405
|
+
|
|
406
|
+
// Determine if we should use AspectRatio wrapper or fill mode
|
|
407
|
+
const isFillMode = aspectRatio === 'fill';
|
|
408
|
+
const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
|
|
409
|
+
|
|
410
|
+
// Render error fallback
|
|
411
|
+
const renderErrorFallback = () => {
|
|
412
|
+
const fallbackProps: ErrorFallbackProps = { error: errorMessage, retry };
|
|
413
|
+
|
|
414
|
+
if (typeof errorFallback === 'function') {
|
|
415
|
+
return errorFallback(fallbackProps);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (errorFallback) {
|
|
419
|
+
return errorFallback;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return <DefaultErrorFallback {...fallbackProps} />;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Error state
|
|
426
|
+
if (!videoUrl || hasError) {
|
|
427
|
+
if (isFillMode) {
|
|
428
|
+
return (
|
|
429
|
+
<div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
|
|
430
|
+
{renderErrorFallback()}
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<div className={cn('relative overflow-hidden bg-black', className)}>
|
|
437
|
+
<AspectRatio ratio={computedAspectRatio}>
|
|
438
|
+
{renderErrorFallback()}
|
|
439
|
+
</AspectRatio>
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Video content
|
|
445
|
+
const videoContent = (
|
|
446
|
+
<>
|
|
447
|
+
{/* Loading indicator */}
|
|
448
|
+
{showPreloader && isLoading && (
|
|
449
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
|
|
450
|
+
<Preloader size="lg" spinnerClassName="text-white" />
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
|
|
454
|
+
{/* Video element */}
|
|
455
|
+
<video
|
|
456
|
+
ref={videoRef}
|
|
457
|
+
src={videoUrl}
|
|
458
|
+
className={cn(
|
|
459
|
+
'w-full h-full object-contain',
|
|
460
|
+
isLoading && 'opacity-0',
|
|
461
|
+
videoClassName
|
|
462
|
+
)}
|
|
463
|
+
autoPlay={autoPlay}
|
|
464
|
+
muted={muted}
|
|
465
|
+
loop={loop}
|
|
466
|
+
playsInline={playsInline}
|
|
467
|
+
preload={preload}
|
|
468
|
+
controls={controls}
|
|
469
|
+
crossOrigin="anonymous"
|
|
470
|
+
poster={source.poster}
|
|
471
|
+
onContextMenu={handleContextMenu}
|
|
472
|
+
onLoadStart={onLoadStart}
|
|
473
|
+
onCanPlay={handleCanPlay}
|
|
474
|
+
onLoadedData={handleLoadedData}
|
|
475
|
+
onPlay={onPlay}
|
|
476
|
+
onPause={handlePause}
|
|
477
|
+
onEnded={onEnded}
|
|
478
|
+
onError={handleError}
|
|
479
|
+
onTimeUpdate={handleTimeUpdate}
|
|
480
|
+
onProgress={handleProgress}
|
|
481
|
+
/>
|
|
482
|
+
</>
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// Fill mode - no AspectRatio wrapper
|
|
486
|
+
if (isFillMode) {
|
|
487
|
+
return (
|
|
488
|
+
<div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
|
|
489
|
+
{videoContent}
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Normal mode with AspectRatio
|
|
495
|
+
return (
|
|
496
|
+
<div className={cn('relative overflow-hidden bg-black', className)}>
|
|
497
|
+
<AspectRatio ratio={computedAspectRatio}>
|
|
498
|
+
{videoContent}
|
|
499
|
+
</AspectRatio>
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
StreamProvider.displayName = 'StreamProvider';
|