@vanillaskyai/video 0.10.7 → 0.10.8
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/CHANGELOG.md +10 -0
- package/dist/check-runtime.js +2 -2
- package/dist/{chunk-OBRKB7PL.js → chunk-J7EGPURK.js} +291 -127
- package/dist/{chunk-24TCMDAA.js → chunk-KUCEOG2L.js} +31 -94
- package/dist/{chunk-PMJNBR4F.js → chunk-Q3LPOPHL.js} +1 -1
- package/dist/{chunk-MKMCX3AF.js → chunk-R5ZX2LJV.js} +1 -1
- package/dist/{cinema-media-BVQFZL3A.js → cinema-media-WUS7CKPC.js} +3 -4
- package/dist/{comparison-T27C7FSV.js → comparison-T4OYCOH3.js} +3 -4
- package/dist/{editorial-timeline-JADHHNEV.js → editorial-timeline-JHQZIPNE.js} +3 -4
- package/dist/{key-figure-TZQWPFMI.js → key-figure-YWN2OBQU.js} +3 -4
- package/dist/{mobile-message-GADX632R.js → mobile-message-CDKOERRZ.js} +3 -4
- package/dist/{quote-FRCEC34C.js → quote-JKABOMIG.js} +3 -4
- package/dist/react.js +8 -8
- package/package.json +1 -1
- package/registry/items/backgrounds.json +2 -2
- package/starters/video-chat/README.md +4 -8
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/server.ts +0 -5
- package/dist/chunk-X4RGAEL5.js +0 -207
- package/dist/scene-video-backdrop-NK2XK3XE.js +0 -7
- package/dist/{chunk-OOBT4X46.js → chunk-5JBMYQP6.js} +34 -34
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExternalVideoBackdropProvider,
|
|
3
|
+
resolveDensity
|
|
4
|
+
} from "./chunk-5JBMYQP6.js";
|
|
1
5
|
import {
|
|
2
6
|
limitsConcurrentVideoDecoders,
|
|
3
7
|
resolveMediaType
|
|
4
8
|
} from "./chunk-224QNWRA.js";
|
|
5
|
-
import {
|
|
6
|
-
ExternalVideoBackdropProvider,
|
|
7
|
-
resolveDensity
|
|
8
|
-
} from "./chunk-OOBT4X46.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveVideoTimeline
|
|
11
11
|
} from "./chunk-SPVTJH3F.js";
|
|
@@ -60,8 +60,7 @@ function MountedSceneReadiness({ scene, playing, fallback = false, onFailure })
|
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
if (isVideo) {
|
|
63
|
-
const
|
|
64
|
-
const video = (persistent?.getAttribute("data-persistent-video-scene-id") === scene.id ? persistent : layer)?.querySelector("video");
|
|
63
|
+
const video = layer.querySelector("video");
|
|
65
64
|
if (video && video.getAttribute("src") === mediaUrl && video.currentSrc === video.src && video.readyState >= 2) {
|
|
66
65
|
if (presented === video) {
|
|
67
66
|
finish(void 0, true);
|
|
@@ -116,9 +115,9 @@ function MountedSceneReadiness({ scene, playing, fallback = false, onFailure })
|
|
|
116
115
|
import {
|
|
117
116
|
createElement,
|
|
118
117
|
Component,
|
|
119
|
-
lazy,
|
|
120
118
|
Suspense,
|
|
121
119
|
useState,
|
|
120
|
+
useRef as useRef2,
|
|
122
121
|
useCallback,
|
|
123
122
|
useEffect as useEffect2,
|
|
124
123
|
useSyncExternalStore
|
|
@@ -193,7 +192,6 @@ var SceneBoundary = class extends Component {
|
|
|
193
192
|
}
|
|
194
193
|
};
|
|
195
194
|
var SCENE_TRANSITION_SECONDS = 0.3;
|
|
196
|
-
var SceneVideoBackdrop = lazy(() => import("./scene-video-backdrop-NK2XK3XE.js").then((module) => ({ default: module.SceneVideoBackdrop })));
|
|
197
195
|
var MEDIA_PREROLL_SECONDS = 1.2;
|
|
198
196
|
var CONTIGUITY_ULP_FACTOR = 4;
|
|
199
197
|
var subscribeToDecoderPolicy = () => () => {
|
|
@@ -353,19 +351,25 @@ function VideoFrame({
|
|
|
353
351
|
className,
|
|
354
352
|
style
|
|
355
353
|
}) {
|
|
354
|
+
const recoveryRoot = useRef2(null);
|
|
355
|
+
const reportedFailures = useRef2(/* @__PURE__ */ new Set());
|
|
356
356
|
const [failedMedia, setFailedMedia] = useState(() => /* @__PURE__ */ new Set());
|
|
357
|
-
const markMediaFailed = useCallback((key) => {
|
|
357
|
+
const markMediaFailed = useCallback((key, reason = "playback-error") => {
|
|
358
358
|
if (!key || !config.scenes.some((scene) => sceneReadinessKey(scene) === key)) return;
|
|
359
|
+
if (!reportedFailures.current.has(key)) {
|
|
360
|
+
reportedFailures.current.add(key);
|
|
361
|
+
recoveryRoot.current?.dispatchEvent(new CustomEvent("vanillasky:media-recovery", { bubbles: true, detail: { reason: ["decode-error", "frame-readiness-timeout", "stalled-media", "playback-error"].includes(reason) ? reason : "playback-error" } }));
|
|
362
|
+
}
|
|
359
363
|
setFailedMedia((previous) => previous.has(key) ? previous : /* @__PURE__ */ new Set([...previous, key]));
|
|
360
364
|
}, [config.scenes]);
|
|
361
365
|
useEffect2(() => {
|
|
362
366
|
const currentKeys = new Set(config.scenes.map(sceneReadinessKey));
|
|
367
|
+
for (const key of reportedFailures.current) if (!currentKeys.has(key)) reportedFailures.current.delete(key);
|
|
363
368
|
setFailedMedia((previous) => {
|
|
364
369
|
const retained = new Set([...previous].filter((key) => currentKeys.has(key)));
|
|
365
370
|
return retained.size === previous.size ? previous : retained;
|
|
366
371
|
});
|
|
367
372
|
}, [config.scenes]);
|
|
368
|
-
const [readyPersistentVideo, setReadyPersistentVideo] = useState();
|
|
369
373
|
const decoderConstrainedDevice = useDecoderConstraint();
|
|
370
374
|
const timeline = resolveVideoTimeline(config);
|
|
371
375
|
const lastRange = timeline.at(-1);
|
|
@@ -419,34 +423,13 @@ function VideoFrame({
|
|
|
419
423
|
);
|
|
420
424
|
const prerollDuration = prerollsNext ? Math.min(Math.max(MEDIA_PREROLL_SECONDS, blendDuration), Math.max(0, duration)) : blendDuration;
|
|
421
425
|
const prerollStart = active.end - prerollDuration;
|
|
426
|
+
const boundedPreparation = decoderConstrainedDevice && registrySupportsExternalVideoBackdrop(kit);
|
|
422
427
|
const decoderConstrainedTransition = Boolean(
|
|
423
|
-
contiguousNext && decoderConstrainedDevice && sceneHasVideoBackdrop(active) && sceneHasVideoBackdrop(contiguousNext)
|
|
428
|
+
contiguousNext && decoderConstrainedDevice && !boundedPreparation && sceneHasVideoBackdrop(active) && sceneHasVideoBackdrop(contiguousNext)
|
|
424
429
|
);
|
|
425
|
-
const persistentVideoEnabled = decoderConstrainedDevice && registrySupportsExternalVideoBackdrop(kit);
|
|
426
|
-
const activeUsesPersistentVideo = persistentVideoEnabled && sceneHasVideoBackdrop(active);
|
|
427
|
-
const incomingUsesPersistentVideo = Boolean(
|
|
428
|
-
persistentVideoEnabled && contiguousNext && !sceneHasVideoBackdrop(active) && sceneHasVideoBackdrop(contiguousNext) && time >= prerollStart && time < blendEnd
|
|
429
|
-
);
|
|
430
|
-
const persistentVideoRange = activeUsesPersistentVideo ? active : incomingUsesPersistentVideo && contiguousNext ? contiguousNext : void 0;
|
|
431
|
-
const persistentVideoKey = persistentVideoRange ? `${persistentVideoRange.scene.id}\0${String(persistentVideoRange.scene.variables.mediaUrl || "")}` : void 0;
|
|
432
|
-
const firstVideoRange = timeline.find(sceneHasVideoBackdrop);
|
|
433
|
-
const posterPreparationRange = activeUsesPersistentVideo ? contiguousNext && sceneHasVideoBackdrop(contiguousNext) ? contiguousNext : activeIndex === timeline.length - 1 && firstVideoRange?.scene.id !== active.scene.id ? firstVideoRange : void 0 : void 0;
|
|
434
|
-
const preparedPoster = posterPreparationRange && String(
|
|
435
|
-
posterPreparationRange.scene.variables.mediaPoster || ""
|
|
436
|
-
) ? {
|
|
437
|
-
presentationKey: `${posterPreparationRange.scene.id}\0${String(
|
|
438
|
-
posterPreparationRange.scene.variables.mediaUrl || ""
|
|
439
|
-
)}`,
|
|
440
|
-
mediaPoster: String(posterPreparationRange.scene.variables.mediaPoster),
|
|
441
|
-
mediaPosition: String(posterPreparationRange.scene.variables.mediaPosition || "center"),
|
|
442
|
-
backgroundEffect: posterPreparationRange.scene.backgroundEffect ?? config.style.defaultBackgroundEffect
|
|
443
|
-
} : void 0;
|
|
444
430
|
const activeMediaFailed = failedMedia.has(sceneReadinessKey(active.scene));
|
|
445
|
-
const persistentVideoFailed = persistentVideoKey !== void 0 && failedMedia.has(persistentVideoKey);
|
|
446
|
-
const persistentVideoReady = persistentVideoRange !== void 0 && (String(persistentVideoRange.scene.variables.mediaPoster || "") !== "" || readyPersistentVideo === persistentVideoKey);
|
|
447
|
-
const persistentVideoMode = persistentVideoFailed ? "fallback" : persistentVideoReady ? "ready" : "pending";
|
|
448
431
|
const mountingNext = Boolean(
|
|
449
|
-
contiguousNext && !decoderConstrainedTransition && time >= prerollStart && time < blendEnd && (eligibleNextTransition || prerollsNext)
|
|
432
|
+
contiguousNext && !decoderConstrainedTransition && (boundedPreparation || time >= prerollStart) && time < blendEnd && (eligibleNextTransition || prerollsNext || boundedPreparation && sceneHasVideoBackdrop(contiguousNext))
|
|
450
433
|
);
|
|
451
434
|
const previewingNext = Boolean(
|
|
452
435
|
eligibleNextTransition && time >= blendStart && time < blendEnd
|
|
@@ -464,10 +447,15 @@ function VideoFrame({
|
|
|
464
447
|
return /* @__PURE__ */ jsxs(
|
|
465
448
|
"div",
|
|
466
449
|
{
|
|
450
|
+
ref: recoveryRoot,
|
|
467
451
|
onErrorCapture: (event) => {
|
|
468
452
|
const target = event.target;
|
|
469
|
-
if ((target instanceof HTMLVideoElement || target instanceof HTMLImageElement)
|
|
470
|
-
|
|
453
|
+
if (!(target instanceof HTMLVideoElement || target instanceof HTMLImageElement)) return;
|
|
454
|
+
const ownerId = target.closest("[data-layer-scene-id]")?.getAttribute("data-layer-scene-id");
|
|
455
|
+
const owner = [active, contiguousNext].find((range) => range?.scene.id === ownerId);
|
|
456
|
+
const template = owner && kit.getTemplate(owner.scene.templateId);
|
|
457
|
+
if (owner && template && supportsExternalVideoBackdrop(template) && target.getAttribute("src") === owner.scene.variables.mediaUrl) {
|
|
458
|
+
markMediaFailed(sceneReadinessKey(owner.scene), "decode-error");
|
|
471
459
|
}
|
|
472
460
|
},
|
|
473
461
|
"data-video-frame": "ready",
|
|
@@ -489,7 +477,7 @@ function VideoFrame({
|
|
|
489
477
|
scene: active.scene,
|
|
490
478
|
playing,
|
|
491
479
|
fallback: activeMediaFailed,
|
|
492
|
-
onFailure: sceneHasBackdrop(active) && supportsExternalVideoBackdrop(activeTemplate) && !activeMediaFailed ? () => markMediaFailed(sceneReadinessKey(active.scene)) : void 0
|
|
480
|
+
onFailure: sceneHasBackdrop(active) && supportsExternalVideoBackdrop(activeTemplate) && !activeMediaFailed ? () => markMediaFailed(sceneReadinessKey(active.scene), "frame-readiness-timeout") : void 0
|
|
493
481
|
}
|
|
494
482
|
),
|
|
495
483
|
/* @__PURE__ */ jsxs(
|
|
@@ -520,58 +508,6 @@ function VideoFrame({
|
|
|
520
508
|
}
|
|
521
509
|
}
|
|
522
510
|
),
|
|
523
|
-
persistentVideoRange && !persistentVideoFailed && /* @__PURE__ */ jsx2(
|
|
524
|
-
"div",
|
|
525
|
-
{
|
|
526
|
-
"data-persistent-video-scene-id": persistentVideoRange.scene.id,
|
|
527
|
-
"aria-hidden": "true",
|
|
528
|
-
style: { position: "absolute", inset: 0, zIndex: 0 },
|
|
529
|
-
children: /* @__PURE__ */ jsx2(Suspense, { fallback: String(persistentVideoRange.scene.variables.mediaPoster || "") ? /* @__PURE__ */ jsx2(
|
|
530
|
-
"img",
|
|
531
|
-
{
|
|
532
|
-
src: String(persistentVideoRange.scene.variables.mediaPoster),
|
|
533
|
-
alt: "",
|
|
534
|
-
"data-video-poster-plane": "loading",
|
|
535
|
-
"data-video-poster-visible": "true",
|
|
536
|
-
style: {
|
|
537
|
-
position: "absolute",
|
|
538
|
-
inset: 0,
|
|
539
|
-
width: "100%",
|
|
540
|
-
height: "100%",
|
|
541
|
-
objectFit: "cover",
|
|
542
|
-
objectPosition: String(persistentVideoRange.scene.variables.mediaPosition || "center")
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
) : null, children: /* @__PURE__ */ jsx2(
|
|
546
|
-
SceneVideoBackdrop,
|
|
547
|
-
{
|
|
548
|
-
mediaUrl: String(persistentVideoRange.scene.variables.mediaUrl || ""),
|
|
549
|
-
mediaPoster: String(persistentVideoRange.scene.variables.mediaPoster || "") || void 0,
|
|
550
|
-
mediaPosition: String(persistentVideoRange.scene.variables.mediaPosition || "center"),
|
|
551
|
-
backgroundEffect: persistentVideoRange.scene.backgroundEffect ?? config.style.defaultBackgroundEffect,
|
|
552
|
-
progress: persistentVideoRange.scene.id === active.scene.id ? rawProgress : 0,
|
|
553
|
-
sceneDuration: persistentVideoRange.end - persistentVideoRange.start,
|
|
554
|
-
isPlaying: persistentVideoRange.scene.id === active.scene.id && playing,
|
|
555
|
-
preparingNarration,
|
|
556
|
-
muted: mediaAudioMuted || persistentVideoRange.scene.id !== active.scene.id || !playing,
|
|
557
|
-
volume: mediaAudioVolume,
|
|
558
|
-
playbackId: persistentVideoRange.scene.id,
|
|
559
|
-
retainPoster: true,
|
|
560
|
-
persistent: true,
|
|
561
|
-
preparedPoster: preparedPoster ? {
|
|
562
|
-
...preparedPoster,
|
|
563
|
-
// The image is already decoded and costs no second video
|
|
564
|
-
// decoder. Fade it above the outgoing video using the same
|
|
565
|
-
// authored transition clock, then reuse that exact DOM image
|
|
566
|
-
// as the new source's underlay at the cut.
|
|
567
|
-
opacity: decoderConstrainedTransition ? blendProgress : 0
|
|
568
|
-
} : void 0,
|
|
569
|
-
onReady: () => setReadyPersistentVideo(persistentVideoKey),
|
|
570
|
-
onError: () => markMediaFailed(persistentVideoKey)
|
|
571
|
-
}
|
|
572
|
-
) })
|
|
573
|
-
}
|
|
574
|
-
),
|
|
575
511
|
mountingNext && contiguousNext ? [
|
|
576
512
|
/* @__PURE__ */ jsx2(
|
|
577
513
|
SceneLayer,
|
|
@@ -580,7 +516,7 @@ function VideoFrame({
|
|
|
580
516
|
kit,
|
|
581
517
|
config,
|
|
582
518
|
range: active,
|
|
583
|
-
onMediaError: () => markMediaFailed(sceneReadinessKey(active.scene)),
|
|
519
|
+
onMediaError: (reason) => markMediaFailed(sceneReadinessKey(active.scene), reason),
|
|
584
520
|
progress,
|
|
585
521
|
motionProgress,
|
|
586
522
|
width: canvas.width,
|
|
@@ -593,7 +529,7 @@ function VideoFrame({
|
|
|
593
529
|
opacity: 1 - blendProgress,
|
|
594
530
|
interactive: true,
|
|
595
531
|
zIndex: 1,
|
|
596
|
-
externalVideoBackdrop: activeMediaFailed ? "fallback" :
|
|
532
|
+
externalVideoBackdrop: activeMediaFailed ? "fallback" : false
|
|
597
533
|
},
|
|
598
534
|
active.scene.id
|
|
599
535
|
),
|
|
@@ -615,7 +551,8 @@ function VideoFrame({
|
|
|
615
551
|
opacity: blendProgress,
|
|
616
552
|
interactive: false,
|
|
617
553
|
zIndex: 2,
|
|
618
|
-
|
|
554
|
+
onMediaError: (reason) => markMediaFailed(sceneReadinessKey(contiguousNext.scene), reason),
|
|
555
|
+
externalVideoBackdrop: failedMedia.has(sceneReadinessKey(contiguousNext.scene)) ? "fallback" : false
|
|
619
556
|
},
|
|
620
557
|
contiguousNext.scene.id
|
|
621
558
|
)
|
|
@@ -627,7 +564,7 @@ function VideoFrame({
|
|
|
627
564
|
kit,
|
|
628
565
|
config,
|
|
629
566
|
range: active,
|
|
630
|
-
onMediaError: () => markMediaFailed(sceneReadinessKey(active.scene)),
|
|
567
|
+
onMediaError: (reason) => markMediaFailed(sceneReadinessKey(active.scene), reason),
|
|
631
568
|
progress,
|
|
632
569
|
motionProgress,
|
|
633
570
|
width: canvas.width,
|
|
@@ -640,7 +577,7 @@ function VideoFrame({
|
|
|
640
577
|
opacity: 1,
|
|
641
578
|
interactive: true,
|
|
642
579
|
zIndex: 1,
|
|
643
|
-
externalVideoBackdrop: activeMediaFailed ? "fallback" :
|
|
580
|
+
externalVideoBackdrop: activeMediaFailed ? "fallback" : false
|
|
644
581
|
},
|
|
645
582
|
active.scene.id
|
|
646
583
|
)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MediaScene,
|
|
3
3
|
MediaSceneTemplate
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-Q3LPOPHL.js";
|
|
5
|
+
import "./chunk-J7EGPURK.js";
|
|
6
|
+
import "./chunk-5JBMYQP6.js";
|
|
7
7
|
import "./chunk-224QNWRA.js";
|
|
8
|
-
import "./chunk-OOBT4X46.js";
|
|
9
8
|
export {
|
|
10
9
|
MediaScene,
|
|
11
10
|
MediaSceneTemplate
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EditorialSurface
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-OBRKB7PL.js";
|
|
3
|
+
} from "./chunk-R5ZX2LJV.js";
|
|
4
|
+
import "./chunk-J7EGPURK.js";
|
|
6
5
|
import {
|
|
7
6
|
editorialLabel,
|
|
8
7
|
fade
|
|
9
8
|
} from "./chunk-4YM2M62S.js";
|
|
9
|
+
import "./chunk-5JBMYQP6.js";
|
|
10
10
|
import {
|
|
11
11
|
hasSceneMedia
|
|
12
12
|
} from "./chunk-224QNWRA.js";
|
|
13
|
-
import "./chunk-OOBT4X46.js";
|
|
14
13
|
|
|
15
14
|
// src/visual-system/scene-templates/comparison.tsx
|
|
16
15
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EditorialSurface
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-OBRKB7PL.js";
|
|
3
|
+
} from "./chunk-R5ZX2LJV.js";
|
|
4
|
+
import "./chunk-J7EGPURK.js";
|
|
6
5
|
import {
|
|
7
6
|
fade
|
|
8
7
|
} from "./chunk-4YM2M62S.js";
|
|
8
|
+
import "./chunk-5JBMYQP6.js";
|
|
9
9
|
import "./chunk-224QNWRA.js";
|
|
10
|
-
import "./chunk-OOBT4X46.js";
|
|
11
10
|
|
|
12
11
|
// src/visual-system/scene-templates/editorial-timeline.tsx
|
|
13
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EditorialSurface
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-OBRKB7PL.js";
|
|
3
|
+
} from "./chunk-R5ZX2LJV.js";
|
|
4
|
+
import "./chunk-J7EGPURK.js";
|
|
6
5
|
import {
|
|
7
6
|
editorialLabel,
|
|
8
7
|
fade
|
|
9
8
|
} from "./chunk-4YM2M62S.js";
|
|
9
|
+
import "./chunk-5JBMYQP6.js";
|
|
10
10
|
import {
|
|
11
11
|
hasSceneMedia
|
|
12
12
|
} from "./chunk-224QNWRA.js";
|
|
13
|
-
import "./chunk-OOBT4X46.js";
|
|
14
13
|
|
|
15
14
|
// src/visual-system/scene-templates/key-figure.tsx
|
|
16
15
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MediaScene
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-X4RGAEL5.js";
|
|
3
|
+
} from "./chunk-Q3LPOPHL.js";
|
|
5
4
|
import {
|
|
6
5
|
spring
|
|
7
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-J7EGPURK.js";
|
|
8
7
|
import {
|
|
9
8
|
editorialFont
|
|
10
9
|
} from "./chunk-4YM2M62S.js";
|
|
10
|
+
import "./chunk-5JBMYQP6.js";
|
|
11
11
|
import "./chunk-224QNWRA.js";
|
|
12
|
-
import "./chunk-OOBT4X46.js";
|
|
13
12
|
|
|
14
13
|
// src/visual-system/scene-templates/mobile-message.tsx
|
|
15
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EditorialSurface
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-OBRKB7PL.js";
|
|
3
|
+
} from "./chunk-R5ZX2LJV.js";
|
|
4
|
+
import "./chunk-J7EGPURK.js";
|
|
6
5
|
import {
|
|
7
6
|
fade
|
|
8
7
|
} from "./chunk-4YM2M62S.js";
|
|
8
|
+
import "./chunk-5JBMYQP6.js";
|
|
9
9
|
import {
|
|
10
10
|
hasSceneMedia
|
|
11
11
|
} from "./chunk-224QNWRA.js";
|
|
12
|
-
import "./chunk-OOBT4X46.js";
|
|
13
12
|
|
|
14
13
|
// src/visual-system/scene-templates/quote.tsx
|
|
15
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
package/dist/react.js
CHANGED
|
@@ -40,9 +40,9 @@ import {
|
|
|
40
40
|
VideoFrame,
|
|
41
41
|
getDimensions,
|
|
42
42
|
sceneReadinessKey
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-KUCEOG2L.js";
|
|
44
|
+
import "./chunk-5JBMYQP6.js";
|
|
44
45
|
import "./chunk-224QNWRA.js";
|
|
45
|
-
import "./chunk-OOBT4X46.js";
|
|
46
46
|
import {
|
|
47
47
|
getVideoDuration,
|
|
48
48
|
resolveVideoTimeline
|
|
@@ -94,13 +94,13 @@ import { createElement } from "react";
|
|
|
94
94
|
|
|
95
95
|
// src/visual-system/catalog/builtin-loaders.generated.ts
|
|
96
96
|
var GENERATED_BUILTIN_TEMPLATE_LOADERS = {
|
|
97
|
-
"cinemaMedia": () => import("./cinema-media-
|
|
97
|
+
"cinemaMedia": () => import("./cinema-media-WUS7CKPC.js").then((module) => ({ default: module.MediaSceneTemplate })),
|
|
98
98
|
"chapterTitle": () => import("./chapter-title-2JVDU62E.js").then((module) => ({ default: module.TitleSceneTemplate })),
|
|
99
|
-
"editorialTimeline": () => import("./editorial-timeline-
|
|
100
|
-
"mobileMessage": () => import("./mobile-message-
|
|
101
|
-
"comparison": () => import("./comparison-
|
|
102
|
-
"quote": () => import("./quote-
|
|
103
|
-
"keyFigure": () => import("./key-figure-
|
|
99
|
+
"editorialTimeline": () => import("./editorial-timeline-JHQZIPNE.js").then((module) => ({ default: module.TimelineSceneTemplate })),
|
|
100
|
+
"mobileMessage": () => import("./mobile-message-CDKOERRZ.js").then((module) => ({ default: module.NotificationSceneTemplate })),
|
|
101
|
+
"comparison": () => import("./comparison-T4OYCOH3.js").then((module) => ({ default: module.ComparisonSceneTemplate })),
|
|
102
|
+
"quote": () => import("./quote-JKABOMIG.js").then((module) => ({ default: module.QuoteSceneTemplate })),
|
|
103
|
+
"keyFigure": () => import("./key-figure-YWN2OBQU.js").then((module) => ({ default: module.KeyFigureSceneTemplate }))
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
// src/visual-system/catalog/builtin-player.generated.ts
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"path": "src/visual-system/scene-templates/external-video-backdrop.tsx",
|
|
29
29
|
"type": "registry:lib",
|
|
30
30
|
"target": "vanillasky/scene-templates/external-video-backdrop.tsx",
|
|
31
|
-
"content": "import React from \"react\";\n\n// Source-owned templates may live in a consumer's tree while VideoFrame comes\n// from the package. Both copies must observe the same internal context or the\n// consumer template would mount a second video over the player-owned plane,\n// and would never inherit the player's native media audio state.\nexport type ExternalVideoBackdropMode = false | \"pending\" | \"ready\" | \"fallback\";\n\ninterface BackdropContextValue {\n mode: ExternalVideoBackdropMode;\n audioMuted: boolean;\n audioVolume: number;\n preparingNarration?: boolean;\n onMediaError?: () => void;\n}\n\nconst DEFAULT: BackdropContextValue = { mode: false, audioMuted: true, audioVolume: 1 };\n\nconst sharedContext = globalThis as typeof globalThis & {\n __vanillaskyVideoBackdropContext?: React.Context<BackdropContextValue>;\n};\nconst BackdropContext = sharedContext.__vanillaskyVideoBackdropContext\n ??= React.createContext<BackdropContextValue>(DEFAULT);\n\nexport function ExternalVideoBackdropProvider({\n mode,\n audioMuted = true,\n audioVolume = 1,\n preparingNarration = false,\n onMediaError,\n children,\n}: {\n mode: ExternalVideoBackdropMode;\n audioMuted?: boolean;\n audioVolume?: number;\n preparingNarration?: boolean;\n onMediaError?: () => void;\n children: React.ReactNode;\n}) {\n const value = React.useMemo(\n () => ({ mode, audioMuted, audioVolume, preparingNarration, onMediaError }),\n [mode, audioMuted, audioVolume, preparingNarration, onMediaError],\n );\n return (\n <BackdropContext.Provider value={value}>\n {children}\n </BackdropContext.Provider>\n );\n}\n\nexport function useExternalVideoBackdrop(): ExternalVideoBackdropMode {\n return React.useContext(BackdropContext).mode;\n}\n\nexport function useMediaAudio(): { muted: boolean; volume: number } {\n const { audioMuted, audioVolume } = React.useContext(BackdropContext);\n return { muted: audioMuted, volume: audioVolume };\n}\n\n/** Internal first-frame priming state; an explicit viewer pause never sets it. */\nexport function useNarrationPreroll(): boolean {\n return React.useContext(BackdropContext).preparingNarration === true;\n}\n\n/** Routes local decoder/playback failures to the scene-owned recovery surface. */\nexport function useMediaFailure(): (() => void) | undefined {\n return React.useContext(BackdropContext).onMediaError;\n}\n"
|
|
31
|
+
"content": "import React from \"react\";\n\n// Source-owned templates may live in a consumer's tree while VideoFrame comes\n// from the package. Both copies must observe the same internal context or the\n// consumer template would mount a second video over the player-owned plane,\n// and would never inherit the player's native media audio state.\nexport type ExternalVideoBackdropMode = false | \"pending\" | \"ready\" | \"fallback\";\n\nexport type MediaRecoveryReason = \"decode-error\" | \"frame-readiness-timeout\" | \"stalled-media\" | \"playback-error\";\n\ninterface BackdropContextValue {\n mode: ExternalVideoBackdropMode;\n audioMuted: boolean;\n audioVolume: number;\n preparingNarration?: boolean;\n onMediaError?: (reason?: MediaRecoveryReason) => void;\n}\n\nconst DEFAULT: BackdropContextValue = { mode: false, audioMuted: true, audioVolume: 1 };\n\nconst sharedContext = globalThis as typeof globalThis & {\n __vanillaskyVideoBackdropContext?: React.Context<BackdropContextValue>;\n};\nconst BackdropContext = sharedContext.__vanillaskyVideoBackdropContext\n ??= React.createContext<BackdropContextValue>(DEFAULT);\n\nexport function ExternalVideoBackdropProvider({\n mode,\n audioMuted = true,\n audioVolume = 1,\n preparingNarration = false,\n onMediaError,\n children,\n}: {\n mode: ExternalVideoBackdropMode;\n audioMuted?: boolean;\n audioVolume?: number;\n preparingNarration?: boolean;\n onMediaError?: (reason?: MediaRecoveryReason) => void;\n children: React.ReactNode;\n}) {\n const value = React.useMemo(\n () => ({ mode, audioMuted, audioVolume, preparingNarration, onMediaError }),\n [mode, audioMuted, audioVolume, preparingNarration, onMediaError],\n );\n return (\n <BackdropContext.Provider value={value}>\n {children}\n </BackdropContext.Provider>\n );\n}\n\nexport function useExternalVideoBackdrop(): ExternalVideoBackdropMode {\n return React.useContext(BackdropContext).mode;\n}\n\nexport function useMediaAudio(): { muted: boolean; volume: number } {\n const { audioMuted, audioVolume } = React.useContext(BackdropContext);\n return { muted: audioMuted, volume: audioVolume };\n}\n\n/** Internal first-frame priming state; an explicit viewer pause never sets it. */\nexport function useNarrationPreroll(): boolean {\n return React.useContext(BackdropContext).preparingNarration === true;\n}\n\n/** Routes local decoder/playback failures to the scene-owned recovery surface. */\nexport function useMediaFailure(): ((reason?: MediaRecoveryReason) => void) | undefined {\n return React.useContext(BackdropContext).onMediaError;\n}\n"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"path": "src/visual-system/scene-templates/media-position.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"path": "src/visual-system/scene-templates/scene-video-backdrop.tsx",
|
|
41
41
|
"type": "registry:lib",
|
|
42
42
|
"target": "vanillasky/scene-templates/scene-video-backdrop.tsx",
|
|
43
|
-
"content": "import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { getBackgroundTransform } from \"../backgrounds\";\nimport { useMediaAudio, useMediaFailure, useNarrationPreroll } from \"./external-video-backdrop\";\nimport { resolveMediaPosition } from \"./media-position\";\n\nexport interface SceneVideoBackdropProps {\n mediaUrl: string;\n mediaPoster?: string;\n mediaPosition?: string;\n backgroundEffect?: string;\n progress: number;\n /** Narration-led visible duration; muted or pitch-preserving footage may be gently retimed. */\n sceneDuration?: number;\n /** Internal player-owned decoder priming, distinct from viewer pause. */\n preparingNarration?: boolean;\n beatIntensity?: number;\n isPlaying: boolean;\n muted?: boolean;\n volume?: number;\n playbackId?: string;\n retainPoster?: boolean;\n persistent?: boolean;\n preparedPoster?: {\n presentationKey: string;\n mediaPoster: string;\n mediaPosition: string;\n backgroundEffect?: string;\n /** Existing global transition progress. On decoder-constrained Safari,\n * this fades the decoded incoming still above the outgoing video before\n * the single video element changes source. */\n opacity?: number;\n };\n onReady?: () => void;\n onError?: () => void;\n}\n\nexport const SceneVideoBackdrop: React.FC<SceneVideoBackdropProps> = ({\n mediaUrl,\n mediaPoster,\n mediaPosition = \"center\",\n backgroundEffect,\n progress,\n sceneDuration,\n preparingNarration = false,\n beatIntensity = 0,\n isPlaying,\n muted,\n volume,\n playbackId = mediaUrl,\n retainPoster = false,\n persistent = false,\n preparedPoster,\n onReady,\n onError,\n}) => {\n const inheritedAudio = useMediaAudio();\n const reportMediaFailure = useMediaFailure();\n const inheritedPreroll = useNarrationPreroll();\n const rewindPreroll = preparingNarration || inheritedPreroll;\n const resolvedMuted = muted ?? inheritedAudio.muted;\n const resolvedVolume = volume ?? inheritedAudio.volume;\n const resolvedPosition = resolveMediaPosition(mediaPosition);\n const bgTransform = getBackgroundTransform(backgroundEffect, progress, beatIntensity);\n const [decodedVideoUrl, setDecodedVideoUrl] = useState<string>();\n const [waitingKey, setWaitingKey] = useState<string>();\n const [exhaustedKey, setExhaustedKey] = useState<string>();\n\n const videoRef = useRef<HTMLVideoElement>(null);\n const presentedVideoUrl = useRef<string | undefined>(undefined);\n const startedVideoUrl = useRef<string | undefined>(undefined);\n const startedPlaybackId = useRef<string | undefined>(undefined);\n const videoPresentationKey = `${playbackId}\\0${mediaUrl}`;\n\n const presentationRef = useRef({ key: videoPresentationKey, playing: isPlaying });\n presentationRef.current = { key: videoPresentationKey, playing: isPlaying };\n const unavailable = () => {\n if (presentationRef.current.key === videoPresentationKey && presentationRef.current.playing) {\n setExhaustedKey(videoPresentationKey);\n onError?.();\n reportMediaFailure?.();\n }\n };\n useEffect(() => {\n if (!isPlaying || waitingKey !== videoPresentationKey) {\n if (waitingKey) setWaitingKey(undefined);\n return;\n }\n const video = videoRef.current;\n if (!video) return;\n const expectedSource = video.getAttribute(\"src\") === mediaUrl ? video.src : undefined;\n let awaitingFirstFrame = presentedVideoUrl.current !== mediaUrl || video.currentSrc !== expectedSource;\n let previousTime = video.currentTime;\n let forwardFrames = 0;\n let stopped = false;\n let frame: number | undefined;\n let poll: ReturnType<typeof setTimeout> | undefined;\n const observe = (_now?: number, metadata?: VideoFrameCallbackMetadata) => {\n if (stopped) return;\n const currentSource = video.currentSrc === expectedSource;\n if (currentSource && awaitingFirstFrame && (metadata || presentedVideoUrl.current === mediaUrl)) {\n awaitingFirstFrame = false;\n clearTimeout(deadline);\n deadline = setTimeout(fail, 1000);\n }\n const time = metadata?.mediaTime ?? video.currentTime;\n if (!currentSource || video.seeking || time < previousTime) forwardFrames = 0;\n else if (time > previousTime + .001) forwardFrames++;\n previousTime = time;\n // One seek frame is not resumed motion. Require consecutive forward\n // observations before releasing the original bounded stall deadline.\n if (forwardFrames >= 2) {\n stopped = true;\n clearTimeout(deadline);\n setWaitingKey(undefined);\n return;\n }\n if (video.requestVideoFrameCallback) frame = video.requestVideoFrameCallback(observe);\n else poll = setTimeout(observe, 50);\n };\n // A seek can emit waiting without another playing event, even while frames\n // resume. Keep the decoder visible and observe motion directly. A real\n // stall gets the player's authored chapter instead of an endless spinner.\n const fail = () => { if (!stopped) { stopped = true; unavailable(); } };\n // Initial network/decode work has the same bound as mounted readiness.\n // Only a source that has presented a frame can be judged as stalled motion.\n let deadline = setTimeout(fail, awaitingFirstFrame ? 8000 : 1000);\n observe();\n return () => {\n stopped = true;\n clearTimeout(deadline);\n clearTimeout(poll);\n if (frame !== undefined) video.cancelVideoFrameCallback?.(frame);\n };\n }, [waitingKey, videoPresentationKey, isPlaying]);\n\n const fitDuration = useCallback((video: HTMLVideoElement) => {\n // Allow a small decode-to-speech onset margin without changing narration.\n video.playbackRate = (resolvedMuted || video.preservesPitch === true) && sceneDuration && Number.isFinite(video.duration) && video.duration > 0\n ? Math.max(.75, Math.min(1, video.duration / (sceneDuration + .2))) : 1;\n }, [resolvedMuted, sceneDuration]);\n useEffect(() => {\n if (videoRef.current) fitDuration(videoRef.current);\n }, [fitDuration]);\n const continueMotion = (video: HTMLVideoElement) => {\n if (!isPlaying) return;\n // The finite scene clock bounds silent coverage. Speech may outlast a\n // short clip; repeat motion until the scene ends, never audible dialogue.\n if (!resolvedMuted) {\n unavailable();\n return;\n }\n video.currentTime = 0;\n void video.play().catch(unavailable);\n };\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n // React Strict Mode rehearses setup → cleanup → setup in development.\n // The cleanup deliberately releases the decoder, so the repeated setup\n // must restore the declarative source before the playback effect runs.\n if (video.getAttribute(\"src\") !== mediaUrl) {\n video.setAttribute(\"src\", mediaUrl);\n video.load();\n }\n }, [mediaUrl]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n // A source change already starts a native load via React's src update.\n // Tear down the decoder only on unmount, never cancel that new request.\n return () => {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n startedVideoUrl.current = undefined;\n startedPlaybackId.current = undefined;\n };\n }, []);\n\n useEffect(() => {\n const video = videoRef.current;\n if (video) video.volume = resolvedVolume;\n }, [resolvedVolume]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n if (!isPlaying) {\n video.pause();\n if (rewindPreroll && video.currentTime > 0) video.currentTime = 0;\n return;\n }\n if (startedPlaybackId.current === playbackId) {\n if (video.ended) continueMotion(video);\n else void video.play().catch(unavailable);\n return;\n }\n const changingSource = startedVideoUrl.current !== undefined && startedVideoUrl.current !== mediaUrl;\n fitDuration(video);\n if (!changingSource && video.currentTime > 0) video.currentTime = 0;\n video.play().catch(unavailable);\n startedVideoUrl.current = mediaUrl;\n startedPlaybackId.current = playbackId;\n }, [isPlaying, mediaUrl, playbackId, rewindPreroll]);\n\n const mediaStyle: React.CSSProperties = {\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n objectPosition: resolvedPosition,\n transform: bgTransform.transform,\n transformOrigin: bgTransform.transformOrigin,\n zIndex: persistent ? 1 : undefined,\n };\n const preparedPosition = preparedPoster\n ? resolveMediaPosition(preparedPoster.mediaPosition)\n : resolvedPosition;\n const preparedTransform = getBackgroundTransform(preparedPoster?.backgroundEffect, 0, 0);\n const posterPlanes = [\n ...(persistent && mediaPoster ? [{\n presentationKey: videoPresentationKey,\n mediaPoster,\n mediaPosition: resolvedPosition,\n transform: bgTransform.transform,\n transformOrigin: bgTransform.transformOrigin,\n opacity: 1,\n zIndex: 0,\n role: \"current\",\n }] : []),\n ...(preparedPoster && preparedPoster.presentationKey !== videoPresentationKey ? [{\n presentationKey: preparedPoster.presentationKey,\n mediaPoster: preparedPoster.mediaPoster,\n mediaPosition: preparedPosition,\n transform: preparedTransform.transform,\n transformOrigin: preparedTransform.transformOrigin,\n opacity: preparedPoster.opacity ?? 0,\n zIndex: 2,\n role: \"prepared\",\n }] : []),\n ];\n\n return (\n <>\n {posterPlanes.map((posterPlane) => (\n <img\n key={posterPlane.presentationKey}\n src={posterPlane.mediaPoster}\n alt=\"\"\n aria-hidden=\"true\"\n draggable={false}\n data-video-poster-plane={posterPlane.role}\n data-video-poster-visible={posterPlane.opacity > 0 ? \"true\" : \"false\"}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n objectPosition: posterPlane.mediaPosition,\n transform: posterPlane.transform,\n transformOrigin: posterPlane.transformOrigin,\n zIndex: posterPlane.zIndex,\n opacity: posterPlane.opacity,\n pointerEvents: \"none\",\n }}\n />\n ))}\n {exhaustedKey === videoPresentationKey && <div\n role=\"status\" data-media-continuity=\"exhausted\"\n style={{ position: \"absolute\", inset: 0, zIndex: 3, background: \"#000\", color: \"#bbb\", display: \"grid\", placeContent: \"center\", font: \"14px system-ui\" }}\n >Visual unavailable</div>}\n <video\n ref={videoRef}\n src={mediaUrl}\n poster={retainPoster || decodedVideoUrl !== mediaUrl ? mediaPoster || undefined : undefined}\n muted={resolvedMuted}\n loop={false}\n playsInline\n preload=\"auto\"\n onLoadedMetadata={event => fitDuration(event.currentTarget)}\n onEnded={event => continueMotion(event.currentTarget)}\n onWaiting={() => { if (isPlaying) setWaitingKey(videoPresentationKey); }}\n onLoadedData={(event) => {\n const video = event.currentTarget;\n const markPresented = () => {\n if (!video.isConnected || presentationRef.current.key !== videoPresentationKey\n || video.getAttribute(\"src\") !== mediaUrl || video.currentSrc !== video.src) return;\n presentedVideoUrl.current = mediaUrl;\n video.dispatchEvent(new Event(\"vanillasky:video-frame-presented\", { bubbles: true }));\n onReady?.();\n if (!retainPoster) setDecodedVideoUrl(mediaUrl);\n };\n if (video.requestVideoFrameCallback) {\n video.requestVideoFrameCallback(markPresented);\n return;\n }\n markPresented();\n }}\n onError={onError}\n data-media-position={mediaPosition}\n data-video-backdrop={persistent ? \"persistent\" : \"scene\"}\n style={{ ...mediaStyle, visibility: exhaustedKey === videoPresentationKey ? \"hidden\" : undefined }}\n />\n </>\n );\n};\n"
|
|
43
|
+
"content": "import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { getBackgroundTransform } from \"../backgrounds\";\nimport { type MediaRecoveryReason, useMediaAudio, useMediaFailure, useNarrationPreroll } from \"./external-video-backdrop\";\nimport { resolveMediaPosition } from \"./media-position\";\n\nexport interface SceneVideoBackdropProps {\n mediaUrl: string;\n mediaPoster?: string;\n mediaPosition?: string;\n backgroundEffect?: string;\n progress: number;\n /** Narration-led visible duration; muted or pitch-preserving footage may be gently retimed. */\n sceneDuration?: number;\n /** Internal player-owned decoder priming, distinct from viewer pause. */\n preparingNarration?: boolean;\n beatIntensity?: number;\n isPlaying: boolean;\n muted?: boolean;\n volume?: number;\n playbackId?: string;\n onReady?: () => void;\n onError?: () => void;\n}\n\nexport const SceneVideoBackdrop: React.FC<SceneVideoBackdropProps> = ({\n mediaUrl,\n mediaPoster,\n mediaPosition = \"center\",\n backgroundEffect,\n progress,\n sceneDuration,\n preparingNarration = false,\n beatIntensity = 0,\n isPlaying,\n muted,\n volume,\n playbackId = mediaUrl,\n onReady,\n onError,\n}) => {\n const inheritedAudio = useMediaAudio();\n const reportMediaFailure = useMediaFailure();\n const inheritedPreroll = useNarrationPreroll();\n const rewindPreroll = preparingNarration || inheritedPreroll;\n const resolvedMuted = muted ?? inheritedAudio.muted;\n const resolvedVolume = volume ?? inheritedAudio.volume;\n const resolvedPosition = resolveMediaPosition(mediaPosition);\n const bgTransform = getBackgroundTransform(backgroundEffect, progress, beatIntensity);\n const [decodedVideoUrl, setDecodedVideoUrl] = useState<string>();\n const [waitingKey, setWaitingKey] = useState<string>();\n const [exhaustedKey, setExhaustedKey] = useState<string>();\n\n const videoRef = useRef<HTMLVideoElement>(null);\n const presentedVideoUrl = useRef<string | undefined>(undefined);\n const startedVideoUrl = useRef<string | undefined>(undefined);\n const startedPlaybackId = useRef<string | undefined>(undefined);\n const videoPresentationKey = `${playbackId}\\0${mediaUrl}`;\n\n const presentationRef = useRef({ key: videoPresentationKey, playing: isPlaying });\n presentationRef.current = { key: videoPresentationKey, playing: isPlaying };\n const unavailable = (reason: MediaRecoveryReason = \"playback-error\") => {\n if (presentationRef.current.key === videoPresentationKey && presentationRef.current.playing) {\n setExhaustedKey(videoPresentationKey);\n onError?.();\n reportMediaFailure?.(reason);\n }\n };\n useEffect(() => {\n if (!isPlaying || waitingKey !== videoPresentationKey) {\n if (waitingKey) setWaitingKey(undefined);\n return;\n }\n const video = videoRef.current;\n if (!video) return;\n const expectedSource = video.getAttribute(\"src\") === mediaUrl ? video.src : undefined;\n let awaitingFirstFrame = presentedVideoUrl.current !== mediaUrl || video.currentSrc !== expectedSource;\n let previousTime = video.currentTime;\n let forwardFrames = 0;\n let stopped = false;\n let frame: number | undefined;\n let poll: ReturnType<typeof setTimeout> | undefined;\n const observe = (_now?: number, metadata?: VideoFrameCallbackMetadata) => {\n if (stopped) return;\n const currentSource = video.currentSrc === expectedSource;\n if (currentSource && awaitingFirstFrame && (metadata || presentedVideoUrl.current === mediaUrl)) {\n awaitingFirstFrame = false;\n clearTimeout(deadline);\n deadline = setTimeout(fail, 1000);\n }\n const time = metadata?.mediaTime ?? video.currentTime;\n if (!currentSource || video.seeking || time < previousTime) forwardFrames = 0;\n else if (time > previousTime + .001) forwardFrames++;\n previousTime = time;\n // One seek frame is not resumed motion. Require consecutive forward\n // observations before releasing the original bounded stall deadline.\n if (forwardFrames >= 2) {\n stopped = true;\n clearTimeout(deadline);\n setWaitingKey(undefined);\n return;\n }\n if (video.requestVideoFrameCallback) frame = video.requestVideoFrameCallback(observe);\n else poll = setTimeout(observe, 50);\n };\n // A seek can emit waiting without another playing event, even while frames\n // resume. Keep the decoder visible and observe motion directly. A real\n // stall gets the player's authored chapter instead of an endless spinner.\n const fail = () => { if (!stopped) { stopped = true; unavailable(awaitingFirstFrame ? \"frame-readiness-timeout\" : \"stalled-media\"); } };\n // Initial network/decode work has the same bound as mounted readiness.\n // Only a source that has presented a frame can be judged as stalled motion.\n let deadline = setTimeout(fail, awaitingFirstFrame ? 8000 : 1000);\n observe();\n return () => {\n stopped = true;\n clearTimeout(deadline);\n clearTimeout(poll);\n if (frame !== undefined) video.cancelVideoFrameCallback?.(frame);\n };\n }, [waitingKey, videoPresentationKey, isPlaying]);\n\n const onReadyRef = useRef(onReady);\n onReadyRef.current = onReady;\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n let stopped = false;\n let frame: number | undefined;\n const markPresented = () => {\n if (stopped || !video.isConnected || presentationRef.current.key !== videoPresentationKey\n || video.getAttribute(\"src\") !== mediaUrl || video.currentSrc !== video.src) return false;\n presentedVideoUrl.current = mediaUrl;\n video.dispatchEvent(new Event(\"vanillasky:video-frame-presented\", { bubbles: true }));\n onReadyRef.current?.();\n setDecodedVideoUrl(mediaUrl);\n stopped = true;\n return true;\n };\n const observe = () => {\n if (stopped || frame !== undefined) return;\n if (video.requestVideoFrameCallback) {\n frame = video.requestVideoFrameCallback(() => {\n frame = undefined;\n if (!markPresented()) observe();\n });\n } else if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) markPresented();\n };\n // A cached resource can finish loading while its Suspense tree is still\n // detached. Observe the mounted frame even if loadeddata was missed.\n video.addEventListener(\"loadeddata\", observe);\n observe();\n return () => {\n stopped = true;\n video.removeEventListener(\"loadeddata\", observe);\n if (frame !== undefined) video.cancelVideoFrameCallback?.(frame);\n };\n }, [mediaUrl, videoPresentationKey]);\n\n const fitDuration = useCallback((video: HTMLVideoElement) => {\n // Allow a small decode-to-speech onset margin without changing narration.\n video.playbackRate = (resolvedMuted || video.preservesPitch === true) && sceneDuration && Number.isFinite(video.duration) && video.duration > 0\n ? Math.max(.75, Math.min(1, video.duration / (sceneDuration + .2))) : 1;\n }, [resolvedMuted, sceneDuration]);\n useEffect(() => {\n if (videoRef.current) fitDuration(videoRef.current);\n }, [fitDuration]);\n const continueMotion = (video: HTMLVideoElement) => {\n if (!isPlaying) return;\n // The finite scene clock bounds silent coverage. Speech may outlast a\n // short clip; repeat motion until the scene ends, never audible dialogue.\n if (!resolvedMuted) {\n unavailable();\n return;\n }\n video.currentTime = 0;\n void video.play().catch(() => unavailable());\n };\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n // React Strict Mode rehearses setup → cleanup → setup in development.\n // The cleanup deliberately releases the decoder, so the repeated setup\n // must restore the declarative source before the playback effect runs.\n if (video.getAttribute(\"src\") !== mediaUrl) {\n video.setAttribute(\"src\", mediaUrl);\n video.load();\n }\n }, [mediaUrl]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n // A source change already starts a native load via React's src update.\n // Tear down the decoder only on unmount, never cancel that new request.\n return () => {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n startedVideoUrl.current = undefined;\n startedPlaybackId.current = undefined;\n };\n }, []);\n\n useEffect(() => {\n const video = videoRef.current;\n if (video) video.volume = resolvedVolume;\n }, [resolvedVolume]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n if (!isPlaying) {\n video.pause();\n if (rewindPreroll && video.currentTime > 0) video.currentTime = 0;\n return;\n }\n if (startedPlaybackId.current === playbackId) {\n if (video.ended) continueMotion(video);\n else void video.play().catch(() => unavailable());\n return;\n }\n const changingSource = startedVideoUrl.current !== undefined && startedVideoUrl.current !== mediaUrl;\n fitDuration(video);\n if (!changingSource && video.currentTime > 0) video.currentTime = 0;\n video.play().catch(() => unavailable());\n startedVideoUrl.current = mediaUrl;\n startedPlaybackId.current = playbackId;\n }, [isPlaying, mediaUrl, playbackId, rewindPreroll]);\n\n const enforceRequestedPause = (event: React.SyntheticEvent<HTMLVideoElement>) => {\n // WebKit may enter playback without a playing event while seeking or\n // waiting. Both native start events must honor the latest requested hold.\n if (presentationRef.current.playing) return;\n event.currentTarget.pause();\n if (rewindPreroll && event.currentTarget.currentTime > 0) event.currentTarget.currentTime = 0;\n };\n\n const mediaStyle: React.CSSProperties = {\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n objectPosition: resolvedPosition,\n transform: bgTransform.transform,\n transformOrigin: bgTransform.transformOrigin,\n };\n return (\n <>\n {exhaustedKey === videoPresentationKey && <div\n role=\"status\" data-media-continuity=\"exhausted\"\n style={{ position: \"absolute\", inset: 0, zIndex: 3, background: \"#000\", color: \"#bbb\", display: \"grid\", placeContent: \"center\", font: \"14px system-ui\" }}\n >Visual unavailable</div>}\n <video\n ref={videoRef}\n src={mediaUrl}\n poster={decodedVideoUrl !== mediaUrl ? mediaPoster || undefined : undefined}\n muted={resolvedMuted}\n loop={false}\n playsInline\n preload=\"auto\"\n onLoadedMetadata={event => fitDuration(event.currentTarget)}\n onEnded={event => continueMotion(event.currentTarget)}\n onPlay={enforceRequestedPause}\n onPlaying={enforceRequestedPause}\n onWaiting={() => { if (isPlaying) setWaitingKey(videoPresentationKey); }}\n onError={onError}\n data-media-position={mediaPosition}\n data-video-backdrop=\"scene\"\n style={{ ...mediaStyle, visibility: exhaustedKey === videoPresentationKey ? \"hidden\" : undefined }}\n />\n </>\n );\n};\n"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"path": "src/visual-system/scene-templates/color-utils.ts",
|
|
@@ -49,15 +49,11 @@ call. A template introduction starts while footage prepares; body shots contain
|
|
|
49
49
|
moving footage, narration and subtitles. Intent changes the visible actions and
|
|
50
50
|
pacing, not the rendering pipeline.
|
|
51
51
|
|
|
52
|
-
The SDK tries generated footage first, then relevant approved stock when the
|
|
53
|
-
provider is unavailable, denied, or fails. Attempts, including failures, share
|
|
54
|
-
the host's `maxGeneratedVideos` ceiling. A stock miss never broadens the subject
|
|
55
|
-
automatically. If no relevant footage is available, narration and subtitles
|
|
56
|
-
continue as authored chapter scenes.
|
|
57
|
-
|
|
58
52
|
Choose AI video or Pexels in Settings. Each mode uses only its selected footage
|
|
59
|
-
provider, and both use chapter scenes when footage cannot be prepared.
|
|
60
|
-
|
|
53
|
+
provider, and both use chapter scenes when footage cannot be prepared. AI video
|
|
54
|
+
attempts, including failures, share the host's `maxGeneratedVideos` ceiling.
|
|
55
|
+
Exhausted allowance recovers directly to authored chapters without stock calls.
|
|
56
|
+
The Pexels adapter searches the full catalog with bounded subject matching,
|
|
61
57
|
orientation selection and caching; it no longer requires a reviewed index.
|
|
62
58
|
Custom interfaces must display a prominent [Pexels](https://www.pexels.com) credit.
|
|
63
59
|
|
|
@@ -25,11 +25,6 @@ export const handleVideoChat = createVideoChatHandler({
|
|
|
25
25
|
output_config: { effort: "medium" },
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
|
-
onFinish: ({ finishReason, text }) => {
|
|
29
|
-
if (finishReason !== "stop") console.warn(`[planner] ${finishReason}: ${text.slice(0, 300)}`);
|
|
30
|
-
const keyed = (text.match(/"mediaKeyword"/g) ?? []).length;
|
|
31
|
-
console.log(`[planner] ${(text.match(/"scene\.add"/g) ?? []).length} scenes, ${keyed} asked for footage`);
|
|
32
|
-
},
|
|
33
28
|
}),
|
|
34
29
|
generateText: async ({ systemPrompt, userPrompt, maxOutputTokens, signal }) => {
|
|
35
30
|
const { text } = await generateText({
|