ai-design-system 0.1.105 → 0.1.107
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/components/composites/ScenePlayer/ScenePlayer.tsx +217 -42
- package/dist/index.cjs +196 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +4 -0
- package/dist/index.d.ts +15 -15
- package/dist/index.js +196 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -94,6 +94,27 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
94
94
|
return 0;
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// Dual-video buffer for gapless playback between scenes:
|
|
98
|
+
// One slot actively plays the current scene, while the other slot preloads the next scene in the background.
|
|
99
|
+
const [activeSlot, setActiveSlot] = React.useState<0 | 1>(0);
|
|
100
|
+
const [slot0Index, setSlot0Index] = React.useState<number>(() => {
|
|
101
|
+
if (initialSceneIndex >= 0 && initialSceneIndex < scenes.length) {
|
|
102
|
+
return initialSceneIndex;
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
});
|
|
106
|
+
const [slot1Index, setSlot1Index] = React.useState<number>(() => {
|
|
107
|
+
if (scenes.length > 1) {
|
|
108
|
+
const init = initialSceneIndex >= 0 && initialSceneIndex < scenes.length ? initialSceneIndex : 0;
|
|
109
|
+
return (init + 1) % scenes.length;
|
|
110
|
+
}
|
|
111
|
+
return 0;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const videoRef0 = React.useRef<HTMLVideoElement | null>(null);
|
|
115
|
+
const videoRef1 = React.useRef<HTMLVideoElement | null>(null);
|
|
116
|
+
const isTransitioningRef = React.useRef<boolean>(false);
|
|
117
|
+
|
|
97
118
|
const [isPlayingState, setIsPlayingState] = React.useState<boolean>(isPlayingProp ?? true);
|
|
98
119
|
const isPlaying = isPlayingProp !== undefined ? isPlayingProp : isPlayingState;
|
|
99
120
|
|
|
@@ -111,10 +132,82 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
111
132
|
}, [setIsPlaying]);
|
|
112
133
|
|
|
113
134
|
const [isMuted, setIsMuted] = React.useState<boolean>(initialMuted);
|
|
114
|
-
|
|
135
|
+
|
|
136
|
+
const toggleMute = React.useCallback(() => {
|
|
137
|
+
setIsMuted((prev) => {
|
|
138
|
+
const next = !prev;
|
|
139
|
+
if (videoRef0.current) videoRef0.current.muted = next;
|
|
140
|
+
if (videoRef1.current) videoRef1.current.muted = next;
|
|
141
|
+
return next;
|
|
142
|
+
});
|
|
143
|
+
}, []);
|
|
115
144
|
|
|
116
145
|
const activeScene = scenes[currentSceneIndex] || scenes[0];
|
|
117
146
|
|
|
147
|
+
const goToScene = React.useCallback(
|
|
148
|
+
(targetIdx: number) => {
|
|
149
|
+
if (!scenes.length) return;
|
|
150
|
+
const clampedIdx = (targetIdx + scenes.length) % scenes.length;
|
|
151
|
+
onSceneChange?.(clampedIdx, scenes[clampedIdx]);
|
|
152
|
+
setCurrentSceneIndex(clampedIdx);
|
|
153
|
+
isTransitioningRef.current = false;
|
|
154
|
+
|
|
155
|
+
if (scenes.length <= 1) {
|
|
156
|
+
setSlot0Index(clampedIdx);
|
|
157
|
+
const v0 = videoRef0.current;
|
|
158
|
+
if (v0) {
|
|
159
|
+
v0.currentTime = 0;
|
|
160
|
+
v0.muted = isMuted;
|
|
161
|
+
if (isPlaying) {
|
|
162
|
+
v0.play().catch((err) => {
|
|
163
|
+
if (err?.name === "NotAllowedError") {
|
|
164
|
+
v0.muted = true;
|
|
165
|
+
v0.play().catch(() => {});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const nextActiveSlot: 0 | 1 = activeSlot === 0 ? 1 : 0;
|
|
174
|
+
const nextVideo = nextActiveSlot === 0 ? videoRef0.current : videoRef1.current;
|
|
175
|
+
const currentVideo = activeSlot === 0 ? videoRef0.current : videoRef1.current;
|
|
176
|
+
const sceneAfterTarget = (clampedIdx + 1) % scenes.length;
|
|
177
|
+
|
|
178
|
+
// 1. Immediately start next video synchronously (0ms transition gap)
|
|
179
|
+
if (nextVideo) {
|
|
180
|
+
nextVideo.currentTime = 0;
|
|
181
|
+
nextVideo.muted = isMuted;
|
|
182
|
+
if (isPlaying) {
|
|
183
|
+
nextVideo.play().catch((err) => {
|
|
184
|
+
if (err?.name === "NotAllowedError") {
|
|
185
|
+
nextVideo.muted = true;
|
|
186
|
+
nextVideo.play().catch(() => {});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 2. Pause previous video
|
|
193
|
+
if (currentVideo) {
|
|
194
|
+
currentVideo.pause();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 3. Swap slots and prepare idle slot to preload upcoming scene
|
|
198
|
+
if (nextActiveSlot === 0) {
|
|
199
|
+
setSlot0Index(clampedIdx);
|
|
200
|
+
setSlot1Index(sceneAfterTarget);
|
|
201
|
+
} else {
|
|
202
|
+
setSlot1Index(clampedIdx);
|
|
203
|
+
setSlot0Index(sceneAfterTarget);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setActiveSlot(nextActiveSlot);
|
|
207
|
+
},
|
|
208
|
+
[scenes, activeSlot, isPlaying, isMuted, onSceneChange]
|
|
209
|
+
);
|
|
210
|
+
|
|
118
211
|
const handlePromptSubmit = React.useCallback(
|
|
119
212
|
(message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => {
|
|
120
213
|
if (onSubmit) {
|
|
@@ -130,30 +223,34 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
130
223
|
|
|
131
224
|
const handlePrev = React.useCallback(() => {
|
|
132
225
|
if (!scenes.length) return;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return nextIdx;
|
|
137
|
-
});
|
|
138
|
-
}, [scenes, onSceneChange]);
|
|
226
|
+
const prevIdx = currentSceneIndex > 0 ? currentSceneIndex - 1 : scenes.length - 1;
|
|
227
|
+
goToScene(prevIdx);
|
|
228
|
+
}, [scenes.length, currentSceneIndex, goToScene]);
|
|
139
229
|
|
|
140
230
|
const handleNext = React.useCallback(() => {
|
|
141
231
|
if (!scenes.length) return;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return nextIdx;
|
|
146
|
-
});
|
|
147
|
-
}, [scenes, onSceneChange]);
|
|
232
|
+
const nextIdx = currentSceneIndex < scenes.length - 1 ? currentSceneIndex + 1 : 0;
|
|
233
|
+
goToScene(nextIdx);
|
|
234
|
+
}, [scenes.length, currentSceneIndex, goToScene]);
|
|
148
235
|
|
|
149
236
|
const handleSceneEnded = React.useCallback(() => {
|
|
150
|
-
if (!scenes.length || !isPlaying) return;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
237
|
+
if (!scenes.length || !isPlaying || isTransitioningRef.current) return;
|
|
238
|
+
isTransitioningRef.current = true;
|
|
239
|
+
handleNext();
|
|
240
|
+
}, [scenes.length, isPlaying, handleNext]);
|
|
241
|
+
|
|
242
|
+
const handleTimeUpdate = React.useCallback(
|
|
243
|
+
(e: React.SyntheticEvent<HTMLVideoElement>) => {
|
|
244
|
+
const video = e.currentTarget;
|
|
245
|
+
if (!scenes.length || !isPlaying || isTransitioningRef.current) return;
|
|
246
|
+
// Trigger next scene 60ms before end so decoder starts seamlessly with zero gap
|
|
247
|
+
if (video.duration > 0.1 && video.currentTime >= video.duration - 0.06) {
|
|
248
|
+
isTransitioningRef.current = true;
|
|
249
|
+
handleNext();
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
[scenes.length, isPlaying, handleNext]
|
|
253
|
+
);
|
|
157
254
|
|
|
158
255
|
const handleDownload = React.useCallback(async () => {
|
|
159
256
|
if (onDownload) {
|
|
@@ -187,28 +284,84 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
187
284
|
}
|
|
188
285
|
}, [onDownload, downloadUrl, videoUrl, activeScene?.scene_clip_url]);
|
|
189
286
|
|
|
190
|
-
// Playback & mute synchronization
|
|
287
|
+
// Playback & mute synchronization across dual slots
|
|
191
288
|
React.useEffect(() => {
|
|
192
|
-
const
|
|
193
|
-
|
|
289
|
+
const activeVideo = activeSlot === 0 ? videoRef0.current : videoRef1.current;
|
|
290
|
+
const idleVideo = activeSlot === 0 ? videoRef1.current : videoRef0.current;
|
|
194
291
|
|
|
195
|
-
|
|
292
|
+
if (idleVideo) {
|
|
293
|
+
idleVideo.pause();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!activeVideo) return;
|
|
297
|
+
|
|
298
|
+
activeVideo.muted = isMuted;
|
|
196
299
|
|
|
197
300
|
if (isPlaying) {
|
|
198
|
-
|
|
301
|
+
if (activeVideo.ended) {
|
|
302
|
+
activeVideo.currentTime = 0;
|
|
303
|
+
}
|
|
304
|
+
const playPromise = activeVideo.play();
|
|
199
305
|
if (playPromise !== undefined) {
|
|
200
|
-
playPromise.catch(() => {
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
video.play().catch(() => {});
|
|
306
|
+
playPromise.catch((err) => {
|
|
307
|
+
if (err?.name === "NotAllowedError") {
|
|
308
|
+
activeVideo.muted = true;
|
|
309
|
+
activeVideo.play().catch(() => {});
|
|
205
310
|
}
|
|
206
311
|
});
|
|
207
312
|
}
|
|
208
313
|
} else {
|
|
209
|
-
|
|
314
|
+
activeVideo.pause();
|
|
210
315
|
}
|
|
211
|
-
}, [
|
|
316
|
+
}, [activeSlot, isPlaying, isMuted]);
|
|
317
|
+
|
|
318
|
+
// Ensure idle video element actively loads and buffers upcoming scene
|
|
319
|
+
React.useEffect(() => {
|
|
320
|
+
if (scenes.length <= 1) return;
|
|
321
|
+
const idleVideo = activeSlot === 0 ? videoRef1.current : videoRef0.current;
|
|
322
|
+
const idleIndex = activeSlot === 0 ? slot1Index : slot0Index;
|
|
323
|
+
const idleUrl = scenes[idleIndex]?.scene_clip_url;
|
|
324
|
+
|
|
325
|
+
if (idleVideo && idleUrl) {
|
|
326
|
+
if (idleVideo.src !== idleUrl) {
|
|
327
|
+
idleVideo.src = idleUrl;
|
|
328
|
+
}
|
|
329
|
+
idleVideo.preload = "auto";
|
|
330
|
+
idleVideo.load();
|
|
331
|
+
}
|
|
332
|
+
}, [activeSlot, slot0Index, slot1Index, scenes]);
|
|
333
|
+
|
|
334
|
+
// Prefetch upcoming clips into browser HTTP cache
|
|
335
|
+
React.useEffect(() => {
|
|
336
|
+
if (!scenes || scenes.length <= 2) return;
|
|
337
|
+
const toPrefetch = [
|
|
338
|
+
(currentSceneIndex + 2) % scenes.length,
|
|
339
|
+
(currentSceneIndex + 3) % scenes.length,
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
toPrefetch.forEach((idx) => {
|
|
343
|
+
const url = scenes[idx]?.scene_clip_url;
|
|
344
|
+
if (url) {
|
|
345
|
+
const link = document.createElement("link");
|
|
346
|
+
link.rel = "prefetch";
|
|
347
|
+
link.href = url;
|
|
348
|
+
document.head.appendChild(link);
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
if (link.parentNode) link.parentNode.removeChild(link);
|
|
351
|
+
}, 15000);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}, [currentSceneIndex, scenes]);
|
|
355
|
+
|
|
356
|
+
React.useEffect(() => {
|
|
357
|
+
if (!scenes || scenes.length === 0) return;
|
|
358
|
+
if (currentSceneIndex >= scenes.length) {
|
|
359
|
+
setCurrentSceneIndex(0);
|
|
360
|
+
setSlot0Index(0);
|
|
361
|
+
setSlot1Index(scenes.length > 1 ? 1 : 0);
|
|
362
|
+
setActiveSlot(0);
|
|
363
|
+
}
|
|
364
|
+
}, [scenes, currentSceneIndex]);
|
|
212
365
|
|
|
213
366
|
// Keyboard navigation & playback controls
|
|
214
367
|
React.useEffect(() => {
|
|
@@ -237,7 +390,7 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
237
390
|
handleBack?.();
|
|
238
391
|
} else if (e.key === "m" || e.key === "M") {
|
|
239
392
|
e.preventDefault();
|
|
240
|
-
|
|
393
|
+
toggleMute();
|
|
241
394
|
}
|
|
242
395
|
};
|
|
243
396
|
|
|
@@ -345,7 +498,7 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
345
498
|
size="icon"
|
|
346
499
|
data-testid="btn-toggle-mute"
|
|
347
500
|
aria-label={isMuted ? "Unmute audio" : "Mute audio"}
|
|
348
|
-
onClick={
|
|
501
|
+
onClick={toggleMute}
|
|
349
502
|
className="h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer"
|
|
350
503
|
>
|
|
351
504
|
<Icon name={isMuted ? "volume-x" : "volume-2"} className="w-4 h-4" />
|
|
@@ -367,20 +520,42 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
|
|
|
367
520
|
|
|
368
521
|
{/* Main Scene Video Player Stage */}
|
|
369
522
|
<div className="flex-1 relative flex items-center justify-center bg-black overflow-hidden min-h-0">
|
|
523
|
+
{/* Slot 0 Video */}
|
|
370
524
|
<video
|
|
371
|
-
ref={
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
autoPlay
|
|
525
|
+
ref={videoRef0}
|
|
526
|
+
src={scenes[slot0Index]?.scene_clip_url}
|
|
527
|
+
preload="auto"
|
|
375
528
|
playsInline
|
|
376
|
-
loop={false}
|
|
377
529
|
muted={isMuted}
|
|
378
|
-
onEnded={handleSceneEnded}
|
|
379
|
-
|
|
530
|
+
onEnded={activeSlot === 0 ? handleSceneEnded : undefined}
|
|
531
|
+
onTimeUpdate={activeSlot === 0 ? handleTimeUpdate : undefined}
|
|
532
|
+
data-testid="scene-video-0"
|
|
533
|
+
className={cn(
|
|
534
|
+
"w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-75",
|
|
535
|
+
activeSlot === 0 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
|
|
536
|
+
)}
|
|
380
537
|
/>
|
|
381
538
|
|
|
539
|
+
{/* Slot 1 Video (preloads next scene in background for gapless playback) */}
|
|
540
|
+
{scenes.length > 1 && (
|
|
541
|
+
<video
|
|
542
|
+
ref={videoRef1}
|
|
543
|
+
src={scenes[slot1Index]?.scene_clip_url}
|
|
544
|
+
preload="auto"
|
|
545
|
+
playsInline
|
|
546
|
+
muted={isMuted}
|
|
547
|
+
onEnded={activeSlot === 1 ? handleSceneEnded : undefined}
|
|
548
|
+
onTimeUpdate={activeSlot === 1 ? handleTimeUpdate : undefined}
|
|
549
|
+
data-testid="scene-video-1"
|
|
550
|
+
className={cn(
|
|
551
|
+
"w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-75",
|
|
552
|
+
activeSlot === 1 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
|
|
553
|
+
)}
|
|
554
|
+
/>
|
|
555
|
+
)}
|
|
556
|
+
|
|
382
557
|
{/* Invisible Interactive Tap Zones for Stepping Scenes */}
|
|
383
|
-
<div className="absolute inset-x-0 top-[60px] bottom-[70px] flex z-
|
|
558
|
+
<div className="absolute inset-x-0 top-[60px] bottom-[70px] flex z-20">
|
|
384
559
|
<Button
|
|
385
560
|
variant="ghost"
|
|
386
561
|
data-testid="btn-prev-scene"
|
package/dist/index.cjs
CHANGED
|
@@ -14692,7 +14692,7 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14692
14692
|
"onSceneChange",
|
|
14693
14693
|
"className"
|
|
14694
14694
|
]);
|
|
14695
|
-
var _a2;
|
|
14695
|
+
var _a2, _b2, _c;
|
|
14696
14696
|
const handleBack = onBack || onClose;
|
|
14697
14697
|
const [currentSceneIndex, setCurrentSceneIndex] = React3__namespace.useState(() => {
|
|
14698
14698
|
if (initialSceneIndex >= 0 && initialSceneIndex < scenes.length) {
|
|
@@ -14700,6 +14700,23 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14700
14700
|
}
|
|
14701
14701
|
return 0;
|
|
14702
14702
|
});
|
|
14703
|
+
const [activeSlot, setActiveSlot] = React3__namespace.useState(0);
|
|
14704
|
+
const [slot0Index, setSlot0Index] = React3__namespace.useState(() => {
|
|
14705
|
+
if (initialSceneIndex >= 0 && initialSceneIndex < scenes.length) {
|
|
14706
|
+
return initialSceneIndex;
|
|
14707
|
+
}
|
|
14708
|
+
return 0;
|
|
14709
|
+
});
|
|
14710
|
+
const [slot1Index, setSlot1Index] = React3__namespace.useState(() => {
|
|
14711
|
+
if (scenes.length > 1) {
|
|
14712
|
+
const init = initialSceneIndex >= 0 && initialSceneIndex < scenes.length ? initialSceneIndex : 0;
|
|
14713
|
+
return (init + 1) % scenes.length;
|
|
14714
|
+
}
|
|
14715
|
+
return 0;
|
|
14716
|
+
});
|
|
14717
|
+
const videoRef0 = React3__namespace.useRef(null);
|
|
14718
|
+
const videoRef1 = React3__namespace.useRef(null);
|
|
14719
|
+
const isTransitioningRef = React3__namespace.useRef(false);
|
|
14703
14720
|
const [isPlayingState, setIsPlayingState] = React3__namespace.useState(isPlayingProp != null ? isPlayingProp : true);
|
|
14704
14721
|
const isPlaying = isPlayingProp !== void 0 ? isPlayingProp : isPlayingState;
|
|
14705
14722
|
const setIsPlaying = React3__namespace.useCallback(
|
|
@@ -14714,8 +14731,71 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14714
14731
|
setIsPlaying((prev) => !prev);
|
|
14715
14732
|
}, [setIsPlaying]);
|
|
14716
14733
|
const [isMuted, setIsMuted] = React3__namespace.useState(initialMuted);
|
|
14717
|
-
const
|
|
14734
|
+
const toggleMute = React3__namespace.useCallback(() => {
|
|
14735
|
+
setIsMuted((prev) => {
|
|
14736
|
+
const next = !prev;
|
|
14737
|
+
if (videoRef0.current) videoRef0.current.muted = next;
|
|
14738
|
+
if (videoRef1.current) videoRef1.current.muted = next;
|
|
14739
|
+
return next;
|
|
14740
|
+
});
|
|
14741
|
+
}, []);
|
|
14718
14742
|
const activeScene = scenes[currentSceneIndex] || scenes[0];
|
|
14743
|
+
const goToScene = React3__namespace.useCallback(
|
|
14744
|
+
(targetIdx) => {
|
|
14745
|
+
if (!scenes.length) return;
|
|
14746
|
+
const clampedIdx = (targetIdx + scenes.length) % scenes.length;
|
|
14747
|
+
onSceneChange == null ? void 0 : onSceneChange(clampedIdx, scenes[clampedIdx]);
|
|
14748
|
+
setCurrentSceneIndex(clampedIdx);
|
|
14749
|
+
isTransitioningRef.current = false;
|
|
14750
|
+
if (scenes.length <= 1) {
|
|
14751
|
+
setSlot0Index(clampedIdx);
|
|
14752
|
+
const v0 = videoRef0.current;
|
|
14753
|
+
if (v0) {
|
|
14754
|
+
v0.currentTime = 0;
|
|
14755
|
+
v0.muted = isMuted;
|
|
14756
|
+
if (isPlaying) {
|
|
14757
|
+
v0.play().catch((err) => {
|
|
14758
|
+
if ((err == null ? void 0 : err.name) === "NotAllowedError") {
|
|
14759
|
+
v0.muted = true;
|
|
14760
|
+
v0.play().catch(() => {
|
|
14761
|
+
});
|
|
14762
|
+
}
|
|
14763
|
+
});
|
|
14764
|
+
}
|
|
14765
|
+
}
|
|
14766
|
+
return;
|
|
14767
|
+
}
|
|
14768
|
+
const nextActiveSlot = activeSlot === 0 ? 1 : 0;
|
|
14769
|
+
const nextVideo = nextActiveSlot === 0 ? videoRef0.current : videoRef1.current;
|
|
14770
|
+
const currentVideo = activeSlot === 0 ? videoRef0.current : videoRef1.current;
|
|
14771
|
+
const sceneAfterTarget = (clampedIdx + 1) % scenes.length;
|
|
14772
|
+
if (nextVideo) {
|
|
14773
|
+
nextVideo.currentTime = 0;
|
|
14774
|
+
nextVideo.muted = isMuted;
|
|
14775
|
+
if (isPlaying) {
|
|
14776
|
+
nextVideo.play().catch((err) => {
|
|
14777
|
+
if ((err == null ? void 0 : err.name) === "NotAllowedError") {
|
|
14778
|
+
nextVideo.muted = true;
|
|
14779
|
+
nextVideo.play().catch(() => {
|
|
14780
|
+
});
|
|
14781
|
+
}
|
|
14782
|
+
});
|
|
14783
|
+
}
|
|
14784
|
+
}
|
|
14785
|
+
if (currentVideo) {
|
|
14786
|
+
currentVideo.pause();
|
|
14787
|
+
}
|
|
14788
|
+
if (nextActiveSlot === 0) {
|
|
14789
|
+
setSlot0Index(clampedIdx);
|
|
14790
|
+
setSlot1Index(sceneAfterTarget);
|
|
14791
|
+
} else {
|
|
14792
|
+
setSlot1Index(clampedIdx);
|
|
14793
|
+
setSlot0Index(sceneAfterTarget);
|
|
14794
|
+
}
|
|
14795
|
+
setActiveSlot(nextActiveSlot);
|
|
14796
|
+
},
|
|
14797
|
+
[scenes, activeSlot, isPlaying, isMuted, onSceneChange]
|
|
14798
|
+
);
|
|
14719
14799
|
const handlePromptSubmit = React3__namespace.useCallback(
|
|
14720
14800
|
(message, event) => {
|
|
14721
14801
|
if (onSubmit) {
|
|
@@ -14730,30 +14810,32 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14730
14810
|
);
|
|
14731
14811
|
const handlePrev = React3__namespace.useCallback(() => {
|
|
14732
14812
|
if (!scenes.length) return;
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
return nextIdx;
|
|
14737
|
-
});
|
|
14738
|
-
}, [scenes, onSceneChange]);
|
|
14813
|
+
const prevIdx = currentSceneIndex > 0 ? currentSceneIndex - 1 : scenes.length - 1;
|
|
14814
|
+
goToScene(prevIdx);
|
|
14815
|
+
}, [scenes.length, currentSceneIndex, goToScene]);
|
|
14739
14816
|
const handleNext = React3__namespace.useCallback(() => {
|
|
14740
14817
|
if (!scenes.length) return;
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
return nextIdx;
|
|
14745
|
-
});
|
|
14746
|
-
}, [scenes, onSceneChange]);
|
|
14818
|
+
const nextIdx = currentSceneIndex < scenes.length - 1 ? currentSceneIndex + 1 : 0;
|
|
14819
|
+
goToScene(nextIdx);
|
|
14820
|
+
}, [scenes.length, currentSceneIndex, goToScene]);
|
|
14747
14821
|
const handleSceneEnded = React3__namespace.useCallback(() => {
|
|
14748
|
-
if (!scenes.length || !isPlaying) return;
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14822
|
+
if (!scenes.length || !isPlaying || isTransitioningRef.current) return;
|
|
14823
|
+
isTransitioningRef.current = true;
|
|
14824
|
+
handleNext();
|
|
14825
|
+
}, [scenes.length, isPlaying, handleNext]);
|
|
14826
|
+
const handleTimeUpdate = React3__namespace.useCallback(
|
|
14827
|
+
(e) => {
|
|
14828
|
+
const video = e.currentTarget;
|
|
14829
|
+
if (!scenes.length || !isPlaying || isTransitioningRef.current) return;
|
|
14830
|
+
if (video.duration > 0.1 && video.currentTime >= video.duration - 0.06) {
|
|
14831
|
+
isTransitioningRef.current = true;
|
|
14832
|
+
handleNext();
|
|
14833
|
+
}
|
|
14834
|
+
},
|
|
14835
|
+
[scenes.length, isPlaying, handleNext]
|
|
14836
|
+
);
|
|
14755
14837
|
const handleDownload = React3__namespace.useCallback(async () => {
|
|
14756
|
-
var _a3,
|
|
14838
|
+
var _a3, _b3;
|
|
14757
14839
|
if (onDownload) {
|
|
14758
14840
|
onDownload();
|
|
14759
14841
|
return;
|
|
@@ -14775,7 +14857,7 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14775
14857
|
} catch (e) {
|
|
14776
14858
|
const link = document.createElement("a");
|
|
14777
14859
|
link.href = targetUrl;
|
|
14778
|
-
link.download = ((
|
|
14860
|
+
link.download = ((_b3 = targetUrl.split("/").pop()) == null ? void 0 : _b3.split("?")[0]) || "video.mp4";
|
|
14779
14861
|
link.target = "_blank";
|
|
14780
14862
|
link.rel = "noopener noreferrer";
|
|
14781
14863
|
document.body.appendChild(link);
|
|
@@ -14784,25 +14866,74 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14784
14866
|
}
|
|
14785
14867
|
}, [onDownload, downloadUrl, videoUrl, activeScene == null ? void 0 : activeScene.scene_clip_url]);
|
|
14786
14868
|
React3__namespace.useEffect(() => {
|
|
14787
|
-
const
|
|
14788
|
-
|
|
14789
|
-
|
|
14869
|
+
const activeVideo = activeSlot === 0 ? videoRef0.current : videoRef1.current;
|
|
14870
|
+
const idleVideo = activeSlot === 0 ? videoRef1.current : videoRef0.current;
|
|
14871
|
+
if (idleVideo) {
|
|
14872
|
+
idleVideo.pause();
|
|
14873
|
+
}
|
|
14874
|
+
if (!activeVideo) return;
|
|
14875
|
+
activeVideo.muted = isMuted;
|
|
14790
14876
|
if (isPlaying) {
|
|
14791
|
-
|
|
14877
|
+
if (activeVideo.ended) {
|
|
14878
|
+
activeVideo.currentTime = 0;
|
|
14879
|
+
}
|
|
14880
|
+
const playPromise = activeVideo.play();
|
|
14792
14881
|
if (playPromise !== void 0) {
|
|
14793
|
-
playPromise.catch(() => {
|
|
14794
|
-
if (
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
video.play().catch(() => {
|
|
14882
|
+
playPromise.catch((err) => {
|
|
14883
|
+
if ((err == null ? void 0 : err.name) === "NotAllowedError") {
|
|
14884
|
+
activeVideo.muted = true;
|
|
14885
|
+
activeVideo.play().catch(() => {
|
|
14798
14886
|
});
|
|
14799
14887
|
}
|
|
14800
14888
|
});
|
|
14801
14889
|
}
|
|
14802
14890
|
} else {
|
|
14803
|
-
|
|
14891
|
+
activeVideo.pause();
|
|
14892
|
+
}
|
|
14893
|
+
}, [activeSlot, isPlaying, isMuted]);
|
|
14894
|
+
React3__namespace.useEffect(() => {
|
|
14895
|
+
var _a3;
|
|
14896
|
+
if (scenes.length <= 1) return;
|
|
14897
|
+
const idleVideo = activeSlot === 0 ? videoRef1.current : videoRef0.current;
|
|
14898
|
+
const idleIndex = activeSlot === 0 ? slot1Index : slot0Index;
|
|
14899
|
+
const idleUrl = (_a3 = scenes[idleIndex]) == null ? void 0 : _a3.scene_clip_url;
|
|
14900
|
+
if (idleVideo && idleUrl) {
|
|
14901
|
+
if (idleVideo.src !== idleUrl) {
|
|
14902
|
+
idleVideo.src = idleUrl;
|
|
14903
|
+
}
|
|
14904
|
+
idleVideo.preload = "auto";
|
|
14905
|
+
idleVideo.load();
|
|
14906
|
+
}
|
|
14907
|
+
}, [activeSlot, slot0Index, slot1Index, scenes]);
|
|
14908
|
+
React3__namespace.useEffect(() => {
|
|
14909
|
+
if (!scenes || scenes.length <= 2) return;
|
|
14910
|
+
const toPrefetch = [
|
|
14911
|
+
(currentSceneIndex + 2) % scenes.length,
|
|
14912
|
+
(currentSceneIndex + 3) % scenes.length
|
|
14913
|
+
];
|
|
14914
|
+
toPrefetch.forEach((idx) => {
|
|
14915
|
+
var _a3;
|
|
14916
|
+
const url = (_a3 = scenes[idx]) == null ? void 0 : _a3.scene_clip_url;
|
|
14917
|
+
if (url) {
|
|
14918
|
+
const link = document.createElement("link");
|
|
14919
|
+
link.rel = "prefetch";
|
|
14920
|
+
link.href = url;
|
|
14921
|
+
document.head.appendChild(link);
|
|
14922
|
+
setTimeout(() => {
|
|
14923
|
+
if (link.parentNode) link.parentNode.removeChild(link);
|
|
14924
|
+
}, 15e3);
|
|
14925
|
+
}
|
|
14926
|
+
});
|
|
14927
|
+
}, [currentSceneIndex, scenes]);
|
|
14928
|
+
React3__namespace.useEffect(() => {
|
|
14929
|
+
if (!scenes || scenes.length === 0) return;
|
|
14930
|
+
if (currentSceneIndex >= scenes.length) {
|
|
14931
|
+
setCurrentSceneIndex(0);
|
|
14932
|
+
setSlot0Index(0);
|
|
14933
|
+
setSlot1Index(scenes.length > 1 ? 1 : 0);
|
|
14934
|
+
setActiveSlot(0);
|
|
14804
14935
|
}
|
|
14805
|
-
}, [
|
|
14936
|
+
}, [scenes, currentSceneIndex]);
|
|
14806
14937
|
React3__namespace.useEffect(() => {
|
|
14807
14938
|
const handleKeyDown = (e) => {
|
|
14808
14939
|
const target = e.target;
|
|
@@ -14823,7 +14954,7 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14823
14954
|
handleBack == null ? void 0 : handleBack();
|
|
14824
14955
|
} else if (e.key === "m" || e.key === "M") {
|
|
14825
14956
|
e.preventDefault();
|
|
14826
|
-
|
|
14957
|
+
toggleMute();
|
|
14827
14958
|
}
|
|
14828
14959
|
};
|
|
14829
14960
|
window.addEventListener("keydown", handleKeyDown);
|
|
@@ -14928,7 +15059,7 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14928
15059
|
size: "icon",
|
|
14929
15060
|
"data-testid": "btn-toggle-mute",
|
|
14930
15061
|
"aria-label": isMuted ? "Unmute audio" : "Mute audio",
|
|
14931
|
-
onClick:
|
|
15062
|
+
onClick: toggleMute,
|
|
14932
15063
|
className: "h-8 w-8 rounded-full bg-black/40 hover:bg-white/20 text-white p-0 cursor-pointer",
|
|
14933
15064
|
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: isMuted ? "volume-x" : "volume-2", className: "w-4 h-4" })
|
|
14934
15065
|
}
|
|
@@ -14951,18 +15082,38 @@ var ScenePlayer = React3__namespace.forwardRef(
|
|
|
14951
15082
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14952
15083
|
"video",
|
|
14953
15084
|
{
|
|
14954
|
-
ref:
|
|
14955
|
-
src:
|
|
14956
|
-
|
|
15085
|
+
ref: videoRef0,
|
|
15086
|
+
src: (_b2 = scenes[slot0Index]) == null ? void 0 : _b2.scene_clip_url,
|
|
15087
|
+
preload: "auto",
|
|
14957
15088
|
playsInline: true,
|
|
14958
|
-
loop: false,
|
|
14959
15089
|
muted: isMuted,
|
|
14960
|
-
onEnded: handleSceneEnded,
|
|
14961
|
-
|
|
14962
|
-
|
|
14963
|
-
|
|
15090
|
+
onEnded: activeSlot === 0 ? handleSceneEnded : void 0,
|
|
15091
|
+
onTimeUpdate: activeSlot === 0 ? handleTimeUpdate : void 0,
|
|
15092
|
+
"data-testid": "scene-video-0",
|
|
15093
|
+
className: cn(
|
|
15094
|
+
"w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-75",
|
|
15095
|
+
activeSlot === 0 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
|
|
15096
|
+
)
|
|
15097
|
+
}
|
|
15098
|
+
),
|
|
15099
|
+
scenes.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
15100
|
+
"video",
|
|
15101
|
+
{
|
|
15102
|
+
ref: videoRef1,
|
|
15103
|
+
src: (_c = scenes[slot1Index]) == null ? void 0 : _c.scene_clip_url,
|
|
15104
|
+
preload: "auto",
|
|
15105
|
+
playsInline: true,
|
|
15106
|
+
muted: isMuted,
|
|
15107
|
+
onEnded: activeSlot === 1 ? handleSceneEnded : void 0,
|
|
15108
|
+
onTimeUpdate: activeSlot === 1 ? handleTimeUpdate : void 0,
|
|
15109
|
+
"data-testid": "scene-video-1",
|
|
15110
|
+
className: cn(
|
|
15111
|
+
"w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-75",
|
|
15112
|
+
activeSlot === 1 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
|
|
15113
|
+
)
|
|
15114
|
+
}
|
|
14964
15115
|
),
|
|
14965
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-x-0 top-[60px] bottom-[70px] flex z-
|
|
15116
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-x-0 top-[60px] bottom-[70px] flex z-20", children: [
|
|
14966
15117
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14967
15118
|
Button2,
|
|
14968
15119
|
{
|