@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.
Files changed (174) hide show
  1. package/dist/LottiePlayer.client-LBEC2JKY.mjs +161 -0
  2. package/dist/LottiePlayer.client-LBEC2JKY.mjs.map +1 -0
  3. package/dist/LottiePlayer.client-WFMG2OOW.cjs +168 -0
  4. package/dist/LottiePlayer.client-WFMG2OOW.cjs.map +1 -0
  5. package/dist/Mermaid.client-4TU2TSH3.mjs +477 -0
  6. package/dist/Mermaid.client-4TU2TSH3.mjs.map +1 -0
  7. package/dist/Mermaid.client-SBYY364Q.cjs +483 -0
  8. package/dist/Mermaid.client-SBYY364Q.cjs.map +1 -0
  9. package/dist/PlaygroundLayout-3YVSAEAF.cjs +1003 -0
  10. package/dist/PlaygroundLayout-3YVSAEAF.cjs.map +1 -0
  11. package/dist/PlaygroundLayout-4DYBORAS.mjs +996 -0
  12. package/dist/PlaygroundLayout-4DYBORAS.mjs.map +1 -0
  13. package/dist/PrettyCode.client-LCBPPTIX.mjs +152 -0
  14. package/dist/PrettyCode.client-LCBPPTIX.mjs.map +1 -0
  15. package/dist/PrettyCode.client-PNPLXRH6.cjs +154 -0
  16. package/dist/PrettyCode.client-PNPLXRH6.cjs.map +1 -0
  17. package/dist/chunk-37ZI6VD4.mjs +12 -0
  18. package/dist/chunk-37ZI6VD4.mjs.map +1 -0
  19. package/dist/chunk-3HK2OE62.cjs +81 -0
  20. package/dist/chunk-3HK2OE62.cjs.map +1 -0
  21. package/dist/chunk-7DGDQVQW.cjs +591 -0
  22. package/dist/chunk-7DGDQVQW.cjs.map +1 -0
  23. package/dist/chunk-M6P2FU7L.mjs +572 -0
  24. package/dist/chunk-M6P2FU7L.mjs.map +1 -0
  25. package/dist/chunk-UQ3XI5MY.cjs +15 -0
  26. package/dist/chunk-UQ3XI5MY.cjs.map +1 -0
  27. package/dist/chunk-YFRNE2IR.mjs +79 -0
  28. package/dist/chunk-YFRNE2IR.mjs.map +1 -0
  29. package/dist/index.cjs +5042 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +1591 -0
  32. package/dist/index.d.ts +1591 -0
  33. package/dist/index.mjs +4941 -0
  34. package/dist/index.mjs.map +1 -0
  35. package/package.json +86 -0
  36. package/src/components/markdown/MarkdownMessage.tsx +340 -0
  37. package/src/components/markdown/index.ts +5 -0
  38. package/src/index.ts +26 -0
  39. package/src/stores/index.ts +9 -0
  40. package/src/stores/mediaCache.ts +534 -0
  41. package/src/tools/AudioPlayer/README.md +206 -0
  42. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
  43. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +280 -0
  44. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
  45. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +149 -0
  46. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  47. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  48. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  49. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  50. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  51. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  52. package/src/tools/AudioPlayer/components/index.ts +22 -0
  53. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +158 -0
  54. package/src/tools/AudioPlayer/context/index.ts +16 -0
  55. package/src/tools/AudioPlayer/effects/index.ts +412 -0
  56. package/src/tools/AudioPlayer/hooks/index.ts +35 -0
  57. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
  58. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
  59. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +207 -0
  60. package/src/tools/AudioPlayer/index.ts +133 -0
  61. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  62. package/src/tools/AudioPlayer/types/index.ts +27 -0
  63. package/src/tools/AudioPlayer/utils/debug.ts +14 -0
  64. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  65. package/src/tools/AudioPlayer/utils/index.ts +6 -0
  66. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  67. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  68. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  69. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  70. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  71. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  72. package/src/tools/ImageViewer/README.md +200 -0
  73. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  74. package/src/tools/ImageViewer/components/ImageToolbar.tsx +145 -0
  75. package/src/tools/ImageViewer/components/ImageViewer.tsx +241 -0
  76. package/src/tools/ImageViewer/components/index.ts +7 -0
  77. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  78. package/src/tools/ImageViewer/hooks/useImageLoading.ts +204 -0
  79. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  80. package/src/tools/ImageViewer/index.ts +60 -0
  81. package/src/tools/ImageViewer/types.ts +81 -0
  82. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  83. package/src/tools/ImageViewer/utils/debug.ts +14 -0
  84. package/src/tools/ImageViewer/utils/index.ts +17 -0
  85. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  86. package/src/tools/JsonForm/JsonSchemaForm.tsx +197 -0
  87. package/src/tools/JsonForm/examples/BotConfigExample.tsx +249 -0
  88. package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +161 -0
  89. package/src/tools/JsonForm/index.ts +46 -0
  90. package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +47 -0
  91. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +74 -0
  92. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +107 -0
  93. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +35 -0
  94. package/src/tools/JsonForm/templates/FieldTemplate.tsx +62 -0
  95. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +116 -0
  96. package/src/tools/JsonForm/templates/index.ts +12 -0
  97. package/src/tools/JsonForm/types.ts +83 -0
  98. package/src/tools/JsonForm/utils.ts +213 -0
  99. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +37 -0
  100. package/src/tools/JsonForm/widgets/ColorWidget.tsx +219 -0
  101. package/src/tools/JsonForm/widgets/NumberWidget.tsx +89 -0
  102. package/src/tools/JsonForm/widgets/SelectWidget.tsx +97 -0
  103. package/src/tools/JsonForm/widgets/SliderWidget.tsx +148 -0
  104. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +35 -0
  105. package/src/tools/JsonForm/widgets/TextWidget.tsx +96 -0
  106. package/src/tools/JsonForm/widgets/index.ts +14 -0
  107. package/src/tools/JsonTree/index.tsx +243 -0
  108. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +213 -0
  109. package/src/tools/LottiePlayer/index.tsx +56 -0
  110. package/src/tools/LottiePlayer/types.ts +108 -0
  111. package/src/tools/LottiePlayer/useLottie.ts +164 -0
  112. package/src/tools/Mermaid/Mermaid.client.tsx +82 -0
  113. package/src/tools/Mermaid/components/MermaidCodeViewer.tsx +95 -0
  114. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +103 -0
  115. package/src/tools/Mermaid/hooks/index.ts +4 -0
  116. package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +73 -0
  117. package/src/tools/Mermaid/hooks/useMermaidFullscreen.ts +46 -0
  118. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +226 -0
  119. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +29 -0
  120. package/src/tools/Mermaid/index.tsx +44 -0
  121. package/src/tools/Mermaid/utils/mermaid-helpers.ts +33 -0
  122. package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +149 -0
  123. package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +263 -0
  124. package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +125 -0
  125. package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +100 -0
  126. package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +157 -0
  127. package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
  128. package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +173 -0
  129. package/src/tools/OpenapiViewer/components/VersionSelector.tsx +68 -0
  130. package/src/tools/OpenapiViewer/components/index.ts +14 -0
  131. package/src/tools/OpenapiViewer/constants.ts +39 -0
  132. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +337 -0
  133. package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
  134. package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
  135. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +199 -0
  136. package/src/tools/OpenapiViewer/index.tsx +37 -0
  137. package/src/tools/OpenapiViewer/types.ts +151 -0
  138. package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
  139. package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
  140. package/src/tools/OpenapiViewer/utils/index.ts +9 -0
  141. package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
  142. package/src/tools/PrettyCode/PrettyCode.client.tsx +208 -0
  143. package/src/tools/PrettyCode/index.tsx +47 -0
  144. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  145. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  146. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  147. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  148. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  149. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  150. package/src/tools/VideoPlayer/README.md +264 -0
  151. package/src/tools/VideoPlayer/components/VideoControls.tsx +138 -0
  152. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +172 -0
  153. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
  154. package/src/tools/VideoPlayer/components/index.ts +14 -0
  155. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  156. package/src/tools/VideoPlayer/context/index.ts +8 -0
  157. package/src/tools/VideoPlayer/hooks/index.ts +12 -0
  158. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +70 -0
  159. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +116 -0
  160. package/src/tools/VideoPlayer/index.ts +77 -0
  161. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +284 -0
  162. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +505 -0
  163. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +400 -0
  164. package/src/tools/VideoPlayer/providers/index.ts +8 -0
  165. package/src/tools/VideoPlayer/types/index.ts +38 -0
  166. package/src/tools/VideoPlayer/types/player.ts +116 -0
  167. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  168. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  169. package/src/tools/VideoPlayer/utils/debug.ts +14 -0
  170. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  171. package/src/tools/VideoPlayer/utils/index.ts +12 -0
  172. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  173. package/src/tools/_shared.ts +29 -0
  174. 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';