@vanillaskyai/video 0.10.22 → 0.11.0
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 +74 -1
- package/PUBLIC-API.md +45 -438
- package/README.md +57 -96
- package/dist/builtin-metadata-OT6V7TB4.js +8 -0
- package/dist/{chapter-title-2JVDU62E.js → chapter-title-2RCXX7SN.js} +1 -2
- package/dist/{chunk-PDQFQIQW.js → chunk-3EQ6PVWL.js} +1 -1
- package/dist/chunk-4G4JBMCM.js +37 -0
- package/dist/{chunk-7AA2JWHZ.js → chunk-5DOQTIMD.js} +6 -6
- package/dist/chunk-666HGTVZ.js +41 -0
- package/dist/{chunk-7M56IUUX.js → chunk-6Z3ID54H.js} +0 -18
- package/dist/{chunk-5BSJLT6H.js → chunk-E7FHBLAC.js} +16 -29
- package/dist/chunk-HIQM4J3U.js +30 -0
- package/dist/chunk-ISWQ6T5X.js +36 -0
- package/dist/{chunk-224QNWRA.js → chunk-K5J7ESRO.js} +1 -10
- package/dist/chunk-KMVRBUL5.js +50 -0
- package/dist/chunk-NM4CXXZY.js +25 -0
- package/dist/{chunk-44WND2VP.js → chunk-V2CP7PVY.js} +19 -63
- package/dist/{chunk-3O7OMMMF.js → chunk-WZESNEPT.js} +0 -1
- package/dist/{chunk-RXTN2CW6.js → chunk-Z3DLSLAJ.js} +1 -1
- package/dist/cinema-media-YZ2UANJG.js +365 -0
- package/dist/cli.js +154 -1753
- package/dist/{compose-video-BH5K6XWT.js → compose-video-H2YUQDUY.js} +4 -4
- package/dist/{events-B4YCc4vc.d.ts → events-BZAfl0Dm.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -6
- package/dist/preload-media-23JXUFCF.js +77 -0
- package/dist/react.d.ts +39 -15
- package/dist/react.js +1350 -739
- package/dist/scene-validation-SGLLLFY5.js +9 -0
- package/dist/server.d.ts +67 -117
- package/dist/server.js +262 -614
- package/dist/test.d.ts +2 -2
- package/dist/test.js +21 -21
- package/dist/{text-stream-LD364KBI.js → text-stream-SOVLYR2L.js} +2 -2
- package/dist/{types-BqB8zC9u.d.ts → types-CG-kPI81.d.ts} +3 -1
- package/dist/{types-DdZw4GRQ.d.ts → types-ht-Zw3Wv.d.ts} +1 -1
- package/docs/agent-integration.md +19 -64
- package/docs/architecture.md +82 -87
- package/docs/customization.md +49 -66
- package/docs/development.md +18 -17
- package/docs/errors.md +4 -4
- package/docs/getting-started.md +74 -93
- package/docs/media-and-audio.md +9 -7
- package/docs/performance.md +11 -6
- package/docs/persistence.md +3 -7
- package/docs/production.md +4 -15
- package/docs/prompt-and-input.md +1 -2
- package/docs/provider-integration.md +134 -181
- package/docs/reference/protocol.md +5 -14
- package/docs/reference/provider-adapters.md +21 -13
- package/docs/security.md +1 -14
- package/docs/testing.md +56 -123
- package/package.json +5 -27
- package/starters/video-chat/.env.example +12 -4
- package/starters/video-chat/.env.native.example +13 -0
- package/starters/video-chat/README.md +72 -11
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/providers/text-native.ts +94 -0
- package/starters/video-chat/providers/text.ts +19 -0
- package/starters/video-chat/providers/transcription.ts +32 -0
- package/starters/video-chat/providers/video-custom.ts +45 -0
- package/starters/video-chat/providers/video-delivery.ts +57 -0
- package/starters/video-chat/providers/video-google.ts +48 -0
- package/starters/video-chat/providers/video-job.ts +103 -0
- package/starters/video-chat/providers/video-runway.ts +45 -0
- package/starters/video-chat/providers/video.ts +39 -63
- package/starters/video-chat/server.ts +3 -29
- package/starters/video-chat/vite.config.ts +2 -8
- package/styles/video-chat.css +8 -105
- package/dist/builtin-server-W4PLJUZ5.js +0 -8
- package/dist/catalog-types-WTbLP6Jh.d.ts +0 -78
- package/dist/check-runtime.d.ts +0 -15
- package/dist/check-runtime.js +0 -96
- package/dist/chunk-2E6T633S.js +0 -27
- package/dist/chunk-4YM2M62S.js +0 -13
- package/dist/chunk-4ZJLPHBV.js +0 -684
- package/dist/chunk-5JBMYQP6.js +0 -156
- package/dist/chunk-73NTSFFI.js +0 -81
- package/dist/chunk-EGVQODKU.js +0 -83
- package/dist/chunk-HFVNAPHZ.js +0 -35
- package/dist/chunk-IIN5M5HW.js +0 -697
- package/dist/chunk-IQMYK5DX.js +0 -133
- package/dist/chunk-IR44XKBI.js +0 -46
- package/dist/chunk-JKVOBTRO.js +0 -145
- package/dist/chunk-LVM5Q2DL.js +0 -68
- package/dist/chunk-M4QTEJTK.js +0 -709
- package/dist/chunk-QSBDB4J2.js +0 -16
- package/dist/chunk-R3XAOMKP.js +0 -29
- package/dist/chunk-SPVTJH3F.js +0 -24
- package/dist/chunk-YAHT3LST.js +0 -663
- package/dist/chunk-ZD2VTUYR.js +0 -28
- package/dist/cinema-media-HYCDG65Z.js +0 -11
- package/dist/comparison-XXAP4S4J.js +0 -36
- package/dist/editorial-timeline-VUQC6KPA.js +0 -42
- package/dist/key-figure-4TJK7HLT.js +0 -29
- package/dist/kit-DrRpdn0p.d.ts +0 -79
- package/dist/mobile-message-FIHAO6XC.js +0 -49
- package/dist/preload-media-LJXWKTWG.js +0 -57
- package/dist/quote-GRPIYKQJ.js +0 -32
- package/dist/system-prompt-AG26KJAA.js +0 -12
- package/dist/template-catalog.d.ts +0 -400
- package/dist/template-catalog.js +0 -6
- package/dist/templates.d.ts +0 -25
- package/dist/templates.js +0 -102
- package/dist/validate-4OUD2CLX.js +0 -10
- package/docs/concepts.md +0 -107
- package/docs/custom-templates.md +0 -346
- package/docs/immersive-interface.md +0 -86
- package/docs/maintainers/cinematic-migration.md +0 -38
- package/docs/motion-and-effects.md +0 -109
- package/docs/reference/design-system.html +0 -125
- package/docs/responsive-orientation.md +0 -37
- package/docs/streaming-protocol.md +0 -15
- package/examples/custom-template/README.md +0 -19
- package/examples/custom-template/minimal-text.tsx +0 -82
- package/examples/custom-template/structured-data.tsx +0 -104
- package/registry/items/backgrounds.json +0 -70
- package/registry/items/chapterTitle.json +0 -80
- package/registry/items/cinemaMedia.json +0 -136
- package/registry/items/comparison.json +0 -168
- package/registry/items/editorialTimeline.json +0 -172
- package/registry/items/keyFigure.json +0 -162
- package/registry/items/mobileMessage.json +0 -153
- package/registry/items/motion.json +0 -45
- package/registry/items/quote.json +0 -161
- package/registry/items/template-context.json +0 -31
- package/registry/items/theme.json +0 -47
- package/registry/items/typography.json +0 -47
package/dist/react.js
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SCENE_DEFINITIONS,
|
|
3
|
+
getBuiltinSceneDefinition
|
|
4
|
+
} from "./chunk-NM4CXXZY.js";
|
|
5
|
+
import {
|
|
6
|
+
ExternalVideoBackdropProvider
|
|
7
|
+
} from "./chunk-666HGTVZ.js";
|
|
1
8
|
import {
|
|
2
9
|
TitleSceneTemplate
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import
|
|
10
|
+
} from "./chunk-5DOQTIMD.js";
|
|
11
|
+
import {
|
|
12
|
+
resolveMediaType
|
|
13
|
+
} from "./chunk-K5J7ESRO.js";
|
|
5
14
|
import {
|
|
6
15
|
getSceneDuration,
|
|
7
|
-
getSceneDurationBounds
|
|
8
|
-
|
|
16
|
+
getSceneDurationBounds,
|
|
17
|
+
getVideoDuration,
|
|
18
|
+
resolveVideoTimeline
|
|
19
|
+
} from "./chunk-KMVRBUL5.js";
|
|
9
20
|
import {
|
|
21
|
+
CLIP_NARRATION_TAIL_SEC,
|
|
10
22
|
MEDIA_RECOVERY_NOTICE,
|
|
11
23
|
decodeVideoSse,
|
|
24
|
+
estimateNarrationSeconds,
|
|
12
25
|
orderWelcomeCards,
|
|
13
|
-
|
|
14
|
-
welcomeVisitSeed
|
|
26
|
+
speechFitsClip,
|
|
27
|
+
welcomeVisitSeed
|
|
28
|
+
} from "./chunk-V2CP7PVY.js";
|
|
29
|
+
import {
|
|
15
30
|
withDeadline
|
|
16
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-4G4JBMCM.js";
|
|
17
32
|
import {
|
|
18
33
|
applyVideoEvent,
|
|
19
34
|
checksumVideo,
|
|
20
35
|
createVideoEventFactory,
|
|
21
36
|
createVideoState
|
|
22
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-Z3DLSLAJ.js";
|
|
38
|
+
import "./chunk-WZESNEPT.js";
|
|
23
39
|
import {
|
|
24
40
|
safePublicDiagnostic
|
|
25
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-6Z3ID54H.js";
|
|
42
|
+
import {
|
|
43
|
+
sanitizeVideoChatMedia
|
|
44
|
+
} from "./chunk-ISWQ6T5X.js";
|
|
26
45
|
import {
|
|
27
46
|
parseVideo
|
|
28
47
|
} from "./chunk-AUPC6MDK.js";
|
|
@@ -30,179 +49,195 @@ import {
|
|
|
30
49
|
VIDEO_SCHEMA_VERSION,
|
|
31
50
|
validateNarrationGroups
|
|
32
51
|
} from "./chunk-7NZOMTAL.js";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from "
|
|
36
|
-
import "
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
|
|
53
|
+
// src/player/mounted-scene-readiness.tsx
|
|
54
|
+
import { createContext, useContext, useEffect, useRef } from "react";
|
|
55
|
+
import { jsx } from "react/jsx-runtime";
|
|
56
|
+
var MountedReadinessContext = createContext(void 0);
|
|
57
|
+
var sceneReadinessKey = (scene) => `${scene.id}\0${String(scene.variables.mediaUrl || "")}`;
|
|
58
|
+
function MountedSceneReadiness({
|
|
59
|
+
scene,
|
|
60
|
+
playing,
|
|
61
|
+
fallback = false,
|
|
62
|
+
onFailure,
|
|
63
|
+
onReady,
|
|
64
|
+
observeIncoming = false,
|
|
65
|
+
timeoutMs = 8e3,
|
|
66
|
+
preparedProof
|
|
67
|
+
}) {
|
|
68
|
+
const marker = useRef(null);
|
|
69
|
+
const report = useContext(MountedReadinessContext);
|
|
70
|
+
const key = sceneReadinessKey(scene);
|
|
71
|
+
const onFailureRef = useRef(onFailure);
|
|
72
|
+
onFailureRef.current = onFailure;
|
|
73
|
+
const onReadyRef = useRef(onReady);
|
|
74
|
+
onReadyRef.current = onReady;
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!report && !onReadyRef.current || !playing) return;
|
|
77
|
+
let stopped = false;
|
|
78
|
+
let frame = 0;
|
|
79
|
+
let callback;
|
|
80
|
+
let observed;
|
|
81
|
+
let presented;
|
|
82
|
+
let previousMediaTime;
|
|
83
|
+
let forwardFrames = 0;
|
|
84
|
+
let motionRevision = 0;
|
|
85
|
+
const root = marker.current?.closest("[data-video-frame]");
|
|
86
|
+
const start = performance.now();
|
|
87
|
+
const finish = (error, actualVideoFrame = false) => {
|
|
88
|
+
if (stopped) return;
|
|
89
|
+
stopped = true;
|
|
90
|
+
if (callback !== void 0) observed?.cancelVideoFrameCallback?.(callback);
|
|
91
|
+
if (error && onFailureRef.current) onFailureRef.current();
|
|
92
|
+
else if (onReadyRef.current && !error) {
|
|
93
|
+
const video = presented;
|
|
94
|
+
const mediaTime = video?.currentTime ?? 0;
|
|
95
|
+
const confirmedAt = performance.now();
|
|
96
|
+
const source = video?.currentSrc;
|
|
97
|
+
const revision = motionRevision;
|
|
98
|
+
let consumed = false;
|
|
99
|
+
onReadyRef.current(actualVideoFrame && video ? { consume: (candidate) => {
|
|
100
|
+
const valid = !consumed && candidate === video && video.isConnected && motionRevision === revision && !video.paused && !video.seeking && video.currentSrc === source && video.currentSrc === video.src && performance.now() - confirmedAt <= 200 && video.currentTime >= mediaTime && video.currentTime - mediaTime <= 0.2;
|
|
101
|
+
consumed = true;
|
|
102
|
+
return valid;
|
|
103
|
+
} } : void 0);
|
|
104
|
+
} else report?.(key, error, actualVideoFrame);
|
|
105
|
+
};
|
|
106
|
+
const check = () => {
|
|
107
|
+
if (stopped) return;
|
|
108
|
+
if (fallback && root?.querySelector("[data-scene-fallback]") && !root.querySelector("[data-template-loading]") && document.fonts?.status !== "loading") {
|
|
109
|
+
finish();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const layer = observeIncoming ? [...root?.querySelectorAll("[data-layer-scene-id]") ?? []].find((node) => node.getAttribute("data-layer-scene-id") === scene.id) : root?.querySelector('[data-scene-layer="active"]');
|
|
113
|
+
const loading = layer?.querySelector("[data-template-loading]");
|
|
114
|
+
const mediaUrl = String(scene.variables.mediaUrl || "");
|
|
115
|
+
const isVideo = scene.variables.mediaType === "video" || /\.(mp4|webm|mov)(?:[?#]|$)/i.test(mediaUrl);
|
|
116
|
+
if (root && layer && !loading && document.fonts?.status !== "loading") {
|
|
117
|
+
if (!mediaUrl || scene.variables.mediaType === "gradient") {
|
|
118
|
+
finish();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isVideo) {
|
|
122
|
+
const video = layer.querySelector("video");
|
|
123
|
+
if (video && video.getAttribute("src") === mediaUrl && video.currentSrc === video.src && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
124
|
+
if (preparedProof?.consume(video)) {
|
|
125
|
+
finish(void 0, true);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (observed !== video) {
|
|
129
|
+
previousMediaTime = void 0;
|
|
130
|
+
forwardFrames = 0;
|
|
131
|
+
}
|
|
132
|
+
if (presented === video && (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA || forwardFrames >= 2)) {
|
|
133
|
+
finish(void 0, true);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
observed = video;
|
|
137
|
+
if (video.requestVideoFrameCallback) {
|
|
138
|
+
callback = video.requestVideoFrameCallback((_now, metadata) => {
|
|
139
|
+
callback = void 0;
|
|
140
|
+
if (stopped) return;
|
|
141
|
+
const sameSource = video.isConnected && video.getAttribute("src") === mediaUrl && video.currentSrc === video.src;
|
|
142
|
+
const mediaTime = metadata.mediaTime;
|
|
143
|
+
if (!sameSource || video.paused || video.seeking || !Number.isFinite(mediaTime) || previousMediaTime !== void 0 && mediaTime <= previousMediaTime + 1e-3) forwardFrames = 0;
|
|
144
|
+
else if (previousMediaTime !== void 0 && mediaTime > previousMediaTime + 1e-3) forwardFrames++;
|
|
145
|
+
previousMediaTime = sameSource && !video.paused && !video.seeking ? mediaTime : void 0;
|
|
146
|
+
if (sameSource) presented = video;
|
|
147
|
+
check();
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
|
|
152
|
+
finish(void 0, true);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
const image = [...layer.querySelectorAll("img") ?? []].find((element) => element.getAttribute("src") === mediaUrl);
|
|
158
|
+
if (image?.complete && image.naturalWidth > 0) {
|
|
159
|
+
finish();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (timeoutMs !== null && performance.now() - start >= timeoutMs) {
|
|
165
|
+
finish(new Error("Scene media did not become ready"));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
frame = requestAnimationFrame(check);
|
|
169
|
+
};
|
|
170
|
+
const onPresented = (event) => {
|
|
171
|
+
const video = event.target;
|
|
172
|
+
if (!(video instanceof HTMLVideoElement) || stopped || video.getAttribute("src") !== String(scene.variables.mediaUrl || "") || video.currentSrc !== video.src) return;
|
|
173
|
+
presented = video;
|
|
174
|
+
cancelAnimationFrame(frame);
|
|
175
|
+
if (callback !== void 0) observed?.cancelVideoFrameCallback?.(callback);
|
|
176
|
+
callback = void 0;
|
|
177
|
+
check();
|
|
178
|
+
};
|
|
179
|
+
const resetMotion = (event) => {
|
|
180
|
+
if (event.target === observed || event.target === presented) {
|
|
181
|
+
motionRevision++;
|
|
182
|
+
previousMediaTime = void 0;
|
|
183
|
+
forwardFrames = 0;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
for (const type of ["pause", "waiting", "seeking"]) root?.addEventListener(type, resetMotion, true);
|
|
187
|
+
root?.addEventListener("vanillasky:video-frame-presented", onPresented);
|
|
188
|
+
check();
|
|
189
|
+
const timeout = timeoutMs === null ? void 0 : setTimeout(() => finish(new Error("Scene media did not become ready")), timeoutMs);
|
|
190
|
+
return () => {
|
|
191
|
+
root?.removeEventListener("vanillasky:video-frame-presented", onPresented);
|
|
192
|
+
for (const type of ["pause", "waiting", "seeking"]) root?.removeEventListener(type, resetMotion, true);
|
|
193
|
+
stopped = true;
|
|
194
|
+
clearTimeout(timeout);
|
|
195
|
+
cancelAnimationFrame(frame);
|
|
196
|
+
if (callback !== void 0) observed?.cancelVideoFrameCallback?.(callback);
|
|
197
|
+
};
|
|
198
|
+
}, [key, report, scene, playing, fallback, observeIncoming, timeoutMs, preparedProof]);
|
|
199
|
+
return /* @__PURE__ */ jsx("span", { ref: marker, hidden: true });
|
|
200
|
+
}
|
|
57
201
|
|
|
58
202
|
// src/player/video-player.tsx
|
|
59
203
|
import {
|
|
60
|
-
useEffect as
|
|
204
|
+
useEffect as useEffect5,
|
|
61
205
|
useMemo,
|
|
62
|
-
useRef,
|
|
63
|
-
useState
|
|
206
|
+
useRef as useRef4,
|
|
207
|
+
useState as useState2
|
|
64
208
|
} from "react";
|
|
65
209
|
|
|
66
|
-
// src/visual-system/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
for (const template of templates) {
|
|
70
|
-
assertTemplateTransitionMetadata(template);
|
|
71
|
-
if (!template.defaults && !template.schema) {
|
|
72
|
-
throw new Error(`${template.id} requires defaults or a schema`);
|
|
73
|
-
}
|
|
74
|
-
if (byId.has(template.id)) throw new Error(`Duplicate template id: ${template.id}`);
|
|
75
|
-
byId.set(template.id, template);
|
|
76
|
-
}
|
|
77
|
-
const frozenTemplates = Object.freeze([...templates]);
|
|
78
|
-
return Object.freeze({
|
|
79
|
-
templates: frozenTemplates,
|
|
80
|
-
capabilities: { templates: frozenTemplates.map(({ id }) => id) },
|
|
81
|
-
getTemplate: (id) => byId.get(id)
|
|
82
|
-
});
|
|
210
|
+
// src/visual-system/layout.ts
|
|
211
|
+
function getDimensions(orientation = "portrait") {
|
|
212
|
+
return orientation === "landscape" ? { width: 1920, height: 1080, aspectRatio: "16 / 9" } : { width: 1080, height: 1920, aspectRatio: "9 / 16" };
|
|
83
213
|
}
|
|
84
|
-
function overlayPlayerTemplateRegistry(defaults, customer) {
|
|
85
|
-
if (!customer) return defaults;
|
|
86
|
-
const customerTemplates = new Map(customer.templates.map((template) => [template.id, template]));
|
|
87
|
-
const defaultIds = new Set(defaults.templates.map(({ id }) => id));
|
|
88
|
-
return createPlayerTemplateRegistry([
|
|
89
|
-
...defaults.templates.map((template) => customerTemplates.get(template.id) ?? template),
|
|
90
|
-
...customer.templates.filter(({ id }) => !defaultIds.has(id))
|
|
91
|
-
]);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/visual-system/catalog/builtin-player.ts
|
|
95
|
-
import { createElement } from "react";
|
|
96
214
|
|
|
97
|
-
// src/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// src/visual-system/catalog/builtin-player.generated.ts
|
|
109
|
-
var GENERATED_BUILTIN_PLAYER_TEMPLATES = [
|
|
110
|
-
{
|
|
111
|
-
"id": "cinemaMedia",
|
|
112
|
-
"defaults": {
|
|
113
|
-
"mediaUrl": "",
|
|
114
|
-
"mediaType": "video",
|
|
115
|
-
"mediaPoster": ""
|
|
116
|
-
},
|
|
117
|
-
"usesGlobalTransition": false
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
"id": "chapterTitle",
|
|
121
|
-
"defaults": {
|
|
122
|
-
"title": "A different perspective"
|
|
123
|
-
},
|
|
124
|
-
"usesGlobalTransition": false
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
"id": "editorialTimeline",
|
|
128
|
-
"defaults": {
|
|
129
|
-
"events": [
|
|
130
|
-
{
|
|
131
|
-
"label": "Observe"
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
"label": "Understand"
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
"label": "Act"
|
|
138
|
-
}
|
|
139
|
-
],
|
|
140
|
-
"mediaUrl": "",
|
|
141
|
-
"mediaType": "video",
|
|
142
|
-
"mediaPoster": ""
|
|
143
|
-
},
|
|
144
|
-
"usesGlobalTransition": false
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
"id": "mobileMessage",
|
|
148
|
-
"defaults": {
|
|
149
|
-
"mediaUrl": "",
|
|
150
|
-
"mediaType": "video",
|
|
151
|
-
"mediaPoster": "",
|
|
152
|
-
"message": "Can we talk?",
|
|
153
|
-
"app": "Messages"
|
|
154
|
-
},
|
|
155
|
-
"usesGlobalTransition": false
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
"id": "comparison",
|
|
159
|
-
"defaults": {
|
|
160
|
-
"leftText": "More distractions",
|
|
161
|
-
"rightText": "More room to think",
|
|
162
|
-
"mediaUrl": "",
|
|
163
|
-
"mediaType": "video",
|
|
164
|
-
"mediaPoster": ""
|
|
165
|
-
},
|
|
166
|
-
"usesGlobalTransition": false
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
"id": "quote",
|
|
170
|
-
"defaults": {
|
|
171
|
-
"mediaUrl": "",
|
|
172
|
-
"mediaType": "video",
|
|
173
|
-
"mediaPoster": ""
|
|
174
|
-
},
|
|
175
|
-
"usesGlobalTransition": false
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
"id": "keyFigure",
|
|
179
|
-
"defaults": {
|
|
180
|
-
"mediaUrl": "",
|
|
181
|
-
"mediaType": "video",
|
|
182
|
-
"mediaPoster": ""
|
|
183
|
-
},
|
|
184
|
-
"usesGlobalTransition": false
|
|
185
|
-
}
|
|
186
|
-
];
|
|
187
|
-
var GENERATED_BUILTIN_VIDEO_BACKDROP_IDS = ["cinemaMedia", "editorialTimeline", "mobileMessage", "comparison", "quote", "keyFigure"];
|
|
215
|
+
// src/player/video-frame.tsx
|
|
216
|
+
import {
|
|
217
|
+
createElement as createElement2,
|
|
218
|
+
Component,
|
|
219
|
+
Suspense,
|
|
220
|
+
useState,
|
|
221
|
+
useRef as useRef2,
|
|
222
|
+
useCallback,
|
|
223
|
+
useEffect as useEffect2
|
|
224
|
+
} from "react";
|
|
188
225
|
|
|
189
226
|
// src/visual-system/catalog/builtin-player.ts
|
|
190
|
-
|
|
227
|
+
import { createElement } from "react";
|
|
228
|
+
function createRenderer(defaults, loader) {
|
|
191
229
|
let loaded;
|
|
192
230
|
let failure;
|
|
193
231
|
let pending;
|
|
194
232
|
const preload = () => {
|
|
195
233
|
if (loaded) return Promise.resolve();
|
|
196
234
|
if (failure) return Promise.reject(failure);
|
|
197
|
-
pending ??= loader().then(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
throw cause;
|
|
204
|
-
}
|
|
205
|
-
);
|
|
235
|
+
pending ??= loader().then((module) => {
|
|
236
|
+
loaded = module;
|
|
237
|
+
}, (cause) => {
|
|
238
|
+
failure = cause;
|
|
239
|
+
throw cause;
|
|
240
|
+
});
|
|
206
241
|
return pending;
|
|
207
242
|
};
|
|
208
243
|
const component = (props) => {
|
|
@@ -210,40 +245,513 @@ function createPreloadableBuiltinTemplate(loader, mediaBackdrop) {
|
|
|
210
245
|
if (failure) throw failure;
|
|
211
246
|
throw preload();
|
|
212
247
|
};
|
|
213
|
-
|
|
214
|
-
return mediaBackdrop ? markExternalVideoBackdropTemplate(template) : template;
|
|
248
|
+
return Object.freeze({ component, defaults, preload });
|
|
215
249
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
250
|
+
var loaders = {
|
|
251
|
+
cinemaMedia: () => import("./cinema-media-YZ2UANJG.js").then((module) => ({ default: module.MediaSceneTemplate })),
|
|
252
|
+
chapterTitle: () => import("./chapter-title-2RCXX7SN.js").then((module) => ({ default: module.TitleSceneTemplate }))
|
|
253
|
+
};
|
|
254
|
+
var renderers = new Map(SCENE_DEFINITIONS.map((scene) => [
|
|
255
|
+
scene.id,
|
|
256
|
+
createRenderer(scene.defaults, loaders[scene.id])
|
|
257
|
+
]));
|
|
258
|
+
function getBuiltinSceneRenderer(id) {
|
|
259
|
+
return renderers.get(id);
|
|
222
260
|
}
|
|
223
|
-
var preloadableById = new Map(
|
|
224
|
-
GENERATED_BUILTIN_PLAYER_TEMPLATES.map(({ id }) => [
|
|
225
|
-
id,
|
|
226
|
-
createPreloadableBuiltinTemplate(GENERATED_BUILTIN_TEMPLATE_LOADERS[id], GENERATED_BUILTIN_VIDEO_BACKDROP_IDS.includes(id))
|
|
227
|
-
])
|
|
228
|
-
);
|
|
229
|
-
var builtinPlayerTemplates = Object.freeze(
|
|
230
|
-
GENERATED_BUILTIN_PLAYER_TEMPLATES.map((template) => Object.freeze({
|
|
231
|
-
...template,
|
|
232
|
-
defaults: freezeValue({ ...template.defaults }),
|
|
233
|
-
component: preloadableById.get(template.id).component
|
|
234
|
-
}))
|
|
235
|
-
);
|
|
236
|
-
var BUILTIN_PLAYER_KIT = createPlayerTemplateRegistry(builtinPlayerTemplates);
|
|
237
261
|
function preloadBuiltinTemplate(id) {
|
|
238
|
-
return
|
|
262
|
+
return getBuiltinSceneRenderer(id)?.preload();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/player/video-frame.tsx
|
|
266
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
267
|
+
function PresentedScene({ notify }) {
|
|
268
|
+
useEffect2(() => {
|
|
269
|
+
if (!notify) return;
|
|
270
|
+
const frame = requestAnimationFrame(() => {
|
|
271
|
+
try {
|
|
272
|
+
void Promise.resolve(notify()).catch(() => void 0);
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
return () => cancelAnimationFrame(frame);
|
|
277
|
+
}, [notify]);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function SafeScene({ scene, onFramePresented }) {
|
|
281
|
+
const copy = scene.narration?.trim() || Object.values(scene.variables).filter((value) => typeof value === "string" && !/^https?:/i.test(value)).join(" ");
|
|
282
|
+
return /* @__PURE__ */ jsxs(
|
|
283
|
+
"div",
|
|
284
|
+
{
|
|
285
|
+
"data-scene-fallback": "true",
|
|
286
|
+
style: {
|
|
287
|
+
width: "100%",
|
|
288
|
+
height: "100%",
|
|
289
|
+
display: "grid",
|
|
290
|
+
placeContent: "center",
|
|
291
|
+
boxSizing: "border-box",
|
|
292
|
+
padding: "8%",
|
|
293
|
+
background: "#090712",
|
|
294
|
+
color: "#fff",
|
|
295
|
+
font: "500 clamp(20px, 4vw, 64px)/1.3 system-ui",
|
|
296
|
+
overflow: "hidden"
|
|
297
|
+
},
|
|
298
|
+
children: [
|
|
299
|
+
/* @__PURE__ */ jsx2(MountedSceneReadiness, { scene, playing: true, fallback: true }),
|
|
300
|
+
/* @__PURE__ */ jsx2(PresentedScene, { notify: onFramePresented }),
|
|
301
|
+
/* @__PURE__ */ jsx2("p", { children: copy.slice(0, 600) || "Your response continues." }),
|
|
302
|
+
/* @__PURE__ */ jsx2("small", { style: { fontSize: "0.3em" }, role: "status", children: "This scene uses a simpler layout." })
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
var SceneBoundary = class extends Component {
|
|
308
|
+
state = { failed: false };
|
|
309
|
+
static getDerivedStateFromError() {
|
|
310
|
+
return { failed: true };
|
|
311
|
+
}
|
|
312
|
+
render() {
|
|
313
|
+
return this.state.failed ? /* @__PURE__ */ jsx2(SafeScene, { scene: this.props.scene, onFramePresented: this.props.onFramePresented }) : this.props.children;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
var CONTIGUITY_ULP_FACTOR = 4;
|
|
317
|
+
function clamp01(value) {
|
|
318
|
+
return Math.max(0, Math.min(1, value));
|
|
319
|
+
}
|
|
320
|
+
function rangesAreContiguous(left, right) {
|
|
321
|
+
const scale = Math.max(1, Math.abs(left.end), Math.abs(right.start));
|
|
322
|
+
const ulpTolerance = Number.EPSILON * scale * CONTIGUITY_ULP_FACTOR;
|
|
323
|
+
return Math.abs(left.end - right.start) <= ulpTolerance;
|
|
324
|
+
}
|
|
325
|
+
function sceneHasBackdrop(range) {
|
|
326
|
+
return String(range.scene.variables.mediaType || "auto") !== "gradient" && String(range.scene.variables.mediaUrl || "").trim() !== "";
|
|
327
|
+
}
|
|
328
|
+
function sceneHasVideoBackdrop(range) {
|
|
329
|
+
const mediaUrl = String(range.scene.variables.mediaUrl || "").trim();
|
|
330
|
+
return mediaUrl !== "" && resolveMediaType(
|
|
331
|
+
String(range.scene.variables.mediaType || "auto"),
|
|
332
|
+
mediaUrl
|
|
333
|
+
) === "video";
|
|
334
|
+
}
|
|
335
|
+
function SceneLayer({
|
|
336
|
+
onFramePresented,
|
|
337
|
+
onMediaError,
|
|
338
|
+
range,
|
|
339
|
+
progress,
|
|
340
|
+
motionProgress,
|
|
341
|
+
width,
|
|
342
|
+
height,
|
|
343
|
+
playing,
|
|
344
|
+
preparingNarration,
|
|
345
|
+
mediaAudioMuted,
|
|
346
|
+
mediaAudioVolume,
|
|
347
|
+
layer,
|
|
348
|
+
opacity,
|
|
349
|
+
interactive,
|
|
350
|
+
zIndex,
|
|
351
|
+
externalVideoBackdrop
|
|
352
|
+
}) {
|
|
353
|
+
const mediaFailed = externalVideoBackdrop === "fallback" || range.scene.templateId === "cinemaMedia" && !String(range.scene.variables.mediaUrl || "").trim();
|
|
354
|
+
const recoveryTitle = mediaFailed && range.scene.templateId === "cinemaMedia" ? typeof range.scene.variables.fallbackText === "string" && range.scene.variables.fallbackText.trim() || "Your response continues." : void 0;
|
|
355
|
+
const template = getBuiltinSceneRenderer(recoveryTitle ? "chapterTitle" : range.scene.templateId);
|
|
356
|
+
const duration = range.end - range.start;
|
|
357
|
+
return /* @__PURE__ */ jsx2(
|
|
358
|
+
ExternalVideoBackdropProvider,
|
|
359
|
+
{
|
|
360
|
+
mode: externalVideoBackdrop,
|
|
361
|
+
audioMuted: mediaAudioMuted || !playing,
|
|
362
|
+
audioVolume: mediaAudioVolume,
|
|
363
|
+
preparingNarration,
|
|
364
|
+
onMediaError,
|
|
365
|
+
children: /* @__PURE__ */ jsxs(
|
|
366
|
+
"div",
|
|
367
|
+
{
|
|
368
|
+
"data-scene-layer": layer,
|
|
369
|
+
"data-scene-fallback": mediaFailed ? "true" : void 0,
|
|
370
|
+
"data-layer-scene-id": range.scene.id,
|
|
371
|
+
"data-layer-template-id": range.scene.templateId,
|
|
372
|
+
"aria-hidden": interactive ? void 0 : true,
|
|
373
|
+
...interactive ? {} : { inert: "inert" },
|
|
374
|
+
style: {
|
|
375
|
+
position: "absolute",
|
|
376
|
+
inset: 0,
|
|
377
|
+
opacity,
|
|
378
|
+
zIndex,
|
|
379
|
+
pointerEvents: interactive ? "auto" : "none"
|
|
380
|
+
},
|
|
381
|
+
children: [
|
|
382
|
+
range.scene.templateId === "cinemaMedia" && !recoveryTitle && (mediaFailed || !String(range.scene.variables.mediaUrl || "").trim()) && /* @__PURE__ */ jsx2(
|
|
383
|
+
"div",
|
|
384
|
+
{
|
|
385
|
+
role: "status",
|
|
386
|
+
"data-media-unavailable": "true",
|
|
387
|
+
style: { position: "absolute", inset: 0, zIndex: 2, display: "grid", placeContent: "center", color: "#bbb", font: "14px system-ui", background: "#000" },
|
|
388
|
+
children: "Visual unavailable"
|
|
389
|
+
}
|
|
390
|
+
),
|
|
391
|
+
template ? /* @__PURE__ */ jsx2(SceneBoundary, { scene: range.scene, onFramePresented, children: /* @__PURE__ */ jsxs(Suspense, { fallback: /* @__PURE__ */ jsx2(
|
|
392
|
+
"div",
|
|
393
|
+
{
|
|
394
|
+
"data-template-loading": range.scene.templateId,
|
|
395
|
+
style: { position: "absolute", inset: 0 }
|
|
396
|
+
}
|
|
397
|
+
), children: [
|
|
398
|
+
/* @__PURE__ */ jsx2(PresentedScene, { notify: onFramePresented }),
|
|
399
|
+
createElement2(template.component, {
|
|
400
|
+
variables: {
|
|
401
|
+
...template.defaults,
|
|
402
|
+
...range.scene.variables,
|
|
403
|
+
...mediaFailed ? { mediaUrl: "", mediaPoster: "", mediaType: "gradient" } : {},
|
|
404
|
+
...recoveryTitle ? { title: recoveryTitle } : {}
|
|
405
|
+
},
|
|
406
|
+
progress,
|
|
407
|
+
motionProgress,
|
|
408
|
+
width,
|
|
409
|
+
height,
|
|
410
|
+
sceneDuration: duration,
|
|
411
|
+
isPlaying: playing
|
|
412
|
+
})
|
|
413
|
+
] }) }, range.scene.id) : /* @__PURE__ */ jsx2(SafeScene, { scene: range.scene, onFramePresented })
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
function VideoFrame({
|
|
421
|
+
onFramePresented,
|
|
422
|
+
config,
|
|
423
|
+
time,
|
|
424
|
+
width,
|
|
425
|
+
height,
|
|
426
|
+
playing = false,
|
|
427
|
+
preparingNarration = false,
|
|
428
|
+
mediaAudioMuted = true,
|
|
429
|
+
mediaAudioVolume = 1,
|
|
430
|
+
className,
|
|
431
|
+
style
|
|
432
|
+
}) {
|
|
433
|
+
const recoveryRoot = useRef2(null);
|
|
434
|
+
const displayedKey = useRef2(void 0);
|
|
435
|
+
const displayedWasPlaying = useRef2(false);
|
|
436
|
+
const handoffProof = useRef2(void 0);
|
|
437
|
+
const confirmedHandoff = useRef2(void 0);
|
|
438
|
+
const [preparedMedia, setPreparedMedia] = useState(() => /* @__PURE__ */ new Set());
|
|
439
|
+
const markPrepared = useCallback((key) => setPreparedMedia((previous2) => /* @__PURE__ */ new Set([...previous2, key])), []);
|
|
440
|
+
const reportedFailures = useRef2(/* @__PURE__ */ new Set());
|
|
441
|
+
const [failedMedia, setFailedMedia] = useState(() => /* @__PURE__ */ new Set());
|
|
442
|
+
const markMediaFailed = useCallback((key, reason = "playback-error") => {
|
|
443
|
+
if (!key || !config.scenes.some((scene) => sceneReadinessKey(scene) === key)) return;
|
|
444
|
+
if (!reportedFailures.current.has(key)) {
|
|
445
|
+
reportedFailures.current.add(key);
|
|
446
|
+
recoveryRoot.current?.dispatchEvent(new CustomEvent("vanillasky:media-recovery", { bubbles: true, detail: { reason: ["decode-error", "frame-readiness-timeout", "stalled-media", "playback-error", "duration-mismatch"].includes(reason) ? reason : "playback-error" } }));
|
|
447
|
+
}
|
|
448
|
+
setFailedMedia((previous2) => previous2.has(key) ? previous2 : /* @__PURE__ */ new Set([...previous2, key]));
|
|
449
|
+
}, [config.scenes]);
|
|
450
|
+
useEffect2(() => {
|
|
451
|
+
const currentKeys = new Set(config.scenes.map(sceneReadinessKey));
|
|
452
|
+
setPreparedMedia((previous2) => {
|
|
453
|
+
const retained = new Set([...previous2].filter((key) => currentKeys.has(key)));
|
|
454
|
+
return retained.size === previous2.size ? previous2 : retained;
|
|
455
|
+
});
|
|
456
|
+
for (const key of reportedFailures.current) if (!currentKeys.has(key)) reportedFailures.current.delete(key);
|
|
457
|
+
setFailedMedia((previous2) => {
|
|
458
|
+
const retained = new Set([...previous2].filter((key) => currentKeys.has(key)));
|
|
459
|
+
return retained.size === previous2.size ? previous2 : retained;
|
|
460
|
+
});
|
|
461
|
+
}, [config.scenes]);
|
|
462
|
+
const timeline = resolveVideoTimeline(config);
|
|
463
|
+
const lastRange = timeline.at(-1);
|
|
464
|
+
const foundIndex = timeline.findIndex((range) => time >= range.start && time < range.end);
|
|
465
|
+
const afterEnd = lastRange && time >= lastRange.end;
|
|
466
|
+
const targetIndex = foundIndex >= 0 ? foundIndex : afterEnd ? timeline.length - 1 : -1;
|
|
467
|
+
const target = timeline[targetIndex];
|
|
468
|
+
const targetKey = target ? sceneReadinessKey(target.scene) : void 0;
|
|
469
|
+
if (confirmedHandoff.current !== targetKey || !(playing || preparingNarration)) {
|
|
470
|
+
confirmedHandoff.current = void 0;
|
|
471
|
+
handoffProof.current = void 0;
|
|
472
|
+
}
|
|
473
|
+
const previousIndex = timeline.findIndex((range) => sceneReadinessKey(range.scene) === displayedKey.current);
|
|
474
|
+
const previous = timeline[previousIndex];
|
|
475
|
+
const canPrepare = (range) => Boolean(range && (mediaAudioMuted || !sceneHasVideoBackdrop(range)) && sceneHasBackdrop(range) && range.scene.templateId === "cinemaMedia");
|
|
476
|
+
const canRetain = (range) => canPrepare(range) || range?.scene.templateId === "chapterTitle";
|
|
477
|
+
const hasPlayableMedia = (range) => {
|
|
478
|
+
if (!range || !preparedMedia.has(sceneReadinessKey(range.scene))) return false;
|
|
479
|
+
const layer = [...recoveryRoot.current?.querySelectorAll("[data-layer-scene-id]") ?? []].find((node) => node.getAttribute("data-layer-scene-id") === range.scene.id);
|
|
480
|
+
if (!sceneHasVideoBackdrop(range)) {
|
|
481
|
+
const image = [...layer?.querySelectorAll("img") ?? []].find((element) => element.getAttribute("src") === range.scene.variables.mediaUrl);
|
|
482
|
+
return Boolean(image && image.getAttribute("src") === range.scene.variables.mediaUrl && image.complete && image.naturalWidth > 0);
|
|
483
|
+
}
|
|
484
|
+
const video = layer?.querySelector("video");
|
|
485
|
+
return Boolean(video && video.getAttribute("src") === range.scene.variables.mediaUrl && video.currentSrc === video.src && video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA);
|
|
486
|
+
};
|
|
487
|
+
const handoffPending = Boolean(displayedWasPlaying.current && target && previous && targetIndex === previousIndex + 1 && time <= target.start + 1e-3 && rangesAreContiguous(previous, target) && canRetain(previous) && canPrepare(target) && !failedMedia.has(sceneReadinessKey(target.scene)) && !hasPlayableMedia(target) && confirmedHandoff.current !== targetKey);
|
|
488
|
+
const activeIndex = handoffPending ? previousIndex : targetIndex;
|
|
489
|
+
const active = timeline[activeIndex];
|
|
490
|
+
const displayKey = active ? sceneReadinessKey(active.scene) : void 0;
|
|
491
|
+
displayedWasPlaying.current = displayedKey.current === displayKey && displayedWasPlaying.current || playing || preparingNarration;
|
|
492
|
+
displayedKey.current = displayKey;
|
|
493
|
+
if (!active) {
|
|
494
|
+
return /* @__PURE__ */ jsx2(
|
|
495
|
+
"div",
|
|
496
|
+
{
|
|
497
|
+
"data-video-frame": timeline.length === 0 ? "empty" : "gap",
|
|
498
|
+
className,
|
|
499
|
+
style: { width, height, background: "#000", ...style }
|
|
500
|
+
}
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
if (!getBuiltinSceneRenderer(active.scene.templateId)) {
|
|
504
|
+
return /* @__PURE__ */ jsx2(
|
|
505
|
+
"div",
|
|
506
|
+
{
|
|
507
|
+
"data-video-frame": "unsupported",
|
|
508
|
+
"data-template-id": active.scene.templateId,
|
|
509
|
+
className,
|
|
510
|
+
style: { width, height, background: "#090712", color: "#fff", ...style },
|
|
511
|
+
children: /* @__PURE__ */ jsx2(SafeScene, { scene: active.scene, onFramePresented })
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const duration = active.end - active.start;
|
|
516
|
+
const rawProgress = clamp01((time - active.start) / duration);
|
|
517
|
+
const next = activeIndex < timeline.length - 1 ? timeline[activeIndex + 1] : void 0;
|
|
518
|
+
const contiguousNext = next && rangesAreContiguous(active, next) ? next : void 0;
|
|
519
|
+
const activeMediaFailed = failedMedia.has(sceneReadinessKey(active.scene));
|
|
520
|
+
const preparingNext = canRetain(active) && canPrepare(contiguousNext);
|
|
521
|
+
const nextPlayable = hasPlayableMedia(contiguousNext);
|
|
522
|
+
const mountingNext = handoffPending || Boolean(contiguousNext && time < active.end && sceneHasBackdrop(contiguousNext));
|
|
523
|
+
const progress = rawProgress;
|
|
524
|
+
const isFinalScene = activeIndex === timeline.length - 1;
|
|
525
|
+
const presentsChapter = active.scene.templateId === "chapterTitle" || active.scene.templateId === "cinemaMedia" && (activeMediaFailed || !String(active.scene.variables.mediaUrl || "").trim());
|
|
526
|
+
const finalHold = presentsChapter ? 0.76 : void 0;
|
|
527
|
+
const holdChapter = presentsChapter && preparingNext && !nextPlayable;
|
|
528
|
+
const motionProgress = (isFinalScene || holdChapter) && finalHold !== void 0 ? Math.min(rawProgress, finalHold) : rawProgress;
|
|
529
|
+
const canvas = getDimensions(config.orientation);
|
|
530
|
+
const scale = Math.min(width / canvas.width, height / canvas.height);
|
|
531
|
+
const canvasLeft = (width - canvas.width * scale) / 2;
|
|
532
|
+
const canvasTop = (height - canvas.height * scale) / 2;
|
|
533
|
+
return /* @__PURE__ */ jsxs(
|
|
534
|
+
"div",
|
|
535
|
+
{
|
|
536
|
+
ref: recoveryRoot,
|
|
537
|
+
onErrorCapture: (event) => {
|
|
538
|
+
const target2 = event.target;
|
|
539
|
+
if (!(target2 instanceof HTMLVideoElement || target2 instanceof HTMLImageElement)) return;
|
|
540
|
+
const ownerId = target2.closest("[data-layer-scene-id]")?.getAttribute("data-layer-scene-id");
|
|
541
|
+
const owner = [active, contiguousNext].find((range) => range?.scene.id === ownerId);
|
|
542
|
+
if (owner?.scene.templateId === "cinemaMedia" && target2.getAttribute("src") === owner.scene.variables.mediaUrl) {
|
|
543
|
+
markMediaFailed(sceneReadinessKey(owner.scene), "decode-error");
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
"data-video-frame": "ready",
|
|
547
|
+
"data-scene-id": active.scene.id,
|
|
548
|
+
"data-template-id": active.scene.templateId,
|
|
549
|
+
className,
|
|
550
|
+
style: {
|
|
551
|
+
width,
|
|
552
|
+
height,
|
|
553
|
+
position: "relative",
|
|
554
|
+
overflow: "hidden",
|
|
555
|
+
background: "#000",
|
|
556
|
+
...style
|
|
557
|
+
},
|
|
558
|
+
children: [
|
|
559
|
+
/* @__PURE__ */ jsx2(
|
|
560
|
+
MountedSceneReadiness,
|
|
561
|
+
{
|
|
562
|
+
scene: active.scene,
|
|
563
|
+
playing: playing && !handoffPending,
|
|
564
|
+
fallback: activeMediaFailed,
|
|
565
|
+
preparedProof: confirmedHandoff.current === sceneReadinessKey(active.scene) ? handoffProof.current : void 0,
|
|
566
|
+
onFailure: sceneHasBackdrop(active) && active.scene.templateId === "cinemaMedia" && !activeMediaFailed ? () => markMediaFailed(sceneReadinessKey(active.scene), "frame-readiness-timeout") : void 0
|
|
567
|
+
}
|
|
568
|
+
),
|
|
569
|
+
mountingNext && contiguousNext && preparingNext && /* @__PURE__ */ jsx2(
|
|
570
|
+
MountedSceneReadiness,
|
|
571
|
+
{
|
|
572
|
+
scene: contiguousNext.scene,
|
|
573
|
+
playing: playing || preparingNarration,
|
|
574
|
+
observeIncoming: true,
|
|
575
|
+
timeoutMs: handoffPending ? 8e3 : null,
|
|
576
|
+
onReady: (proof) => {
|
|
577
|
+
const key = sceneReadinessKey(contiguousNext.scene);
|
|
578
|
+
if (handoffPending) {
|
|
579
|
+
confirmedHandoff.current = key;
|
|
580
|
+
handoffProof.current = proof;
|
|
581
|
+
}
|
|
582
|
+
markPrepared(key);
|
|
583
|
+
},
|
|
584
|
+
onFailure: () => markMediaFailed(sceneReadinessKey(contiguousNext.scene), "frame-readiness-timeout")
|
|
585
|
+
}
|
|
586
|
+
),
|
|
587
|
+
/* @__PURE__ */ jsxs(
|
|
588
|
+
"div",
|
|
589
|
+
{
|
|
590
|
+
"data-video-canvas": "true",
|
|
591
|
+
style: {
|
|
592
|
+
position: "absolute",
|
|
593
|
+
left: canvasLeft,
|
|
594
|
+
top: canvasTop,
|
|
595
|
+
width: canvas.width,
|
|
596
|
+
height: canvas.height,
|
|
597
|
+
transform: `scale(${scale})`,
|
|
598
|
+
transformOrigin: "top left"
|
|
599
|
+
},
|
|
600
|
+
children: [
|
|
601
|
+
/* @__PURE__ */ jsx2(
|
|
602
|
+
"div",
|
|
603
|
+
{
|
|
604
|
+
"data-player-background": "black",
|
|
605
|
+
"aria-hidden": "true",
|
|
606
|
+
style: {
|
|
607
|
+
position: "absolute",
|
|
608
|
+
inset: 0,
|
|
609
|
+
background: "#000",
|
|
610
|
+
backgroundColor: "#000",
|
|
611
|
+
pointerEvents: "none"
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
),
|
|
615
|
+
mountingNext && contiguousNext ? [
|
|
616
|
+
/* @__PURE__ */ jsx2(
|
|
617
|
+
SceneLayer,
|
|
618
|
+
{
|
|
619
|
+
onFramePresented,
|
|
620
|
+
range: active,
|
|
621
|
+
onMediaError: (reason) => markMediaFailed(sceneReadinessKey(active.scene), reason),
|
|
622
|
+
progress,
|
|
623
|
+
motionProgress,
|
|
624
|
+
width: canvas.width,
|
|
625
|
+
height: canvas.height,
|
|
626
|
+
playing: handoffPending ? playing || preparingNarration : playing,
|
|
627
|
+
preparingNarration: handoffPending ? false : preparingNarration,
|
|
628
|
+
mediaAudioMuted,
|
|
629
|
+
mediaAudioVolume,
|
|
630
|
+
layer: "active",
|
|
631
|
+
opacity: 1,
|
|
632
|
+
interactive: true,
|
|
633
|
+
zIndex: 1,
|
|
634
|
+
externalVideoBackdrop: activeMediaFailed ? "fallback" : false
|
|
635
|
+
},
|
|
636
|
+
active.scene.id
|
|
637
|
+
),
|
|
638
|
+
/* @__PURE__ */ jsx2(
|
|
639
|
+
SceneLayer,
|
|
640
|
+
{
|
|
641
|
+
range: contiguousNext,
|
|
642
|
+
progress: 0,
|
|
643
|
+
motionProgress: 0,
|
|
644
|
+
width: canvas.width,
|
|
645
|
+
height: canvas.height,
|
|
646
|
+
playing: Boolean(preparingNext && (!preparedMedia.has(sceneReadinessKey(contiguousNext.scene)) || handoffPending) && (playing || preparingNarration)),
|
|
647
|
+
preparingNarration: preparingNext,
|
|
648
|
+
mediaAudioMuted: true,
|
|
649
|
+
mediaAudioVolume,
|
|
650
|
+
layer: "incoming",
|
|
651
|
+
opacity: 0,
|
|
652
|
+
interactive: false,
|
|
653
|
+
zIndex: 2,
|
|
654
|
+
onMediaError: (reason) => markMediaFailed(sceneReadinessKey(contiguousNext.scene), reason),
|
|
655
|
+
externalVideoBackdrop: failedMedia.has(sceneReadinessKey(contiguousNext.scene)) ? "fallback" : false
|
|
656
|
+
},
|
|
657
|
+
contiguousNext.scene.id
|
|
658
|
+
)
|
|
659
|
+
] : [
|
|
660
|
+
/* @__PURE__ */ jsx2(
|
|
661
|
+
SceneLayer,
|
|
662
|
+
{
|
|
663
|
+
onFramePresented,
|
|
664
|
+
range: active,
|
|
665
|
+
onMediaError: (reason) => markMediaFailed(sceneReadinessKey(active.scene), reason),
|
|
666
|
+
progress,
|
|
667
|
+
motionProgress,
|
|
668
|
+
width: canvas.width,
|
|
669
|
+
height: canvas.height,
|
|
670
|
+
playing,
|
|
671
|
+
preparingNarration,
|
|
672
|
+
mediaAudioMuted,
|
|
673
|
+
mediaAudioVolume,
|
|
674
|
+
layer: "active",
|
|
675
|
+
opacity: 1,
|
|
676
|
+
interactive: true,
|
|
677
|
+
zIndex: 1,
|
|
678
|
+
externalVideoBackdrop: activeMediaFailed ? "fallback" : false
|
|
679
|
+
},
|
|
680
|
+
active.scene.id
|
|
681
|
+
)
|
|
682
|
+
]
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
]
|
|
687
|
+
}
|
|
688
|
+
);
|
|
239
689
|
}
|
|
240
690
|
|
|
241
691
|
// src/player/warm-scene-media.ts
|
|
242
|
-
function warmSceneMedia(variables) {
|
|
243
|
-
void import("./preload-media-
|
|
692
|
+
function warmSceneMedia(variables, signal) {
|
|
693
|
+
void import("./preload-media-23JXUFCF.js").then((module) => module.preloadSceneMedia(variables, signal)).catch(() => {
|
|
244
694
|
});
|
|
245
695
|
}
|
|
246
696
|
|
|
697
|
+
// src/player/use-playback-diagnostics.ts
|
|
698
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
699
|
+
function usePlaybackDiagnostics(root, config, report) {
|
|
700
|
+
const current = useRef3({ config, report });
|
|
701
|
+
current.current = { config, report };
|
|
702
|
+
const enabled = Boolean(report);
|
|
703
|
+
useEffect3(() => {
|
|
704
|
+
if (!enabled) return;
|
|
705
|
+
const observed = /* @__PURE__ */ new WeakMap();
|
|
706
|
+
let lastBuffer = -1;
|
|
707
|
+
const emit = (metric) => {
|
|
708
|
+
try {
|
|
709
|
+
void Promise.resolve(current.current.report?.(metric)).catch(() => void 0);
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
const sample = () => {
|
|
714
|
+
const video = current.current.config;
|
|
715
|
+
if (!root.current || !video) return;
|
|
716
|
+
const ranges = resolveVideoTimeline(video);
|
|
717
|
+
let bufferedSeconds = 0;
|
|
718
|
+
for (const media of root.current.querySelectorAll("video")) {
|
|
719
|
+
const layer = media.closest("[data-layer-scene-id]");
|
|
720
|
+
const sceneId = layer?.getAttribute("data-layer-scene-id");
|
|
721
|
+
const range = ranges.find((value) => value.scene.id === sceneId);
|
|
722
|
+
if (!range || !Number.isFinite(media.duration) || media.duration <= 0) continue;
|
|
723
|
+
const active = layer?.getAttribute("data-scene-layer") === "active";
|
|
724
|
+
const previous = observed.get(media);
|
|
725
|
+
const same = previous?.sceneId === sceneId;
|
|
726
|
+
const repeated = same && previous && !media.paused && !media.seeking && media.currentTime < previous.time - 0.5;
|
|
727
|
+
const repeats = (same && previous ? previous.repeats : 0) + (repeated ? 1 : 0);
|
|
728
|
+
if (active && (!same || repeated)) emit({
|
|
729
|
+
type: "media-playback",
|
|
730
|
+
clipDurationSec: media.duration,
|
|
731
|
+
sceneDurationSec: range.end - range.start,
|
|
732
|
+
repeatCount: repeats
|
|
733
|
+
});
|
|
734
|
+
if (active) observed.set(media, { time: media.currentTime, repeats, sceneId });
|
|
735
|
+
if (media.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) continue;
|
|
736
|
+
for (let index = 0; index < media.buffered.length; index++) {
|
|
737
|
+
if (media.buffered.start(index) <= media.currentTime && media.buffered.end(index) >= media.currentTime) {
|
|
738
|
+
bufferedSeconds += Math.max(0, Math.min(media.buffered.end(index), range.end - range.start) - media.currentTime);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
bufferedSeconds = Math.round(bufferedSeconds * 10) / 10;
|
|
744
|
+
if (bufferedSeconds !== lastBuffer) {
|
|
745
|
+
lastBuffer = bufferedSeconds;
|
|
746
|
+
emit({ type: "buffer", bufferedSeconds });
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
sample();
|
|
750
|
+
const timer = setInterval(sample, 500);
|
|
751
|
+
return () => clearInterval(timer);
|
|
752
|
+
}, [enabled, root]);
|
|
753
|
+
}
|
|
754
|
+
|
|
247
755
|
// src/player/playback-policy.ts
|
|
248
756
|
function resolvePlaybackPolicy({
|
|
249
757
|
playbackMode,
|
|
@@ -263,7 +771,7 @@ function resolvePlaybackPolicy({
|
|
|
263
771
|
}
|
|
264
772
|
|
|
265
773
|
// src/player/player-controls.tsx
|
|
266
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
774
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
267
775
|
function PlayerIcon({ name, size = 22 }) {
|
|
268
776
|
const shared = {
|
|
269
777
|
width: size,
|
|
@@ -276,33 +784,33 @@ function PlayerIcon({ name, size = 22 }) {
|
|
|
276
784
|
strokeLinejoin: "round"
|
|
277
785
|
};
|
|
278
786
|
if (name === "play") {
|
|
279
|
-
return /* @__PURE__ */
|
|
787
|
+
return /* @__PURE__ */ jsx3("svg", { ...shared, children: /* @__PURE__ */ jsx3("path", { d: "M8 5v14l11-7z", fill: "currentColor", stroke: "none" }) });
|
|
280
788
|
}
|
|
281
789
|
if (name === "pause") {
|
|
282
|
-
return /* @__PURE__ */
|
|
790
|
+
return /* @__PURE__ */ jsx3("svg", { ...shared, children: /* @__PURE__ */ jsx3("path", { d: "M7 5h4v14H7zm6 0h4v14h-4z", fill: "currentColor", stroke: "none" }) });
|
|
283
791
|
}
|
|
284
792
|
if (name === "replay") {
|
|
285
|
-
return /* @__PURE__ */
|
|
286
|
-
/* @__PURE__ */
|
|
287
|
-
/* @__PURE__ */
|
|
793
|
+
return /* @__PURE__ */ jsxs2("svg", { ...shared, children: [
|
|
794
|
+
/* @__PURE__ */ jsx3("path", { d: "M4.5 9A8 8 0 1 1 5 16" }),
|
|
795
|
+
/* @__PURE__ */ jsx3("path", { d: "M4.5 4.5V9H9" })
|
|
288
796
|
] });
|
|
289
797
|
}
|
|
290
798
|
if (name === "volume-off") {
|
|
291
|
-
return /* @__PURE__ */
|
|
292
|
-
/* @__PURE__ */
|
|
293
|
-
/* @__PURE__ */
|
|
799
|
+
return /* @__PURE__ */ jsxs2("svg", { ...shared, children: [
|
|
800
|
+
/* @__PURE__ */ jsx3("path", { d: "M11 5 6.5 9H3v6h3.5L11 19z" }),
|
|
801
|
+
/* @__PURE__ */ jsx3("path", { d: "m16 9 5 5M21 9l-5 5" })
|
|
294
802
|
] });
|
|
295
803
|
}
|
|
296
804
|
if (name === "volume") {
|
|
297
|
-
return /* @__PURE__ */
|
|
298
|
-
/* @__PURE__ */
|
|
299
|
-
/* @__PURE__ */
|
|
805
|
+
return /* @__PURE__ */ jsxs2("svg", { ...shared, children: [
|
|
806
|
+
/* @__PURE__ */ jsx3("path", { d: "M11 5 6.5 9H3v6h3.5L11 19z" }),
|
|
807
|
+
/* @__PURE__ */ jsx3("path", { d: "M15 9.5a4 4 0 0 1 0 5M17.8 7a7.5 7.5 0 0 1 0 10" })
|
|
300
808
|
] });
|
|
301
809
|
}
|
|
302
810
|
if (name === "exit-fullscreen") {
|
|
303
|
-
return /* @__PURE__ */
|
|
811
|
+
return /* @__PURE__ */ jsx3("svg", { ...shared, children: /* @__PURE__ */ jsx3("path", { d: "M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" }) });
|
|
304
812
|
}
|
|
305
|
-
return /* @__PURE__ */
|
|
813
|
+
return /* @__PURE__ */ jsx3("svg", { ...shared, children: /* @__PURE__ */ jsx3("path", { d: "M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" }) });
|
|
306
814
|
}
|
|
307
815
|
var startButtonStyle = {
|
|
308
816
|
display: "inline-flex",
|
|
@@ -341,7 +849,7 @@ function GenerationCover({
|
|
|
341
849
|
onStart
|
|
342
850
|
}) {
|
|
343
851
|
if (!visible) return null;
|
|
344
|
-
return /* @__PURE__ */
|
|
852
|
+
return /* @__PURE__ */ jsxs2(
|
|
345
853
|
"div",
|
|
346
854
|
{
|
|
347
855
|
"data-testid": "video-generation-cover",
|
|
@@ -359,12 +867,12 @@ function GenerationCover({
|
|
|
359
867
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Helvetica Neue", Roboto, Arial, sans-serif'
|
|
360
868
|
},
|
|
361
869
|
children: [
|
|
362
|
-
/* @__PURE__ */
|
|
363
|
-
/* @__PURE__ */
|
|
364
|
-
/* @__PURE__ */
|
|
365
|
-
/* @__PURE__ */
|
|
870
|
+
/* @__PURE__ */ jsxs2("div", { style: { position: "absolute", top: "28%", left: "10%", right: "10%" }, children: [
|
|
871
|
+
/* @__PURE__ */ jsx3("div", { "aria-hidden": "true", style: { fontSize: "clamp(28px, 8vw, 72px)", opacity: 0.9 }, children: "\u2726" }),
|
|
872
|
+
/* @__PURE__ */ jsx3("strong", { style: { display: "block", marginTop: 18, fontSize: "clamp(24px, 5vw, 56px)", lineHeight: 1.08 }, children: "Creating your video\u2026" }),
|
|
873
|
+
/* @__PURE__ */ jsx3("span", { style: { display: "block", marginTop: 14, fontSize: "clamp(14px, 2vw, 22px)", lineHeight: 1.4, opacity: 0.78 }, children: "Choosing the best scenes for your content." })
|
|
366
874
|
] }),
|
|
367
|
-
!startRequested && !isPlaying ? /* @__PURE__ */
|
|
875
|
+
!startRequested && !isPlaying ? /* @__PURE__ */ jsxs2(
|
|
368
876
|
"button",
|
|
369
877
|
{
|
|
370
878
|
type: "button",
|
|
@@ -372,7 +880,7 @@ function GenerationCover({
|
|
|
372
880
|
onClick: onStart,
|
|
373
881
|
style: { ...startButtonStyle, ...startButtonPositionStyle },
|
|
374
882
|
children: [
|
|
375
|
-
/* @__PURE__ */
|
|
883
|
+
/* @__PURE__ */ jsx3(PlayerIcon, { name: "play", size: 20 }),
|
|
376
884
|
playLabel(config, isMuted)
|
|
377
885
|
]
|
|
378
886
|
}
|
|
@@ -388,7 +896,7 @@ function StartPosterButton({
|
|
|
388
896
|
onStart
|
|
389
897
|
}) {
|
|
390
898
|
if (!visible) return null;
|
|
391
|
-
return /* @__PURE__ */
|
|
899
|
+
return /* @__PURE__ */ jsxs2(
|
|
392
900
|
"button",
|
|
393
901
|
{
|
|
394
902
|
type: "button",
|
|
@@ -397,7 +905,7 @@ function StartPosterButton({
|
|
|
397
905
|
onClick: onStart,
|
|
398
906
|
style: { ...startButtonStyle, ...startButtonPositionStyle },
|
|
399
907
|
children: [
|
|
400
|
-
/* @__PURE__ */
|
|
908
|
+
/* @__PURE__ */ jsx3(PlayerIcon, { name: "play", size: 20 }),
|
|
401
909
|
playLabel(config, isMuted)
|
|
402
910
|
]
|
|
403
911
|
}
|
|
@@ -405,8 +913,8 @@ function StartPosterButton({
|
|
|
405
913
|
}
|
|
406
914
|
function EndedOverlay({ ended, onReplay }) {
|
|
407
915
|
if (!ended) return null;
|
|
408
|
-
return /* @__PURE__ */
|
|
409
|
-
/* @__PURE__ */
|
|
916
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
917
|
+
/* @__PURE__ */ jsx3(
|
|
410
918
|
"div",
|
|
411
919
|
{
|
|
412
920
|
"data-testid": "video-ended-scrim",
|
|
@@ -421,7 +929,7 @@ function EndedOverlay({ ended, onReplay }) {
|
|
|
421
929
|
}
|
|
422
930
|
}
|
|
423
931
|
),
|
|
424
|
-
/* @__PURE__ */
|
|
932
|
+
/* @__PURE__ */ jsxs2(
|
|
425
933
|
"button",
|
|
426
934
|
{
|
|
427
935
|
type: "button",
|
|
@@ -430,7 +938,7 @@ function EndedOverlay({ ended, onReplay }) {
|
|
|
430
938
|
onClick: onReplay,
|
|
431
939
|
style: { ...startButtonStyle, ...startButtonPositionStyle, zIndex: 3 },
|
|
432
940
|
children: [
|
|
433
|
-
/* @__PURE__ */
|
|
941
|
+
/* @__PURE__ */ jsx3(PlayerIcon, { name: "replay", size: 21 }),
|
|
434
942
|
"Replay"
|
|
435
943
|
]
|
|
436
944
|
}
|
|
@@ -468,36 +976,36 @@ function PlayerControls({
|
|
|
468
976
|
backdropFilter: "blur(12px)",
|
|
469
977
|
cursor: "pointer"
|
|
470
978
|
};
|
|
471
|
-
return /* @__PURE__ */
|
|
979
|
+
return /* @__PURE__ */ jsxs2(
|
|
472
980
|
"div",
|
|
473
981
|
{
|
|
474
982
|
"data-testid": "video-controls",
|
|
475
983
|
style: { position: "absolute", inset: 0, zIndex: 4, pointerEvents: "none" },
|
|
476
984
|
children: [
|
|
477
|
-
/* @__PURE__ */
|
|
985
|
+
/* @__PURE__ */ jsx3(
|
|
478
986
|
"div",
|
|
479
987
|
{
|
|
480
988
|
"data-testid": "video-primary-controls",
|
|
481
989
|
style: { position: "absolute", left: inset, bottom: inset, display: "flex", pointerEvents: "auto" },
|
|
482
|
-
children: /* @__PURE__ */
|
|
990
|
+
children: /* @__PURE__ */ jsx3(
|
|
483
991
|
"button",
|
|
484
992
|
{
|
|
485
993
|
type: "button",
|
|
486
994
|
"aria-label": ended ? "Play video response from beginning" : isPlaying ? "Pause video response" : "Play video response",
|
|
487
995
|
onClick: onTogglePlayback,
|
|
488
996
|
style: buttonStyle,
|
|
489
|
-
children: /* @__PURE__ */
|
|
997
|
+
children: /* @__PURE__ */ jsx3(PlayerIcon, { name: isPlaying ? "pause" : "play" })
|
|
490
998
|
}
|
|
491
999
|
)
|
|
492
1000
|
}
|
|
493
1001
|
),
|
|
494
|
-
/* @__PURE__ */
|
|
1002
|
+
/* @__PURE__ */ jsxs2(
|
|
495
1003
|
"div",
|
|
496
1004
|
{
|
|
497
1005
|
"data-testid": "video-secondary-controls",
|
|
498
1006
|
style: { position: "absolute", right: inset, bottom: inset, display: "flex", gap: 10, pointerEvents: "auto" },
|
|
499
1007
|
children: [
|
|
500
|
-
hasAudio ? /* @__PURE__ */
|
|
1008
|
+
hasAudio ? /* @__PURE__ */ jsx3(
|
|
501
1009
|
"button",
|
|
502
1010
|
{
|
|
503
1011
|
type: "button",
|
|
@@ -505,17 +1013,17 @@ function PlayerControls({
|
|
|
505
1013
|
"aria-pressed": !isMuted,
|
|
506
1014
|
onClick: onToggleMuted,
|
|
507
1015
|
style: buttonStyle,
|
|
508
|
-
children: /* @__PURE__ */
|
|
1016
|
+
children: /* @__PURE__ */ jsx3(PlayerIcon, { name: isMuted ? "volume-off" : "volume" })
|
|
509
1017
|
}
|
|
510
1018
|
) : null,
|
|
511
|
-
/* @__PURE__ */
|
|
1019
|
+
/* @__PURE__ */ jsx3(
|
|
512
1020
|
"button",
|
|
513
1021
|
{
|
|
514
1022
|
type: "button",
|
|
515
1023
|
"aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
|
|
516
1024
|
onClick: onToggleFullscreen,
|
|
517
1025
|
style: buttonStyle,
|
|
518
|
-
children: /* @__PURE__ */
|
|
1026
|
+
children: /* @__PURE__ */ jsx3(PlayerIcon, { name: isFullscreen ? "exit-fullscreen" : "enter-fullscreen" })
|
|
519
1027
|
}
|
|
520
1028
|
)
|
|
521
1029
|
]
|
|
@@ -527,7 +1035,7 @@ function PlayerControls({
|
|
|
527
1035
|
}
|
|
528
1036
|
|
|
529
1037
|
// src/player/use-playback-clock.ts
|
|
530
|
-
import { useEffect } from "react";
|
|
1038
|
+
import { useEffect as useEffect4 } from "react";
|
|
531
1039
|
function usePlaybackClock({
|
|
532
1040
|
isPlaying,
|
|
533
1041
|
stateRef,
|
|
@@ -540,12 +1048,14 @@ function usePlaybackClock({
|
|
|
540
1048
|
setCurrentTime,
|
|
541
1049
|
setIsPlaying
|
|
542
1050
|
}) {
|
|
543
|
-
|
|
1051
|
+
useEffect4(() => {
|
|
544
1052
|
if (!isPlaying) return;
|
|
545
1053
|
let stalled = false;
|
|
1054
|
+
let stallReason;
|
|
546
1055
|
let onsetWaitSeconds = 0;
|
|
547
1056
|
let clockWaitSeconds = 0;
|
|
548
1057
|
let lastNarrationTime;
|
|
1058
|
+
let completionHold;
|
|
549
1059
|
let committedTime = timeRef.current;
|
|
550
1060
|
let groupHandoff;
|
|
551
1061
|
let requestId = stateRef.current.requestId;
|
|
@@ -557,11 +1067,18 @@ function usePlaybackClock({
|
|
|
557
1067
|
} catch {
|
|
558
1068
|
}
|
|
559
1069
|
};
|
|
560
|
-
const reportStall = (next) => {
|
|
561
|
-
if (stalled === next) return;
|
|
1070
|
+
const reportStall = (next, reason) => {
|
|
1071
|
+
if (stalled === next && (!next || stallReason === reason)) return;
|
|
1072
|
+
if (stalled && next) {
|
|
1073
|
+
try {
|
|
1074
|
+
void Promise.resolve(callbacksRef.current.onStallChange?.(false, stallReason)).catch(() => void 0);
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
562
1078
|
stalled = next;
|
|
1079
|
+
stallReason = reason;
|
|
563
1080
|
try {
|
|
564
|
-
void Promise.resolve(callbacksRef.current.onStallChange?.(next)).catch(() => void 0);
|
|
1081
|
+
void Promise.resolve(callbacksRef.current.onStallChange?.(next, reason)).catch(() => void 0);
|
|
565
1082
|
} catch {
|
|
566
1083
|
}
|
|
567
1084
|
};
|
|
@@ -574,8 +1091,12 @@ function usePlaybackClock({
|
|
|
574
1091
|
const replaced = current.requestId !== requestId || current.runId !== runId;
|
|
575
1092
|
requestId = current.requestId;
|
|
576
1093
|
runId = current.runId;
|
|
577
|
-
if (externallySeeked || replaced)
|
|
1094
|
+
if (externallySeeked || replaced) {
|
|
1095
|
+
groupHandoff = void 0;
|
|
1096
|
+
completionHold = void 0;
|
|
1097
|
+
}
|
|
578
1098
|
let deferGroupStall = false;
|
|
1099
|
+
let completionBlocked = false;
|
|
579
1100
|
const elapsed = Math.max(0, (now - previous) / 1e3);
|
|
580
1101
|
let narrationReady = true;
|
|
581
1102
|
try {
|
|
@@ -616,7 +1137,35 @@ function usePlaybackClock({
|
|
|
616
1137
|
failNarration(new Error("Narration audio clock did not advance within eight seconds"), current);
|
|
617
1138
|
return;
|
|
618
1139
|
}
|
|
619
|
-
|
|
1140
|
+
let raw = narrationTime !== void 0 && cued ? cued.start - (cued.scene.narrationGroup?.offsetSeconds ?? 0) + narrationTime : timeRef.current + delta;
|
|
1141
|
+
if (cued && !cued.scene.narrationGroup && narrationTime === void 0) {
|
|
1142
|
+
let speaking = false;
|
|
1143
|
+
try {
|
|
1144
|
+
speaking = callbacksRef.current.narrationActive?.(cued.scene) === true;
|
|
1145
|
+
} catch (cause) {
|
|
1146
|
+
failNarration(cause instanceof Error ? cause : new Error("Narration completion failed"), current);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (completionHold?.sceneId !== cued.scene.id) completionHold = void 0;
|
|
1150
|
+
if (raw >= cued.end - CLIP_NARRATION_TAIL_SEC && speaking) {
|
|
1151
|
+
completionHold ??= { sceneId: cued.scene.id, wait: 0, tail: 0 };
|
|
1152
|
+
if (raw >= cued.end) completionHold.wait += elapsed;
|
|
1153
|
+
if (completionHold.wait >= 8) {
|
|
1154
|
+
failNarration(new Error("Narration did not finish within eight seconds of its scene budget"), current);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (raw >= cued.end) {
|
|
1158
|
+
raw = Math.max(cued.start, cued.end - 0.01);
|
|
1159
|
+
completionBlocked = true;
|
|
1160
|
+
}
|
|
1161
|
+
} else if (completionHold && !speaking) {
|
|
1162
|
+
completionHold.tail += elapsed;
|
|
1163
|
+
if (completionHold.tail < CLIP_NARRATION_TAIL_SEC && raw >= cued.end) {
|
|
1164
|
+
raw = Math.max(cued.start, cued.end - 0.01);
|
|
1165
|
+
completionBlocked = true;
|
|
1166
|
+
} else if (completionHold.tail >= CLIP_NARRATION_TAIL_SEC) completionHold = void 0;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
620
1169
|
let nextTime;
|
|
621
1170
|
if (looping && duration2 > 0 && raw >= duration2) {
|
|
622
1171
|
nextTime = raw % duration2;
|
|
@@ -667,7 +1216,8 @@ function usePlaybackClock({
|
|
|
667
1216
|
}
|
|
668
1217
|
const duration = current.config ? getVideoDuration(current.config) : 0;
|
|
669
1218
|
const active = current.config ? resolveVideoTimeline(current.config).find((range) => timeRef.current >= range.start && timeRef.current < range.end) : void 0;
|
|
670
|
-
|
|
1219
|
+
const reason = !narrationReady || completionBlocked ? "speech" : active && visualReadyRef && visualReadyRef.current !== sceneReadinessKey(active.scene) && !deferGroupStall ? "media-decoding" : !settled && Boolean(current.config?.scenes.length) && duration > 0 && timeRef.current >= duration ? "scene-generation" : void 0;
|
|
1220
|
+
reportStall(Boolean(reason), reason);
|
|
671
1221
|
committedTime = timeRef.current;
|
|
672
1222
|
if (!settled || looping || timeRef.current < duration) frame = requestAnimationFrame(tick);
|
|
673
1223
|
else setIsPlaying(false);
|
|
@@ -681,7 +1231,7 @@ function usePlaybackClock({
|
|
|
681
1231
|
}
|
|
682
1232
|
|
|
683
1233
|
// src/player/video-player.tsx
|
|
684
|
-
import { jsx as
|
|
1234
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
685
1235
|
var MINIMUM_GENERATION_INTRO_MS = 3e3;
|
|
686
1236
|
function savedVideoState(video) {
|
|
687
1237
|
return {
|
|
@@ -691,7 +1241,6 @@ function savedVideoState(video) {
|
|
|
691
1241
|
};
|
|
692
1242
|
}
|
|
693
1243
|
function VideoPlayerRuntime({
|
|
694
|
-
kit,
|
|
695
1244
|
stream,
|
|
696
1245
|
video,
|
|
697
1246
|
playbackMode,
|
|
@@ -713,6 +1262,8 @@ function VideoPlayerRuntime({
|
|
|
713
1262
|
onSceneChange,
|
|
714
1263
|
narrationReady,
|
|
715
1264
|
narrationTime,
|
|
1265
|
+
narrationActive,
|
|
1266
|
+
onPlaybackMetric,
|
|
716
1267
|
onFramePresented,
|
|
717
1268
|
onMediaFramePresented,
|
|
718
1269
|
onStallChange,
|
|
@@ -722,10 +1273,10 @@ function VideoPlayerRuntime({
|
|
|
722
1273
|
1,
|
|
723
1274
|
typeof nativeMediaAudio?.volume === "number" && Number.isFinite(nativeMediaAudio.volume) ? nativeMediaAudio.volume : 1
|
|
724
1275
|
));
|
|
725
|
-
const [reducedMotion, setReducedMotion] =
|
|
1276
|
+
const [reducedMotion, setReducedMotion] = useState2(
|
|
726
1277
|
() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
727
1278
|
);
|
|
728
|
-
const [audioUnlocked, setAudioUnlocked] =
|
|
1279
|
+
const [audioUnlocked, setAudioUnlocked] = useState2(false);
|
|
729
1280
|
const { resolvedStartMuted, shouldAutoPlay, autoStartGeneration } = resolvePlaybackPolicy({
|
|
730
1281
|
playbackMode,
|
|
731
1282
|
autoPlay,
|
|
@@ -734,28 +1285,40 @@ function VideoPlayerRuntime({
|
|
|
734
1285
|
reducedMotion,
|
|
735
1286
|
hasStream: Boolean(stream)
|
|
736
1287
|
});
|
|
737
|
-
const [isPlaying, setIsPlaying] =
|
|
738
|
-
const [isMuted, setIsMuted] =
|
|
739
|
-
const [fullscreenMode, setFullscreenMode] =
|
|
740
|
-
const [state, setState] =
|
|
741
|
-
const [currentTime, setCurrentTime] =
|
|
742
|
-
const
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
const
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
const
|
|
1288
|
+
const [isPlaying, setIsPlaying] = useState2(() => shouldAutoPlay && !reducedMotion && !autoStartGeneration);
|
|
1289
|
+
const [isMuted, setIsMuted] = useState2(resolvedStartMuted);
|
|
1290
|
+
const [fullscreenMode, setFullscreenMode] = useState2("none");
|
|
1291
|
+
const [state, setState] = useState2(() => video ? savedVideoState(video) : createVideoState());
|
|
1292
|
+
const [currentTime, setCurrentTime] = useState2(0);
|
|
1293
|
+
const savedActiveIndex = video ? resolveVideoTimeline(video).findIndex((range) => currentTime >= range.start && currentTime < range.end) : -1;
|
|
1294
|
+
const savedPreparationIndex = savedActiveIndex >= 0 ? savedActiveIndex : Math.max(0, (video?.scenes.length ?? 1) - 1);
|
|
1295
|
+
useEffect5(() => {
|
|
1296
|
+
if (!video) return;
|
|
1297
|
+
const controller = new AbortController();
|
|
1298
|
+
for (const scene of video.scenes.slice(savedPreparationIndex, savedPreparationIndex + 2)) {
|
|
1299
|
+
preloadBuiltinTemplate(scene.templateId);
|
|
1300
|
+
warmSceneMedia(scene.variables, controller.signal);
|
|
1301
|
+
}
|
|
1302
|
+
return () => controller.abort();
|
|
1303
|
+
}, [video, savedPreparationIndex]);
|
|
1304
|
+
const [activeStream, setActiveStream] = useState2(stream);
|
|
1305
|
+
const [activeSavedVideo, setActiveSavedVideo] = useState2(video);
|
|
1306
|
+
const [replacementPending, setReplacementPending] = useState2(false);
|
|
1307
|
+
const [startRequested, setStartRequested] = useState2(autoStartGeneration);
|
|
1308
|
+
const [introPlaying, setIntroPlaying] = useState2(autoStartGeneration);
|
|
1309
|
+
const [generationIntroComplete, setGenerationIntroComplete] = useState2(() => !playbackMode || !stream);
|
|
1310
|
+
const [observedWidth, setObservedWidth] = useState2(0);
|
|
1311
|
+
const containerRef = useRef4(null);
|
|
1312
|
+
usePlaybackDiagnostics(containerRef, state.config ?? void 0, onPlaybackMetric);
|
|
1313
|
+
const stateRef = useRef4(state);
|
|
1314
|
+
const timeRef = useRef4(currentTime);
|
|
1315
|
+
const audioRef = useRef4(null);
|
|
1316
|
+
const introStartedAtRef = useRef4(autoStartGeneration ? performance.now() : null);
|
|
1317
|
+
const callbacksRef = useRef4({ onComplete, onPlaybackEnd, onError, onSceneChange, narrationReady, narrationTime, narrationActive, onFramePresented, onMediaFramePresented, onStallChange, onStateChange });
|
|
1318
|
+
const loopRef = useRef4(loop);
|
|
1319
|
+
const sceneIndexRef = useRef4(-1);
|
|
1320
|
+
const mediaFrameReportedRef = useRef4(false);
|
|
1321
|
+
const visualReadyRef = useRef4(void 0);
|
|
759
1322
|
const reportVisualReady = useMemo(() => (key, error, actualVideoFrame = false) => {
|
|
760
1323
|
if (error) {
|
|
761
1324
|
setIsPlaying(false);
|
|
@@ -771,7 +1334,7 @@ function VideoPlayerRuntime({
|
|
|
771
1334
|
}
|
|
772
1335
|
}
|
|
773
1336
|
}, []);
|
|
774
|
-
const playbackEndedRef =
|
|
1337
|
+
const playbackEndedRef = useRef4(false);
|
|
775
1338
|
let mediaPlaying = isPlaying;
|
|
776
1339
|
if (sceneIndexRef.current >= 0) {
|
|
777
1340
|
try {
|
|
@@ -782,7 +1345,7 @@ function VideoPlayerRuntime({
|
|
|
782
1345
|
}
|
|
783
1346
|
stateRef.current = state;
|
|
784
1347
|
timeRef.current = currentTime;
|
|
785
|
-
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, narrationReady, narrationTime, onFramePresented, onMediaFramePresented, onStallChange, onStateChange };
|
|
1348
|
+
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, narrationReady, narrationTime, narrationActive, onFramePresented, onMediaFramePresented, onStallChange, onStateChange };
|
|
786
1349
|
loopRef.current = loop;
|
|
787
1350
|
const reportFramePresented = useMemo(() => {
|
|
788
1351
|
let reported = false;
|
|
@@ -827,7 +1390,7 @@ function VideoPlayerRuntime({
|
|
|
827
1390
|
setGenerationIntroComplete(!playbackMode || !stream);
|
|
828
1391
|
introStartedAtRef.current = autoStartReplacement ? performance.now() : null;
|
|
829
1392
|
}
|
|
830
|
-
|
|
1393
|
+
useEffect5(() => {
|
|
831
1394
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
832
1395
|
const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
833
1396
|
const handleChange = (event) => {
|
|
@@ -842,12 +1405,12 @@ function VideoPlayerRuntime({
|
|
|
842
1405
|
preference.addEventListener?.("change", handleChange);
|
|
843
1406
|
return () => preference.removeEventListener?.("change", handleChange);
|
|
844
1407
|
}, []);
|
|
845
|
-
|
|
1408
|
+
useEffect5(() => {
|
|
846
1409
|
import("./control-visibility-NQOWTKOQ.js");
|
|
847
1410
|
}, []);
|
|
848
|
-
|
|
849
|
-
const pausedRef =
|
|
850
|
-
|
|
1411
|
+
useEffect5(() => setIsMuted(resolvedStartMuted), [resolvedStartMuted]);
|
|
1412
|
+
const pausedRef = useRef4(paused);
|
|
1413
|
+
useEffect5(() => {
|
|
851
1414
|
const previous = pausedRef.current;
|
|
852
1415
|
pausedRef.current = paused;
|
|
853
1416
|
if (paused === void 0 || paused === previous) return;
|
|
@@ -863,7 +1426,7 @@ function VideoPlayerRuntime({
|
|
|
863
1426
|
const audio = audioRef.current;
|
|
864
1427
|
if (audio?.paused) void audio.play().catch(Boolean);
|
|
865
1428
|
}, [paused]);
|
|
866
|
-
|
|
1429
|
+
useEffect5(() => {
|
|
867
1430
|
let cancelled = false;
|
|
868
1431
|
let started = false;
|
|
869
1432
|
const reset = video ? savedVideoState(video) : createVideoState();
|
|
@@ -871,13 +1434,7 @@ function VideoPlayerRuntime({
|
|
|
871
1434
|
timeRef.current = 0;
|
|
872
1435
|
setState(reset);
|
|
873
1436
|
setCurrentTime(0);
|
|
874
|
-
if (video)
|
|
875
|
-
for (const scene of video.scenes) {
|
|
876
|
-
preloadBuiltinTemplate(scene.templateId);
|
|
877
|
-
warmSceneMedia(scene.variables);
|
|
878
|
-
}
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
1437
|
+
if (video) return;
|
|
881
1438
|
if (!stream) return;
|
|
882
1439
|
const consume = async () => {
|
|
883
1440
|
try {
|
|
@@ -913,7 +1470,7 @@ function VideoPlayerRuntime({
|
|
|
913
1470
|
}
|
|
914
1471
|
};
|
|
915
1472
|
}, [stream, video]);
|
|
916
|
-
|
|
1473
|
+
useEffect5(() => {
|
|
917
1474
|
if (width != null) return;
|
|
918
1475
|
const container = containerRef.current;
|
|
919
1476
|
if (!container) return;
|
|
@@ -938,12 +1495,12 @@ function VideoPlayerRuntime({
|
|
|
938
1495
|
setCurrentTime,
|
|
939
1496
|
setIsPlaying
|
|
940
1497
|
});
|
|
941
|
-
|
|
1498
|
+
useEffect5(() => {
|
|
942
1499
|
const audio = audioRef.current;
|
|
943
1500
|
if (!audio) return;
|
|
944
1501
|
audio.volume = state.config?.audio?.volume ?? 1;
|
|
945
1502
|
}, [state.config?.audio?.audioUrl, state.config?.audio?.volume]);
|
|
946
|
-
|
|
1503
|
+
useEffect5(() => {
|
|
947
1504
|
const audio = audioRef.current;
|
|
948
1505
|
if (!audio) return;
|
|
949
1506
|
if (isPlaying || introPlaying) {
|
|
@@ -961,7 +1518,7 @@ function VideoPlayerRuntime({
|
|
|
961
1518
|
audio.pause();
|
|
962
1519
|
}
|
|
963
1520
|
}, [introPlaying, isPlaying, state.config?.audio?.audioUrl]);
|
|
964
|
-
|
|
1521
|
+
useEffect5(() => {
|
|
965
1522
|
if (!state.config?.scenes.length) {
|
|
966
1523
|
const terminalWithoutVideo = state.status === "complete" || state.status === "error" || state.status === "aborted";
|
|
967
1524
|
if (terminalWithoutVideo) {
|
|
@@ -1019,7 +1576,7 @@ function VideoPlayerRuntime({
|
|
|
1019
1576
|
const terminal = state.status === "complete" || state.status === "error" || state.status === "aborted";
|
|
1020
1577
|
const playheadAtEnd = terminal && duration > 0 && currentTime >= duration - 1e-3;
|
|
1021
1578
|
const ended = !loop && playheadAtEnd;
|
|
1022
|
-
|
|
1579
|
+
useEffect5(() => {
|
|
1023
1580
|
if (!playheadAtEnd) {
|
|
1024
1581
|
playbackEndedRef.current = false;
|
|
1025
1582
|
return;
|
|
@@ -1032,7 +1589,7 @@ function VideoPlayerRuntime({
|
|
|
1032
1589
|
const firstSceneRange = config ? resolveVideoTimeline(config)[0] : void 0;
|
|
1033
1590
|
const hasSuppliedOpening = firstSceneRange?.scene.id === "supplied-opening";
|
|
1034
1591
|
const showStartPoster = (!generationIntroWaiting || hasSuppliedOpening) && !startRequested && !isPlaying && !ended && currentTime <= 1e-3 && Boolean(config?.scenes.length);
|
|
1035
|
-
const firstSceneHoldProgress = firstSceneRange ?
|
|
1592
|
+
const firstSceneHoldProgress = firstSceneRange ? 0.7 : 0;
|
|
1036
1593
|
const posterTime = firstSceneRange ? firstSceneRange.start + Math.max(0, firstSceneRange.end - firstSceneRange.start) * firstSceneHoldProgress : currentTime;
|
|
1037
1594
|
const startPlayback = () => {
|
|
1038
1595
|
setGenerationIntroComplete(true);
|
|
@@ -1095,7 +1652,7 @@ function VideoPlayerRuntime({
|
|
|
1095
1652
|
const pendingReplacement = replacementPending && state.status === "idle";
|
|
1096
1653
|
const displayedStatus = pendingReplacement ? "streaming" : state.status;
|
|
1097
1654
|
const generationCoverVisible = generationIntroWaiting && !hasSuppliedOpening || displayedStatus === "streaming" && !config?.scenes.length;
|
|
1098
|
-
return /* @__PURE__ */
|
|
1655
|
+
return /* @__PURE__ */ jsxs3(
|
|
1099
1656
|
"div",
|
|
1100
1657
|
{
|
|
1101
1658
|
ref: containerRef,
|
|
@@ -1135,7 +1692,7 @@ function VideoPlayerRuntime({
|
|
|
1135
1692
|
...style
|
|
1136
1693
|
},
|
|
1137
1694
|
children: [
|
|
1138
|
-
/* @__PURE__ */
|
|
1695
|
+
/* @__PURE__ */ jsx4(
|
|
1139
1696
|
GenerationCover,
|
|
1140
1697
|
{
|
|
1141
1698
|
visible: generationCoverVisible,
|
|
@@ -1146,10 +1703,9 @@ function VideoPlayerRuntime({
|
|
|
1146
1703
|
onStart: armPlayback
|
|
1147
1704
|
}
|
|
1148
1705
|
),
|
|
1149
|
-
!generationCoverVisible && config?.scenes.length ? /* @__PURE__ */
|
|
1706
|
+
!generationCoverVisible && config?.scenes.length ? /* @__PURE__ */ jsx4(MountedReadinessContext.Provider, { value: reportVisualReady, children: /* @__PURE__ */ jsx4(
|
|
1150
1707
|
VideoFrame,
|
|
1151
1708
|
{
|
|
1152
|
-
kit,
|
|
1153
1709
|
onFramePresented: !showStartPoster && onFramePresented ? reportFramePresented : void 0,
|
|
1154
1710
|
config: displayConfig,
|
|
1155
1711
|
time: showStartPoster ? posterTime : currentTime,
|
|
@@ -1168,7 +1724,7 @@ function VideoPlayerRuntime({
|
|
|
1168
1724
|
}
|
|
1169
1725
|
}
|
|
1170
1726
|
) }) : null,
|
|
1171
|
-
/* @__PURE__ */
|
|
1727
|
+
/* @__PURE__ */ jsx4(
|
|
1172
1728
|
StartPosterButton,
|
|
1173
1729
|
{
|
|
1174
1730
|
visible: showStartPoster,
|
|
@@ -1177,7 +1733,7 @@ function VideoPlayerRuntime({
|
|
|
1177
1733
|
onStart: startPlayback
|
|
1178
1734
|
}
|
|
1179
1735
|
),
|
|
1180
|
-
config?.audio ? /* @__PURE__ */
|
|
1736
|
+
config?.audio ? /* @__PURE__ */ jsx4(
|
|
1181
1737
|
"audio",
|
|
1182
1738
|
{
|
|
1183
1739
|
ref: audioRef,
|
|
@@ -1190,8 +1746,8 @@ function VideoPlayerRuntime({
|
|
|
1190
1746
|
},
|
|
1191
1747
|
config.audio.audioUrl
|
|
1192
1748
|
) : null,
|
|
1193
|
-
/* @__PURE__ */
|
|
1194
|
-
/* @__PURE__ */
|
|
1749
|
+
/* @__PURE__ */ jsx4(EndedOverlay, { ended: controls && ended, onReplay: togglePlayback }),
|
|
1750
|
+
/* @__PURE__ */ jsx4(
|
|
1195
1751
|
PlayerControls,
|
|
1196
1752
|
{
|
|
1197
1753
|
visible: controls && !generationCoverVisible && !showStartPoster && Boolean(config?.scenes.length),
|
|
@@ -1214,7 +1770,6 @@ function VideoPlayerRuntime({
|
|
|
1214
1770
|
);
|
|
1215
1771
|
}
|
|
1216
1772
|
function VideoPlayer({
|
|
1217
|
-
templates,
|
|
1218
1773
|
stream,
|
|
1219
1774
|
video,
|
|
1220
1775
|
onPlaybackEnd,
|
|
@@ -1223,16 +1778,11 @@ function VideoPlayer({
|
|
|
1223
1778
|
...props
|
|
1224
1779
|
}) {
|
|
1225
1780
|
const savedVideo = useMemo(() => video ? parseVideo(video) : void 0, [video]);
|
|
1226
|
-
const kit = useMemo(
|
|
1227
|
-
() => overlayPlayerTemplateRegistry(BUILTIN_PLAYER_KIT, templates),
|
|
1228
|
-
[templates]
|
|
1229
|
-
);
|
|
1230
1781
|
if (!stream && !savedVideo) return null;
|
|
1231
|
-
return /* @__PURE__ */
|
|
1782
|
+
return /* @__PURE__ */ jsx4(
|
|
1232
1783
|
VideoPlayerRuntime,
|
|
1233
1784
|
{
|
|
1234
1785
|
...props,
|
|
1235
|
-
kit,
|
|
1236
1786
|
stream,
|
|
1237
1787
|
video: savedVideo,
|
|
1238
1788
|
onPlaybackEnd: (state) => {
|
|
@@ -1335,21 +1885,144 @@ function createCaptionVoice(source, now = () => performance.now()) {
|
|
|
1335
1885
|
return { voice, getCaptionProgress: read, reset };
|
|
1336
1886
|
}
|
|
1337
1887
|
|
|
1888
|
+
// src/player/prepare-scene-media.ts
|
|
1889
|
+
async function prepareSceneMedia(variables, signal) {
|
|
1890
|
+
signal.throwIfAborted();
|
|
1891
|
+
const url = typeof variables.mediaUrl === "string" ? variables.mediaUrl.trim() : "";
|
|
1892
|
+
if (!url || variables.mediaType === "gradient") return "graphic";
|
|
1893
|
+
const video = variables.mediaType === "video" || /\.(mp4|webm|mov)(?:[?#]|$)/i.test(url);
|
|
1894
|
+
if (video) return "awaiting-video-frame";
|
|
1895
|
+
await new Promise((resolve, reject) => {
|
|
1896
|
+
const image = new Image();
|
|
1897
|
+
let settled = false;
|
|
1898
|
+
const finish = (error) => {
|
|
1899
|
+
if (settled) return;
|
|
1900
|
+
settled = true;
|
|
1901
|
+
clearTimeout(timer);
|
|
1902
|
+
signal.removeEventListener("abort", abort);
|
|
1903
|
+
image.onload = null;
|
|
1904
|
+
image.onerror = null;
|
|
1905
|
+
if (error) {
|
|
1906
|
+
image.src = "";
|
|
1907
|
+
reject(error);
|
|
1908
|
+
} else resolve();
|
|
1909
|
+
};
|
|
1910
|
+
const abort = () => finish(signal.reason ?? new DOMException("Cancelled", "AbortError"));
|
|
1911
|
+
const timer = setTimeout(() => finish(new DOMException("Media preparation timed out", "TimeoutError")), 8e3);
|
|
1912
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1913
|
+
image.onload = () => {
|
|
1914
|
+
void (image.decode ? image.decode() : Promise.resolve()).then(() => finish(), () => finish(new Error("Could not prepare scene image")));
|
|
1915
|
+
};
|
|
1916
|
+
image.onerror = () => finish(new Error("Could not prepare scene image"));
|
|
1917
|
+
image.src = url;
|
|
1918
|
+
if (image.complete && image.naturalWidth > 0) image.onload(new Event("load"));
|
|
1919
|
+
});
|
|
1920
|
+
return "image";
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1338
1923
|
// src/player/recover-scene-media.ts
|
|
1339
1924
|
function recoverSceneMedia(scene) {
|
|
1340
1925
|
if (scene.templateId === "cinemaMedia") {
|
|
1341
1926
|
const title = scene.variables.fallbackText;
|
|
1342
|
-
|
|
1343
|
-
return { ...scene, templateId: "chapterTitle", variables: { title:
|
|
1927
|
+
const fallback = typeof title === "string" && title.trim() && [...title].length <= 65 ? title.trim() : "Your response continues.";
|
|
1928
|
+
return { ...scene, templateId: "chapterTitle", variables: { title: fallback } };
|
|
1344
1929
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1930
|
+
return void 0;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// src/player/scene-readiness.ts
|
|
1934
|
+
function preparedSceneDuration(scene, spokenSeconds, metadata) {
|
|
1935
|
+
const timing = metadata?.timing;
|
|
1936
|
+
const authored = (timing?.revealSeconds ?? 0) + (timing?.holdSeconds ?? 0) + (timing?.exitSeconds ?? 0);
|
|
1937
|
+
const readable = Math.max(getSceneDurationBounds(scene, metadata).readable, authored);
|
|
1938
|
+
return Number.isFinite(spokenSeconds) && spokenSeconds > 0 ? Math.max(readable, spokenSeconds + CLIP_NARRATION_TAIL_SEC) : Math.max(readable, getSceneDuration(scene, metadata));
|
|
1939
|
+
}
|
|
1940
|
+
function prepareNarratedScene(scene, spokenSeconds) {
|
|
1941
|
+
const requested = scene.timing.fixedDuration;
|
|
1942
|
+
const actual = scene.variables.mediaDurationSec;
|
|
1943
|
+
const durations = [requested, actual].filter((value) => typeof value === "number" && Number.isFinite(value) && value > 0);
|
|
1944
|
+
const clipDurationSec = scene.templateId === "cinemaMedia" && durations.length ? Math.min(...durations) : void 0;
|
|
1945
|
+
const recovered = clipDurationSec !== void 0 && spokenSeconds !== void 0 && !speechFitsClip(spokenSeconds, clipDurationSec);
|
|
1946
|
+
const visual = recovered ? recoverSceneMedia(scene) : scene;
|
|
1947
|
+
const duration = preparedSceneDuration(visual, spokenSeconds, getBuiltinSceneDefinition(visual.templateId));
|
|
1948
|
+
const { startTime: _start, endTime: _end, beatStart: _beatStart, beatEnd: _beatEnd, ...timing } = visual.timing;
|
|
1949
|
+
return { scene: { ...visual, timing: { ...timing, fixedDuration: duration } }, recovered, clipDurationSec };
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// src/video-chat/scene-preparation.ts
|
|
1953
|
+
function createScenePreparation(options) {
|
|
1954
|
+
const speechLoads = /* @__PURE__ */ new Map();
|
|
1955
|
+
const lanes = [Promise.resolve(), Promise.resolve()];
|
|
1956
|
+
const announced = /* @__PURE__ */ new Set();
|
|
1957
|
+
const announcedMedia = /* @__PURE__ */ new Set();
|
|
1958
|
+
let nextLane = 0;
|
|
1959
|
+
let customVoiceFailed = false;
|
|
1960
|
+
const prepareSpeech = (text, signal = options.signal) => {
|
|
1961
|
+
const cached = speechLoads.get(text);
|
|
1962
|
+
if (cached) return cached;
|
|
1963
|
+
const lane = nextLane++ % lanes.length;
|
|
1964
|
+
const prepared = lanes[lane].then(async () => {
|
|
1965
|
+
signal.throwIfAborted();
|
|
1966
|
+
try {
|
|
1967
|
+
if (customVoiceFailed) throw new Error("Voice preparation did not respond");
|
|
1968
|
+
return options.customVoice ? await withDeadline((child) => options.voice().prepare(text, { signal: child }), 3e3, signal) : await options.voice().prepare(text, { signal });
|
|
1969
|
+
} catch (cause) {
|
|
1970
|
+
if (options.customVoice && !signal.aborted && !options.signal.aborted) {
|
|
1971
|
+
options.onVoiceUnavailable(text);
|
|
1972
|
+
if (cause instanceof DOMException && cause.name === "TimeoutError") customVoiceFailed = true;
|
|
1973
|
+
}
|
|
1974
|
+
throw cause;
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
speechLoads.set(text, prepared);
|
|
1978
|
+
lanes[lane] = prepared.catch(() => void 0);
|
|
1979
|
+
return prepared;
|
|
1980
|
+
};
|
|
1981
|
+
const announce = (value) => {
|
|
1982
|
+
if (options.signal.aborted || !value || typeof value !== "object" || Array.isArray(value)) return;
|
|
1983
|
+
const data = value;
|
|
1984
|
+
if (typeof data.sceneId !== "string" || !data.sceneId || data.sceneId.length > 200 || typeof data.narration !== "string" || !data.narration.trim() || data.narration.length > 2e3 || !announced.has(data.sceneId) && announced.size >= 32) return;
|
|
1985
|
+
announced.add(data.sceneId);
|
|
1986
|
+
if (speechLoads.size < 32 || speechLoads.has(data.narration.trim())) void prepareSpeech(data.narration.trim()).catch(() => void 0);
|
|
1987
|
+
const media = sanitizeVideoChatMedia(data.media);
|
|
1988
|
+
if (media && !announcedMedia.has(data.sceneId)) {
|
|
1989
|
+
announcedMedia.add(data.sceneId);
|
|
1990
|
+
warmSceneMedia({ mediaUrl: media.url, mediaType: media.type === "image" ? "photo" : "video", mediaPoster: media.posterUrl }, options.signal);
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
const prepareVisual = (scene) => {
|
|
1994
|
+
warmSceneMedia(scene.variables, options.signal);
|
|
1995
|
+
const prepared = Promise.resolve(preloadBuiltinTemplate(scene.templateId)).then(async () => {
|
|
1996
|
+
try {
|
|
1997
|
+
await prepareSceneMedia(scene.variables, options.signal);
|
|
1998
|
+
return scene;
|
|
1999
|
+
} catch (cause) {
|
|
2000
|
+
if (options.signal.aborted) throw cause;
|
|
2001
|
+
const recovered = recoverSceneMedia(scene);
|
|
2002
|
+
if (!recovered) throw new VideoError("Scene could not prepare its visual", { code: "media_not_ready" });
|
|
2003
|
+
options.warn(MEDIA_RECOVERY_NOTICE);
|
|
2004
|
+
return recovered;
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
void prepared.catch(() => void 0);
|
|
2008
|
+
return prepared;
|
|
2009
|
+
};
|
|
2010
|
+
const pace = (scene, seconds) => {
|
|
2011
|
+
const prepared = prepareNarratedScene(scene, seconds);
|
|
2012
|
+
if (prepared.recovered) options.warn(MEDIA_RECOVERY_NOTICE);
|
|
2013
|
+
if (seconds !== void 0 && prepared.clipDurationSec !== void 0) {
|
|
2014
|
+
try {
|
|
2015
|
+
options.onDuration?.({ speechDurationSec: seconds, clipDurationSec: prepared.clipDurationSec, recovered: prepared.recovered });
|
|
2016
|
+
} catch {
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
return prepared.scene;
|
|
2020
|
+
};
|
|
2021
|
+
return { prepareSpeech, announce, prepareVisual, pace };
|
|
1349
2022
|
}
|
|
1350
2023
|
|
|
1351
2024
|
// src/video-chat/use-video-chat.ts
|
|
1352
|
-
import { useCallback as
|
|
2025
|
+
import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo2, useReducer, useRef as useRef6 } from "react";
|
|
1353
2026
|
|
|
1354
2027
|
// src/protocol/scene-timeline.ts
|
|
1355
2028
|
function createQueue() {
|
|
@@ -1457,25 +2130,17 @@ function createSceneTimeline(options) {
|
|
|
1457
2130
|
};
|
|
1458
2131
|
}
|
|
1459
2132
|
|
|
1460
|
-
// src/player/scene-readiness.ts
|
|
1461
|
-
function preparedSceneDuration(scene, spokenSeconds, metadata) {
|
|
1462
|
-
const timing = metadata?.timing;
|
|
1463
|
-
const authored = (timing?.revealSeconds ?? 0) + (timing?.holdSeconds ?? 0) + (timing?.exitSeconds ?? 0);
|
|
1464
|
-
const readable = Math.max(getSceneDurationBounds(scene, metadata).readable, authored);
|
|
1465
|
-
return Number.isFinite(spokenSeconds) && spokenSeconds > 0 ? Math.max(readable, spokenSeconds + 0.8) : Math.max(readable, getSceneDuration(scene, metadata));
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
2133
|
// src/player/use-narration.ts
|
|
1469
|
-
import { useCallback, useEffect as
|
|
2134
|
+
import { useCallback as useCallback2, useEffect as useEffect6, useRef as useRef5, useState as useState3 } from "react";
|
|
1470
2135
|
function useNarration(options) {
|
|
1471
|
-
const [speaking, setSpeaking] =
|
|
1472
|
-
const optionsRef =
|
|
2136
|
+
const [speaking, setSpeaking] = useState3(false);
|
|
2137
|
+
const optionsRef = useRef5(options);
|
|
1473
2138
|
optionsRef.current = options;
|
|
1474
|
-
const groupRef =
|
|
1475
|
-
const clockRef =
|
|
1476
|
-
const clockSceneRef =
|
|
1477
|
-
const readyRef =
|
|
1478
|
-
const getTime =
|
|
2139
|
+
const groupRef = useRef5(void 0);
|
|
2140
|
+
const clockRef = useRef5(void 0);
|
|
2141
|
+
const clockSceneRef = useRef5(void 0);
|
|
2142
|
+
const readyRef = useRef5(true);
|
|
2143
|
+
const getTime = useCallback2((scene) => {
|
|
1479
2144
|
const group = groupRef.current;
|
|
1480
2145
|
if (optionsRef.current.enabled === false || (group ? scene.narrationGroup?.id !== group.id : scene.id !== clockSceneRef.current)) return void 0;
|
|
1481
2146
|
if (!optionsRef.current.voice.getCurrentTime) return void 0;
|
|
@@ -1488,10 +2153,11 @@ function useNarration(options) {
|
|
|
1488
2153
|
}
|
|
1489
2154
|
return clockRef.current;
|
|
1490
2155
|
}, []);
|
|
1491
|
-
const isReady =
|
|
1492
|
-
const currentRef =
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
2156
|
+
const isReady = useCallback2(() => optionsRef.current.enabled === false || readyRef.current || clockRef.current !== void 0 && clockRef.current >= (groupRef.current?.offsetSeconds ?? 0) + 0.04, []);
|
|
2157
|
+
const currentRef = useRef5(void 0);
|
|
2158
|
+
const isSpeaking = useCallback2((scene) => optionsRef.current.enabled !== false && Boolean(currentRef.current) && (groupRef.current ? scene.narrationGroup?.id === groupRef.current.id : scene.id === clockSceneRef.current), []);
|
|
2159
|
+
const spokenIndexRef = useRef5(void 0);
|
|
2160
|
+
const stop = useCallback2(() => {
|
|
1495
2161
|
readyRef.current = true;
|
|
1496
2162
|
currentRef.current?.abort();
|
|
1497
2163
|
currentRef.current = void 0;
|
|
@@ -1500,12 +2166,12 @@ function useNarration(options) {
|
|
|
1500
2166
|
clockSceneRef.current = void 0;
|
|
1501
2167
|
setSpeaking(false);
|
|
1502
2168
|
}, []);
|
|
1503
|
-
const interrupt =
|
|
2169
|
+
const interrupt = useCallback2(() => {
|
|
1504
2170
|
spokenIndexRef.current = void 0;
|
|
1505
2171
|
stop();
|
|
1506
2172
|
}, [stop]);
|
|
1507
|
-
|
|
1508
|
-
const onSceneChange =
|
|
2173
|
+
useEffect6(() => () => currentRef.current?.abort(), []);
|
|
2174
|
+
const onSceneChange = useCallback2((scene, index) => {
|
|
1509
2175
|
const { voice, enabled = true } = optionsRef.current;
|
|
1510
2176
|
if (!enabled) return;
|
|
1511
2177
|
if (spokenIndexRef.current === index) return;
|
|
@@ -1565,42 +2231,7 @@ function useNarration(options) {
|
|
|
1565
2231
|
}
|
|
1566
2232
|
})();
|
|
1567
2233
|
}, [stop]);
|
|
1568
|
-
return { onSceneChange, interrupt, speaking, isReady, getTime };
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// src/player/prepare-scene-media.ts
|
|
1572
|
-
async function prepareSceneMedia(variables, signal) {
|
|
1573
|
-
signal.throwIfAborted();
|
|
1574
|
-
const url = typeof variables.mediaUrl === "string" ? variables.mediaUrl.trim() : "";
|
|
1575
|
-
if (!url || variables.mediaType === "gradient") return "graphic";
|
|
1576
|
-
const video = variables.mediaType === "video" || /\.(mp4|webm|mov)(?:[?#]|$)/i.test(url);
|
|
1577
|
-
if (video) return "awaiting-video-frame";
|
|
1578
|
-
await new Promise((resolve, reject) => {
|
|
1579
|
-
const image = new Image();
|
|
1580
|
-
let settled = false;
|
|
1581
|
-
const finish = (error) => {
|
|
1582
|
-
if (settled) return;
|
|
1583
|
-
settled = true;
|
|
1584
|
-
clearTimeout(timer);
|
|
1585
|
-
signal.removeEventListener("abort", abort);
|
|
1586
|
-
image.onload = null;
|
|
1587
|
-
image.onerror = null;
|
|
1588
|
-
if (error) {
|
|
1589
|
-
image.src = "";
|
|
1590
|
-
reject(error);
|
|
1591
|
-
} else resolve();
|
|
1592
|
-
};
|
|
1593
|
-
const abort = () => finish(signal.reason ?? new DOMException("Cancelled", "AbortError"));
|
|
1594
|
-
const timer = setTimeout(() => finish(new DOMException("Media preparation timed out", "TimeoutError")), 8e3);
|
|
1595
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
1596
|
-
image.onload = () => {
|
|
1597
|
-
void (image.decode ? image.decode() : Promise.resolve()).then(() => finish(), () => finish(new Error("Could not prepare scene image")));
|
|
1598
|
-
};
|
|
1599
|
-
image.onerror = () => finish(new Error("Could not prepare scene image"));
|
|
1600
|
-
image.src = url;
|
|
1601
|
-
if (image.complete && image.naturalWidth > 0) image.onload(new Event("load"));
|
|
1602
|
-
});
|
|
1603
|
-
return "image";
|
|
2234
|
+
return { onSceneChange, interrupt, speaking, isReady, getTime, isSpeaking };
|
|
1604
2235
|
}
|
|
1605
2236
|
|
|
1606
2237
|
// src/video-chat/voice.ts
|
|
@@ -1614,8 +2245,7 @@ function actionEndpoint(endpoint, action) {
|
|
|
1614
2245
|
return `${value}${value.includes("?") ? "&" : "?"}action=${encodeURIComponent(action)}`;
|
|
1615
2246
|
}
|
|
1616
2247
|
function estimatedBrowserSeconds(text) {
|
|
1617
|
-
|
|
1618
|
-
return Math.max(1, words / 2.5);
|
|
2248
|
+
return Math.max(1, estimateNarrationSeconds(text));
|
|
1619
2249
|
}
|
|
1620
2250
|
async function measureSeconds(bytes) {
|
|
1621
2251
|
try {
|
|
@@ -1950,7 +2580,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
1950
2580
|
}
|
|
1951
2581
|
|
|
1952
2582
|
// src/video-chat/use-video-chat.ts
|
|
1953
|
-
var DEFAULT_TIMEOUT_MS =
|
|
2583
|
+
var DEFAULT_TIMEOUT_MS = 66e4;
|
|
1954
2584
|
function observe(callback) {
|
|
1955
2585
|
try {
|
|
1956
2586
|
void Promise.resolve(callback?.()).catch(() => void 0);
|
|
@@ -2185,29 +2815,14 @@ async function responseError(response) {
|
|
|
2185
2815
|
recoverable: false
|
|
2186
2816
|
});
|
|
2187
2817
|
}
|
|
2188
|
-
function pacedScene(scene, spokenSeconds, templates) {
|
|
2189
|
-
const held = preparedSceneDuration(
|
|
2190
|
-
scene,
|
|
2191
|
-
spokenSeconds,
|
|
2192
|
-
templates?.getTemplateMetadata(scene.templateId) ?? getBuiltinTemplateMetadata(scene.templateId)
|
|
2193
|
-
);
|
|
2194
|
-
const {
|
|
2195
|
-
startTime: _startTime,
|
|
2196
|
-
endTime: _endTime,
|
|
2197
|
-
beatStart: _beatStart,
|
|
2198
|
-
beatEnd: _beatEnd,
|
|
2199
|
-
...timing
|
|
2200
|
-
} = scene.timing ?? {};
|
|
2201
|
-
return { ...scene, timing: { ...timing, fixedDuration: held } };
|
|
2202
|
-
}
|
|
2203
2818
|
function useVideoChat(options = {}) {
|
|
2204
2819
|
return useVideoChatSession(options).chat;
|
|
2205
2820
|
}
|
|
2206
2821
|
function useVideoChatSession(options = {}) {
|
|
2207
|
-
const optionsRef =
|
|
2822
|
+
const optionsRef = useRef6(options);
|
|
2208
2823
|
optionsRef.current = options;
|
|
2209
|
-
const voiceWarningRef =
|
|
2210
|
-
const ownedVoiceRef =
|
|
2824
|
+
const voiceWarningRef = useRef6(() => void 0);
|
|
2825
|
+
const ownedVoiceRef = useRef6(void 0);
|
|
2211
2826
|
if (!options.voice && !ownedVoiceRef.current) {
|
|
2212
2827
|
ownedVoiceRef.current = createVideoChatVoice({
|
|
2213
2828
|
endpoint: options.endpoint,
|
|
@@ -2219,12 +2834,12 @@ function useVideoChatSession(options = {}) {
|
|
|
2219
2834
|
}
|
|
2220
2835
|
const rawVoice = options.voice ?? ownedVoiceRef.current;
|
|
2221
2836
|
const captionVoice = useMemo2(() => createCaptionVoice(rawVoice), [rawVoice]);
|
|
2222
|
-
|
|
2837
|
+
useEffect7(() => () => captionVoice.reset(), [captionVoice]);
|
|
2223
2838
|
const voice = captionVoice.voice;
|
|
2224
|
-
const voiceRef =
|
|
2839
|
+
const voiceRef = useRef6(voice);
|
|
2225
2840
|
voiceRef.current = voice;
|
|
2226
|
-
const unavailableVoiceLines =
|
|
2227
|
-
const speechStartRef =
|
|
2841
|
+
const unavailableVoiceLines = useRef6(/* @__PURE__ */ new Set());
|
|
2842
|
+
const speechStartRef = useRef6(() => void 0);
|
|
2228
2843
|
const narration = useNarration({ onSpeechStart: (source) => speechStartRef.current(source), voice: {
|
|
2229
2844
|
supportsOffsets: voice.supportsOffsets,
|
|
2230
2845
|
...voice.getCurrentTime ? { getCurrentTime: () => voice.getCurrentTime() } : {},
|
|
@@ -2238,24 +2853,24 @@ function useVideoChatSession(options = {}) {
|
|
|
2238
2853
|
}
|
|
2239
2854
|
}
|
|
2240
2855
|
} });
|
|
2241
|
-
const narrationRef =
|
|
2856
|
+
const narrationRef = useRef6(narration);
|
|
2242
2857
|
narrationRef.current = narration;
|
|
2243
2858
|
const [state, dispatch] = useReducer(reducer, options.initialMuted ?? false, initialState);
|
|
2244
|
-
const stateRef =
|
|
2859
|
+
const stateRef = useRef6(state);
|
|
2245
2860
|
stateRef.current = state;
|
|
2246
|
-
const runRef =
|
|
2247
|
-
const inFlightRef =
|
|
2248
|
-
const suggestionsRef =
|
|
2249
|
-
const openingRef =
|
|
2250
|
-
const timelineRef =
|
|
2251
|
-
const heldRef =
|
|
2252
|
-
const flushRef =
|
|
2253
|
-
const mountedRef =
|
|
2254
|
-
const firstFrameRef =
|
|
2255
|
-
const reportMetric =
|
|
2861
|
+
const runRef = useRef6(0);
|
|
2862
|
+
const inFlightRef = useRef6(void 0);
|
|
2863
|
+
const suggestionsRef = useRef6(void 0);
|
|
2864
|
+
const openingRef = useRef6(void 0);
|
|
2865
|
+
const timelineRef = useRef6(void 0);
|
|
2866
|
+
const heldRef = useRef6(false);
|
|
2867
|
+
const flushRef = useRef6(void 0);
|
|
2868
|
+
const mountedRef = useRef6(true);
|
|
2869
|
+
const firstFrameRef = useRef6(void 0);
|
|
2870
|
+
const reportMetric = useCallback3((metric) => {
|
|
2256
2871
|
observe(() => optionsRef.current.onPlaybackMetric?.(metric));
|
|
2257
2872
|
}, []);
|
|
2258
|
-
const finishStall =
|
|
2873
|
+
const finishStall = useCallback3(() => {
|
|
2259
2874
|
const timing = firstFrameRef.current;
|
|
2260
2875
|
if (!timing?.active || timing.stallStartedAt == null) return;
|
|
2261
2876
|
const now = monotonicNow();
|
|
@@ -2267,10 +2882,10 @@ function useVideoChatSession(options = {}) {
|
|
|
2267
2882
|
mode: timing.mode,
|
|
2268
2883
|
elapsedMs: Math.max(0, Math.round(now - timing.startedAt)),
|
|
2269
2884
|
durationMs: Math.max(0, Math.round(now - started)),
|
|
2270
|
-
reason: "scene-generation"
|
|
2885
|
+
reason: timing.stallReason ?? "scene-generation"
|
|
2271
2886
|
});
|
|
2272
2887
|
}, [reportMetric]);
|
|
2273
|
-
const endTiming =
|
|
2888
|
+
const endTiming = useCallback3(() => {
|
|
2274
2889
|
finishStall();
|
|
2275
2890
|
if (firstFrameRef.current) firstFrameRef.current.active = false;
|
|
2276
2891
|
}, [finishStall]);
|
|
@@ -2286,7 +2901,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2286
2901
|
source: optionsRef.current.voice ? "custom" : source === "generated" ? "generated" : "browser"
|
|
2287
2902
|
});
|
|
2288
2903
|
};
|
|
2289
|
-
const request =
|
|
2904
|
+
const request = useCallback3(async (action, init = {}, signal) => {
|
|
2290
2905
|
const current = optionsRef.current;
|
|
2291
2906
|
const headers = new Headers(current.headers);
|
|
2292
2907
|
new Headers(init.headers).forEach((header, name) => headers.set(name, header));
|
|
@@ -2300,7 +2915,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2300
2915
|
signal
|
|
2301
2916
|
});
|
|
2302
2917
|
}, []);
|
|
2303
|
-
|
|
2918
|
+
useEffect7(() => {
|
|
2304
2919
|
mountedRef.current = true;
|
|
2305
2920
|
const controller = new AbortController();
|
|
2306
2921
|
void request("capabilities", {}, controller.signal).then(async (response) => {
|
|
@@ -2331,8 +2946,8 @@ function useVideoChatSession(options = {}) {
|
|
|
2331
2946
|
});
|
|
2332
2947
|
};
|
|
2333
2948
|
}, [request, endTiming]);
|
|
2334
|
-
|
|
2335
|
-
const cancel =
|
|
2949
|
+
useEffect7(() => voice.setMuted(state.muted), [state.muted, voice]);
|
|
2950
|
+
const cancel = useCallback3((reason = "Video chat was cancelled") => {
|
|
2336
2951
|
endTiming();
|
|
2337
2952
|
runRef.current += 1;
|
|
2338
2953
|
inFlightRef.current?.abort(new DOMException(reason, "AbortError"));
|
|
@@ -2346,7 +2961,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2346
2961
|
narrationRef.current.interrupt();
|
|
2347
2962
|
dispatch({ type: "cancelled" });
|
|
2348
2963
|
}, [endTiming]);
|
|
2349
|
-
const ask =
|
|
2964
|
+
const ask = useCallback3(async (value, askOptions = {}) => {
|
|
2350
2965
|
const prompt = value.trim();
|
|
2351
2966
|
if (!prompt) return void 0;
|
|
2352
2967
|
endTiming();
|
|
@@ -2456,31 +3071,26 @@ function useVideoChatSession(options = {}) {
|
|
|
2456
3071
|
}
|
|
2457
3072
|
};
|
|
2458
3073
|
flushRef.current = flush;
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
});
|
|
2480
|
-
speechLoads.set(text, prepared);
|
|
2481
|
-
speechLanes[lane] = prepared.catch(() => void 0);
|
|
2482
|
-
return prepared;
|
|
2483
|
-
};
|
|
3074
|
+
const preparation = createScenePreparation({
|
|
3075
|
+
voice: () => voiceRef.current,
|
|
3076
|
+
customVoice: Boolean(currentOptions.voice),
|
|
3077
|
+
signal: controller.signal,
|
|
3078
|
+
onVoiceUnavailable: (text) => {
|
|
3079
|
+
if (isCurrent()) unavailableVoiceLines.current.add(text);
|
|
3080
|
+
},
|
|
3081
|
+
warn,
|
|
3082
|
+
onDuration: (value2) => {
|
|
3083
|
+
const timing = firstFrameRef.current;
|
|
3084
|
+
if (isCurrent() && timing?.active) reportMetric({
|
|
3085
|
+
type: "scene-duration",
|
|
3086
|
+
...value2,
|
|
3087
|
+
turnId: id,
|
|
3088
|
+
mode,
|
|
3089
|
+
elapsedMs: Math.max(0, Math.round(monotonicNow() - timing.startedAt))
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
const { prepareSpeech } = preparation;
|
|
2484
3094
|
const speakOpening = async (text) => {
|
|
2485
3095
|
try {
|
|
2486
3096
|
if (!isOpeningCurrent()) return;
|
|
@@ -2539,10 +3149,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2539
3149
|
warn(event.type === "response.warning" && event.data.warning.message === MEDIA_RECOVERY_NOTICE ? MEDIA_RECOVERY_NOTICE : "Some parts were simplified so the response could continue.");
|
|
2540
3150
|
}
|
|
2541
3151
|
if (event.type === "data.video-chat-preparation") {
|
|
2542
|
-
|
|
2543
|
-
if (data && typeof data.sceneId === "string" && data.sceneId.length <= 200 && typeof data.narration === "string" && data.narration.trim() && data.narration.length <= 2e3 && speechLoads.size < 32) {
|
|
2544
|
-
void prepareSpeech(data.narration.trim(), controller.signal).catch(() => void 0);
|
|
2545
|
-
}
|
|
3152
|
+
preparation.announce(event.data);
|
|
2546
3153
|
continue;
|
|
2547
3154
|
}
|
|
2548
3155
|
if (event.type === "data.video-chat-opening") {
|
|
@@ -2572,22 +3179,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2572
3179
|
const plannedScene = event.data.scene;
|
|
2573
3180
|
planned[position] = plannedScene;
|
|
2574
3181
|
received[position] = plannedScene;
|
|
2575
|
-
const
|
|
2576
|
-
const visualPreparation = rendererReady.then(async () => {
|
|
2577
|
-
try {
|
|
2578
|
-
await prepareSceneMedia(plannedScene.variables, controller.signal);
|
|
2579
|
-
return plannedScene;
|
|
2580
|
-
} catch (cause) {
|
|
2581
|
-
if (controller.signal.aborted) throw cause;
|
|
2582
|
-
const override = currentOptions.templates?.getTemplate(plannedScene.templateId);
|
|
2583
|
-
const fallback = !override || supportsExternalVideoBackdrop(override) ? recoverSceneMedia(plannedScene) : void 0;
|
|
2584
|
-
if (!fallback) throw new VideoError("Scene could not prepare its visual", { code: "media_not_ready" });
|
|
2585
|
-
warn(MEDIA_RECOVERY_NOTICE);
|
|
2586
|
-
return fallback;
|
|
2587
|
-
}
|
|
2588
|
-
});
|
|
2589
|
-
void visualPreparation.catch(() => void 0);
|
|
2590
|
-
warmSceneMedia(plannedScene.variables);
|
|
3182
|
+
const visualPreparation = preparation.prepareVisual(plannedScene);
|
|
2591
3183
|
const narrated = narrating.then(async () => {
|
|
2592
3184
|
const supplied = plannedScene.narration?.trim();
|
|
2593
3185
|
if (supplied) return supplied;
|
|
@@ -2623,7 +3215,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2623
3215
|
const withNarration = line ? { ...visual, narration: line } : visual;
|
|
2624
3216
|
const group = plannedScene.narrationGroup;
|
|
2625
3217
|
if (group && (spoken?.supportsOffsets !== true || voiceRef.current.supportsOffsets !== true || Math.abs(spoken.seconds - group.totalSeconds) > 0.1)) throw new VideoError("Narration group requires matching measured audio with offset support", { code: "narration_group_invalid" });
|
|
2626
|
-
ready[position] = group ? withNarration :
|
|
3218
|
+
ready[position] = group ? withNarration : preparation.pace(withNarration, spoken?.seconds);
|
|
2627
3219
|
flush();
|
|
2628
3220
|
}).catch((cause) => {
|
|
2629
3221
|
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
@@ -2664,7 +3256,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2664
3256
|
try {
|
|
2665
3257
|
response = await untilAborted(runAttempt(attempt), controller.signal);
|
|
2666
3258
|
} catch (cause) {
|
|
2667
|
-
if (controller.signal.aborted || timeline || spokenHook || cause instanceof VideoError && ["media_not_ready", "narration_group_invalid"].includes(cause.code) || received.some((scene) => scene?.narrationGroup)) throw cause;
|
|
3259
|
+
if (mode !== "pexels" || controller.signal.aborted || timeline || spokenHook || cause instanceof VideoError && ["media_not_ready", "narration_group_invalid"].includes(cause.code) || received.some((scene) => scene?.narrationGroup)) throw cause;
|
|
2668
3260
|
attempt += 1;
|
|
2669
3261
|
ready = [];
|
|
2670
3262
|
received = [];
|
|
@@ -2727,19 +3319,19 @@ function useVideoChatSession(options = {}) {
|
|
|
2727
3319
|
if (runRef.current === run) inFlightRef.current = void 0;
|
|
2728
3320
|
}
|
|
2729
3321
|
}, [request, endTiming]);
|
|
2730
|
-
const pause =
|
|
3322
|
+
const pause = useCallback3(() => {
|
|
2731
3323
|
finishStall();
|
|
2732
3324
|
heldRef.current = true;
|
|
2733
3325
|
voiceRef.current.pause();
|
|
2734
3326
|
dispatch({ type: "pause" });
|
|
2735
3327
|
}, [finishStall]);
|
|
2736
|
-
const resume =
|
|
3328
|
+
const resume = useCallback3(() => {
|
|
2737
3329
|
heldRef.current = false;
|
|
2738
3330
|
voiceRef.current.resume();
|
|
2739
3331
|
dispatch({ type: "resume" });
|
|
2740
3332
|
flushRef.current?.();
|
|
2741
3333
|
}, []);
|
|
2742
|
-
const prepareSavedGroups =
|
|
3334
|
+
const prepareSavedGroups = useCallback3(async (video) => {
|
|
2743
3335
|
validateNarrationGroups(video.scenes);
|
|
2744
3336
|
const groups = new Map(video.scenes.flatMap((scene) => scene.narrationGroup ? [[scene.narrationGroup.id, scene.narrationGroup]] : []));
|
|
2745
3337
|
for (const group of groups.values()) {
|
|
@@ -2747,7 +3339,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2747
3339
|
if (voiceRef.current.supportsOffsets !== true || prepared.supportsOffsets !== true || Math.abs(prepared.seconds - group.totalSeconds) > 0.1) throw new VideoError("Saved narration group requires matching measured audio", { code: "narration_group_invalid" });
|
|
2748
3340
|
}
|
|
2749
3341
|
}, []);
|
|
2750
|
-
const replay =
|
|
3342
|
+
const replay = useCallback3(() => {
|
|
2751
3343
|
const turn = stateRef.current.turns.find((entry) => entry.id === stateRef.current.shownTurnId);
|
|
2752
3344
|
if (!turn?.completed || !turn.video) return;
|
|
2753
3345
|
runRef.current += 1;
|
|
@@ -2774,7 +3366,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2774
3366
|
});
|
|
2775
3367
|
} else dispatch({ type: "replay" });
|
|
2776
3368
|
}, [endTiming, prepareSavedGroups]);
|
|
2777
|
-
const selectTurn =
|
|
3369
|
+
const selectTurn = useCallback3((id) => {
|
|
2778
3370
|
const turn = stateRef.current.turns.find((entry) => entry.id === id);
|
|
2779
3371
|
if (!turn?.completed || !turn.video) return;
|
|
2780
3372
|
runRef.current += 1;
|
|
@@ -2801,19 +3393,19 @@ function useVideoChatSession(options = {}) {
|
|
|
2801
3393
|
});
|
|
2802
3394
|
} else dispatch({ type: "select", id });
|
|
2803
3395
|
}, [endTiming, prepareSavedGroups]);
|
|
2804
|
-
const reset =
|
|
3396
|
+
const reset = useCallback3(() => {
|
|
2805
3397
|
cancel("Session reset");
|
|
2806
3398
|
heldRef.current = false;
|
|
2807
3399
|
voiceRef.current.resume();
|
|
2808
3400
|
dispatch({ type: "reset" });
|
|
2809
3401
|
}, [cancel]);
|
|
2810
|
-
const restoreSession =
|
|
3402
|
+
const restoreSession = useCallback3((turns) => {
|
|
2811
3403
|
cancel("Restoring session");
|
|
2812
3404
|
heldRef.current = false;
|
|
2813
3405
|
voiceRef.current.resume();
|
|
2814
3406
|
dispatch({ type: "restore", turns: turns.filter((turn) => turn.completed && turn.video).slice(-100) });
|
|
2815
3407
|
}, [cancel]);
|
|
2816
|
-
const setMuted =
|
|
3408
|
+
const setMuted = useCallback3((muted) => dispatch({ type: "mute", value: muted }), []);
|
|
2817
3409
|
const currentTurn = state.turns.at(-1);
|
|
2818
3410
|
const shownTurn = state.turns.find((turn) => turn.id === state.shownTurnId) ?? currentTurn;
|
|
2819
3411
|
const availableModes = state.capabilities?.modes ?? ["cinematic"];
|
|
@@ -2822,13 +3414,13 @@ function useVideoChatSession(options = {}) {
|
|
|
2822
3414
|
const transcript = shownTurn && shownTurn === currentTurn && state.playback?.kind !== "video" ? fullTranscript.slice(0, (shownTurn.opening ? 1 : 0) + state.spokenUpTo + 1) : fullTranscript;
|
|
2823
3415
|
const playbackKey = state.playerKey;
|
|
2824
3416
|
const playerProps = state.playback ? {
|
|
2825
|
-
...options.templates ? { templates: options.templates } : {},
|
|
2826
3417
|
...state.playback.kind === "stream" ? { stream: state.playback.stream } : { video: state.playback.video },
|
|
2827
3418
|
autoPlay: true,
|
|
2828
3419
|
paused: state.status === "paused",
|
|
2829
3420
|
controls: false,
|
|
2830
3421
|
narrationReady: narration.isReady,
|
|
2831
3422
|
narrationTime: narration.getTime,
|
|
3423
|
+
narrationActive: narration.isSpeaking,
|
|
2832
3424
|
orientation: shownTurn?.fixedOrientation ? shownTurn.orientation : "auto",
|
|
2833
3425
|
onFramePresented: () => {
|
|
2834
3426
|
const timing = firstFrameRef.current;
|
|
@@ -2849,15 +3441,23 @@ function useVideoChatSession(options = {}) {
|
|
|
2849
3441
|
elapsedMs: Math.max(0, Math.round(monotonicNow() - timing.startedAt))
|
|
2850
3442
|
});
|
|
2851
3443
|
},
|
|
2852
|
-
|
|
3444
|
+
onPlaybackMetric: options.onPlaybackMetric ? (metric) => {
|
|
3445
|
+
const timing = firstFrameRef.current;
|
|
3446
|
+
if (stateRef.current.playerKey !== playbackKey || !timing?.active || state.playback?.kind !== "stream") return;
|
|
3447
|
+
reportMetric({ ...metric, turnId: timing.turnId, mode: timing.mode, elapsedMs: Math.max(0, Math.round(monotonicNow() - timing.startedAt)) });
|
|
3448
|
+
} : void 0,
|
|
3449
|
+
onStallChange: (stalled, reason = "scene-generation") => {
|
|
2853
3450
|
if (stateRef.current.playerKey !== playbackKey) return;
|
|
2854
|
-
if (stalled) voiceRef.current.pause();
|
|
3451
|
+
if (stalled && reason !== "speech") voiceRef.current.pause();
|
|
2855
3452
|
else if (!heldRef.current) voiceRef.current.resume();
|
|
2856
3453
|
if (state.playback?.kind !== "stream") return;
|
|
2857
3454
|
const timing = firstFrameRef.current;
|
|
2858
3455
|
if (!timing?.active || !timing.reported) return;
|
|
2859
3456
|
if (!stalled) finishStall();
|
|
2860
|
-
else if (!heldRef.current && timing.stallStartedAt == null)
|
|
3457
|
+
else if (!heldRef.current && timing.stallStartedAt == null) {
|
|
3458
|
+
timing.stallStartedAt = monotonicNow();
|
|
3459
|
+
timing.stallReason = reason;
|
|
3460
|
+
}
|
|
2861
3461
|
},
|
|
2862
3462
|
onSceneChange: (scene, index) => {
|
|
2863
3463
|
if (stateRef.current.playerKey !== playbackKey) return;
|
|
@@ -2907,8 +3507,8 @@ function useVideoChatSession(options = {}) {
|
|
|
2907
3507
|
}
|
|
2908
3508
|
|
|
2909
3509
|
// src/video-chat/caption-pages.tsx
|
|
2910
|
-
import { useEffect as
|
|
2911
|
-
import { jsx as
|
|
3510
|
+
import { useEffect as useEffect8, useLayoutEffect, useRef as useRef7, useState as useState4 } from "react";
|
|
3511
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
2912
3512
|
function splitCaptionPages(text, fits) {
|
|
2913
3513
|
const pages = [];
|
|
2914
3514
|
let rest = Array.from(text);
|
|
@@ -2941,14 +3541,14 @@ function captionPageAt(pages, elapsed, duration) {
|
|
|
2941
3541
|
return Math.max(0, pages.length - 1);
|
|
2942
3542
|
}
|
|
2943
3543
|
function CaptionPages({ text, getProgress }) {
|
|
2944
|
-
const line =
|
|
2945
|
-
const lastProgress =
|
|
2946
|
-
const getter =
|
|
3544
|
+
const line = useRef7(null);
|
|
3545
|
+
const lastProgress = useRef7(void 0);
|
|
3546
|
+
const getter = useRef7(getProgress);
|
|
2947
3547
|
getter.current = getProgress;
|
|
2948
|
-
const [source, setSource] =
|
|
2949
|
-
const [pages, setPages] =
|
|
2950
|
-
const [index, setIndex] =
|
|
2951
|
-
const [timing, setTiming] =
|
|
3548
|
+
const [source, setSource] = useState4(text);
|
|
3549
|
+
const [pages, setPages] = useState4([]);
|
|
3550
|
+
const [index, setIndex] = useState4(0);
|
|
3551
|
+
const [timing, setTiming] = useState4();
|
|
2952
3552
|
useLayoutEffect(() => {
|
|
2953
3553
|
const element = line.current;
|
|
2954
3554
|
if (!element) return;
|
|
@@ -2991,7 +3591,7 @@ function CaptionPages({ text, getProgress }) {
|
|
|
2991
3591
|
document.fonts?.removeEventListener("loadingdone", measure);
|
|
2992
3592
|
};
|
|
2993
3593
|
}, [source]);
|
|
2994
|
-
|
|
3594
|
+
useEffect8(() => {
|
|
2995
3595
|
let frame = 0;
|
|
2996
3596
|
const update = () => {
|
|
2997
3597
|
const observed = text ? getter.current() : void 0;
|
|
@@ -3006,14 +3606,14 @@ function CaptionPages({ text, getProgress }) {
|
|
|
3006
3606
|
update();
|
|
3007
3607
|
return () => cancelAnimationFrame(frame);
|
|
3008
3608
|
}, [text, source, pages]);
|
|
3009
|
-
return /* @__PURE__ */
|
|
3609
|
+
return /* @__PURE__ */ jsx5("p", { ref: line, className: "line", "aria-live": "polite", "data-caption-timing": timing, "data-caption-page": index, children: pages[index] ?? "" });
|
|
3010
3610
|
}
|
|
3011
3611
|
|
|
3012
3612
|
// src/video-chat/video-chat.tsx
|
|
3013
|
-
import { useCallback as
|
|
3613
|
+
import { useCallback as useCallback6, useEffect as useEffect13, useId as useId2, useLayoutEffect as useLayoutEffect3, useMemo as useMemo3, useRef as useRef12, useState as useState9 } from "react";
|
|
3014
3614
|
|
|
3015
3615
|
// src/video-chat/icons.tsx
|
|
3016
|
-
import { jsx as
|
|
3616
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3017
3617
|
var stroke = {
|
|
3018
3618
|
fill: "none",
|
|
3019
3619
|
stroke: "currentColor",
|
|
@@ -3022,50 +3622,50 @@ var stroke = {
|
|
|
3022
3622
|
strokeLinejoin: "round"
|
|
3023
3623
|
};
|
|
3024
3624
|
function Glyph({ children }) {
|
|
3025
|
-
return /* @__PURE__ */
|
|
3625
|
+
return /* @__PURE__ */ jsx6("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", ...stroke, children });
|
|
3026
3626
|
}
|
|
3027
|
-
var Sessions = () => /* @__PURE__ */
|
|
3028
|
-
/* @__PURE__ */
|
|
3029
|
-
/* @__PURE__ */
|
|
3627
|
+
var Sessions = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3628
|
+
/* @__PURE__ */ jsx6("path", { d: "M5 3h10a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H8l-5 3V5a2 2 0 0 1 2-2Z" }),
|
|
3629
|
+
/* @__PURE__ */ jsx6("path", { d: "M17 8h2a2 2 0 0 1 2 2v11l-4-3h-6a2 2 0 0 1-2-2v-2" })
|
|
3030
3630
|
] });
|
|
3031
|
-
var Plus = () => /* @__PURE__ */
|
|
3032
|
-
var Gear = () => /* @__PURE__ */
|
|
3033
|
-
/* @__PURE__ */
|
|
3034
|
-
/* @__PURE__ */
|
|
3631
|
+
var Plus = () => /* @__PURE__ */ jsx6(Glyph, { children: /* @__PURE__ */ jsx6("path", { d: "M12 5v14M5 12h14" }) });
|
|
3632
|
+
var Gear = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3633
|
+
/* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "3" }),
|
|
3634
|
+
/* @__PURE__ */ jsx6("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" })
|
|
3035
3635
|
] });
|
|
3036
|
-
var Sound = () => /* @__PURE__ */
|
|
3037
|
-
/* @__PURE__ */
|
|
3038
|
-
/* @__PURE__ */
|
|
3636
|
+
var Sound = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3637
|
+
/* @__PURE__ */ jsx6("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
|
|
3638
|
+
/* @__PURE__ */ jsx6("path", { d: "M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13" })
|
|
3039
3639
|
] });
|
|
3040
|
-
var Muted = () => /* @__PURE__ */
|
|
3041
|
-
/* @__PURE__ */
|
|
3042
|
-
/* @__PURE__ */
|
|
3640
|
+
var Muted = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3641
|
+
/* @__PURE__ */ jsx6("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
|
|
3642
|
+
/* @__PURE__ */ jsx6("path", { d: "m22 9-6 6M16 9l6 6" })
|
|
3043
3643
|
] });
|
|
3044
|
-
var Mic = () => /* @__PURE__ */
|
|
3045
|
-
/* @__PURE__ */
|
|
3046
|
-
/* @__PURE__ */
|
|
3644
|
+
var Mic = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3645
|
+
/* @__PURE__ */ jsx6("rect", { x: "9", y: "2", width: "6", height: "12", rx: "3" }),
|
|
3646
|
+
/* @__PURE__ */ jsx6("path", { d: "M5 11a7 7 0 0 0 14 0M12 18v4" })
|
|
3047
3647
|
] });
|
|
3048
|
-
var Send = () => /* @__PURE__ */
|
|
3049
|
-
var Stop = () => /* @__PURE__ */
|
|
3050
|
-
/* @__PURE__ */
|
|
3051
|
-
/* @__PURE__ */
|
|
3648
|
+
var Send = () => /* @__PURE__ */ jsx6(Glyph, { children: /* @__PURE__ */ jsx6("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
|
|
3649
|
+
var Stop = () => /* @__PURE__ */ jsxs4("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "currentColor", children: [
|
|
3650
|
+
/* @__PURE__ */ jsx6("rect", { x: "7", y: "5", width: "3.4", height: "14", rx: "1.4" }),
|
|
3651
|
+
/* @__PURE__ */ jsx6("rect", { x: "13.6", y: "5", width: "3.4", height: "14", rx: "1.4" })
|
|
3052
3652
|
] });
|
|
3053
|
-
var Play = () => /* @__PURE__ */
|
|
3054
|
-
var Replay = () => /* @__PURE__ */
|
|
3055
|
-
/* @__PURE__ */
|
|
3056
|
-
/* @__PURE__ */
|
|
3653
|
+
var Play = () => /* @__PURE__ */ jsx6("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "currentColor", children: /* @__PURE__ */ jsx6("path", { d: "M8 5.5v13l11-6.5z" }) });
|
|
3654
|
+
var Replay = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3655
|
+
/* @__PURE__ */ jsx6("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
3656
|
+
/* @__PURE__ */ jsx6("path", { d: "M3 3v5h5" })
|
|
3057
3657
|
] });
|
|
3058
|
-
var ChevronUp = () => /* @__PURE__ */
|
|
3059
|
-
var Close = () => /* @__PURE__ */
|
|
3060
|
-
var Warning = () => /* @__PURE__ */
|
|
3061
|
-
/* @__PURE__ */
|
|
3062
|
-
/* @__PURE__ */
|
|
3658
|
+
var ChevronUp = () => /* @__PURE__ */ jsx6("svg", { viewBox: "0 0 24 24", width: "14", height: "14", "aria-hidden": "true", ...stroke, children: /* @__PURE__ */ jsx6("path", { d: "m6 15 6-6 6 6" }) });
|
|
3659
|
+
var Close = () => /* @__PURE__ */ jsx6(Glyph, { children: /* @__PURE__ */ jsx6("path", { d: "M6 6l12 12M18 6 6 18" }) });
|
|
3660
|
+
var Warning = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3661
|
+
/* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "9" }),
|
|
3662
|
+
/* @__PURE__ */ jsx6("path", { d: "M12 8v5M12 16.2v.1" })
|
|
3063
3663
|
] });
|
|
3064
3664
|
|
|
3065
3665
|
// src/video-chat/use-dismiss.ts
|
|
3066
|
-
import { useEffect as
|
|
3666
|
+
import { useEffect as useEffect9 } from "react";
|
|
3067
3667
|
function useDismiss(open, close, surfaces) {
|
|
3068
|
-
|
|
3668
|
+
useEffect9(() => {
|
|
3069
3669
|
if (!open) return;
|
|
3070
3670
|
const onKeyDown = (event) => {
|
|
3071
3671
|
if (event.key !== "Escape") return;
|
|
@@ -3087,7 +3687,7 @@ function useDismiss(open, close, surfaces) {
|
|
|
3087
3687
|
}, [open, close, surfaces]);
|
|
3088
3688
|
}
|
|
3089
3689
|
function useFocusTrap(active, surface) {
|
|
3090
|
-
|
|
3690
|
+
useEffect9(() => {
|
|
3091
3691
|
if (!active) return;
|
|
3092
3692
|
const returnTo = document.activeElement;
|
|
3093
3693
|
const focusable = () => Array.from(
|
|
@@ -3120,12 +3720,12 @@ function useFocusTrap(active, surface) {
|
|
|
3120
3720
|
}
|
|
3121
3721
|
|
|
3122
3722
|
// src/video-chat/suggestion-cards.tsx
|
|
3123
|
-
import { useEffect as
|
|
3124
|
-
import { Fragment as Fragment2, jsx as
|
|
3723
|
+
import { useEffect as useEffect10, useId, useRef as useRef8, useState as useState5 } from "react";
|
|
3724
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3125
3725
|
function Frame({ media, poster, playing, onReady, onError, revealWhenReady = false }) {
|
|
3126
|
-
const video =
|
|
3127
|
-
const [playingUrl, setPlayingUrl] =
|
|
3128
|
-
|
|
3726
|
+
const video = useRef8(null);
|
|
3727
|
+
const [playingUrl, setPlayingUrl] = useState5(null);
|
|
3728
|
+
useEffect10(() => {
|
|
3129
3729
|
const element = video.current;
|
|
3130
3730
|
if (!element || playing === void 0) return;
|
|
3131
3731
|
if (playing) void element.play().catch(() => void 0);
|
|
@@ -3133,16 +3733,16 @@ function Frame({ media, poster, playing, onReady, onError, revealWhenReady = fal
|
|
|
3133
3733
|
}, [playing]);
|
|
3134
3734
|
if (!media) return null;
|
|
3135
3735
|
if (playing === false && media.type === "video" && media.posterUrl) {
|
|
3136
|
-
return /* @__PURE__ */
|
|
3736
|
+
return /* @__PURE__ */ jsx7("img", { className: "frame-media frame-poster", src: media.posterUrl, alt: "", onLoad: onReady, onError });
|
|
3137
3737
|
}
|
|
3138
3738
|
const ready = () => {
|
|
3139
3739
|
setPlayingUrl(media.url);
|
|
3140
3740
|
onReady?.();
|
|
3141
3741
|
};
|
|
3142
3742
|
const appearance = revealWhenReady ? { opacity: playingUrl === media.url ? 1 : 0, transition: "opacity 200ms ease" } : void 0;
|
|
3143
|
-
if (media.type === "image") return /* @__PURE__ */
|
|
3144
|
-
return /* @__PURE__ */
|
|
3145
|
-
/* @__PURE__ */
|
|
3743
|
+
if (media.type === "image") return /* @__PURE__ */ jsx7("img", { className: "frame-media", src: media.url, alt: "", style: appearance, onLoad: ready, onError });
|
|
3744
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
3745
|
+
/* @__PURE__ */ jsx7(
|
|
3146
3746
|
"video",
|
|
3147
3747
|
{
|
|
3148
3748
|
ref: video,
|
|
@@ -3163,15 +3763,15 @@ function Frame({ media, poster, playing, onReady, onError, revealWhenReady = fal
|
|
|
3163
3763
|
"aria-hidden": "true"
|
|
3164
3764
|
}
|
|
3165
3765
|
),
|
|
3166
|
-
poster && media.posterUrl && playingUrl !== media.url && /* @__PURE__ */
|
|
3766
|
+
poster && media.posterUrl && playingUrl !== media.url && /* @__PURE__ */ jsx7("img", { className: "frame-media frame-poster", src: media.posterUrl, alt: "", onLoad: onReady })
|
|
3167
3767
|
] });
|
|
3168
3768
|
}
|
|
3169
3769
|
function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
3170
|
-
const railRef =
|
|
3770
|
+
const railRef = useRef8(null);
|
|
3171
3771
|
const railId = useId();
|
|
3172
|
-
const [at, setAt] =
|
|
3173
|
-
const [overflow, setOverflow] =
|
|
3174
|
-
|
|
3772
|
+
const [at, setAt] = useState5(0);
|
|
3773
|
+
const [overflow, setOverflow] = useState5({ before: false, after: false });
|
|
3774
|
+
useEffect10(() => {
|
|
3175
3775
|
const rail = railRef.current;
|
|
3176
3776
|
if (!browse || !rail) return;
|
|
3177
3777
|
const update = () => setOverflow({ before: rail.scrollLeft > 2, after: rail.scrollLeft + rail.clientWidth < rail.scrollWidth - 2 });
|
|
@@ -3190,8 +3790,8 @@ function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
|
3190
3790
|
rail.scrollBy({ left: direction * rail.clientWidth, behavior: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
|
|
3191
3791
|
};
|
|
3192
3792
|
if (suggestions.length === 0) return null;
|
|
3193
|
-
const cards = /* @__PURE__ */
|
|
3194
|
-
/* @__PURE__ */
|
|
3793
|
+
const cards = /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
3794
|
+
/* @__PURE__ */ jsx7("ul", { className: "cards", id: railId, ref: railRef, "aria-label": label, children: suggestions.map((card, index) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsxs5(
|
|
3195
3795
|
"button",
|
|
3196
3796
|
{
|
|
3197
3797
|
type: "button",
|
|
@@ -3200,13 +3800,13 @@ function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
|
3200
3800
|
onPointerEnter: () => setAt(index),
|
|
3201
3801
|
onClick: () => onAsk(card),
|
|
3202
3802
|
children: [
|
|
3203
|
-
/* @__PURE__ */
|
|
3204
|
-
/* @__PURE__ */
|
|
3205
|
-
/* @__PURE__ */
|
|
3803
|
+
/* @__PURE__ */ jsx7(Frame, { media: card.media, poster: true, playing: index === at }),
|
|
3804
|
+
/* @__PURE__ */ jsx7("span", { className: "card-wash", "aria-hidden": "true" }),
|
|
3805
|
+
/* @__PURE__ */ jsx7("span", { className: "card-prompt", children: card.prompt })
|
|
3206
3806
|
]
|
|
3207
3807
|
}
|
|
3208
3808
|
) }, card.prompt)) }),
|
|
3209
|
-
suggestions.length > 1 && /* @__PURE__ */
|
|
3809
|
+
suggestions.length > 1 && /* @__PURE__ */ jsx7("div", { className: "card-dots", role: "group", "aria-label": `${label} navigation`, children: suggestions.map((card, index) => /* @__PURE__ */ jsx7(
|
|
3210
3810
|
"button",
|
|
3211
3811
|
{
|
|
3212
3812
|
type: "button",
|
|
@@ -3223,39 +3823,39 @@ function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
|
3223
3823
|
card.prompt
|
|
3224
3824
|
)) })
|
|
3225
3825
|
] });
|
|
3226
|
-
return browse ? /* @__PURE__ */
|
|
3826
|
+
return browse ? /* @__PURE__ */ jsxs5("div", { className: "suggestion-rail", children: [
|
|
3227
3827
|
cards,
|
|
3228
|
-
(overflow.before || overflow.after) && /* @__PURE__ */
|
|
3229
|
-
/* @__PURE__ */
|
|
3230
|
-
/* @__PURE__ */
|
|
3828
|
+
(overflow.before || overflow.after) && /* @__PURE__ */ jsxs5("div", { className: "rail-arrows", children: [
|
|
3829
|
+
/* @__PURE__ */ jsx7("button", { type: "button", "aria-label": "Previous suggestions", "aria-controls": railId, disabled: !overflow.before, onClick: () => move(-1), children: "\u2039" }),
|
|
3830
|
+
/* @__PURE__ */ jsx7("button", { type: "button", "aria-label": "Next suggestions", "aria-controls": railId, disabled: !overflow.after, onClick: () => move(1), children: "\u203A" })
|
|
3231
3831
|
] })
|
|
3232
3832
|
] }) : cards;
|
|
3233
3833
|
}
|
|
3234
3834
|
|
|
3235
3835
|
// src/video-chat/welcome.tsx
|
|
3236
|
-
import { Fragment as Fragment3, jsx as
|
|
3836
|
+
import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3237
3837
|
function Welcome({ data, onAsk, title }) {
|
|
3238
|
-
return /* @__PURE__ */
|
|
3239
|
-
/* @__PURE__ */
|
|
3240
|
-
/* @__PURE__ */
|
|
3241
|
-
/* @__PURE__ */
|
|
3242
|
-
/* @__PURE__ */
|
|
3838
|
+
return /* @__PURE__ */ jsxs6("div", { className: "welcome", children: [
|
|
3839
|
+
/* @__PURE__ */ jsx8(Frame, { media: data?.hero ?? null, poster: true }),
|
|
3840
|
+
/* @__PURE__ */ jsx8("div", { className: "welcome-wash", "aria-hidden": "true" }),
|
|
3841
|
+
/* @__PURE__ */ jsxs6("div", { className: "welcome-body", children: [
|
|
3842
|
+
/* @__PURE__ */ jsx8("h1", { className: "welcome-title", children: title ?? /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
3243
3843
|
"An AI chat that responds",
|
|
3244
|
-
/* @__PURE__ */
|
|
3245
|
-
/* @__PURE__ */
|
|
3844
|
+
/* @__PURE__ */ jsx8("br", {}),
|
|
3845
|
+
/* @__PURE__ */ jsx8("em", { children: "in video, not text." })
|
|
3246
3846
|
] }) }),
|
|
3247
|
-
/* @__PURE__ */
|
|
3847
|
+
/* @__PURE__ */ jsx8(SuggestionCards, { browse: true, suggestions: data?.cards ?? [], label: "Suggested prompts", onAsk })
|
|
3248
3848
|
] })
|
|
3249
3849
|
] });
|
|
3250
3850
|
}
|
|
3251
3851
|
|
|
3252
3852
|
// src/video-chat/opening-chapter.tsx
|
|
3253
|
-
import { useLayoutEffect as useLayoutEffect2, useRef as
|
|
3254
|
-
import { jsx as
|
|
3853
|
+
import { useLayoutEffect as useLayoutEffect2, useRef as useRef9, useState as useState6 } from "react";
|
|
3854
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3255
3855
|
function OpeningChapter({ title, preparing = false }) {
|
|
3256
|
-
const root =
|
|
3257
|
-
const [size, setSize] =
|
|
3258
|
-
const [titleBottom, setTitleBottom] =
|
|
3856
|
+
const root = useRef9(null);
|
|
3857
|
+
const [size, setSize] = useState6({ width: 1080, height: 1080 });
|
|
3858
|
+
const [titleBottom, setTitleBottom] = useState6(0);
|
|
3259
3859
|
useLayoutEffect2(() => {
|
|
3260
3860
|
const element = root.current;
|
|
3261
3861
|
if (!element) return;
|
|
@@ -3273,28 +3873,17 @@ function OpeningChapter({ title, preparing = false }) {
|
|
|
3273
3873
|
if (title2) observer.observe(title2);
|
|
3274
3874
|
return () => observer.disconnect();
|
|
3275
3875
|
}, [title]);
|
|
3276
|
-
return /* @__PURE__ */
|
|
3277
|
-
/* @__PURE__ */
|
|
3278
|
-
|
|
3279
|
-
{
|
|
3280
|
-
|
|
3281
|
-
style: {},
|
|
3282
|
-
width: size.width,
|
|
3283
|
-
height: size.height,
|
|
3284
|
-
progress: 0.3,
|
|
3285
|
-
beatIntensity: 0,
|
|
3286
|
-
safeZone: { top: 0, right: 0, bottom: 0, left: 0 }
|
|
3287
|
-
}
|
|
3288
|
-
),
|
|
3289
|
-
preparing && /* @__PURE__ */ jsxs6("div", { className: "video-preparation", style: { top: titleBottom + 24 }, role: "status", "aria-label": "Video preparation", children: [
|
|
3290
|
-
/* @__PURE__ */ jsx7("span", { className: "video-preparation-spinner", "aria-hidden": "true" }),
|
|
3291
|
-
/* @__PURE__ */ jsx7("span", { className: "video-preparation-text", children: "Preparing your video\u2026" })
|
|
3876
|
+
return /* @__PURE__ */ jsxs7("div", { ref: root, "data-opening-chapter": true, className: "opening-chapter", children: [
|
|
3877
|
+
/* @__PURE__ */ jsx9(TitleSceneTemplate, { variables: { title }, width: size.width, height: size.height, progress: 0.3 }),
|
|
3878
|
+
preparing && /* @__PURE__ */ jsxs7("div", { className: "video-preparation", style: { top: titleBottom + 24 }, role: "status", "aria-label": "Video preparation", children: [
|
|
3879
|
+
/* @__PURE__ */ jsx9("span", { className: "video-preparation-spinner", "aria-hidden": "true" }),
|
|
3880
|
+
/* @__PURE__ */ jsx9("span", { className: "video-preparation-text", children: "Preparing your video\u2026" })
|
|
3292
3881
|
] })
|
|
3293
3882
|
] });
|
|
3294
3883
|
}
|
|
3295
3884
|
|
|
3296
3885
|
// src/video-chat/use-voice-input.ts
|
|
3297
|
-
import { useCallback as
|
|
3886
|
+
import { useCallback as useCallback4, useEffect as useEffect11, useRef as useRef10, useState as useState7 } from "react";
|
|
3298
3887
|
function recognitionConstructor() {
|
|
3299
3888
|
if (typeof window === "undefined") return void 0;
|
|
3300
3889
|
const holder = window;
|
|
@@ -3364,19 +3953,19 @@ async function recordAndTranscribe(signal, ready, captured, options) {
|
|
|
3364
3953
|
return clip ? transcribeRecording(clip, signal, options) : "";
|
|
3365
3954
|
}
|
|
3366
3955
|
function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {}) {
|
|
3367
|
-
const [supported, setSupported] =
|
|
3368
|
-
const [listening, setListening] =
|
|
3369
|
-
const [error, setError] =
|
|
3370
|
-
const [thinking, setThinking] =
|
|
3371
|
-
const recognitionRef =
|
|
3372
|
-
const useRecorderRef =
|
|
3373
|
-
const operationRef =
|
|
3374
|
-
const handlerRef =
|
|
3956
|
+
const [supported, setSupported] = useState7(false);
|
|
3957
|
+
const [listening, setListening] = useState7(false);
|
|
3958
|
+
const [error, setError] = useState7();
|
|
3959
|
+
const [thinking, setThinking] = useState7(false);
|
|
3960
|
+
const recognitionRef = useRef10(void 0);
|
|
3961
|
+
const useRecorderRef = useRef10(false);
|
|
3962
|
+
const operationRef = useRef10(void 0);
|
|
3963
|
+
const handlerRef = useRef10(onTranscript);
|
|
3375
3964
|
handlerRef.current = onTranscript;
|
|
3376
|
-
|
|
3965
|
+
useEffect11(() => {
|
|
3377
3966
|
setSupported(supportsVoiceInput(transcriptionAvailable));
|
|
3378
3967
|
}, [transcriptionAvailable]);
|
|
3379
|
-
const stop =
|
|
3968
|
+
const stop = useCallback4(() => {
|
|
3380
3969
|
recognitionRef.current?.abort();
|
|
3381
3970
|
recognitionRef.current = void 0;
|
|
3382
3971
|
const operation = operationRef.current;
|
|
@@ -3386,7 +3975,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3386
3975
|
setListening(false);
|
|
3387
3976
|
setThinking(false);
|
|
3388
3977
|
}, []);
|
|
3389
|
-
|
|
3978
|
+
useEffect11(() => () => {
|
|
3390
3979
|
recognitionRef.current?.abort();
|
|
3391
3980
|
recognitionRef.current = void 0;
|
|
3392
3981
|
const operation = operationRef.current;
|
|
@@ -3394,7 +3983,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3394
3983
|
operation?.controller.abort();
|
|
3395
3984
|
operation?.finish?.();
|
|
3396
3985
|
}, []);
|
|
3397
|
-
const finish =
|
|
3986
|
+
const finish = useCallback4(() => {
|
|
3398
3987
|
const recognition = recognitionRef.current;
|
|
3399
3988
|
if (recognition) {
|
|
3400
3989
|
recognition.stop();
|
|
@@ -3408,7 +3997,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3408
3997
|
}
|
|
3409
3998
|
stop();
|
|
3410
3999
|
}, [stop]);
|
|
3411
|
-
const record =
|
|
4000
|
+
const record = useCallback4(async () => {
|
|
3412
4001
|
if (!transcriptionAvailable || !recorderAvailable()) {
|
|
3413
4002
|
setError("Server transcription is not configured.");
|
|
3414
4003
|
return;
|
|
@@ -3446,7 +4035,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3446
4035
|
}
|
|
3447
4036
|
}
|
|
3448
4037
|
}, [transcriptionAvailable, options.endpoint, options.headers, options.credentials, options.fetcher]);
|
|
3449
|
-
const toggle =
|
|
4038
|
+
const toggle = useCallback4(() => {
|
|
3450
4039
|
if (listening) {
|
|
3451
4040
|
finish();
|
|
3452
4041
|
return;
|
|
@@ -3498,18 +4087,18 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3498
4087
|
}
|
|
3499
4088
|
|
|
3500
4089
|
// src/video-chat/use-immersive-controls.ts
|
|
3501
|
-
import { useCallback as
|
|
4090
|
+
import { useCallback as useCallback5, useEffect as useEffect12, useRef as useRef11, useState as useState8 } from "react";
|
|
3502
4091
|
function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
3503
|
-
const [visible, setVisible] =
|
|
3504
|
-
const hovered =
|
|
3505
|
-
const focused =
|
|
3506
|
-
const previousCaptions =
|
|
3507
|
-
const timer =
|
|
3508
|
-
const clearTimer =
|
|
4092
|
+
const [visible, setVisible] = useState8(true);
|
|
4093
|
+
const hovered = useRef11(false);
|
|
4094
|
+
const focused = useRef11(false);
|
|
4095
|
+
const previousCaptions = useRef11(false);
|
|
4096
|
+
const timer = useRef11(null);
|
|
4097
|
+
const clearTimer = useCallback5(() => {
|
|
3509
4098
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
3510
4099
|
timer.current = null;
|
|
3511
4100
|
}, []);
|
|
3512
|
-
const reveal =
|
|
4101
|
+
const reveal = useCallback5(() => {
|
|
3513
4102
|
clearTimer();
|
|
3514
4103
|
setVisible(true);
|
|
3515
4104
|
if (playing && !pinned && !hovered.current && !focused.current) {
|
|
@@ -3519,7 +4108,7 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3519
4108
|
}, 2e3);
|
|
3520
4109
|
}
|
|
3521
4110
|
}, [clearTimer, playing, pinned]);
|
|
3522
|
-
|
|
4111
|
+
useEffect12(() => {
|
|
3523
4112
|
const firstCaption = hasCaptions && !previousCaptions.current;
|
|
3524
4113
|
previousCaptions.current = hasCaptions;
|
|
3525
4114
|
if (firstCaption && playing && !pinned && !focused.current) {
|
|
@@ -3531,21 +4120,21 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3531
4120
|
}
|
|
3532
4121
|
return clearTimer;
|
|
3533
4122
|
}, [hasCaptions, playing, pinned, reveal, clearTimer]);
|
|
3534
|
-
const onPointerEnter =
|
|
4123
|
+
const onPointerEnter = useCallback5((event) => {
|
|
3535
4124
|
if (event?.pointerType === "touch") return;
|
|
3536
4125
|
hovered.current = true;
|
|
3537
4126
|
reveal();
|
|
3538
4127
|
}, [reveal]);
|
|
3539
|
-
const onPointerLeave =
|
|
4128
|
+
const onPointerLeave = useCallback5(() => {
|
|
3540
4129
|
if (!hovered.current) return;
|
|
3541
4130
|
hovered.current = false;
|
|
3542
4131
|
reveal();
|
|
3543
4132
|
}, [reveal]);
|
|
3544
|
-
const onFocusCapture =
|
|
4133
|
+
const onFocusCapture = useCallback5((event) => {
|
|
3545
4134
|
focused.current = event.target.matches(":focus-visible");
|
|
3546
4135
|
reveal();
|
|
3547
4136
|
}, [reveal]);
|
|
3548
|
-
const onBlurCapture =
|
|
4137
|
+
const onBlurCapture = useCallback5((event) => {
|
|
3549
4138
|
if (focused.current && !event.currentTarget.contains(event.relatedTarget)) {
|
|
3550
4139
|
focused.current = false;
|
|
3551
4140
|
reveal();
|
|
@@ -3555,9 +4144,9 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3555
4144
|
}
|
|
3556
4145
|
|
|
3557
4146
|
// src/video-chat/logo.tsx
|
|
3558
|
-
import { jsx as
|
|
4147
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
3559
4148
|
function Logo() {
|
|
3560
|
-
return /* @__PURE__ */
|
|
4149
|
+
return /* @__PURE__ */ jsx10("img", { className: "brand-logo", src: new URL("./assets/vanillasky-logo.svg", import.meta.url).href, alt: "VanillaSky", width: 144, height: 36 });
|
|
3561
4150
|
}
|
|
3562
4151
|
|
|
3563
4152
|
// src/video-chat/modes.ts
|
|
@@ -3568,11 +4157,11 @@ var visualModes = [
|
|
|
3568
4157
|
var defaultMode = visualModes[0];
|
|
3569
4158
|
|
|
3570
4159
|
// src/video-chat/video-chat.tsx
|
|
3571
|
-
import { Fragment as Fragment4, jsx as
|
|
4160
|
+
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3572
4161
|
var DESKTOP_WIDTH = 900;
|
|
3573
4162
|
function useViewportOrientation() {
|
|
3574
|
-
const [portrait, setPortrait] =
|
|
3575
|
-
|
|
4163
|
+
const [portrait, setPortrait] = useState9(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`).matches : false);
|
|
4164
|
+
useEffect13(() => {
|
|
3576
4165
|
if (typeof window.matchMedia !== "function") return;
|
|
3577
4166
|
const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`);
|
|
3578
4167
|
const update = () => setPortrait(query.matches);
|
|
@@ -3583,23 +4172,25 @@ function useViewportOrientation() {
|
|
|
3583
4172
|
return portrait ? "portrait" : "landscape";
|
|
3584
4173
|
}
|
|
3585
4174
|
function Waveform({ active, listening }) {
|
|
3586
|
-
return /* @__PURE__ */
|
|
4175
|
+
return /* @__PURE__ */ jsx11("span", { className: `waveform${active ? " on" : ""}${listening ? " hearing" : ""}`, "aria-hidden": "true", children: [0, 1, 2, 3, 4].map((bar) => /* @__PURE__ */ jsx11("span", { style: { animationDelay: `${bar * 140}ms`, animationDuration: `${900 + bar * 130}ms` } }, bar)) });
|
|
3587
4176
|
}
|
|
3588
|
-
function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice = false }) {
|
|
3589
|
-
const
|
|
3590
|
-
const
|
|
3591
|
-
const [
|
|
3592
|
-
const [
|
|
3593
|
-
const [
|
|
3594
|
-
const [
|
|
3595
|
-
const [
|
|
3596
|
-
const [
|
|
3597
|
-
const [
|
|
3598
|
-
const [
|
|
3599
|
-
const [
|
|
3600
|
-
const
|
|
3601
|
-
const
|
|
3602
|
-
const
|
|
4177
|
+
function VideoChat({ options = {}, className, welcomeTitle, branding, showRecoveryNotice = false }) {
|
|
4178
|
+
const appName = branding?.name.trim() || "VanillaSky";
|
|
4179
|
+
const customHome = safeHomeUrl(branding?.homeUrl);
|
|
4180
|
+
const [dismissedNoticeTurn, setDismissedNoticeTurn] = useState9();
|
|
4181
|
+
const [draft, setDraft] = useState9("");
|
|
4182
|
+
const [selectedMode, setSelectedMode] = useState9();
|
|
4183
|
+
const [savedSessions, setSavedSessions] = useState9([]);
|
|
4184
|
+
const [historyOpen, setHistoryOpen] = useState9(false);
|
|
4185
|
+
const [settingsOpen, setSettingsOpen] = useState9(false);
|
|
4186
|
+
const [aboutOpen, setAboutOpen] = useState9(false);
|
|
4187
|
+
const [captionsOn, setCaptionsOn] = useState9(true);
|
|
4188
|
+
const [captionsExpanded, setCaptionsExpanded] = useState9(false);
|
|
4189
|
+
const [alwaysShowControls, setAlwaysShowControls] = useState9(false);
|
|
4190
|
+
const [editing, setEditing] = useState9(false);
|
|
4191
|
+
const resumeAfterInput = useRef12(false);
|
|
4192
|
+
const panelRef = useRef12(null);
|
|
4193
|
+
const composerRef = useRef12(null);
|
|
3603
4194
|
const viewportOrientation = useViewportOrientation();
|
|
3604
4195
|
const sessionOrientation = options.orientation ?? viewportOrientation;
|
|
3605
4196
|
const { chat, restoreSession, getCaptionProgress } = useVideoChatSession({
|
|
@@ -3607,8 +4198,8 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3607
4198
|
orientation: sessionOrientation,
|
|
3608
4199
|
mode: selectedMode ?? options.mode
|
|
3609
4200
|
});
|
|
3610
|
-
const observedMode =
|
|
3611
|
-
|
|
4201
|
+
const observedMode = useRef12(void 0);
|
|
4202
|
+
useEffect13(() => {
|
|
3612
4203
|
const turn = chat.currentTurn;
|
|
3613
4204
|
if (!turn?.mode) {
|
|
3614
4205
|
observedMode.current = void 0;
|
|
@@ -3623,11 +4214,11 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3623
4214
|
const historyId = `${instanceId}-history`;
|
|
3624
4215
|
const settingsId = `${instanceId}-settings`;
|
|
3625
4216
|
const promptId = `${instanceId}-prompt`;
|
|
3626
|
-
const inputRef =
|
|
3627
|
-
const historyRef =
|
|
3628
|
-
const historyButtonRef =
|
|
3629
|
-
const settingsRef =
|
|
3630
|
-
const settingsButtonRef =
|
|
4217
|
+
const inputRef = useRef12(null);
|
|
4218
|
+
const historyRef = useRef12(null);
|
|
4219
|
+
const historyButtonRef = useRef12(null);
|
|
4220
|
+
const settingsRef = useRef12(null);
|
|
4221
|
+
const settingsButtonRef = useRef12(null);
|
|
3631
4222
|
const listen = useVoiceInput(setDraft, chat.capabilities?.transcription ?? false, {
|
|
3632
4223
|
endpoint: options.endpoint,
|
|
3633
4224
|
headers: options.headers,
|
|
@@ -3636,13 +4227,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3636
4227
|
});
|
|
3637
4228
|
const historySurfaces = useMemo3(() => [historyRef, historyButtonRef], []);
|
|
3638
4229
|
const settingsSurfaces = useMemo3(() => [settingsRef, settingsButtonRef], []);
|
|
3639
|
-
const closeHistory =
|
|
3640
|
-
const closeSettings =
|
|
4230
|
+
const closeHistory = useCallback6(() => setHistoryOpen(false), []);
|
|
4231
|
+
const closeSettings = useCallback6(() => setSettingsOpen(false), []);
|
|
3641
4232
|
useDismiss(historyOpen, closeHistory, historySurfaces);
|
|
3642
4233
|
useDismiss(settingsOpen, closeSettings, settingsSurfaces);
|
|
3643
4234
|
useFocusTrap(settingsOpen, settingsRef);
|
|
3644
4235
|
useFocusTrap(historyOpen, historyRef);
|
|
3645
|
-
const ask =
|
|
4236
|
+
const ask = useCallback6((value) => {
|
|
3646
4237
|
const prompt = (typeof value === "string" ? value : value.prompt).trim();
|
|
3647
4238
|
if (!prompt) return;
|
|
3648
4239
|
setDraft("");
|
|
@@ -3657,7 +4248,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3657
4248
|
opening: value.opening
|
|
3658
4249
|
});
|
|
3659
4250
|
}, [chat, listen]);
|
|
3660
|
-
const newSession =
|
|
4251
|
+
const newSession = useCallback6(() => {
|
|
3661
4252
|
if (chat.turns.length === 0) {
|
|
3662
4253
|
setHistoryOpen(false);
|
|
3663
4254
|
setSettingsOpen(false);
|
|
@@ -3679,13 +4270,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3679
4270
|
const shown = chat.shownTurn;
|
|
3680
4271
|
const showing = chat.playerProps != null;
|
|
3681
4272
|
const handoffKey = `${shown?.id ?? ""}:${chat.playerKey}`;
|
|
3682
|
-
const [presentedBody, setPresentedBody] =
|
|
3683
|
-
const handoff =
|
|
4273
|
+
const [presentedBody, setPresentedBody] = useState9();
|
|
4274
|
+
const handoff = useRef12({ key: handoffKey, live: showing, active: false, frame: 0 });
|
|
3684
4275
|
const handoffStopped = chat.status === "error" || chat.status === "cancelled";
|
|
3685
4276
|
const waitingForBody = showing && presentedBody !== handoffKey;
|
|
3686
4277
|
const openingChapter = Boolean(shown?.prompt) && (!showing || waitingForBody) && chat.status !== "error" && chat.status !== "cancelled" && chat.status !== "ended";
|
|
3687
|
-
const [preparingKey, setPreparingKey] =
|
|
3688
|
-
|
|
4278
|
+
const [preparingKey, setPreparingKey] = useState9();
|
|
4279
|
+
useEffect13(() => {
|
|
3689
4280
|
setPreparingKey(void 0);
|
|
3690
4281
|
if (!openingChapter || !shown?.opening || chat.speaking) return;
|
|
3691
4282
|
const timer = setTimeout(() => setPreparingKey(shown.id), 1e3);
|
|
@@ -3724,13 +4315,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3724
4315
|
const status = chat.turns.length === 0 ? "idle" : chat.status === "composing" ? "drawing" : chat.status === "playing" ? "narrating" : chat.status === "error" || chat.status === "cancelled" ? "ended" : chat.status;
|
|
3725
4316
|
const shownOrientation = shown?.orientation ?? sessionOrientation;
|
|
3726
4317
|
const stageOrientation = shown?.fixedOrientation ? shownOrientation : sessionOrientation;
|
|
3727
|
-
|
|
4318
|
+
useEffect13(() => {
|
|
3728
4319
|
setCaptionsExpanded(false);
|
|
3729
4320
|
}, [chat.playbackEnded, shown?.id, chat.playerKey]);
|
|
3730
4321
|
const line = chat.caption ?? "";
|
|
3731
4322
|
const fullTranscript = shown?.video ? [shown.opening, ...shown.video.scenes.map((scene) => scene.narration)].filter((entry) => Boolean(entry)) : chat.transcript;
|
|
3732
|
-
const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */
|
|
3733
|
-
const cancelInput =
|
|
4323
|
+
const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */ jsx11(Stop, {}) } : status === "paused" ? { label: "Continue", action: chat.resume, icon: /* @__PURE__ */ jsx11(Play, {}) } : status === "ended" && shown?.completed && shown.video ? { label: "Play again", action: chat.replay, icon: /* @__PURE__ */ jsx11(Replay, {}) } : void 0;
|
|
4324
|
+
const cancelInput = useCallback6(() => {
|
|
3734
4325
|
listen.stop();
|
|
3735
4326
|
setDraft("");
|
|
3736
4327
|
setEditing(false);
|
|
@@ -3770,7 +4361,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3770
4361
|
observer.observe(composer);
|
|
3771
4362
|
return () => observer.disconnect();
|
|
3772
4363
|
}, []);
|
|
3773
|
-
return /* @__PURE__ */
|
|
4364
|
+
return /* @__PURE__ */ jsxs8(
|
|
3774
4365
|
"div",
|
|
3775
4366
|
{
|
|
3776
4367
|
className: `vanillasky-video-chat${className ? ` ${className}` : ""}`,
|
|
@@ -3782,14 +4373,24 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3782
4373
|
onPointerDown: controls.reveal,
|
|
3783
4374
|
onKeyDownCapture: controls.reveal,
|
|
3784
4375
|
children: [
|
|
3785
|
-
/* @__PURE__ */
|
|
3786
|
-
/* @__PURE__ */
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
4376
|
+
/* @__PURE__ */ jsxs8("header", { className: "chrome", ...controlEvents, children: [
|
|
4377
|
+
/* @__PURE__ */ jsx11("div", { className: "session-brand", children: /* @__PURE__ */ jsx11(
|
|
4378
|
+
"a",
|
|
4379
|
+
{
|
|
4380
|
+
className: "home-link",
|
|
4381
|
+
href: customHome ?? "/",
|
|
4382
|
+
"aria-label": branding ? `${appName} home` : "Home",
|
|
4383
|
+
style: branding ? { color: "inherit", textDecoration: "none" } : void 0,
|
|
4384
|
+
onClick: (event) => {
|
|
4385
|
+
if (customHome || window.location.pathname !== "/" || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
4386
|
+
event.preventDefault();
|
|
4387
|
+
newSession();
|
|
4388
|
+
},
|
|
4389
|
+
children: branding ? branding.logo ?? appName : /* @__PURE__ */ jsx11(Logo, {})
|
|
4390
|
+
}
|
|
4391
|
+
) }),
|
|
4392
|
+
/* @__PURE__ */ jsxs8("div", { className: "group", children: [
|
|
4393
|
+
/* @__PURE__ */ jsxs8(
|
|
3793
4394
|
"button",
|
|
3794
4395
|
{
|
|
3795
4396
|
ref: historyButtonRef,
|
|
@@ -3804,12 +4405,12 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3804
4405
|
setSettingsOpen(false);
|
|
3805
4406
|
},
|
|
3806
4407
|
children: [
|
|
3807
|
-
/* @__PURE__ */
|
|
3808
|
-
/* @__PURE__ */
|
|
4408
|
+
/* @__PURE__ */ jsx11(Sessions, {}),
|
|
4409
|
+
/* @__PURE__ */ jsx11("span", { className: "nav-label", children: "Sessions" })
|
|
3809
4410
|
]
|
|
3810
4411
|
}
|
|
3811
4412
|
),
|
|
3812
|
-
/* @__PURE__ */
|
|
4413
|
+
/* @__PURE__ */ jsx11(
|
|
3813
4414
|
"button",
|
|
3814
4415
|
{
|
|
3815
4416
|
ref: settingsButtonRef,
|
|
@@ -3823,10 +4424,10 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3823
4424
|
setSettingsOpen((open) => !open);
|
|
3824
4425
|
setHistoryOpen(false);
|
|
3825
4426
|
},
|
|
3826
|
-
children: /* @__PURE__ */
|
|
4427
|
+
children: /* @__PURE__ */ jsx11(Gear, {})
|
|
3827
4428
|
}
|
|
3828
4429
|
),
|
|
3829
|
-
/* @__PURE__ */
|
|
4430
|
+
/* @__PURE__ */ jsx11(
|
|
3830
4431
|
"button",
|
|
3831
4432
|
{
|
|
3832
4433
|
type: "button",
|
|
@@ -3834,50 +4435,49 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3834
4435
|
"aria-label": chat.muted ? "Turn the voice on" : "Turn the voice off",
|
|
3835
4436
|
"aria-pressed": chat.muted,
|
|
3836
4437
|
onClick: () => chat.setMuted(!chat.muted),
|
|
3837
|
-
children: chat.muted ? /* @__PURE__ */
|
|
4438
|
+
children: chat.muted ? /* @__PURE__ */ jsx11(Muted, {}) : /* @__PURE__ */ jsx11(Sound, {})
|
|
3838
4439
|
}
|
|
3839
4440
|
)
|
|
3840
4441
|
] })
|
|
3841
4442
|
] }),
|
|
3842
|
-
/* @__PURE__ */
|
|
3843
|
-
/* @__PURE__ */
|
|
3844
|
-
openingChapter && /* @__PURE__ */
|
|
3845
|
-
!showing && chat.turns.length === 0 && /* @__PURE__ */
|
|
3846
|
-
chat.playerProps && /* @__PURE__ */
|
|
4443
|
+
/* @__PURE__ */ jsxs8("div", { className: "stage-area", children: [
|
|
4444
|
+
/* @__PURE__ */ jsxs8("div", { className: "stage", style: { background: "#000" }, children: [
|
|
4445
|
+
openingChapter && /* @__PURE__ */ jsx11(OpeningChapter, { preparing: preparingKey === shown.id && !chat.speaking, title: openingTitle.length > 120 ? `${openingTitle.slice(0, 117).trimEnd()}\u2026` : openingTitle }, shown.id),
|
|
4446
|
+
!showing && chat.turns.length === 0 && /* @__PURE__ */ jsx11(Welcome, { data: chat.welcome, onAsk: ask, title: welcomeTitle }),
|
|
4447
|
+
chat.playerProps && /* @__PURE__ */ jsx11("div", { className: "player-fit", style: { width: stageOrientation === "portrait" ? "min(100cqw, 56.25cqh)" : "min(100cqw, 177.7778cqh)" }, children: /* @__PURE__ */ jsx11(
|
|
3847
4448
|
VideoPlayer,
|
|
3848
4449
|
{
|
|
3849
4450
|
...chat.playerProps,
|
|
3850
4451
|
onSceneChange: cueBody,
|
|
3851
|
-
templates: options.templates,
|
|
3852
4452
|
orientation: stageOrientation,
|
|
3853
4453
|
responsiveBreakpoint: DESKTOP_WIDTH,
|
|
3854
4454
|
ariaLabel: chat.playerProps.video ? "Replay" : "The response"
|
|
3855
4455
|
},
|
|
3856
4456
|
chat.playerKey
|
|
3857
4457
|
) }),
|
|
3858
|
-
showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */
|
|
3859
|
-
/* @__PURE__ */
|
|
3860
|
-
/* @__PURE__ */
|
|
3861
|
-
/* @__PURE__ */
|
|
3862
|
-
/* @__PURE__ */
|
|
4458
|
+
showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */ jsxs8("div", { className: "ending", children: [
|
|
4459
|
+
/* @__PURE__ */ jsx11("div", { className: "ending-wash", "aria-hidden": "true" }),
|
|
4460
|
+
/* @__PURE__ */ jsxs8("div", { className: "ending-body", children: [
|
|
4461
|
+
/* @__PURE__ */ jsx11("p", { className: "ending-label", children: "Ask next" }),
|
|
4462
|
+
/* @__PURE__ */ jsx11(SuggestionCards, { suggestions: [...chat.suggestions], label: "Follow-up prompts", onAsk: ask })
|
|
3863
4463
|
] })
|
|
3864
4464
|
] })
|
|
3865
4465
|
] }),
|
|
3866
|
-
historyOpen && /* @__PURE__ */
|
|
3867
|
-
/* @__PURE__ */
|
|
3868
|
-
/* @__PURE__ */
|
|
3869
|
-
/* @__PURE__ */
|
|
4466
|
+
historyOpen && /* @__PURE__ */ jsxs8("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", role: "dialog", "aria-modal": "true", "aria-label": "Sessions", children: [
|
|
4467
|
+
/* @__PURE__ */ jsxs8("div", { className: "popover-heading", children: [
|
|
4468
|
+
/* @__PURE__ */ jsx11("h2", { children: "Sessions" }),
|
|
4469
|
+
/* @__PURE__ */ jsx11("button", { type: "button", className: "round", "aria-label": "Close sessions", onClick: closeHistory, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
3870
4470
|
] }),
|
|
3871
|
-
/* @__PURE__ */
|
|
3872
|
-
/* @__PURE__ */
|
|
3873
|
-
/* @__PURE__ */
|
|
4471
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", className: "history-row session-new", "aria-label": "New session", onClick: newSession, children: [
|
|
4472
|
+
/* @__PURE__ */ jsx11(Plus, {}),
|
|
4473
|
+
/* @__PURE__ */ jsxs8("span", { className: "prompt", children: [
|
|
3874
4474
|
"New session",
|
|
3875
|
-
/* @__PURE__ */
|
|
4475
|
+
/* @__PURE__ */ jsx11("small", { children: "Start a fresh conversation" })
|
|
3876
4476
|
] })
|
|
3877
4477
|
] }),
|
|
3878
|
-
/* @__PURE__ */
|
|
3879
|
-
chat.turns.length === 0 && /* @__PURE__ */
|
|
3880
|
-
chat.turns.map((turn, index) => /* @__PURE__ */
|
|
4478
|
+
/* @__PURE__ */ jsx11("h3", { className: "section-label", children: "Current session" }),
|
|
4479
|
+
chat.turns.length === 0 && /* @__PURE__ */ jsx11("p", { className: "history-empty", children: "Your questions will appear here." }),
|
|
4480
|
+
chat.turns.map((turn, index) => /* @__PURE__ */ jsxs8(
|
|
3881
4481
|
"button",
|
|
3882
4482
|
{
|
|
3883
4483
|
type: "button",
|
|
@@ -3893,18 +4493,18 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3893
4493
|
setHistoryOpen(false);
|
|
3894
4494
|
},
|
|
3895
4495
|
children: [
|
|
3896
|
-
/* @__PURE__ */
|
|
3897
|
-
/* @__PURE__ */
|
|
4496
|
+
/* @__PURE__ */ jsx11("span", { className: "index", children: String(index + 1).padStart(2, "0") }),
|
|
4497
|
+
/* @__PURE__ */ jsxs8("span", { className: "prompt", children: [
|
|
3898
4498
|
turn.prompt,
|
|
3899
|
-
/* @__PURE__ */
|
|
4499
|
+
/* @__PURE__ */ jsx11("small", { children: turn.id === shown?.id ? "Now showing" : turn.completed ? "Play answer" : "Unfinished answer" })
|
|
3900
4500
|
] })
|
|
3901
4501
|
]
|
|
3902
4502
|
},
|
|
3903
4503
|
turn.id
|
|
3904
4504
|
)),
|
|
3905
|
-
savedSessions.length > 0 && /* @__PURE__ */
|
|
3906
|
-
/* @__PURE__ */
|
|
3907
|
-
savedSessions.map((session) => /* @__PURE__ */
|
|
4505
|
+
savedSessions.length > 0 && /* @__PURE__ */ jsxs8(Fragment4, { children: [
|
|
4506
|
+
/* @__PURE__ */ jsx11("h3", { className: "section-label", children: "Earlier sessions" }),
|
|
4507
|
+
savedSessions.map((session) => /* @__PURE__ */ jsxs8("button", { type: "button", className: "history-row", onClick: () => {
|
|
3908
4508
|
listen.stop();
|
|
3909
4509
|
setDraft("");
|
|
3910
4510
|
setEditing(false);
|
|
@@ -3915,10 +4515,10 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3915
4515
|
setHistoryOpen(false);
|
|
3916
4516
|
setCaptionsExpanded(false);
|
|
3917
4517
|
}, children: [
|
|
3918
|
-
/* @__PURE__ */
|
|
3919
|
-
/* @__PURE__ */
|
|
4518
|
+
/* @__PURE__ */ jsx11(Replay, {}),
|
|
4519
|
+
/* @__PURE__ */ jsxs8("span", { className: "prompt", children: [
|
|
3920
4520
|
session.turns[0]?.prompt,
|
|
3921
|
-
/* @__PURE__ */
|
|
4521
|
+
/* @__PURE__ */ jsxs8("small", { children: [
|
|
3922
4522
|
session.turns.length,
|
|
3923
4523
|
" ",
|
|
3924
4524
|
session.turns.length === 1 ? "answer" : "answers"
|
|
@@ -3927,7 +4527,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3927
4527
|
] }, session.id))
|
|
3928
4528
|
] })
|
|
3929
4529
|
] }),
|
|
3930
|
-
settingsOpen && /* @__PURE__ */
|
|
4530
|
+
settingsOpen && /* @__PURE__ */ jsxs8(
|
|
3931
4531
|
"div",
|
|
3932
4532
|
{
|
|
3933
4533
|
ref: settingsRef,
|
|
@@ -3937,18 +4537,18 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3937
4537
|
"aria-label": "Settings",
|
|
3938
4538
|
"aria-modal": "true",
|
|
3939
4539
|
children: [
|
|
3940
|
-
/* @__PURE__ */
|
|
3941
|
-
/* @__PURE__ */
|
|
3942
|
-
/* @__PURE__ */
|
|
4540
|
+
/* @__PURE__ */ jsxs8("div", { className: "popover-heading", children: [
|
|
4541
|
+
/* @__PURE__ */ jsx11("h2", { children: "Settings" }),
|
|
4542
|
+
/* @__PURE__ */ jsx11("button", { type: "button", className: "round", "aria-label": "Close settings", onClick: closeSettings, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
3943
4543
|
] }),
|
|
3944
|
-
/* @__PURE__ */
|
|
3945
|
-
/* @__PURE__ */
|
|
3946
|
-
visualModes.filter((mode) => chat.availableModes.includes(mode.id)).map((mode) => /* @__PURE__ */
|
|
3947
|
-
/* @__PURE__ */
|
|
3948
|
-
/* @__PURE__ */
|
|
3949
|
-
/* @__PURE__ */
|
|
4544
|
+
/* @__PURE__ */ jsxs8("fieldset", { className: "playback-options", children: [
|
|
4545
|
+
/* @__PURE__ */ jsx11("legend", { children: "Video source" }),
|
|
4546
|
+
visualModes.filter((mode) => chat.availableModes.includes(mode.id)).map((mode) => /* @__PURE__ */ jsxs8("label", { className: "switch-row", children: [
|
|
4547
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
4548
|
+
/* @__PURE__ */ jsx11("strong", { children: mode.label }),
|
|
4549
|
+
/* @__PURE__ */ jsx11("small", { children: mode.note })
|
|
3950
4550
|
] }),
|
|
3951
|
-
/* @__PURE__ */
|
|
4551
|
+
/* @__PURE__ */ jsx11(
|
|
3952
4552
|
"input",
|
|
3953
4553
|
{
|
|
3954
4554
|
type: "radio",
|
|
@@ -3960,51 +4560,51 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3960
4560
|
)
|
|
3961
4561
|
] }, mode.id))
|
|
3962
4562
|
] }),
|
|
3963
|
-
/* @__PURE__ */
|
|
3964
|
-
/* @__PURE__ */
|
|
3965
|
-
/* @__PURE__ */
|
|
3966
|
-
/* @__PURE__ */
|
|
3967
|
-
/* @__PURE__ */
|
|
3968
|
-
/* @__PURE__ */
|
|
4563
|
+
/* @__PURE__ */ jsxs8("fieldset", { className: "playback-options", children: [
|
|
4564
|
+
/* @__PURE__ */ jsx11("legend", { children: "Watching" }),
|
|
4565
|
+
/* @__PURE__ */ jsxs8("label", { className: "switch-row", children: [
|
|
4566
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
4567
|
+
/* @__PURE__ */ jsx11("strong", { children: "Subtitles" }),
|
|
4568
|
+
/* @__PURE__ */ jsx11("small", { children: "Read along with the answer" })
|
|
3969
4569
|
] }),
|
|
3970
|
-
/* @__PURE__ */
|
|
4570
|
+
/* @__PURE__ */ jsx11("input", { type: "checkbox", role: "switch", checked: captionsOn, onChange: (event) => {
|
|
3971
4571
|
setCaptionsOn(event.target.checked);
|
|
3972
4572
|
setCaptionsExpanded(false);
|
|
3973
4573
|
} })
|
|
3974
4574
|
] }),
|
|
3975
|
-
/* @__PURE__ */
|
|
3976
|
-
/* @__PURE__ */
|
|
3977
|
-
/* @__PURE__ */
|
|
3978
|
-
/* @__PURE__ */
|
|
4575
|
+
/* @__PURE__ */ jsxs8("label", { className: "switch-row", children: [
|
|
4576
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
4577
|
+
/* @__PURE__ */ jsx11("strong", { children: "Keep controls visible" }),
|
|
4578
|
+
/* @__PURE__ */ jsx11("small", { children: "Keep the input bar on screen" })
|
|
3979
4579
|
] }),
|
|
3980
|
-
/* @__PURE__ */
|
|
4580
|
+
/* @__PURE__ */ jsx11("input", { type: "checkbox", role: "switch", checked: alwaysShowControls, onChange: (event) => setAlwaysShowControls(event.target.checked) })
|
|
3981
4581
|
] })
|
|
3982
4582
|
] }),
|
|
3983
|
-
/* @__PURE__ */
|
|
3984
|
-
/* @__PURE__ */
|
|
3985
|
-
/* @__PURE__ */
|
|
4583
|
+
branding?.showDeveloperLinks !== false && /* @__PURE__ */ jsxs8("nav", { className: "developer-links", "aria-label": "Build with VanillaSky", children: [
|
|
4584
|
+
/* @__PURE__ */ jsx11("p", { className: "section-label", children: "Build with VanillaSky" }),
|
|
4585
|
+
/* @__PURE__ */ jsxs8("a", { href: "https://github.com/VanillaSkyAi/video/blob/main/docs/getting-started.md", target: "_blank", rel: "noopener noreferrer", children: [
|
|
3986
4586
|
"Docs",
|
|
3987
|
-
/* @__PURE__ */
|
|
4587
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\u2197" })
|
|
3988
4588
|
] }),
|
|
3989
|
-
/* @__PURE__ */
|
|
4589
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", "aria-expanded": aboutOpen, "aria-controls": `${instanceId}-about`, onClick: () => setAboutOpen((open) => !open), children: [
|
|
3990
4590
|
"About",
|
|
3991
|
-
/* @__PURE__ */
|
|
4591
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: aboutOpen ? "\u2212" : "+" })
|
|
3992
4592
|
] }),
|
|
3993
|
-
/* @__PURE__ */
|
|
3994
|
-
/* @__PURE__ */
|
|
4593
|
+
/* @__PURE__ */ jsx11("div", { id: `${instanceId}-about`, className: "developer-about", role: "region", "aria-label": "About VanillaSky", hidden: !aboutOpen, children: /* @__PURE__ */ jsx11("p", { children: "VanillaSky is an open-source SDK for conversations that answer in video. Developers connect their own AI providers through their application server." }) }),
|
|
4594
|
+
/* @__PURE__ */ jsxs8("a", { href: "https://github.com/VanillaSkyAi/video", target: "_blank", rel: "noopener noreferrer", children: [
|
|
3995
4595
|
"GitHub",
|
|
3996
|
-
/* @__PURE__ */
|
|
4596
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\u2197" })
|
|
3997
4597
|
] })
|
|
3998
4598
|
] })
|
|
3999
4599
|
]
|
|
4000
4600
|
}
|
|
4001
4601
|
)
|
|
4002
4602
|
] }),
|
|
4003
|
-
/* @__PURE__ */
|
|
4004
|
-
/* @__PURE__ */
|
|
4603
|
+
/* @__PURE__ */ jsx11("div", { ref: panelRef, className: "panel", "data-input-visible": controls.visible || !captionsOn || !line, children: /* @__PURE__ */ jsxs8("div", { className: "panel-inner", children: [
|
|
4604
|
+
/* @__PURE__ */ jsx11("div", { className: "caption-slot", "data-captions": captionsOn && Boolean(line), "aria-hidden": !captionsOn || !line, children: /* @__PURE__ */ jsx11("div", { className: "caption-clip", children: chat.playbackEnded && !captionsExpanded ? /* @__PURE__ */ jsxs8("button", { type: "button", className: "transcript-toggle", "aria-expanded": false, onClick: () => setCaptionsExpanded(true), children: [
|
|
4005
4605
|
"Show transcript",
|
|
4006
|
-
/* @__PURE__ */
|
|
4007
|
-
] }) : /* @__PURE__ */
|
|
4606
|
+
/* @__PURE__ */ jsx11(ChevronUp, {})
|
|
4607
|
+
] }) : /* @__PURE__ */ jsxs8(
|
|
4008
4608
|
"div",
|
|
4009
4609
|
{
|
|
4010
4610
|
className: "line-row",
|
|
@@ -4017,44 +4617,44 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
4017
4617
|
onFocusCapture: captionControls.onFocusCapture,
|
|
4018
4618
|
onBlurCapture: captionControls.onBlurCapture,
|
|
4019
4619
|
children: [
|
|
4020
|
-
captionsOn && line && /* @__PURE__ */
|
|
4021
|
-
/* @__PURE__ */
|
|
4620
|
+
captionsOn && line && /* @__PURE__ */ jsxs8("div", { className: "caption-actions", children: [
|
|
4621
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", className: "caption-action", "aria-label": captionsExpanded ? "Collapse subtitles" : "Expand subtitles", "aria-expanded": captionsExpanded, onClick: () => setCaptionsExpanded((open) => !open), children: [
|
|
4022
4622
|
captionsExpanded ? "Collapse" : "Expand",
|
|
4023
|
-
/* @__PURE__ */
|
|
4623
|
+
/* @__PURE__ */ jsx11(ChevronUp, {})
|
|
4024
4624
|
] }),
|
|
4025
|
-
/* @__PURE__ */
|
|
4625
|
+
/* @__PURE__ */ jsx11("button", { type: "button", className: "caption-action", "aria-label": "Hide subtitles", onClick: () => {
|
|
4026
4626
|
setCaptionsOn(false);
|
|
4027
4627
|
setCaptionsExpanded(false);
|
|
4028
|
-
}, children: /* @__PURE__ */
|
|
4628
|
+
}, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
4029
4629
|
] }),
|
|
4030
|
-
captionsExpanded ? /* @__PURE__ */
|
|
4630
|
+
captionsExpanded ? /* @__PURE__ */ jsx11("div", { className: "expanded-captions", role: "region", tabIndex: 0, "aria-label": "Expanded subtitles", children: fullTranscript.map((entry, index) => /* @__PURE__ */ jsx11("p", { children: entry }, index)) }) : /* @__PURE__ */ jsx11(CaptionPages, { text: openingChapter && line === shown?.opening ? "" : line, getProgress: getCaptionProgress }, `${shown?.id}:${chat.playerKey}`)
|
|
4031
4631
|
]
|
|
4032
4632
|
}
|
|
4033
4633
|
) }) }),
|
|
4034
|
-
showRecoveryNotice && chat.shownTurn && dismissedNoticeTurn !== chat.shownTurn.id && chat.warnings.includes(MEDIA_RECOVERY_NOTICE) && !chat.error && /* @__PURE__ */
|
|
4035
|
-
/* @__PURE__ */
|
|
4036
|
-
/* @__PURE__ */
|
|
4634
|
+
showRecoveryNotice && chat.shownTurn && dismissedNoticeTurn !== chat.shownTurn.id && chat.warnings.includes(MEDIA_RECOVERY_NOTICE) && !chat.error && /* @__PURE__ */ jsxs8("div", { className: "recovery-notice", children: [
|
|
4635
|
+
/* @__PURE__ */ jsx11("p", { role: "status", children: MEDIA_RECOVERY_NOTICE }),
|
|
4636
|
+
/* @__PURE__ */ jsx11("button", { type: "button", className: "round", "aria-label": "Dismiss notice", onClick: () => setDismissedNoticeTurn(chat.shownTurn?.id), children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
4037
4637
|
] }),
|
|
4038
|
-
(chat.error || listen.error) && /* @__PURE__ */
|
|
4039
|
-
/* @__PURE__ */
|
|
4040
|
-
/* @__PURE__ */
|
|
4638
|
+
(chat.error || listen.error) && /* @__PURE__ */ jsxs8("p", { className: "error", role: "status", children: [
|
|
4639
|
+
/* @__PURE__ */ jsx11(Warning, {}),
|
|
4640
|
+
/* @__PURE__ */ jsx11("span", { children: chat.error?.message ?? listen.error })
|
|
4041
4641
|
] }),
|
|
4042
|
-
/* @__PURE__ */
|
|
4043
|
-
(listen.listening || listen.thinking) && /* @__PURE__ */
|
|
4044
|
-
/* @__PURE__ */
|
|
4642
|
+
/* @__PURE__ */ jsxs8("div", { ref: composerRef, className: "conversation-composer", "data-editing": editing, ...controlEvents, children: [
|
|
4643
|
+
(listen.listening || listen.thinking) && /* @__PURE__ */ jsx11("div", { className: "composer-meta", children: /* @__PURE__ */ jsx11("span", { role: "status", children: listen.listening ? "Listening\u2026 Tap the mic to finish, then review and send." : "Turning your words into a draft\u2026" }) }),
|
|
4644
|
+
/* @__PURE__ */ jsxs8("form", { className: "composer", "aria-label": "Ask a question", onSubmit: (event) => {
|
|
4045
4645
|
event.preventDefault();
|
|
4046
4646
|
ask(draft);
|
|
4047
4647
|
}, children: [
|
|
4048
|
-
transport && /* @__PURE__ */
|
|
4648
|
+
transport && /* @__PURE__ */ jsx11("button", { type: "button", className: "ghost transport", "aria-label": transport.label, onClick: () => {
|
|
4049
4649
|
listen.stop();
|
|
4050
4650
|
setEditing(false);
|
|
4051
4651
|
resumeAfterInput.current = false;
|
|
4052
4652
|
inputRef.current?.blur();
|
|
4053
4653
|
transport.action();
|
|
4054
4654
|
}, children: transport.icon }),
|
|
4055
|
-
/* @__PURE__ */
|
|
4056
|
-
/* @__PURE__ */
|
|
4057
|
-
/* @__PURE__ */
|
|
4655
|
+
/* @__PURE__ */ jsx11(Waveform, { active: listen.listening || chat.speaking && !chat.muted && status !== "paused", listening: listen.listening }),
|
|
4656
|
+
/* @__PURE__ */ jsx11("label", { className: "sr-only", htmlFor: promptId, children: "Prompt" }),
|
|
4657
|
+
/* @__PURE__ */ jsx11(
|
|
4058
4658
|
"textarea",
|
|
4059
4659
|
{
|
|
4060
4660
|
id: promptId,
|
|
@@ -4082,17 +4682,28 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
4082
4682
|
}
|
|
4083
4683
|
}
|
|
4084
4684
|
),
|
|
4085
|
-
listen.supported && /* @__PURE__ */
|
|
4086
|
-
/* @__PURE__ */
|
|
4087
|
-
editing && /* @__PURE__ */
|
|
4685
|
+
listen.supported && /* @__PURE__ */ jsx11("button", { type: "button", className: `ghost${listen.listening ? " listening" : ""}`, "aria-label": listen.listening ? "Stop listening" : "Ask by voice", "aria-pressed": listen.listening, disabled: listen.thinking, onClick: () => beginInput(true), children: /* @__PURE__ */ jsx11(Mic, {}) }),
|
|
4686
|
+
/* @__PURE__ */ jsx11("button", { type: "submit", className: "send", "aria-label": "Ask", disabled: !draft.trim() || listen.thinking, children: /* @__PURE__ */ jsx11(Send, {}) }),
|
|
4687
|
+
editing && /* @__PURE__ */ jsx11("button", { type: "button", className: "ghost", "aria-label": "Cancel question", onClick: cancelInput, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
4088
4688
|
] })
|
|
4089
4689
|
] }),
|
|
4090
|
-
!showing && chat.turns.length === 0 && /* @__PURE__ */
|
|
4690
|
+
!showing && chat.turns.length === 0 && /* @__PURE__ */ jsx11("p", { className: "dock-hint", children: "Speak or type. See where it takes you." })
|
|
4091
4691
|
] }) })
|
|
4092
4692
|
]
|
|
4093
4693
|
}
|
|
4094
4694
|
);
|
|
4095
4695
|
}
|
|
4696
|
+
function safeHomeUrl(value) {
|
|
4697
|
+
const candidate = value?.trim();
|
|
4698
|
+
if (!candidate || !/^(?:https?:\/\/|\/(?![/\\]))/i.test(candidate)) return void 0;
|
|
4699
|
+
try {
|
|
4700
|
+
const url = new URL(candidate, "https://vanillasky.local");
|
|
4701
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password) return void 0;
|
|
4702
|
+
return candidate.startsWith("/") ? `${url.pathname}${url.search}${url.hash}` : url.href;
|
|
4703
|
+
} catch {
|
|
4704
|
+
return void 0;
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4096
4707
|
export {
|
|
4097
4708
|
VideoChat,
|
|
4098
4709
|
VideoError,
|