@vanillaskyai/video 0.10.23 → 0.11.1
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 +69 -0
- package/PUBLIC-API.md +47 -438
- package/README.md +53 -97
- package/dist/builtin-metadata-OT6V7TB4.js +8 -0
- package/dist/{chapter-title-2JVDU62E.js → chapter-title-2RCXX7SN.js} +1 -2
- package/dist/{chunk-5BSJLT6H.js → chunk-2WIETEL7.js} +16 -29
- package/dist/{chunk-3O7OMMMF.js → chunk-35K6IKB2.js} +3 -3
- 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-AUN4S3YW.js +50 -0
- 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-NM4CXXZY.js +25 -0
- package/dist/{chunk-44WND2VP.js → chunk-RFXHLLYU.js} +34 -62
- package/dist/{chunk-RXTN2CW6.js → chunk-Z3DLSLAJ.js} +1 -1
- package/dist/cinema-media-YZ2UANJG.js +365 -0
- package/dist/cli.js +154 -1748
- package/dist/{compose-video-BH5K6XWT.js → compose-video-RSLI4CVP.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 +1355 -740
- package/dist/scene-validation-SGLLLFY5.js +9 -0
- package/dist/server.d.ts +67 -117
- package/dist/server.js +296 -626
- 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 +80 -106
- 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 +156 -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 +4 -25
- 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/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 "./chunk-4YM2M62S.js";
|
|
10
|
+
} from "./chunk-5DOQTIMD.js";
|
|
5
11
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "./chunk-ZD2VTUYR.js";
|
|
12
|
+
resolveMediaType
|
|
13
|
+
} from "./chunk-K5J7ESRO.js";
|
|
9
14
|
import {
|
|
15
|
+
getSceneDurationBounds,
|
|
16
|
+
getSpokenDuration,
|
|
17
|
+
getVideoDuration,
|
|
18
|
+
resolveVideoTimeline
|
|
19
|
+
} from "./chunk-AUN4S3YW.js";
|
|
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-RFXHLLYU.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-35K6IKB2.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
|
-
});
|
|
83
|
-
}
|
|
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
|
-
]);
|
|
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" };
|
|
92
213
|
}
|
|
93
214
|
|
|
94
|
-
// src/
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"quote": () => import("./quote-GRPIYKQJ.js").then((module) => ({ default: module.QuoteSceneTemplate })),
|
|
105
|
-
"keyFigure": () => import("./key-figure-4TJK7HLT.js").then((module) => ({ default: module.KeyFigureSceneTemplate }))
|
|
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,38 @@ 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 (narrationTime !== void 0 && !audioMovedBackwards && !externallySeeked && !replaced) {
|
|
1142
|
+
raw = Math.max(timeRef.current, raw);
|
|
1143
|
+
}
|
|
1144
|
+
if (cued && !cued.scene.narrationGroup && narrationTime === void 0) {
|
|
1145
|
+
let speaking = false;
|
|
1146
|
+
try {
|
|
1147
|
+
speaking = callbacksRef.current.narrationActive?.(cued.scene) === true;
|
|
1148
|
+
} catch (cause) {
|
|
1149
|
+
failNarration(cause instanceof Error ? cause : new Error("Narration completion failed"), current);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (completionHold?.sceneId !== cued.scene.id) completionHold = void 0;
|
|
1153
|
+
if (raw >= cued.end - CLIP_NARRATION_TAIL_SEC && speaking) {
|
|
1154
|
+
completionHold ??= { sceneId: cued.scene.id, wait: 0, tail: 0 };
|
|
1155
|
+
if (raw >= cued.end) completionHold.wait += elapsed;
|
|
1156
|
+
if (completionHold.wait >= 8) {
|
|
1157
|
+
failNarration(new Error("Narration did not finish within eight seconds of its scene budget"), current);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (raw >= cued.end) {
|
|
1161
|
+
raw = Math.max(cued.start, cued.end - 0.01);
|
|
1162
|
+
completionBlocked = true;
|
|
1163
|
+
}
|
|
1164
|
+
} else if (completionHold && !speaking) {
|
|
1165
|
+
completionHold.tail += elapsed;
|
|
1166
|
+
if (completionHold.tail < CLIP_NARRATION_TAIL_SEC && raw >= cued.end) {
|
|
1167
|
+
raw = Math.max(cued.start, cued.end - 0.01);
|
|
1168
|
+
completionBlocked = true;
|
|
1169
|
+
} else if (completionHold.tail >= CLIP_NARRATION_TAIL_SEC) completionHold = void 0;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
620
1172
|
let nextTime;
|
|
621
1173
|
if (looping && duration2 > 0 && raw >= duration2) {
|
|
622
1174
|
nextTime = raw % duration2;
|
|
@@ -667,7 +1219,8 @@ function usePlaybackClock({
|
|
|
667
1219
|
}
|
|
668
1220
|
const duration = current.config ? getVideoDuration(current.config) : 0;
|
|
669
1221
|
const active = current.config ? resolveVideoTimeline(current.config).find((range) => timeRef.current >= range.start && timeRef.current < range.end) : void 0;
|
|
670
|
-
|
|
1222
|
+
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;
|
|
1223
|
+
reportStall(Boolean(reason), reason);
|
|
671
1224
|
committedTime = timeRef.current;
|
|
672
1225
|
if (!settled || looping || timeRef.current < duration) frame = requestAnimationFrame(tick);
|
|
673
1226
|
else setIsPlaying(false);
|
|
@@ -681,7 +1234,7 @@ function usePlaybackClock({
|
|
|
681
1234
|
}
|
|
682
1235
|
|
|
683
1236
|
// src/player/video-player.tsx
|
|
684
|
-
import { jsx as
|
|
1237
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
685
1238
|
var MINIMUM_GENERATION_INTRO_MS = 3e3;
|
|
686
1239
|
function savedVideoState(video) {
|
|
687
1240
|
return {
|
|
@@ -691,7 +1244,6 @@ function savedVideoState(video) {
|
|
|
691
1244
|
};
|
|
692
1245
|
}
|
|
693
1246
|
function VideoPlayerRuntime({
|
|
694
|
-
kit,
|
|
695
1247
|
stream,
|
|
696
1248
|
video,
|
|
697
1249
|
playbackMode,
|
|
@@ -713,6 +1265,8 @@ function VideoPlayerRuntime({
|
|
|
713
1265
|
onSceneChange,
|
|
714
1266
|
narrationReady,
|
|
715
1267
|
narrationTime,
|
|
1268
|
+
narrationActive,
|
|
1269
|
+
onPlaybackMetric,
|
|
716
1270
|
onFramePresented,
|
|
717
1271
|
onMediaFramePresented,
|
|
718
1272
|
onStallChange,
|
|
@@ -722,10 +1276,10 @@ function VideoPlayerRuntime({
|
|
|
722
1276
|
1,
|
|
723
1277
|
typeof nativeMediaAudio?.volume === "number" && Number.isFinite(nativeMediaAudio.volume) ? nativeMediaAudio.volume : 1
|
|
724
1278
|
));
|
|
725
|
-
const [reducedMotion, setReducedMotion] =
|
|
1279
|
+
const [reducedMotion, setReducedMotion] = useState2(
|
|
726
1280
|
() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
727
1281
|
);
|
|
728
|
-
const [audioUnlocked, setAudioUnlocked] =
|
|
1282
|
+
const [audioUnlocked, setAudioUnlocked] = useState2(false);
|
|
729
1283
|
const { resolvedStartMuted, shouldAutoPlay, autoStartGeneration } = resolvePlaybackPolicy({
|
|
730
1284
|
playbackMode,
|
|
731
1285
|
autoPlay,
|
|
@@ -734,28 +1288,40 @@ function VideoPlayerRuntime({
|
|
|
734
1288
|
reducedMotion,
|
|
735
1289
|
hasStream: Boolean(stream)
|
|
736
1290
|
});
|
|
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
|
|
1291
|
+
const [isPlaying, setIsPlaying] = useState2(() => shouldAutoPlay && !reducedMotion && !autoStartGeneration);
|
|
1292
|
+
const [isMuted, setIsMuted] = useState2(resolvedStartMuted);
|
|
1293
|
+
const [fullscreenMode, setFullscreenMode] = useState2("none");
|
|
1294
|
+
const [state, setState] = useState2(() => video ? savedVideoState(video) : createVideoState());
|
|
1295
|
+
const [currentTime, setCurrentTime] = useState2(0);
|
|
1296
|
+
const savedActiveIndex = video ? resolveVideoTimeline(video).findIndex((range) => currentTime >= range.start && currentTime < range.end) : -1;
|
|
1297
|
+
const savedPreparationIndex = savedActiveIndex >= 0 ? savedActiveIndex : Math.max(0, (video?.scenes.length ?? 1) - 1);
|
|
1298
|
+
useEffect5(() => {
|
|
1299
|
+
if (!video) return;
|
|
1300
|
+
const controller = new AbortController();
|
|
1301
|
+
for (const scene of video.scenes.slice(savedPreparationIndex, savedPreparationIndex + 2)) {
|
|
1302
|
+
preloadBuiltinTemplate(scene.templateId);
|
|
1303
|
+
warmSceneMedia(scene.variables, controller.signal);
|
|
1304
|
+
}
|
|
1305
|
+
return () => controller.abort();
|
|
1306
|
+
}, [video, savedPreparationIndex]);
|
|
1307
|
+
const [activeStream, setActiveStream] = useState2(stream);
|
|
1308
|
+
const [activeSavedVideo, setActiveSavedVideo] = useState2(video);
|
|
1309
|
+
const [replacementPending, setReplacementPending] = useState2(false);
|
|
1310
|
+
const [startRequested, setStartRequested] = useState2(autoStartGeneration);
|
|
1311
|
+
const [introPlaying, setIntroPlaying] = useState2(autoStartGeneration);
|
|
1312
|
+
const [generationIntroComplete, setGenerationIntroComplete] = useState2(() => !playbackMode || !stream);
|
|
1313
|
+
const [observedWidth, setObservedWidth] = useState2(0);
|
|
1314
|
+
const containerRef = useRef4(null);
|
|
1315
|
+
usePlaybackDiagnostics(containerRef, state.config ?? void 0, onPlaybackMetric);
|
|
1316
|
+
const stateRef = useRef4(state);
|
|
1317
|
+
const timeRef = useRef4(currentTime);
|
|
1318
|
+
const audioRef = useRef4(null);
|
|
1319
|
+
const introStartedAtRef = useRef4(autoStartGeneration ? performance.now() : null);
|
|
1320
|
+
const callbacksRef = useRef4({ onComplete, onPlaybackEnd, onError, onSceneChange, narrationReady, narrationTime, narrationActive, onFramePresented, onMediaFramePresented, onStallChange, onStateChange });
|
|
1321
|
+
const loopRef = useRef4(loop);
|
|
1322
|
+
const sceneIndexRef = useRef4(-1);
|
|
1323
|
+
const mediaFrameReportedRef = useRef4(false);
|
|
1324
|
+
const visualReadyRef = useRef4(void 0);
|
|
759
1325
|
const reportVisualReady = useMemo(() => (key, error, actualVideoFrame = false) => {
|
|
760
1326
|
if (error) {
|
|
761
1327
|
setIsPlaying(false);
|
|
@@ -771,7 +1337,7 @@ function VideoPlayerRuntime({
|
|
|
771
1337
|
}
|
|
772
1338
|
}
|
|
773
1339
|
}, []);
|
|
774
|
-
const playbackEndedRef =
|
|
1340
|
+
const playbackEndedRef = useRef4(false);
|
|
775
1341
|
let mediaPlaying = isPlaying;
|
|
776
1342
|
if (sceneIndexRef.current >= 0) {
|
|
777
1343
|
try {
|
|
@@ -782,7 +1348,7 @@ function VideoPlayerRuntime({
|
|
|
782
1348
|
}
|
|
783
1349
|
stateRef.current = state;
|
|
784
1350
|
timeRef.current = currentTime;
|
|
785
|
-
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, narrationReady, narrationTime, onFramePresented, onMediaFramePresented, onStallChange, onStateChange };
|
|
1351
|
+
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, narrationReady, narrationTime, narrationActive, onFramePresented, onMediaFramePresented, onStallChange, onStateChange };
|
|
786
1352
|
loopRef.current = loop;
|
|
787
1353
|
const reportFramePresented = useMemo(() => {
|
|
788
1354
|
let reported = false;
|
|
@@ -827,7 +1393,7 @@ function VideoPlayerRuntime({
|
|
|
827
1393
|
setGenerationIntroComplete(!playbackMode || !stream);
|
|
828
1394
|
introStartedAtRef.current = autoStartReplacement ? performance.now() : null;
|
|
829
1395
|
}
|
|
830
|
-
|
|
1396
|
+
useEffect5(() => {
|
|
831
1397
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
832
1398
|
const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
833
1399
|
const handleChange = (event) => {
|
|
@@ -842,12 +1408,12 @@ function VideoPlayerRuntime({
|
|
|
842
1408
|
preference.addEventListener?.("change", handleChange);
|
|
843
1409
|
return () => preference.removeEventListener?.("change", handleChange);
|
|
844
1410
|
}, []);
|
|
845
|
-
|
|
1411
|
+
useEffect5(() => {
|
|
846
1412
|
import("./control-visibility-NQOWTKOQ.js");
|
|
847
1413
|
}, []);
|
|
848
|
-
|
|
849
|
-
const pausedRef =
|
|
850
|
-
|
|
1414
|
+
useEffect5(() => setIsMuted(resolvedStartMuted), [resolvedStartMuted]);
|
|
1415
|
+
const pausedRef = useRef4(paused);
|
|
1416
|
+
useEffect5(() => {
|
|
851
1417
|
const previous = pausedRef.current;
|
|
852
1418
|
pausedRef.current = paused;
|
|
853
1419
|
if (paused === void 0 || paused === previous) return;
|
|
@@ -863,7 +1429,7 @@ function VideoPlayerRuntime({
|
|
|
863
1429
|
const audio = audioRef.current;
|
|
864
1430
|
if (audio?.paused) void audio.play().catch(Boolean);
|
|
865
1431
|
}, [paused]);
|
|
866
|
-
|
|
1432
|
+
useEffect5(() => {
|
|
867
1433
|
let cancelled = false;
|
|
868
1434
|
let started = false;
|
|
869
1435
|
const reset = video ? savedVideoState(video) : createVideoState();
|
|
@@ -871,13 +1437,7 @@ function VideoPlayerRuntime({
|
|
|
871
1437
|
timeRef.current = 0;
|
|
872
1438
|
setState(reset);
|
|
873
1439
|
setCurrentTime(0);
|
|
874
|
-
if (video)
|
|
875
|
-
for (const scene of video.scenes) {
|
|
876
|
-
preloadBuiltinTemplate(scene.templateId);
|
|
877
|
-
warmSceneMedia(scene.variables);
|
|
878
|
-
}
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
1440
|
+
if (video) return;
|
|
881
1441
|
if (!stream) return;
|
|
882
1442
|
const consume = async () => {
|
|
883
1443
|
try {
|
|
@@ -913,7 +1473,7 @@ function VideoPlayerRuntime({
|
|
|
913
1473
|
}
|
|
914
1474
|
};
|
|
915
1475
|
}, [stream, video]);
|
|
916
|
-
|
|
1476
|
+
useEffect5(() => {
|
|
917
1477
|
if (width != null) return;
|
|
918
1478
|
const container = containerRef.current;
|
|
919
1479
|
if (!container) return;
|
|
@@ -938,12 +1498,12 @@ function VideoPlayerRuntime({
|
|
|
938
1498
|
setCurrentTime,
|
|
939
1499
|
setIsPlaying
|
|
940
1500
|
});
|
|
941
|
-
|
|
1501
|
+
useEffect5(() => {
|
|
942
1502
|
const audio = audioRef.current;
|
|
943
1503
|
if (!audio) return;
|
|
944
1504
|
audio.volume = state.config?.audio?.volume ?? 1;
|
|
945
1505
|
}, [state.config?.audio?.audioUrl, state.config?.audio?.volume]);
|
|
946
|
-
|
|
1506
|
+
useEffect5(() => {
|
|
947
1507
|
const audio = audioRef.current;
|
|
948
1508
|
if (!audio) return;
|
|
949
1509
|
if (isPlaying || introPlaying) {
|
|
@@ -961,7 +1521,7 @@ function VideoPlayerRuntime({
|
|
|
961
1521
|
audio.pause();
|
|
962
1522
|
}
|
|
963
1523
|
}, [introPlaying, isPlaying, state.config?.audio?.audioUrl]);
|
|
964
|
-
|
|
1524
|
+
useEffect5(() => {
|
|
965
1525
|
if (!state.config?.scenes.length) {
|
|
966
1526
|
const terminalWithoutVideo = state.status === "complete" || state.status === "error" || state.status === "aborted";
|
|
967
1527
|
if (terminalWithoutVideo) {
|
|
@@ -1019,7 +1579,7 @@ function VideoPlayerRuntime({
|
|
|
1019
1579
|
const terminal = state.status === "complete" || state.status === "error" || state.status === "aborted";
|
|
1020
1580
|
const playheadAtEnd = terminal && duration > 0 && currentTime >= duration - 1e-3;
|
|
1021
1581
|
const ended = !loop && playheadAtEnd;
|
|
1022
|
-
|
|
1582
|
+
useEffect5(() => {
|
|
1023
1583
|
if (!playheadAtEnd) {
|
|
1024
1584
|
playbackEndedRef.current = false;
|
|
1025
1585
|
return;
|
|
@@ -1032,7 +1592,7 @@ function VideoPlayerRuntime({
|
|
|
1032
1592
|
const firstSceneRange = config ? resolveVideoTimeline(config)[0] : void 0;
|
|
1033
1593
|
const hasSuppliedOpening = firstSceneRange?.scene.id === "supplied-opening";
|
|
1034
1594
|
const showStartPoster = (!generationIntroWaiting || hasSuppliedOpening) && !startRequested && !isPlaying && !ended && currentTime <= 1e-3 && Boolean(config?.scenes.length);
|
|
1035
|
-
const firstSceneHoldProgress = firstSceneRange ?
|
|
1595
|
+
const firstSceneHoldProgress = firstSceneRange ? 0.7 : 0;
|
|
1036
1596
|
const posterTime = firstSceneRange ? firstSceneRange.start + Math.max(0, firstSceneRange.end - firstSceneRange.start) * firstSceneHoldProgress : currentTime;
|
|
1037
1597
|
const startPlayback = () => {
|
|
1038
1598
|
setGenerationIntroComplete(true);
|
|
@@ -1095,7 +1655,7 @@ function VideoPlayerRuntime({
|
|
|
1095
1655
|
const pendingReplacement = replacementPending && state.status === "idle";
|
|
1096
1656
|
const displayedStatus = pendingReplacement ? "streaming" : state.status;
|
|
1097
1657
|
const generationCoverVisible = generationIntroWaiting && !hasSuppliedOpening || displayedStatus === "streaming" && !config?.scenes.length;
|
|
1098
|
-
return /* @__PURE__ */
|
|
1658
|
+
return /* @__PURE__ */ jsxs3(
|
|
1099
1659
|
"div",
|
|
1100
1660
|
{
|
|
1101
1661
|
ref: containerRef,
|
|
@@ -1135,7 +1695,7 @@ function VideoPlayerRuntime({
|
|
|
1135
1695
|
...style
|
|
1136
1696
|
},
|
|
1137
1697
|
children: [
|
|
1138
|
-
/* @__PURE__ */
|
|
1698
|
+
/* @__PURE__ */ jsx4(
|
|
1139
1699
|
GenerationCover,
|
|
1140
1700
|
{
|
|
1141
1701
|
visible: generationCoverVisible,
|
|
@@ -1146,10 +1706,9 @@ function VideoPlayerRuntime({
|
|
|
1146
1706
|
onStart: armPlayback
|
|
1147
1707
|
}
|
|
1148
1708
|
),
|
|
1149
|
-
!generationCoverVisible && config?.scenes.length ? /* @__PURE__ */
|
|
1709
|
+
!generationCoverVisible && config?.scenes.length ? /* @__PURE__ */ jsx4(MountedReadinessContext.Provider, { value: reportVisualReady, children: /* @__PURE__ */ jsx4(
|
|
1150
1710
|
VideoFrame,
|
|
1151
1711
|
{
|
|
1152
|
-
kit,
|
|
1153
1712
|
onFramePresented: !showStartPoster && onFramePresented ? reportFramePresented : void 0,
|
|
1154
1713
|
config: displayConfig,
|
|
1155
1714
|
time: showStartPoster ? posterTime : currentTime,
|
|
@@ -1168,7 +1727,7 @@ function VideoPlayerRuntime({
|
|
|
1168
1727
|
}
|
|
1169
1728
|
}
|
|
1170
1729
|
) }) : null,
|
|
1171
|
-
/* @__PURE__ */
|
|
1730
|
+
/* @__PURE__ */ jsx4(
|
|
1172
1731
|
StartPosterButton,
|
|
1173
1732
|
{
|
|
1174
1733
|
visible: showStartPoster,
|
|
@@ -1177,7 +1736,7 @@ function VideoPlayerRuntime({
|
|
|
1177
1736
|
onStart: startPlayback
|
|
1178
1737
|
}
|
|
1179
1738
|
),
|
|
1180
|
-
config?.audio ? /* @__PURE__ */
|
|
1739
|
+
config?.audio ? /* @__PURE__ */ jsx4(
|
|
1181
1740
|
"audio",
|
|
1182
1741
|
{
|
|
1183
1742
|
ref: audioRef,
|
|
@@ -1190,8 +1749,8 @@ function VideoPlayerRuntime({
|
|
|
1190
1749
|
},
|
|
1191
1750
|
config.audio.audioUrl
|
|
1192
1751
|
) : null,
|
|
1193
|
-
/* @__PURE__ */
|
|
1194
|
-
/* @__PURE__ */
|
|
1752
|
+
/* @__PURE__ */ jsx4(EndedOverlay, { ended: controls && ended, onReplay: togglePlayback }),
|
|
1753
|
+
/* @__PURE__ */ jsx4(
|
|
1195
1754
|
PlayerControls,
|
|
1196
1755
|
{
|
|
1197
1756
|
visible: controls && !generationCoverVisible && !showStartPoster && Boolean(config?.scenes.length),
|
|
@@ -1214,7 +1773,6 @@ function VideoPlayerRuntime({
|
|
|
1214
1773
|
);
|
|
1215
1774
|
}
|
|
1216
1775
|
function VideoPlayer({
|
|
1217
|
-
templates,
|
|
1218
1776
|
stream,
|
|
1219
1777
|
video,
|
|
1220
1778
|
onPlaybackEnd,
|
|
@@ -1223,16 +1781,11 @@ function VideoPlayer({
|
|
|
1223
1781
|
...props
|
|
1224
1782
|
}) {
|
|
1225
1783
|
const savedVideo = useMemo(() => video ? parseVideo(video) : void 0, [video]);
|
|
1226
|
-
const kit = useMemo(
|
|
1227
|
-
() => overlayPlayerTemplateRegistry(BUILTIN_PLAYER_KIT, templates),
|
|
1228
|
-
[templates]
|
|
1229
|
-
);
|
|
1230
1784
|
if (!stream && !savedVideo) return null;
|
|
1231
|
-
return /* @__PURE__ */
|
|
1785
|
+
return /* @__PURE__ */ jsx4(
|
|
1232
1786
|
VideoPlayerRuntime,
|
|
1233
1787
|
{
|
|
1234
1788
|
...props,
|
|
1235
|
-
kit,
|
|
1236
1789
|
stream,
|
|
1237
1790
|
video: savedVideo,
|
|
1238
1791
|
onPlaybackEnd: (state) => {
|
|
@@ -1335,21 +1888,145 @@ function createCaptionVoice(source, now = () => performance.now()) {
|
|
|
1335
1888
|
return { voice, getCaptionProgress: read, reset };
|
|
1336
1889
|
}
|
|
1337
1890
|
|
|
1891
|
+
// src/player/prepare-scene-media.ts
|
|
1892
|
+
async function prepareSceneMedia(variables, signal) {
|
|
1893
|
+
signal.throwIfAborted();
|
|
1894
|
+
const url = typeof variables.mediaUrl === "string" ? variables.mediaUrl.trim() : "";
|
|
1895
|
+
if (!url || variables.mediaType === "gradient") return "graphic";
|
|
1896
|
+
const video = variables.mediaType === "video" || /\.(mp4|webm|mov)(?:[?#]|$)/i.test(url);
|
|
1897
|
+
if (video) return "awaiting-video-frame";
|
|
1898
|
+
await new Promise((resolve, reject) => {
|
|
1899
|
+
const image = new Image();
|
|
1900
|
+
let settled = false;
|
|
1901
|
+
const finish = (error) => {
|
|
1902
|
+
if (settled) return;
|
|
1903
|
+
settled = true;
|
|
1904
|
+
clearTimeout(timer);
|
|
1905
|
+
signal.removeEventListener("abort", abort);
|
|
1906
|
+
image.onload = null;
|
|
1907
|
+
image.onerror = null;
|
|
1908
|
+
if (error) {
|
|
1909
|
+
image.src = "";
|
|
1910
|
+
reject(error);
|
|
1911
|
+
} else resolve();
|
|
1912
|
+
};
|
|
1913
|
+
const abort = () => finish(signal.reason ?? new DOMException("Cancelled", "AbortError"));
|
|
1914
|
+
const timer = setTimeout(() => finish(new DOMException("Media preparation timed out", "TimeoutError")), 8e3);
|
|
1915
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1916
|
+
image.onload = () => {
|
|
1917
|
+
void (image.decode ? image.decode() : Promise.resolve()).then(() => finish(), () => finish(new Error("Could not prepare scene image")));
|
|
1918
|
+
};
|
|
1919
|
+
image.onerror = () => finish(new Error("Could not prepare scene image"));
|
|
1920
|
+
image.src = url;
|
|
1921
|
+
if (image.complete && image.naturalWidth > 0) image.onload(new Event("load"));
|
|
1922
|
+
});
|
|
1923
|
+
return "image";
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1338
1926
|
// src/player/recover-scene-media.ts
|
|
1339
1927
|
function recoverSceneMedia(scene) {
|
|
1340
1928
|
if (scene.templateId === "cinemaMedia") {
|
|
1341
1929
|
const title = scene.variables.fallbackText;
|
|
1342
|
-
|
|
1343
|
-
return { ...scene, templateId: "chapterTitle", variables: { title:
|
|
1930
|
+
const fallback = typeof title === "string" && title.trim() && [...title].length <= 65 ? title.trim() : "Your response continues.";
|
|
1931
|
+
return { ...scene, templateId: "chapterTitle", variables: { title: fallback } };
|
|
1344
1932
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1933
|
+
return void 0;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// src/player/scene-readiness.ts
|
|
1937
|
+
function preparedSceneDuration(scene, spokenSeconds, metadata, clipDurationSec) {
|
|
1938
|
+
const timing = metadata?.timing;
|
|
1939
|
+
const authored = (timing?.revealSeconds ?? 0) + (timing?.holdSeconds ?? 0) + (timing?.exitSeconds ?? 0);
|
|
1940
|
+
const readable = Math.max(getSceneDurationBounds(scene, metadata).readable, authored);
|
|
1941
|
+
const floor = scene.templateId === "cinemaMedia" && clipDurationSec !== void 0 && Number.isFinite(clipDurationSec) && clipDurationSec > 0 ? Math.min(readable, clipDurationSec) : readable;
|
|
1942
|
+
const speech = Number.isFinite(spokenSeconds) && spokenSeconds > 0 ? spokenSeconds + CLIP_NARRATION_TAIL_SEC : scene.narration ? getSpokenDuration(scene.narration) : 0;
|
|
1943
|
+
return Math.max(floor, speech);
|
|
1944
|
+
}
|
|
1945
|
+
function prepareNarratedScene(scene, spokenSeconds) {
|
|
1946
|
+
const requested = scene.timing.fixedDuration;
|
|
1947
|
+
const actual = scene.variables.mediaDurationSec;
|
|
1948
|
+
const clipDurationSec = scene.templateId === "cinemaMedia" ? [actual, requested].find((value) => typeof value === "number" && Number.isFinite(value) && value > 0) : void 0;
|
|
1949
|
+
const recovered = clipDurationSec !== void 0 && spokenSeconds !== void 0 && !speechFitsClip(spokenSeconds, clipDurationSec);
|
|
1950
|
+
const visual = recovered ? recoverSceneMedia(scene) : scene;
|
|
1951
|
+
const duration = preparedSceneDuration(visual, spokenSeconds, getBuiltinSceneDefinition(visual.templateId), clipDurationSec);
|
|
1952
|
+
const { startTime: _start, endTime: _end, beatStart: _beatStart, beatEnd: _beatEnd, ...timing } = visual.timing;
|
|
1953
|
+
return { scene: { ...visual, timing: { ...timing, fixedDuration: duration } }, recovered, clipDurationSec };
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// src/video-chat/scene-preparation.ts
|
|
1957
|
+
function createScenePreparation(options) {
|
|
1958
|
+
const speechLoads = /* @__PURE__ */ new Map();
|
|
1959
|
+
const lanes = [Promise.resolve(), Promise.resolve()];
|
|
1960
|
+
const announced = /* @__PURE__ */ new Set();
|
|
1961
|
+
const announcedMedia = /* @__PURE__ */ new Set();
|
|
1962
|
+
let nextLane = 0;
|
|
1963
|
+
let customVoiceFailed = false;
|
|
1964
|
+
const prepareSpeech = (text, signal = options.signal) => {
|
|
1965
|
+
const cached = speechLoads.get(text);
|
|
1966
|
+
if (cached) return cached;
|
|
1967
|
+
const lane = nextLane++ % lanes.length;
|
|
1968
|
+
const prepared = lanes[lane].then(async () => {
|
|
1969
|
+
signal.throwIfAborted();
|
|
1970
|
+
try {
|
|
1971
|
+
if (customVoiceFailed) throw new Error("Voice preparation did not respond");
|
|
1972
|
+
return options.customVoice ? await withDeadline((child) => options.voice().prepare(text, { signal: child }), 3e3, signal) : await options.voice().prepare(text, { signal });
|
|
1973
|
+
} catch (cause) {
|
|
1974
|
+
if (options.customVoice && !signal.aborted && !options.signal.aborted) {
|
|
1975
|
+
options.onVoiceUnavailable(text);
|
|
1976
|
+
if (cause instanceof DOMException && cause.name === "TimeoutError") customVoiceFailed = true;
|
|
1977
|
+
}
|
|
1978
|
+
throw cause;
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
speechLoads.set(text, prepared);
|
|
1982
|
+
lanes[lane] = prepared.catch(() => void 0);
|
|
1983
|
+
return prepared;
|
|
1984
|
+
};
|
|
1985
|
+
const announce = (value) => {
|
|
1986
|
+
if (options.signal.aborted || !value || typeof value !== "object" || Array.isArray(value)) return;
|
|
1987
|
+
const data = value;
|
|
1988
|
+
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;
|
|
1989
|
+
announced.add(data.sceneId);
|
|
1990
|
+
if (speechLoads.size < 32 || speechLoads.has(data.narration.trim())) void prepareSpeech(data.narration.trim()).catch(() => void 0);
|
|
1991
|
+
const media = sanitizeVideoChatMedia(data.media);
|
|
1992
|
+
if (media && !announcedMedia.has(data.sceneId)) {
|
|
1993
|
+
announcedMedia.add(data.sceneId);
|
|
1994
|
+
warmSceneMedia({ mediaUrl: media.url, mediaType: media.type === "image" ? "photo" : "video", mediaPoster: media.posterUrl }, options.signal);
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
const prepareVisual = (scene) => {
|
|
1998
|
+
warmSceneMedia(scene.variables, options.signal);
|
|
1999
|
+
const prepared = Promise.resolve(preloadBuiltinTemplate(scene.templateId)).then(async () => {
|
|
2000
|
+
try {
|
|
2001
|
+
await prepareSceneMedia(scene.variables, options.signal);
|
|
2002
|
+
return scene;
|
|
2003
|
+
} catch (cause) {
|
|
2004
|
+
if (options.signal.aborted) throw cause;
|
|
2005
|
+
const recovered = recoverSceneMedia(scene);
|
|
2006
|
+
if (!recovered) throw new VideoError("Scene could not prepare its visual", { code: "media_not_ready" });
|
|
2007
|
+
options.warn(MEDIA_RECOVERY_NOTICE);
|
|
2008
|
+
return recovered;
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
void prepared.catch(() => void 0);
|
|
2012
|
+
return prepared;
|
|
2013
|
+
};
|
|
2014
|
+
const pace = (scene, seconds) => {
|
|
2015
|
+
const prepared = prepareNarratedScene(scene, seconds);
|
|
2016
|
+
if (prepared.recovered) options.warn(MEDIA_RECOVERY_NOTICE);
|
|
2017
|
+
if (seconds !== void 0 && prepared.clipDurationSec !== void 0) {
|
|
2018
|
+
try {
|
|
2019
|
+
options.onDuration?.({ speechDurationSec: seconds, clipDurationSec: prepared.clipDurationSec, recovered: prepared.recovered });
|
|
2020
|
+
} catch {
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return prepared.scene;
|
|
2024
|
+
};
|
|
2025
|
+
return { prepareSpeech, announce, prepareVisual, pace };
|
|
1349
2026
|
}
|
|
1350
2027
|
|
|
1351
2028
|
// src/video-chat/use-video-chat.ts
|
|
1352
|
-
import { useCallback as
|
|
2029
|
+
import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo2, useReducer, useRef as useRef6 } from "react";
|
|
1353
2030
|
|
|
1354
2031
|
// src/protocol/scene-timeline.ts
|
|
1355
2032
|
function createQueue() {
|
|
@@ -1457,25 +2134,17 @@ function createSceneTimeline(options) {
|
|
|
1457
2134
|
};
|
|
1458
2135
|
}
|
|
1459
2136
|
|
|
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
2137
|
// src/player/use-narration.ts
|
|
1469
|
-
import { useCallback, useEffect as
|
|
2138
|
+
import { useCallback as useCallback2, useEffect as useEffect6, useRef as useRef5, useState as useState3 } from "react";
|
|
1470
2139
|
function useNarration(options) {
|
|
1471
|
-
const [speaking, setSpeaking] =
|
|
1472
|
-
const optionsRef =
|
|
2140
|
+
const [speaking, setSpeaking] = useState3(false);
|
|
2141
|
+
const optionsRef = useRef5(options);
|
|
1473
2142
|
optionsRef.current = options;
|
|
1474
|
-
const groupRef =
|
|
1475
|
-
const clockRef =
|
|
1476
|
-
const clockSceneRef =
|
|
1477
|
-
const readyRef =
|
|
1478
|
-
const getTime =
|
|
2143
|
+
const groupRef = useRef5(void 0);
|
|
2144
|
+
const clockRef = useRef5(void 0);
|
|
2145
|
+
const clockSceneRef = useRef5(void 0);
|
|
2146
|
+
const readyRef = useRef5(true);
|
|
2147
|
+
const getTime = useCallback2((scene) => {
|
|
1479
2148
|
const group = groupRef.current;
|
|
1480
2149
|
if (optionsRef.current.enabled === false || (group ? scene.narrationGroup?.id !== group.id : scene.id !== clockSceneRef.current)) return void 0;
|
|
1481
2150
|
if (!optionsRef.current.voice.getCurrentTime) return void 0;
|
|
@@ -1488,10 +2157,11 @@ function useNarration(options) {
|
|
|
1488
2157
|
}
|
|
1489
2158
|
return clockRef.current;
|
|
1490
2159
|
}, []);
|
|
1491
|
-
const isReady =
|
|
1492
|
-
const currentRef =
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
2160
|
+
const isReady = useCallback2(() => optionsRef.current.enabled === false || readyRef.current || clockRef.current !== void 0 && clockRef.current >= (groupRef.current?.offsetSeconds ?? 0) + 0.04, []);
|
|
2161
|
+
const currentRef = useRef5(void 0);
|
|
2162
|
+
const isSpeaking = useCallback2((scene) => optionsRef.current.enabled !== false && Boolean(currentRef.current) && (groupRef.current ? scene.narrationGroup?.id === groupRef.current.id : scene.id === clockSceneRef.current), []);
|
|
2163
|
+
const spokenIndexRef = useRef5(void 0);
|
|
2164
|
+
const stop = useCallback2(() => {
|
|
1495
2165
|
readyRef.current = true;
|
|
1496
2166
|
currentRef.current?.abort();
|
|
1497
2167
|
currentRef.current = void 0;
|
|
@@ -1500,12 +2170,12 @@ function useNarration(options) {
|
|
|
1500
2170
|
clockSceneRef.current = void 0;
|
|
1501
2171
|
setSpeaking(false);
|
|
1502
2172
|
}, []);
|
|
1503
|
-
const interrupt =
|
|
2173
|
+
const interrupt = useCallback2(() => {
|
|
1504
2174
|
spokenIndexRef.current = void 0;
|
|
1505
2175
|
stop();
|
|
1506
2176
|
}, [stop]);
|
|
1507
|
-
|
|
1508
|
-
const onSceneChange =
|
|
2177
|
+
useEffect6(() => () => currentRef.current?.abort(), []);
|
|
2178
|
+
const onSceneChange = useCallback2((scene, index) => {
|
|
1509
2179
|
const { voice, enabled = true } = optionsRef.current;
|
|
1510
2180
|
if (!enabled) return;
|
|
1511
2181
|
if (spokenIndexRef.current === index) return;
|
|
@@ -1565,42 +2235,7 @@ function useNarration(options) {
|
|
|
1565
2235
|
}
|
|
1566
2236
|
})();
|
|
1567
2237
|
}, [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";
|
|
2238
|
+
return { onSceneChange, interrupt, speaking, isReady, getTime, isSpeaking };
|
|
1604
2239
|
}
|
|
1605
2240
|
|
|
1606
2241
|
// src/video-chat/voice.ts
|
|
@@ -1614,8 +2249,7 @@ function actionEndpoint(endpoint, action) {
|
|
|
1614
2249
|
return `${value}${value.includes("?") ? "&" : "?"}action=${encodeURIComponent(action)}`;
|
|
1615
2250
|
}
|
|
1616
2251
|
function estimatedBrowserSeconds(text) {
|
|
1617
|
-
|
|
1618
|
-
return Math.max(1, words / 2.5);
|
|
2252
|
+
return Math.max(1, estimateNarrationSeconds(text));
|
|
1619
2253
|
}
|
|
1620
2254
|
async function measureSeconds(bytes) {
|
|
1621
2255
|
try {
|
|
@@ -1950,7 +2584,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
1950
2584
|
}
|
|
1951
2585
|
|
|
1952
2586
|
// src/video-chat/use-video-chat.ts
|
|
1953
|
-
var DEFAULT_TIMEOUT_MS =
|
|
2587
|
+
var DEFAULT_TIMEOUT_MS = 66e4;
|
|
1954
2588
|
function observe(callback) {
|
|
1955
2589
|
try {
|
|
1956
2590
|
void Promise.resolve(callback?.()).catch(() => void 0);
|
|
@@ -2185,29 +2819,14 @@ async function responseError(response) {
|
|
|
2185
2819
|
recoverable: false
|
|
2186
2820
|
});
|
|
2187
2821
|
}
|
|
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
2822
|
function useVideoChat(options = {}) {
|
|
2204
2823
|
return useVideoChatSession(options).chat;
|
|
2205
2824
|
}
|
|
2206
2825
|
function useVideoChatSession(options = {}) {
|
|
2207
|
-
const optionsRef =
|
|
2826
|
+
const optionsRef = useRef6(options);
|
|
2208
2827
|
optionsRef.current = options;
|
|
2209
|
-
const voiceWarningRef =
|
|
2210
|
-
const ownedVoiceRef =
|
|
2828
|
+
const voiceWarningRef = useRef6(() => void 0);
|
|
2829
|
+
const ownedVoiceRef = useRef6(void 0);
|
|
2211
2830
|
if (!options.voice && !ownedVoiceRef.current) {
|
|
2212
2831
|
ownedVoiceRef.current = createVideoChatVoice({
|
|
2213
2832
|
endpoint: options.endpoint,
|
|
@@ -2219,12 +2838,12 @@ function useVideoChatSession(options = {}) {
|
|
|
2219
2838
|
}
|
|
2220
2839
|
const rawVoice = options.voice ?? ownedVoiceRef.current;
|
|
2221
2840
|
const captionVoice = useMemo2(() => createCaptionVoice(rawVoice), [rawVoice]);
|
|
2222
|
-
|
|
2841
|
+
useEffect7(() => () => captionVoice.reset(), [captionVoice]);
|
|
2223
2842
|
const voice = captionVoice.voice;
|
|
2224
|
-
const voiceRef =
|
|
2843
|
+
const voiceRef = useRef6(voice);
|
|
2225
2844
|
voiceRef.current = voice;
|
|
2226
|
-
const unavailableVoiceLines =
|
|
2227
|
-
const speechStartRef =
|
|
2845
|
+
const unavailableVoiceLines = useRef6(/* @__PURE__ */ new Set());
|
|
2846
|
+
const speechStartRef = useRef6(() => void 0);
|
|
2228
2847
|
const narration = useNarration({ onSpeechStart: (source) => speechStartRef.current(source), voice: {
|
|
2229
2848
|
supportsOffsets: voice.supportsOffsets,
|
|
2230
2849
|
...voice.getCurrentTime ? { getCurrentTime: () => voice.getCurrentTime() } : {},
|
|
@@ -2238,24 +2857,24 @@ function useVideoChatSession(options = {}) {
|
|
|
2238
2857
|
}
|
|
2239
2858
|
}
|
|
2240
2859
|
} });
|
|
2241
|
-
const narrationRef =
|
|
2860
|
+
const narrationRef = useRef6(narration);
|
|
2242
2861
|
narrationRef.current = narration;
|
|
2243
2862
|
const [state, dispatch] = useReducer(reducer, options.initialMuted ?? false, initialState);
|
|
2244
|
-
const stateRef =
|
|
2863
|
+
const stateRef = useRef6(state);
|
|
2245
2864
|
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 =
|
|
2865
|
+
const runRef = useRef6(0);
|
|
2866
|
+
const inFlightRef = useRef6(void 0);
|
|
2867
|
+
const suggestionsRef = useRef6(void 0);
|
|
2868
|
+
const openingRef = useRef6(void 0);
|
|
2869
|
+
const timelineRef = useRef6(void 0);
|
|
2870
|
+
const heldRef = useRef6(false);
|
|
2871
|
+
const flushRef = useRef6(void 0);
|
|
2872
|
+
const mountedRef = useRef6(true);
|
|
2873
|
+
const firstFrameRef = useRef6(void 0);
|
|
2874
|
+
const reportMetric = useCallback3((metric) => {
|
|
2256
2875
|
observe(() => optionsRef.current.onPlaybackMetric?.(metric));
|
|
2257
2876
|
}, []);
|
|
2258
|
-
const finishStall =
|
|
2877
|
+
const finishStall = useCallback3(() => {
|
|
2259
2878
|
const timing = firstFrameRef.current;
|
|
2260
2879
|
if (!timing?.active || timing.stallStartedAt == null) return;
|
|
2261
2880
|
const now = monotonicNow();
|
|
@@ -2267,10 +2886,10 @@ function useVideoChatSession(options = {}) {
|
|
|
2267
2886
|
mode: timing.mode,
|
|
2268
2887
|
elapsedMs: Math.max(0, Math.round(now - timing.startedAt)),
|
|
2269
2888
|
durationMs: Math.max(0, Math.round(now - started)),
|
|
2270
|
-
reason: "scene-generation"
|
|
2889
|
+
reason: timing.stallReason ?? "scene-generation"
|
|
2271
2890
|
});
|
|
2272
2891
|
}, [reportMetric]);
|
|
2273
|
-
const endTiming =
|
|
2892
|
+
const endTiming = useCallback3(() => {
|
|
2274
2893
|
finishStall();
|
|
2275
2894
|
if (firstFrameRef.current) firstFrameRef.current.active = false;
|
|
2276
2895
|
}, [finishStall]);
|
|
@@ -2286,7 +2905,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2286
2905
|
source: optionsRef.current.voice ? "custom" : source === "generated" ? "generated" : "browser"
|
|
2287
2906
|
});
|
|
2288
2907
|
};
|
|
2289
|
-
const request =
|
|
2908
|
+
const request = useCallback3(async (action, init = {}, signal) => {
|
|
2290
2909
|
const current = optionsRef.current;
|
|
2291
2910
|
const headers = new Headers(current.headers);
|
|
2292
2911
|
new Headers(init.headers).forEach((header, name) => headers.set(name, header));
|
|
@@ -2300,7 +2919,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2300
2919
|
signal
|
|
2301
2920
|
});
|
|
2302
2921
|
}, []);
|
|
2303
|
-
|
|
2922
|
+
useEffect7(() => {
|
|
2304
2923
|
mountedRef.current = true;
|
|
2305
2924
|
const controller = new AbortController();
|
|
2306
2925
|
void request("capabilities", {}, controller.signal).then(async (response) => {
|
|
@@ -2331,8 +2950,8 @@ function useVideoChatSession(options = {}) {
|
|
|
2331
2950
|
});
|
|
2332
2951
|
};
|
|
2333
2952
|
}, [request, endTiming]);
|
|
2334
|
-
|
|
2335
|
-
const cancel =
|
|
2953
|
+
useEffect7(() => voice.setMuted(state.muted), [state.muted, voice]);
|
|
2954
|
+
const cancel = useCallback3((reason = "Video chat was cancelled") => {
|
|
2336
2955
|
endTiming();
|
|
2337
2956
|
runRef.current += 1;
|
|
2338
2957
|
inFlightRef.current?.abort(new DOMException(reason, "AbortError"));
|
|
@@ -2346,7 +2965,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2346
2965
|
narrationRef.current.interrupt();
|
|
2347
2966
|
dispatch({ type: "cancelled" });
|
|
2348
2967
|
}, [endTiming]);
|
|
2349
|
-
const ask =
|
|
2968
|
+
const ask = useCallback3(async (value, askOptions = {}) => {
|
|
2350
2969
|
const prompt = value.trim();
|
|
2351
2970
|
if (!prompt) return void 0;
|
|
2352
2971
|
endTiming();
|
|
@@ -2456,31 +3075,26 @@ function useVideoChatSession(options = {}) {
|
|
|
2456
3075
|
}
|
|
2457
3076
|
};
|
|
2458
3077
|
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
|
-
};
|
|
3078
|
+
const preparation = createScenePreparation({
|
|
3079
|
+
voice: () => voiceRef.current,
|
|
3080
|
+
customVoice: Boolean(currentOptions.voice),
|
|
3081
|
+
signal: controller.signal,
|
|
3082
|
+
onVoiceUnavailable: (text) => {
|
|
3083
|
+
if (isCurrent()) unavailableVoiceLines.current.add(text);
|
|
3084
|
+
},
|
|
3085
|
+
warn,
|
|
3086
|
+
onDuration: (value2) => {
|
|
3087
|
+
const timing = firstFrameRef.current;
|
|
3088
|
+
if (isCurrent() && timing?.active) reportMetric({
|
|
3089
|
+
type: "scene-duration",
|
|
3090
|
+
...value2,
|
|
3091
|
+
turnId: id,
|
|
3092
|
+
mode,
|
|
3093
|
+
elapsedMs: Math.max(0, Math.round(monotonicNow() - timing.startedAt))
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3097
|
+
const { prepareSpeech } = preparation;
|
|
2484
3098
|
const speakOpening = async (text) => {
|
|
2485
3099
|
try {
|
|
2486
3100
|
if (!isOpeningCurrent()) return;
|
|
@@ -2539,10 +3153,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2539
3153
|
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
3154
|
}
|
|
2541
3155
|
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
|
-
}
|
|
3156
|
+
preparation.announce(event.data);
|
|
2546
3157
|
continue;
|
|
2547
3158
|
}
|
|
2548
3159
|
if (event.type === "data.video-chat-opening") {
|
|
@@ -2572,22 +3183,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2572
3183
|
const plannedScene = event.data.scene;
|
|
2573
3184
|
planned[position] = plannedScene;
|
|
2574
3185
|
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);
|
|
3186
|
+
const visualPreparation = preparation.prepareVisual(plannedScene);
|
|
2591
3187
|
const narrated = narrating.then(async () => {
|
|
2592
3188
|
const supplied = plannedScene.narration?.trim();
|
|
2593
3189
|
if (supplied) return supplied;
|
|
@@ -2623,7 +3219,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2623
3219
|
const withNarration = line ? { ...visual, narration: line } : visual;
|
|
2624
3220
|
const group = plannedScene.narrationGroup;
|
|
2625
3221
|
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 :
|
|
3222
|
+
ready[position] = group ? withNarration : preparation.pace(withNarration, spoken?.seconds);
|
|
2627
3223
|
flush();
|
|
2628
3224
|
}).catch((cause) => {
|
|
2629
3225
|
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
@@ -2664,7 +3260,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2664
3260
|
try {
|
|
2665
3261
|
response = await untilAborted(runAttempt(attempt), controller.signal);
|
|
2666
3262
|
} 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;
|
|
3263
|
+
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
3264
|
attempt += 1;
|
|
2669
3265
|
ready = [];
|
|
2670
3266
|
received = [];
|
|
@@ -2727,19 +3323,19 @@ function useVideoChatSession(options = {}) {
|
|
|
2727
3323
|
if (runRef.current === run) inFlightRef.current = void 0;
|
|
2728
3324
|
}
|
|
2729
3325
|
}, [request, endTiming]);
|
|
2730
|
-
const pause =
|
|
3326
|
+
const pause = useCallback3(() => {
|
|
2731
3327
|
finishStall();
|
|
2732
3328
|
heldRef.current = true;
|
|
2733
3329
|
voiceRef.current.pause();
|
|
2734
3330
|
dispatch({ type: "pause" });
|
|
2735
3331
|
}, [finishStall]);
|
|
2736
|
-
const resume =
|
|
3332
|
+
const resume = useCallback3(() => {
|
|
2737
3333
|
heldRef.current = false;
|
|
2738
3334
|
voiceRef.current.resume();
|
|
2739
3335
|
dispatch({ type: "resume" });
|
|
2740
3336
|
flushRef.current?.();
|
|
2741
3337
|
}, []);
|
|
2742
|
-
const prepareSavedGroups =
|
|
3338
|
+
const prepareSavedGroups = useCallback3(async (video) => {
|
|
2743
3339
|
validateNarrationGroups(video.scenes);
|
|
2744
3340
|
const groups = new Map(video.scenes.flatMap((scene) => scene.narrationGroup ? [[scene.narrationGroup.id, scene.narrationGroup]] : []));
|
|
2745
3341
|
for (const group of groups.values()) {
|
|
@@ -2747,7 +3343,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2747
3343
|
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
3344
|
}
|
|
2749
3345
|
}, []);
|
|
2750
|
-
const replay =
|
|
3346
|
+
const replay = useCallback3(() => {
|
|
2751
3347
|
const turn = stateRef.current.turns.find((entry) => entry.id === stateRef.current.shownTurnId);
|
|
2752
3348
|
if (!turn?.completed || !turn.video) return;
|
|
2753
3349
|
runRef.current += 1;
|
|
@@ -2774,7 +3370,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2774
3370
|
});
|
|
2775
3371
|
} else dispatch({ type: "replay" });
|
|
2776
3372
|
}, [endTiming, prepareSavedGroups]);
|
|
2777
|
-
const selectTurn =
|
|
3373
|
+
const selectTurn = useCallback3((id) => {
|
|
2778
3374
|
const turn = stateRef.current.turns.find((entry) => entry.id === id);
|
|
2779
3375
|
if (!turn?.completed || !turn.video) return;
|
|
2780
3376
|
runRef.current += 1;
|
|
@@ -2801,19 +3397,19 @@ function useVideoChatSession(options = {}) {
|
|
|
2801
3397
|
});
|
|
2802
3398
|
} else dispatch({ type: "select", id });
|
|
2803
3399
|
}, [endTiming, prepareSavedGroups]);
|
|
2804
|
-
const reset =
|
|
3400
|
+
const reset = useCallback3(() => {
|
|
2805
3401
|
cancel("Session reset");
|
|
2806
3402
|
heldRef.current = false;
|
|
2807
3403
|
voiceRef.current.resume();
|
|
2808
3404
|
dispatch({ type: "reset" });
|
|
2809
3405
|
}, [cancel]);
|
|
2810
|
-
const restoreSession =
|
|
3406
|
+
const restoreSession = useCallback3((turns) => {
|
|
2811
3407
|
cancel("Restoring session");
|
|
2812
3408
|
heldRef.current = false;
|
|
2813
3409
|
voiceRef.current.resume();
|
|
2814
3410
|
dispatch({ type: "restore", turns: turns.filter((turn) => turn.completed && turn.video).slice(-100) });
|
|
2815
3411
|
}, [cancel]);
|
|
2816
|
-
const setMuted =
|
|
3412
|
+
const setMuted = useCallback3((muted) => dispatch({ type: "mute", value: muted }), []);
|
|
2817
3413
|
const currentTurn = state.turns.at(-1);
|
|
2818
3414
|
const shownTurn = state.turns.find((turn) => turn.id === state.shownTurnId) ?? currentTurn;
|
|
2819
3415
|
const availableModes = state.capabilities?.modes ?? ["cinematic"];
|
|
@@ -2822,13 +3418,13 @@ function useVideoChatSession(options = {}) {
|
|
|
2822
3418
|
const transcript = shownTurn && shownTurn === currentTurn && state.playback?.kind !== "video" ? fullTranscript.slice(0, (shownTurn.opening ? 1 : 0) + state.spokenUpTo + 1) : fullTranscript;
|
|
2823
3419
|
const playbackKey = state.playerKey;
|
|
2824
3420
|
const playerProps = state.playback ? {
|
|
2825
|
-
...options.templates ? { templates: options.templates } : {},
|
|
2826
3421
|
...state.playback.kind === "stream" ? { stream: state.playback.stream } : { video: state.playback.video },
|
|
2827
3422
|
autoPlay: true,
|
|
2828
3423
|
paused: state.status === "paused",
|
|
2829
3424
|
controls: false,
|
|
2830
3425
|
narrationReady: narration.isReady,
|
|
2831
3426
|
narrationTime: narration.getTime,
|
|
3427
|
+
narrationActive: narration.isSpeaking,
|
|
2832
3428
|
orientation: shownTurn?.fixedOrientation ? shownTurn.orientation : "auto",
|
|
2833
3429
|
onFramePresented: () => {
|
|
2834
3430
|
const timing = firstFrameRef.current;
|
|
@@ -2849,15 +3445,23 @@ function useVideoChatSession(options = {}) {
|
|
|
2849
3445
|
elapsedMs: Math.max(0, Math.round(monotonicNow() - timing.startedAt))
|
|
2850
3446
|
});
|
|
2851
3447
|
},
|
|
2852
|
-
|
|
3448
|
+
onPlaybackMetric: options.onPlaybackMetric ? (metric) => {
|
|
3449
|
+
const timing = firstFrameRef.current;
|
|
3450
|
+
if (stateRef.current.playerKey !== playbackKey || !timing?.active || state.playback?.kind !== "stream") return;
|
|
3451
|
+
reportMetric({ ...metric, turnId: timing.turnId, mode: timing.mode, elapsedMs: Math.max(0, Math.round(monotonicNow() - timing.startedAt)) });
|
|
3452
|
+
} : void 0,
|
|
3453
|
+
onStallChange: (stalled, reason = "scene-generation") => {
|
|
2853
3454
|
if (stateRef.current.playerKey !== playbackKey) return;
|
|
2854
|
-
if (stalled) voiceRef.current.pause();
|
|
3455
|
+
if (stalled && reason !== "speech") voiceRef.current.pause();
|
|
2855
3456
|
else if (!heldRef.current) voiceRef.current.resume();
|
|
2856
3457
|
if (state.playback?.kind !== "stream") return;
|
|
2857
3458
|
const timing = firstFrameRef.current;
|
|
2858
3459
|
if (!timing?.active || !timing.reported) return;
|
|
2859
3460
|
if (!stalled) finishStall();
|
|
2860
|
-
else if (!heldRef.current && timing.stallStartedAt == null)
|
|
3461
|
+
else if (!heldRef.current && timing.stallStartedAt == null) {
|
|
3462
|
+
timing.stallStartedAt = monotonicNow();
|
|
3463
|
+
timing.stallReason = reason;
|
|
3464
|
+
}
|
|
2861
3465
|
},
|
|
2862
3466
|
onSceneChange: (scene, index) => {
|
|
2863
3467
|
if (stateRef.current.playerKey !== playbackKey) return;
|
|
@@ -2907,8 +3511,8 @@ function useVideoChatSession(options = {}) {
|
|
|
2907
3511
|
}
|
|
2908
3512
|
|
|
2909
3513
|
// src/video-chat/caption-pages.tsx
|
|
2910
|
-
import { useEffect as
|
|
2911
|
-
import { jsx as
|
|
3514
|
+
import { useEffect as useEffect8, useLayoutEffect, useRef as useRef7, useState as useState4 } from "react";
|
|
3515
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
2912
3516
|
function splitCaptionPages(text, fits) {
|
|
2913
3517
|
const pages = [];
|
|
2914
3518
|
let rest = Array.from(text);
|
|
@@ -2941,14 +3545,14 @@ function captionPageAt(pages, elapsed, duration) {
|
|
|
2941
3545
|
return Math.max(0, pages.length - 1);
|
|
2942
3546
|
}
|
|
2943
3547
|
function CaptionPages({ text, getProgress }) {
|
|
2944
|
-
const line =
|
|
2945
|
-
const lastProgress =
|
|
2946
|
-
const getter =
|
|
3548
|
+
const line = useRef7(null);
|
|
3549
|
+
const lastProgress = useRef7(void 0);
|
|
3550
|
+
const getter = useRef7(getProgress);
|
|
2947
3551
|
getter.current = getProgress;
|
|
2948
|
-
const [source, setSource] =
|
|
2949
|
-
const [pages, setPages] =
|
|
2950
|
-
const [index, setIndex] =
|
|
2951
|
-
const [timing, setTiming] =
|
|
3552
|
+
const [source, setSource] = useState4(text);
|
|
3553
|
+
const [pages, setPages] = useState4([]);
|
|
3554
|
+
const [index, setIndex] = useState4(0);
|
|
3555
|
+
const [timing, setTiming] = useState4();
|
|
2952
3556
|
useLayoutEffect(() => {
|
|
2953
3557
|
const element = line.current;
|
|
2954
3558
|
if (!element) return;
|
|
@@ -2991,7 +3595,7 @@ function CaptionPages({ text, getProgress }) {
|
|
|
2991
3595
|
document.fonts?.removeEventListener("loadingdone", measure);
|
|
2992
3596
|
};
|
|
2993
3597
|
}, [source]);
|
|
2994
|
-
|
|
3598
|
+
useEffect8(() => {
|
|
2995
3599
|
let frame = 0;
|
|
2996
3600
|
const update = () => {
|
|
2997
3601
|
const observed = text ? getter.current() : void 0;
|
|
@@ -3006,14 +3610,14 @@ function CaptionPages({ text, getProgress }) {
|
|
|
3006
3610
|
update();
|
|
3007
3611
|
return () => cancelAnimationFrame(frame);
|
|
3008
3612
|
}, [text, source, pages]);
|
|
3009
|
-
return /* @__PURE__ */
|
|
3613
|
+
return /* @__PURE__ */ jsx5("p", { ref: line, className: "line", "aria-live": "polite", "data-caption-timing": timing, "data-caption-page": index, children: pages[index] ?? "" });
|
|
3010
3614
|
}
|
|
3011
3615
|
|
|
3012
3616
|
// src/video-chat/video-chat.tsx
|
|
3013
|
-
import { useCallback as
|
|
3617
|
+
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
3618
|
|
|
3015
3619
|
// src/video-chat/icons.tsx
|
|
3016
|
-
import { jsx as
|
|
3620
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3017
3621
|
var stroke = {
|
|
3018
3622
|
fill: "none",
|
|
3019
3623
|
stroke: "currentColor",
|
|
@@ -3022,50 +3626,50 @@ var stroke = {
|
|
|
3022
3626
|
strokeLinejoin: "round"
|
|
3023
3627
|
};
|
|
3024
3628
|
function Glyph({ children }) {
|
|
3025
|
-
return /* @__PURE__ */
|
|
3629
|
+
return /* @__PURE__ */ jsx6("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", ...stroke, children });
|
|
3026
3630
|
}
|
|
3027
|
-
var Sessions = () => /* @__PURE__ */
|
|
3028
|
-
/* @__PURE__ */
|
|
3029
|
-
/* @__PURE__ */
|
|
3631
|
+
var Sessions = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3632
|
+
/* @__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" }),
|
|
3633
|
+
/* @__PURE__ */ jsx6("path", { d: "M17 8h2a2 2 0 0 1 2 2v11l-4-3h-6a2 2 0 0 1-2-2v-2" })
|
|
3030
3634
|
] });
|
|
3031
|
-
var Plus = () => /* @__PURE__ */
|
|
3032
|
-
var Gear = () => /* @__PURE__ */
|
|
3033
|
-
/* @__PURE__ */
|
|
3034
|
-
/* @__PURE__ */
|
|
3635
|
+
var Plus = () => /* @__PURE__ */ jsx6(Glyph, { children: /* @__PURE__ */ jsx6("path", { d: "M12 5v14M5 12h14" }) });
|
|
3636
|
+
var Gear = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3637
|
+
/* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "3" }),
|
|
3638
|
+
/* @__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
3639
|
] });
|
|
3036
|
-
var Sound = () => /* @__PURE__ */
|
|
3037
|
-
/* @__PURE__ */
|
|
3038
|
-
/* @__PURE__ */
|
|
3640
|
+
var Sound = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3641
|
+
/* @__PURE__ */ jsx6("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
|
|
3642
|
+
/* @__PURE__ */ jsx6("path", { d: "M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13" })
|
|
3039
3643
|
] });
|
|
3040
|
-
var Muted = () => /* @__PURE__ */
|
|
3041
|
-
/* @__PURE__ */
|
|
3042
|
-
/* @__PURE__ */
|
|
3644
|
+
var Muted = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3645
|
+
/* @__PURE__ */ jsx6("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
|
|
3646
|
+
/* @__PURE__ */ jsx6("path", { d: "m22 9-6 6M16 9l6 6" })
|
|
3043
3647
|
] });
|
|
3044
|
-
var Mic = () => /* @__PURE__ */
|
|
3045
|
-
/* @__PURE__ */
|
|
3046
|
-
/* @__PURE__ */
|
|
3648
|
+
var Mic = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3649
|
+
/* @__PURE__ */ jsx6("rect", { x: "9", y: "2", width: "6", height: "12", rx: "3" }),
|
|
3650
|
+
/* @__PURE__ */ jsx6("path", { d: "M5 11a7 7 0 0 0 14 0M12 18v4" })
|
|
3047
3651
|
] });
|
|
3048
|
-
var Send = () => /* @__PURE__ */
|
|
3049
|
-
var Stop = () => /* @__PURE__ */
|
|
3050
|
-
/* @__PURE__ */
|
|
3051
|
-
/* @__PURE__ */
|
|
3652
|
+
var Send = () => /* @__PURE__ */ jsx6(Glyph, { children: /* @__PURE__ */ jsx6("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
|
|
3653
|
+
var Stop = () => /* @__PURE__ */ jsxs4("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "currentColor", children: [
|
|
3654
|
+
/* @__PURE__ */ jsx6("rect", { x: "7", y: "5", width: "3.4", height: "14", rx: "1.4" }),
|
|
3655
|
+
/* @__PURE__ */ jsx6("rect", { x: "13.6", y: "5", width: "3.4", height: "14", rx: "1.4" })
|
|
3052
3656
|
] });
|
|
3053
|
-
var Play = () => /* @__PURE__ */
|
|
3054
|
-
var Replay = () => /* @__PURE__ */
|
|
3055
|
-
/* @__PURE__ */
|
|
3056
|
-
/* @__PURE__ */
|
|
3657
|
+
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" }) });
|
|
3658
|
+
var Replay = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3659
|
+
/* @__PURE__ */ jsx6("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }),
|
|
3660
|
+
/* @__PURE__ */ jsx6("path", { d: "M3 3v5h5" })
|
|
3057
3661
|
] });
|
|
3058
|
-
var ChevronUp = () => /* @__PURE__ */
|
|
3059
|
-
var Close = () => /* @__PURE__ */
|
|
3060
|
-
var Warning = () => /* @__PURE__ */
|
|
3061
|
-
/* @__PURE__ */
|
|
3062
|
-
/* @__PURE__ */
|
|
3662
|
+
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" }) });
|
|
3663
|
+
var Close = () => /* @__PURE__ */ jsx6(Glyph, { children: /* @__PURE__ */ jsx6("path", { d: "M6 6l12 12M18 6 6 18" }) });
|
|
3664
|
+
var Warning = () => /* @__PURE__ */ jsxs4(Glyph, { children: [
|
|
3665
|
+
/* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "9" }),
|
|
3666
|
+
/* @__PURE__ */ jsx6("path", { d: "M12 8v5M12 16.2v.1" })
|
|
3063
3667
|
] });
|
|
3064
3668
|
|
|
3065
3669
|
// src/video-chat/use-dismiss.ts
|
|
3066
|
-
import { useEffect as
|
|
3670
|
+
import { useEffect as useEffect9 } from "react";
|
|
3067
3671
|
function useDismiss(open, close, surfaces) {
|
|
3068
|
-
|
|
3672
|
+
useEffect9(() => {
|
|
3069
3673
|
if (!open) return;
|
|
3070
3674
|
const onKeyDown = (event) => {
|
|
3071
3675
|
if (event.key !== "Escape") return;
|
|
@@ -3087,7 +3691,7 @@ function useDismiss(open, close, surfaces) {
|
|
|
3087
3691
|
}, [open, close, surfaces]);
|
|
3088
3692
|
}
|
|
3089
3693
|
function useFocusTrap(active, surface) {
|
|
3090
|
-
|
|
3694
|
+
useEffect9(() => {
|
|
3091
3695
|
if (!active) return;
|
|
3092
3696
|
const returnTo = document.activeElement;
|
|
3093
3697
|
const focusable = () => Array.from(
|
|
@@ -3120,12 +3724,12 @@ function useFocusTrap(active, surface) {
|
|
|
3120
3724
|
}
|
|
3121
3725
|
|
|
3122
3726
|
// src/video-chat/suggestion-cards.tsx
|
|
3123
|
-
import { useEffect as
|
|
3124
|
-
import { Fragment as Fragment2, jsx as
|
|
3727
|
+
import { useEffect as useEffect10, useId, useRef as useRef8, useState as useState5 } from "react";
|
|
3728
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3125
3729
|
function Frame({ media, poster, playing, onReady, onError, revealWhenReady = false }) {
|
|
3126
|
-
const video =
|
|
3127
|
-
const [playingUrl, setPlayingUrl] =
|
|
3128
|
-
|
|
3730
|
+
const video = useRef8(null);
|
|
3731
|
+
const [playingUrl, setPlayingUrl] = useState5(null);
|
|
3732
|
+
useEffect10(() => {
|
|
3129
3733
|
const element = video.current;
|
|
3130
3734
|
if (!element || playing === void 0) return;
|
|
3131
3735
|
if (playing) void element.play().catch(() => void 0);
|
|
@@ -3133,16 +3737,16 @@ function Frame({ media, poster, playing, onReady, onError, revealWhenReady = fal
|
|
|
3133
3737
|
}, [playing]);
|
|
3134
3738
|
if (!media) return null;
|
|
3135
3739
|
if (playing === false && media.type === "video" && media.posterUrl) {
|
|
3136
|
-
return /* @__PURE__ */
|
|
3740
|
+
return /* @__PURE__ */ jsx7("img", { className: "frame-media frame-poster", src: media.posterUrl, alt: "", onLoad: onReady, onError });
|
|
3137
3741
|
}
|
|
3138
3742
|
const ready = () => {
|
|
3139
3743
|
setPlayingUrl(media.url);
|
|
3140
3744
|
onReady?.();
|
|
3141
3745
|
};
|
|
3142
3746
|
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__ */
|
|
3747
|
+
if (media.type === "image") return /* @__PURE__ */ jsx7("img", { className: "frame-media", src: media.url, alt: "", style: appearance, onLoad: ready, onError });
|
|
3748
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
3749
|
+
/* @__PURE__ */ jsx7(
|
|
3146
3750
|
"video",
|
|
3147
3751
|
{
|
|
3148
3752
|
ref: video,
|
|
@@ -3163,15 +3767,15 @@ function Frame({ media, poster, playing, onReady, onError, revealWhenReady = fal
|
|
|
3163
3767
|
"aria-hidden": "true"
|
|
3164
3768
|
}
|
|
3165
3769
|
),
|
|
3166
|
-
poster && media.posterUrl && playingUrl !== media.url && /* @__PURE__ */
|
|
3770
|
+
poster && media.posterUrl && playingUrl !== media.url && /* @__PURE__ */ jsx7("img", { className: "frame-media frame-poster", src: media.posterUrl, alt: "", onLoad: onReady })
|
|
3167
3771
|
] });
|
|
3168
3772
|
}
|
|
3169
3773
|
function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
3170
|
-
const railRef =
|
|
3774
|
+
const railRef = useRef8(null);
|
|
3171
3775
|
const railId = useId();
|
|
3172
|
-
const [at, setAt] =
|
|
3173
|
-
const [overflow, setOverflow] =
|
|
3174
|
-
|
|
3776
|
+
const [at, setAt] = useState5(0);
|
|
3777
|
+
const [overflow, setOverflow] = useState5({ before: false, after: false });
|
|
3778
|
+
useEffect10(() => {
|
|
3175
3779
|
const rail = railRef.current;
|
|
3176
3780
|
if (!browse || !rail) return;
|
|
3177
3781
|
const update = () => setOverflow({ before: rail.scrollLeft > 2, after: rail.scrollLeft + rail.clientWidth < rail.scrollWidth - 2 });
|
|
@@ -3190,8 +3794,8 @@ function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
|
3190
3794
|
rail.scrollBy({ left: direction * rail.clientWidth, behavior: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
|
|
3191
3795
|
};
|
|
3192
3796
|
if (suggestions.length === 0) return null;
|
|
3193
|
-
const cards = /* @__PURE__ */
|
|
3194
|
-
/* @__PURE__ */
|
|
3797
|
+
const cards = /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
3798
|
+
/* @__PURE__ */ jsx7("ul", { className: "cards", id: railId, ref: railRef, "aria-label": label, children: suggestions.map((card, index) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsxs5(
|
|
3195
3799
|
"button",
|
|
3196
3800
|
{
|
|
3197
3801
|
type: "button",
|
|
@@ -3200,13 +3804,13 @@ function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
|
3200
3804
|
onPointerEnter: () => setAt(index),
|
|
3201
3805
|
onClick: () => onAsk(card),
|
|
3202
3806
|
children: [
|
|
3203
|
-
/* @__PURE__ */
|
|
3204
|
-
/* @__PURE__ */
|
|
3205
|
-
/* @__PURE__ */
|
|
3807
|
+
/* @__PURE__ */ jsx7(Frame, { media: card.media, poster: true, playing: index === at }),
|
|
3808
|
+
/* @__PURE__ */ jsx7("span", { className: "card-wash", "aria-hidden": "true" }),
|
|
3809
|
+
/* @__PURE__ */ jsx7("span", { className: "card-prompt", children: card.prompt })
|
|
3206
3810
|
]
|
|
3207
3811
|
}
|
|
3208
3812
|
) }, card.prompt)) }),
|
|
3209
|
-
suggestions.length > 1 && /* @__PURE__ */
|
|
3813
|
+
suggestions.length > 1 && /* @__PURE__ */ jsx7("div", { className: "card-dots", role: "group", "aria-label": `${label} navigation`, children: suggestions.map((card, index) => /* @__PURE__ */ jsx7(
|
|
3210
3814
|
"button",
|
|
3211
3815
|
{
|
|
3212
3816
|
type: "button",
|
|
@@ -3223,39 +3827,39 @@ function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
|
3223
3827
|
card.prompt
|
|
3224
3828
|
)) })
|
|
3225
3829
|
] });
|
|
3226
|
-
return browse ? /* @__PURE__ */
|
|
3830
|
+
return browse ? /* @__PURE__ */ jsxs5("div", { className: "suggestion-rail", children: [
|
|
3227
3831
|
cards,
|
|
3228
|
-
(overflow.before || overflow.after) && /* @__PURE__ */
|
|
3229
|
-
/* @__PURE__ */
|
|
3230
|
-
/* @__PURE__ */
|
|
3832
|
+
(overflow.before || overflow.after) && /* @__PURE__ */ jsxs5("div", { className: "rail-arrows", children: [
|
|
3833
|
+
/* @__PURE__ */ jsx7("button", { type: "button", "aria-label": "Previous suggestions", "aria-controls": railId, disabled: !overflow.before, onClick: () => move(-1), children: "\u2039" }),
|
|
3834
|
+
/* @__PURE__ */ jsx7("button", { type: "button", "aria-label": "Next suggestions", "aria-controls": railId, disabled: !overflow.after, onClick: () => move(1), children: "\u203A" })
|
|
3231
3835
|
] })
|
|
3232
3836
|
] }) : cards;
|
|
3233
3837
|
}
|
|
3234
3838
|
|
|
3235
3839
|
// src/video-chat/welcome.tsx
|
|
3236
|
-
import { Fragment as Fragment3, jsx as
|
|
3840
|
+
import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3237
3841
|
function Welcome({ data, onAsk, title }) {
|
|
3238
|
-
return /* @__PURE__ */
|
|
3239
|
-
/* @__PURE__ */
|
|
3240
|
-
/* @__PURE__ */
|
|
3241
|
-
/* @__PURE__ */
|
|
3242
|
-
/* @__PURE__ */
|
|
3842
|
+
return /* @__PURE__ */ jsxs6("div", { className: "welcome", children: [
|
|
3843
|
+
/* @__PURE__ */ jsx8(Frame, { media: data?.hero ?? null, poster: true }),
|
|
3844
|
+
/* @__PURE__ */ jsx8("div", { className: "welcome-wash", "aria-hidden": "true" }),
|
|
3845
|
+
/* @__PURE__ */ jsxs6("div", { className: "welcome-body", children: [
|
|
3846
|
+
/* @__PURE__ */ jsx8("h1", { className: "welcome-title", children: title ?? /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
3243
3847
|
"An AI chat that responds",
|
|
3244
|
-
/* @__PURE__ */
|
|
3245
|
-
/* @__PURE__ */
|
|
3848
|
+
/* @__PURE__ */ jsx8("br", {}),
|
|
3849
|
+
/* @__PURE__ */ jsx8("em", { children: "in video, not text." })
|
|
3246
3850
|
] }) }),
|
|
3247
|
-
/* @__PURE__ */
|
|
3851
|
+
/* @__PURE__ */ jsx8(SuggestionCards, { browse: true, suggestions: data?.cards ?? [], label: "Suggested prompts", onAsk })
|
|
3248
3852
|
] })
|
|
3249
3853
|
] });
|
|
3250
3854
|
}
|
|
3251
3855
|
|
|
3252
3856
|
// src/video-chat/opening-chapter.tsx
|
|
3253
|
-
import { useLayoutEffect as useLayoutEffect2, useRef as
|
|
3254
|
-
import { jsx as
|
|
3857
|
+
import { useLayoutEffect as useLayoutEffect2, useRef as useRef9, useState as useState6 } from "react";
|
|
3858
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3255
3859
|
function OpeningChapter({ title, preparing = false }) {
|
|
3256
|
-
const root =
|
|
3257
|
-
const [size, setSize] =
|
|
3258
|
-
const [titleBottom, setTitleBottom] =
|
|
3860
|
+
const root = useRef9(null);
|
|
3861
|
+
const [size, setSize] = useState6({ width: 1080, height: 1080 });
|
|
3862
|
+
const [titleBottom, setTitleBottom] = useState6(0);
|
|
3259
3863
|
useLayoutEffect2(() => {
|
|
3260
3864
|
const element = root.current;
|
|
3261
3865
|
if (!element) return;
|
|
@@ -3273,28 +3877,17 @@ function OpeningChapter({ title, preparing = false }) {
|
|
|
3273
3877
|
if (title2) observer.observe(title2);
|
|
3274
3878
|
return () => observer.disconnect();
|
|
3275
3879
|
}, [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" })
|
|
3880
|
+
return /* @__PURE__ */ jsxs7("div", { ref: root, "data-opening-chapter": true, className: "opening-chapter", children: [
|
|
3881
|
+
/* @__PURE__ */ jsx9(TitleSceneTemplate, { variables: { title }, width: size.width, height: size.height, progress: 0.3 }),
|
|
3882
|
+
preparing && /* @__PURE__ */ jsxs7("div", { className: "video-preparation", style: { top: titleBottom + 24 }, role: "status", "aria-label": "Video preparation", children: [
|
|
3883
|
+
/* @__PURE__ */ jsx9("span", { className: "video-preparation-spinner", "aria-hidden": "true" }),
|
|
3884
|
+
/* @__PURE__ */ jsx9("span", { className: "video-preparation-text", children: "Preparing your video\u2026" })
|
|
3292
3885
|
] })
|
|
3293
3886
|
] });
|
|
3294
3887
|
}
|
|
3295
3888
|
|
|
3296
3889
|
// src/video-chat/use-voice-input.ts
|
|
3297
|
-
import { useCallback as
|
|
3890
|
+
import { useCallback as useCallback4, useEffect as useEffect11, useRef as useRef10, useState as useState7 } from "react";
|
|
3298
3891
|
function recognitionConstructor() {
|
|
3299
3892
|
if (typeof window === "undefined") return void 0;
|
|
3300
3893
|
const holder = window;
|
|
@@ -3364,19 +3957,19 @@ async function recordAndTranscribe(signal, ready, captured, options) {
|
|
|
3364
3957
|
return clip ? transcribeRecording(clip, signal, options) : "";
|
|
3365
3958
|
}
|
|
3366
3959
|
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 =
|
|
3960
|
+
const [supported, setSupported] = useState7(false);
|
|
3961
|
+
const [listening, setListening] = useState7(false);
|
|
3962
|
+
const [error, setError] = useState7();
|
|
3963
|
+
const [thinking, setThinking] = useState7(false);
|
|
3964
|
+
const recognitionRef = useRef10(void 0);
|
|
3965
|
+
const useRecorderRef = useRef10(false);
|
|
3966
|
+
const operationRef = useRef10(void 0);
|
|
3967
|
+
const handlerRef = useRef10(onTranscript);
|
|
3375
3968
|
handlerRef.current = onTranscript;
|
|
3376
|
-
|
|
3969
|
+
useEffect11(() => {
|
|
3377
3970
|
setSupported(supportsVoiceInput(transcriptionAvailable));
|
|
3378
3971
|
}, [transcriptionAvailable]);
|
|
3379
|
-
const stop =
|
|
3972
|
+
const stop = useCallback4(() => {
|
|
3380
3973
|
recognitionRef.current?.abort();
|
|
3381
3974
|
recognitionRef.current = void 0;
|
|
3382
3975
|
const operation = operationRef.current;
|
|
@@ -3386,7 +3979,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3386
3979
|
setListening(false);
|
|
3387
3980
|
setThinking(false);
|
|
3388
3981
|
}, []);
|
|
3389
|
-
|
|
3982
|
+
useEffect11(() => () => {
|
|
3390
3983
|
recognitionRef.current?.abort();
|
|
3391
3984
|
recognitionRef.current = void 0;
|
|
3392
3985
|
const operation = operationRef.current;
|
|
@@ -3394,7 +3987,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3394
3987
|
operation?.controller.abort();
|
|
3395
3988
|
operation?.finish?.();
|
|
3396
3989
|
}, []);
|
|
3397
|
-
const finish =
|
|
3990
|
+
const finish = useCallback4(() => {
|
|
3398
3991
|
const recognition = recognitionRef.current;
|
|
3399
3992
|
if (recognition) {
|
|
3400
3993
|
recognition.stop();
|
|
@@ -3408,7 +4001,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3408
4001
|
}
|
|
3409
4002
|
stop();
|
|
3410
4003
|
}, [stop]);
|
|
3411
|
-
const record =
|
|
4004
|
+
const record = useCallback4(async () => {
|
|
3412
4005
|
if (!transcriptionAvailable || !recorderAvailable()) {
|
|
3413
4006
|
setError("Server transcription is not configured.");
|
|
3414
4007
|
return;
|
|
@@ -3446,7 +4039,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3446
4039
|
}
|
|
3447
4040
|
}
|
|
3448
4041
|
}, [transcriptionAvailable, options.endpoint, options.headers, options.credentials, options.fetcher]);
|
|
3449
|
-
const toggle =
|
|
4042
|
+
const toggle = useCallback4(() => {
|
|
3450
4043
|
if (listening) {
|
|
3451
4044
|
finish();
|
|
3452
4045
|
return;
|
|
@@ -3498,18 +4091,18 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3498
4091
|
}
|
|
3499
4092
|
|
|
3500
4093
|
// src/video-chat/use-immersive-controls.ts
|
|
3501
|
-
import { useCallback as
|
|
4094
|
+
import { useCallback as useCallback5, useEffect as useEffect12, useRef as useRef11, useState as useState8 } from "react";
|
|
3502
4095
|
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 =
|
|
4096
|
+
const [visible, setVisible] = useState8(true);
|
|
4097
|
+
const hovered = useRef11(false);
|
|
4098
|
+
const focused = useRef11(false);
|
|
4099
|
+
const previousCaptions = useRef11(false);
|
|
4100
|
+
const timer = useRef11(null);
|
|
4101
|
+
const clearTimer = useCallback5(() => {
|
|
3509
4102
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
3510
4103
|
timer.current = null;
|
|
3511
4104
|
}, []);
|
|
3512
|
-
const reveal =
|
|
4105
|
+
const reveal = useCallback5(() => {
|
|
3513
4106
|
clearTimer();
|
|
3514
4107
|
setVisible(true);
|
|
3515
4108
|
if (playing && !pinned && !hovered.current && !focused.current) {
|
|
@@ -3519,7 +4112,7 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3519
4112
|
}, 2e3);
|
|
3520
4113
|
}
|
|
3521
4114
|
}, [clearTimer, playing, pinned]);
|
|
3522
|
-
|
|
4115
|
+
useEffect12(() => {
|
|
3523
4116
|
const firstCaption = hasCaptions && !previousCaptions.current;
|
|
3524
4117
|
previousCaptions.current = hasCaptions;
|
|
3525
4118
|
if (firstCaption && playing && !pinned && !focused.current) {
|
|
@@ -3531,21 +4124,21 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3531
4124
|
}
|
|
3532
4125
|
return clearTimer;
|
|
3533
4126
|
}, [hasCaptions, playing, pinned, reveal, clearTimer]);
|
|
3534
|
-
const onPointerEnter =
|
|
4127
|
+
const onPointerEnter = useCallback5((event) => {
|
|
3535
4128
|
if (event?.pointerType === "touch") return;
|
|
3536
4129
|
hovered.current = true;
|
|
3537
4130
|
reveal();
|
|
3538
4131
|
}, [reveal]);
|
|
3539
|
-
const onPointerLeave =
|
|
4132
|
+
const onPointerLeave = useCallback5(() => {
|
|
3540
4133
|
if (!hovered.current) return;
|
|
3541
4134
|
hovered.current = false;
|
|
3542
4135
|
reveal();
|
|
3543
4136
|
}, [reveal]);
|
|
3544
|
-
const onFocusCapture =
|
|
4137
|
+
const onFocusCapture = useCallback5((event) => {
|
|
3545
4138
|
focused.current = event.target.matches(":focus-visible");
|
|
3546
4139
|
reveal();
|
|
3547
4140
|
}, [reveal]);
|
|
3548
|
-
const onBlurCapture =
|
|
4141
|
+
const onBlurCapture = useCallback5((event) => {
|
|
3549
4142
|
if (focused.current && !event.currentTarget.contains(event.relatedTarget)) {
|
|
3550
4143
|
focused.current = false;
|
|
3551
4144
|
reveal();
|
|
@@ -3555,9 +4148,9 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3555
4148
|
}
|
|
3556
4149
|
|
|
3557
4150
|
// src/video-chat/logo.tsx
|
|
3558
|
-
import { jsx as
|
|
4151
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
3559
4152
|
function Logo() {
|
|
3560
|
-
return /* @__PURE__ */
|
|
4153
|
+
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
4154
|
}
|
|
3562
4155
|
|
|
3563
4156
|
// src/video-chat/modes.ts
|
|
@@ -3568,11 +4161,11 @@ var visualModes = [
|
|
|
3568
4161
|
var defaultMode = visualModes[0];
|
|
3569
4162
|
|
|
3570
4163
|
// src/video-chat/video-chat.tsx
|
|
3571
|
-
import { Fragment as Fragment4, jsx as
|
|
4164
|
+
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3572
4165
|
var DESKTOP_WIDTH = 900;
|
|
3573
4166
|
function useViewportOrientation() {
|
|
3574
|
-
const [portrait, setPortrait] =
|
|
3575
|
-
|
|
4167
|
+
const [portrait, setPortrait] = useState9(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`).matches : false);
|
|
4168
|
+
useEffect13(() => {
|
|
3576
4169
|
if (typeof window.matchMedia !== "function") return;
|
|
3577
4170
|
const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px) and (orientation: portrait)`);
|
|
3578
4171
|
const update = () => setPortrait(query.matches);
|
|
@@ -3583,23 +4176,25 @@ function useViewportOrientation() {
|
|
|
3583
4176
|
return portrait ? "portrait" : "landscape";
|
|
3584
4177
|
}
|
|
3585
4178
|
function Waveform({ active, listening }) {
|
|
3586
|
-
return /* @__PURE__ */
|
|
4179
|
+
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
4180
|
}
|
|
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
|
|
4181
|
+
function VideoChat({ options = {}, className, welcomeTitle, branding, showRecoveryNotice = false }) {
|
|
4182
|
+
const appName = branding?.name.trim() || "VanillaSky";
|
|
4183
|
+
const customHome = safeHomeUrl(branding?.homeUrl);
|
|
4184
|
+
const [dismissedNoticeTurn, setDismissedNoticeTurn] = useState9();
|
|
4185
|
+
const [draft, setDraft] = useState9("");
|
|
4186
|
+
const [selectedMode, setSelectedMode] = useState9();
|
|
4187
|
+
const [savedSessions, setSavedSessions] = useState9([]);
|
|
4188
|
+
const [historyOpen, setHistoryOpen] = useState9(false);
|
|
4189
|
+
const [settingsOpen, setSettingsOpen] = useState9(false);
|
|
4190
|
+
const [aboutOpen, setAboutOpen] = useState9(false);
|
|
4191
|
+
const [captionsOn, setCaptionsOn] = useState9(true);
|
|
4192
|
+
const [captionsExpanded, setCaptionsExpanded] = useState9(false);
|
|
4193
|
+
const [alwaysShowControls, setAlwaysShowControls] = useState9(false);
|
|
4194
|
+
const [editing, setEditing] = useState9(false);
|
|
4195
|
+
const resumeAfterInput = useRef12(false);
|
|
4196
|
+
const panelRef = useRef12(null);
|
|
4197
|
+
const composerRef = useRef12(null);
|
|
3603
4198
|
const viewportOrientation = useViewportOrientation();
|
|
3604
4199
|
const sessionOrientation = options.orientation ?? viewportOrientation;
|
|
3605
4200
|
const { chat, restoreSession, getCaptionProgress } = useVideoChatSession({
|
|
@@ -3607,8 +4202,8 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3607
4202
|
orientation: sessionOrientation,
|
|
3608
4203
|
mode: selectedMode ?? options.mode
|
|
3609
4204
|
});
|
|
3610
|
-
const observedMode =
|
|
3611
|
-
|
|
4205
|
+
const observedMode = useRef12(void 0);
|
|
4206
|
+
useEffect13(() => {
|
|
3612
4207
|
const turn = chat.currentTurn;
|
|
3613
4208
|
if (!turn?.mode) {
|
|
3614
4209
|
observedMode.current = void 0;
|
|
@@ -3623,11 +4218,11 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3623
4218
|
const historyId = `${instanceId}-history`;
|
|
3624
4219
|
const settingsId = `${instanceId}-settings`;
|
|
3625
4220
|
const promptId = `${instanceId}-prompt`;
|
|
3626
|
-
const inputRef =
|
|
3627
|
-
const historyRef =
|
|
3628
|
-
const historyButtonRef =
|
|
3629
|
-
const settingsRef =
|
|
3630
|
-
const settingsButtonRef =
|
|
4221
|
+
const inputRef = useRef12(null);
|
|
4222
|
+
const historyRef = useRef12(null);
|
|
4223
|
+
const historyButtonRef = useRef12(null);
|
|
4224
|
+
const settingsRef = useRef12(null);
|
|
4225
|
+
const settingsButtonRef = useRef12(null);
|
|
3631
4226
|
const listen = useVoiceInput(setDraft, chat.capabilities?.transcription ?? false, {
|
|
3632
4227
|
endpoint: options.endpoint,
|
|
3633
4228
|
headers: options.headers,
|
|
@@ -3636,13 +4231,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3636
4231
|
});
|
|
3637
4232
|
const historySurfaces = useMemo3(() => [historyRef, historyButtonRef], []);
|
|
3638
4233
|
const settingsSurfaces = useMemo3(() => [settingsRef, settingsButtonRef], []);
|
|
3639
|
-
const closeHistory =
|
|
3640
|
-
const closeSettings =
|
|
4234
|
+
const closeHistory = useCallback6(() => setHistoryOpen(false), []);
|
|
4235
|
+
const closeSettings = useCallback6(() => setSettingsOpen(false), []);
|
|
3641
4236
|
useDismiss(historyOpen, closeHistory, historySurfaces);
|
|
3642
4237
|
useDismiss(settingsOpen, closeSettings, settingsSurfaces);
|
|
3643
4238
|
useFocusTrap(settingsOpen, settingsRef);
|
|
3644
4239
|
useFocusTrap(historyOpen, historyRef);
|
|
3645
|
-
const ask =
|
|
4240
|
+
const ask = useCallback6((value) => {
|
|
3646
4241
|
const prompt = (typeof value === "string" ? value : value.prompt).trim();
|
|
3647
4242
|
if (!prompt) return;
|
|
3648
4243
|
setDraft("");
|
|
@@ -3657,7 +4252,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3657
4252
|
opening: value.opening
|
|
3658
4253
|
});
|
|
3659
4254
|
}, [chat, listen]);
|
|
3660
|
-
const newSession =
|
|
4255
|
+
const newSession = useCallback6(() => {
|
|
3661
4256
|
if (chat.turns.length === 0) {
|
|
3662
4257
|
setHistoryOpen(false);
|
|
3663
4258
|
setSettingsOpen(false);
|
|
@@ -3679,13 +4274,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3679
4274
|
const shown = chat.shownTurn;
|
|
3680
4275
|
const showing = chat.playerProps != null;
|
|
3681
4276
|
const handoffKey = `${shown?.id ?? ""}:${chat.playerKey}`;
|
|
3682
|
-
const [presentedBody, setPresentedBody] =
|
|
3683
|
-
const handoff =
|
|
4277
|
+
const [presentedBody, setPresentedBody] = useState9();
|
|
4278
|
+
const handoff = useRef12({ key: handoffKey, live: showing, active: false, frame: 0 });
|
|
3684
4279
|
const handoffStopped = chat.status === "error" || chat.status === "cancelled";
|
|
3685
4280
|
const waitingForBody = showing && presentedBody !== handoffKey;
|
|
3686
4281
|
const openingChapter = Boolean(shown?.prompt) && (!showing || waitingForBody) && chat.status !== "error" && chat.status !== "cancelled" && chat.status !== "ended";
|
|
3687
|
-
const [preparingKey, setPreparingKey] =
|
|
3688
|
-
|
|
4282
|
+
const [preparingKey, setPreparingKey] = useState9();
|
|
4283
|
+
useEffect13(() => {
|
|
3689
4284
|
setPreparingKey(void 0);
|
|
3690
4285
|
if (!openingChapter || !shown?.opening || chat.speaking) return;
|
|
3691
4286
|
const timer = setTimeout(() => setPreparingKey(shown.id), 1e3);
|
|
@@ -3724,13 +4319,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3724
4319
|
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
4320
|
const shownOrientation = shown?.orientation ?? sessionOrientation;
|
|
3726
4321
|
const stageOrientation = shown?.fixedOrientation ? shownOrientation : sessionOrientation;
|
|
3727
|
-
|
|
4322
|
+
useEffect13(() => {
|
|
3728
4323
|
setCaptionsExpanded(false);
|
|
3729
4324
|
}, [chat.playbackEnded, shown?.id, chat.playerKey]);
|
|
3730
4325
|
const line = chat.caption ?? "";
|
|
3731
4326
|
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 =
|
|
4327
|
+
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;
|
|
4328
|
+
const cancelInput = useCallback6(() => {
|
|
3734
4329
|
listen.stop();
|
|
3735
4330
|
setDraft("");
|
|
3736
4331
|
setEditing(false);
|
|
@@ -3770,7 +4365,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3770
4365
|
observer.observe(composer);
|
|
3771
4366
|
return () => observer.disconnect();
|
|
3772
4367
|
}, []);
|
|
3773
|
-
return /* @__PURE__ */
|
|
4368
|
+
return /* @__PURE__ */ jsxs8(
|
|
3774
4369
|
"div",
|
|
3775
4370
|
{
|
|
3776
4371
|
className: `vanillasky-video-chat${className ? ` ${className}` : ""}`,
|
|
@@ -3782,14 +4377,24 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3782
4377
|
onPointerDown: controls.reveal,
|
|
3783
4378
|
onKeyDownCapture: controls.reveal,
|
|
3784
4379
|
children: [
|
|
3785
|
-
/* @__PURE__ */
|
|
3786
|
-
/* @__PURE__ */
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
4380
|
+
/* @__PURE__ */ jsxs8("header", { className: "chrome", ...controlEvents, children: [
|
|
4381
|
+
/* @__PURE__ */ jsx11("div", { className: "session-brand", children: /* @__PURE__ */ jsx11(
|
|
4382
|
+
"a",
|
|
4383
|
+
{
|
|
4384
|
+
className: "home-link",
|
|
4385
|
+
href: customHome ?? "/",
|
|
4386
|
+
"aria-label": branding ? `${appName} home` : "Home",
|
|
4387
|
+
style: branding ? { color: "inherit", textDecoration: "none" } : void 0,
|
|
4388
|
+
onClick: (event) => {
|
|
4389
|
+
if (customHome || window.location.pathname !== "/" || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
4390
|
+
event.preventDefault();
|
|
4391
|
+
newSession();
|
|
4392
|
+
},
|
|
4393
|
+
children: branding ? branding.logo ?? appName : /* @__PURE__ */ jsx11(Logo, {})
|
|
4394
|
+
}
|
|
4395
|
+
) }),
|
|
4396
|
+
/* @__PURE__ */ jsxs8("div", { className: "group", children: [
|
|
4397
|
+
/* @__PURE__ */ jsxs8(
|
|
3793
4398
|
"button",
|
|
3794
4399
|
{
|
|
3795
4400
|
ref: historyButtonRef,
|
|
@@ -3804,12 +4409,12 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3804
4409
|
setSettingsOpen(false);
|
|
3805
4410
|
},
|
|
3806
4411
|
children: [
|
|
3807
|
-
/* @__PURE__ */
|
|
3808
|
-
/* @__PURE__ */
|
|
4412
|
+
/* @__PURE__ */ jsx11(Sessions, {}),
|
|
4413
|
+
/* @__PURE__ */ jsx11("span", { className: "nav-label", children: "Sessions" })
|
|
3809
4414
|
]
|
|
3810
4415
|
}
|
|
3811
4416
|
),
|
|
3812
|
-
/* @__PURE__ */
|
|
4417
|
+
/* @__PURE__ */ jsx11(
|
|
3813
4418
|
"button",
|
|
3814
4419
|
{
|
|
3815
4420
|
ref: settingsButtonRef,
|
|
@@ -3823,10 +4428,10 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3823
4428
|
setSettingsOpen((open) => !open);
|
|
3824
4429
|
setHistoryOpen(false);
|
|
3825
4430
|
},
|
|
3826
|
-
children: /* @__PURE__ */
|
|
4431
|
+
children: /* @__PURE__ */ jsx11(Gear, {})
|
|
3827
4432
|
}
|
|
3828
4433
|
),
|
|
3829
|
-
/* @__PURE__ */
|
|
4434
|
+
/* @__PURE__ */ jsx11(
|
|
3830
4435
|
"button",
|
|
3831
4436
|
{
|
|
3832
4437
|
type: "button",
|
|
@@ -3834,50 +4439,49 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3834
4439
|
"aria-label": chat.muted ? "Turn the voice on" : "Turn the voice off",
|
|
3835
4440
|
"aria-pressed": chat.muted,
|
|
3836
4441
|
onClick: () => chat.setMuted(!chat.muted),
|
|
3837
|
-
children: chat.muted ? /* @__PURE__ */
|
|
4442
|
+
children: chat.muted ? /* @__PURE__ */ jsx11(Muted, {}) : /* @__PURE__ */ jsx11(Sound, {})
|
|
3838
4443
|
}
|
|
3839
4444
|
)
|
|
3840
4445
|
] })
|
|
3841
4446
|
] }),
|
|
3842
|
-
/* @__PURE__ */
|
|
3843
|
-
/* @__PURE__ */
|
|
3844
|
-
openingChapter && /* @__PURE__ */
|
|
3845
|
-
!showing && chat.turns.length === 0 && /* @__PURE__ */
|
|
3846
|
-
chat.playerProps && /* @__PURE__ */
|
|
4447
|
+
/* @__PURE__ */ jsxs8("div", { className: "stage-area", children: [
|
|
4448
|
+
/* @__PURE__ */ jsxs8("div", { className: "stage", style: { background: "#000" }, children: [
|
|
4449
|
+
openingChapter && /* @__PURE__ */ jsx11(OpeningChapter, { preparing: preparingKey === shown.id && !chat.speaking, title: openingTitle.length > 120 ? `${openingTitle.slice(0, 117).trimEnd()}\u2026` : openingTitle }, shown.id),
|
|
4450
|
+
!showing && chat.turns.length === 0 && /* @__PURE__ */ jsx11(Welcome, { data: chat.welcome, onAsk: ask, title: welcomeTitle }),
|
|
4451
|
+
chat.playerProps && /* @__PURE__ */ jsx11("div", { className: "player-fit", style: { width: stageOrientation === "portrait" ? "min(100cqw, 56.25cqh)" : "min(100cqw, 177.7778cqh)" }, children: /* @__PURE__ */ jsx11(
|
|
3847
4452
|
VideoPlayer,
|
|
3848
4453
|
{
|
|
3849
4454
|
...chat.playerProps,
|
|
3850
4455
|
onSceneChange: cueBody,
|
|
3851
|
-
templates: options.templates,
|
|
3852
4456
|
orientation: stageOrientation,
|
|
3853
4457
|
responsiveBreakpoint: DESKTOP_WIDTH,
|
|
3854
4458
|
ariaLabel: chat.playerProps.video ? "Replay" : "The response"
|
|
3855
4459
|
},
|
|
3856
4460
|
chat.playerKey
|
|
3857
4461
|
) }),
|
|
3858
|
-
showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */
|
|
3859
|
-
/* @__PURE__ */
|
|
3860
|
-
/* @__PURE__ */
|
|
3861
|
-
/* @__PURE__ */
|
|
3862
|
-
/* @__PURE__ */
|
|
4462
|
+
showing && chat.playbackEnded && chat.suggestions.length > 0 && /* @__PURE__ */ jsxs8("div", { className: "ending", children: [
|
|
4463
|
+
/* @__PURE__ */ jsx11("div", { className: "ending-wash", "aria-hidden": "true" }),
|
|
4464
|
+
/* @__PURE__ */ jsxs8("div", { className: "ending-body", children: [
|
|
4465
|
+
/* @__PURE__ */ jsx11("p", { className: "ending-label", children: "Ask next" }),
|
|
4466
|
+
/* @__PURE__ */ jsx11(SuggestionCards, { suggestions: [...chat.suggestions], label: "Follow-up prompts", onAsk: ask })
|
|
3863
4467
|
] })
|
|
3864
4468
|
] })
|
|
3865
4469
|
] }),
|
|
3866
|
-
historyOpen && /* @__PURE__ */
|
|
3867
|
-
/* @__PURE__ */
|
|
3868
|
-
/* @__PURE__ */
|
|
3869
|
-
/* @__PURE__ */
|
|
4470
|
+
historyOpen && /* @__PURE__ */ jsxs8("nav", { ref: historyRef, id: historyId, className: "sheet-popover history", role: "dialog", "aria-modal": "true", "aria-label": "Sessions", children: [
|
|
4471
|
+
/* @__PURE__ */ jsxs8("div", { className: "popover-heading", children: [
|
|
4472
|
+
/* @__PURE__ */ jsx11("h2", { children: "Sessions" }),
|
|
4473
|
+
/* @__PURE__ */ jsx11("button", { type: "button", className: "round", "aria-label": "Close sessions", onClick: closeHistory, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
3870
4474
|
] }),
|
|
3871
|
-
/* @__PURE__ */
|
|
3872
|
-
/* @__PURE__ */
|
|
3873
|
-
/* @__PURE__ */
|
|
4475
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", className: "history-row session-new", "aria-label": "New session", onClick: newSession, children: [
|
|
4476
|
+
/* @__PURE__ */ jsx11(Plus, {}),
|
|
4477
|
+
/* @__PURE__ */ jsxs8("span", { className: "prompt", children: [
|
|
3874
4478
|
"New session",
|
|
3875
|
-
/* @__PURE__ */
|
|
4479
|
+
/* @__PURE__ */ jsx11("small", { children: "Start a fresh conversation" })
|
|
3876
4480
|
] })
|
|
3877
4481
|
] }),
|
|
3878
|
-
/* @__PURE__ */
|
|
3879
|
-
chat.turns.length === 0 && /* @__PURE__ */
|
|
3880
|
-
chat.turns.map((turn, index) => /* @__PURE__ */
|
|
4482
|
+
/* @__PURE__ */ jsx11("h3", { className: "section-label", children: "Current session" }),
|
|
4483
|
+
chat.turns.length === 0 && /* @__PURE__ */ jsx11("p", { className: "history-empty", children: "Your questions will appear here." }),
|
|
4484
|
+
chat.turns.map((turn, index) => /* @__PURE__ */ jsxs8(
|
|
3881
4485
|
"button",
|
|
3882
4486
|
{
|
|
3883
4487
|
type: "button",
|
|
@@ -3893,18 +4497,18 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3893
4497
|
setHistoryOpen(false);
|
|
3894
4498
|
},
|
|
3895
4499
|
children: [
|
|
3896
|
-
/* @__PURE__ */
|
|
3897
|
-
/* @__PURE__ */
|
|
4500
|
+
/* @__PURE__ */ jsx11("span", { className: "index", children: String(index + 1).padStart(2, "0") }),
|
|
4501
|
+
/* @__PURE__ */ jsxs8("span", { className: "prompt", children: [
|
|
3898
4502
|
turn.prompt,
|
|
3899
|
-
/* @__PURE__ */
|
|
4503
|
+
/* @__PURE__ */ jsx11("small", { children: turn.id === shown?.id ? "Now showing" : turn.completed ? "Play answer" : "Unfinished answer" })
|
|
3900
4504
|
] })
|
|
3901
4505
|
]
|
|
3902
4506
|
},
|
|
3903
4507
|
turn.id
|
|
3904
4508
|
)),
|
|
3905
|
-
savedSessions.length > 0 && /* @__PURE__ */
|
|
3906
|
-
/* @__PURE__ */
|
|
3907
|
-
savedSessions.map((session) => /* @__PURE__ */
|
|
4509
|
+
savedSessions.length > 0 && /* @__PURE__ */ jsxs8(Fragment4, { children: [
|
|
4510
|
+
/* @__PURE__ */ jsx11("h3", { className: "section-label", children: "Earlier sessions" }),
|
|
4511
|
+
savedSessions.map((session) => /* @__PURE__ */ jsxs8("button", { type: "button", className: "history-row", onClick: () => {
|
|
3908
4512
|
listen.stop();
|
|
3909
4513
|
setDraft("");
|
|
3910
4514
|
setEditing(false);
|
|
@@ -3915,10 +4519,10 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3915
4519
|
setHistoryOpen(false);
|
|
3916
4520
|
setCaptionsExpanded(false);
|
|
3917
4521
|
}, children: [
|
|
3918
|
-
/* @__PURE__ */
|
|
3919
|
-
/* @__PURE__ */
|
|
4522
|
+
/* @__PURE__ */ jsx11(Replay, {}),
|
|
4523
|
+
/* @__PURE__ */ jsxs8("span", { className: "prompt", children: [
|
|
3920
4524
|
session.turns[0]?.prompt,
|
|
3921
|
-
/* @__PURE__ */
|
|
4525
|
+
/* @__PURE__ */ jsxs8("small", { children: [
|
|
3922
4526
|
session.turns.length,
|
|
3923
4527
|
" ",
|
|
3924
4528
|
session.turns.length === 1 ? "answer" : "answers"
|
|
@@ -3927,7 +4531,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3927
4531
|
] }, session.id))
|
|
3928
4532
|
] })
|
|
3929
4533
|
] }),
|
|
3930
|
-
settingsOpen && /* @__PURE__ */
|
|
4534
|
+
settingsOpen && /* @__PURE__ */ jsxs8(
|
|
3931
4535
|
"div",
|
|
3932
4536
|
{
|
|
3933
4537
|
ref: settingsRef,
|
|
@@ -3937,18 +4541,18 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3937
4541
|
"aria-label": "Settings",
|
|
3938
4542
|
"aria-modal": "true",
|
|
3939
4543
|
children: [
|
|
3940
|
-
/* @__PURE__ */
|
|
3941
|
-
/* @__PURE__ */
|
|
3942
|
-
/* @__PURE__ */
|
|
4544
|
+
/* @__PURE__ */ jsxs8("div", { className: "popover-heading", children: [
|
|
4545
|
+
/* @__PURE__ */ jsx11("h2", { children: "Settings" }),
|
|
4546
|
+
/* @__PURE__ */ jsx11("button", { type: "button", className: "round", "aria-label": "Close settings", onClick: closeSettings, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
3943
4547
|
] }),
|
|
3944
|
-
/* @__PURE__ */
|
|
3945
|
-
/* @__PURE__ */
|
|
3946
|
-
visualModes.filter((mode) => chat.availableModes.includes(mode.id)).map((mode) => /* @__PURE__ */
|
|
3947
|
-
/* @__PURE__ */
|
|
3948
|
-
/* @__PURE__ */
|
|
3949
|
-
/* @__PURE__ */
|
|
4548
|
+
/* @__PURE__ */ jsxs8("fieldset", { className: "playback-options", children: [
|
|
4549
|
+
/* @__PURE__ */ jsx11("legend", { children: "Video source" }),
|
|
4550
|
+
visualModes.filter((mode) => chat.availableModes.includes(mode.id)).map((mode) => /* @__PURE__ */ jsxs8("label", { className: "switch-row", children: [
|
|
4551
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
4552
|
+
/* @__PURE__ */ jsx11("strong", { children: mode.label }),
|
|
4553
|
+
/* @__PURE__ */ jsx11("small", { children: mode.note })
|
|
3950
4554
|
] }),
|
|
3951
|
-
/* @__PURE__ */
|
|
4555
|
+
/* @__PURE__ */ jsx11(
|
|
3952
4556
|
"input",
|
|
3953
4557
|
{
|
|
3954
4558
|
type: "radio",
|
|
@@ -3960,51 +4564,51 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3960
4564
|
)
|
|
3961
4565
|
] }, mode.id))
|
|
3962
4566
|
] }),
|
|
3963
|
-
/* @__PURE__ */
|
|
3964
|
-
/* @__PURE__ */
|
|
3965
|
-
/* @__PURE__ */
|
|
3966
|
-
/* @__PURE__ */
|
|
3967
|
-
/* @__PURE__ */
|
|
3968
|
-
/* @__PURE__ */
|
|
4567
|
+
/* @__PURE__ */ jsxs8("fieldset", { className: "playback-options", children: [
|
|
4568
|
+
/* @__PURE__ */ jsx11("legend", { children: "Watching" }),
|
|
4569
|
+
/* @__PURE__ */ jsxs8("label", { className: "switch-row", children: [
|
|
4570
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
4571
|
+
/* @__PURE__ */ jsx11("strong", { children: "Subtitles" }),
|
|
4572
|
+
/* @__PURE__ */ jsx11("small", { children: "Read along with the answer" })
|
|
3969
4573
|
] }),
|
|
3970
|
-
/* @__PURE__ */
|
|
4574
|
+
/* @__PURE__ */ jsx11("input", { type: "checkbox", role: "switch", checked: captionsOn, onChange: (event) => {
|
|
3971
4575
|
setCaptionsOn(event.target.checked);
|
|
3972
4576
|
setCaptionsExpanded(false);
|
|
3973
4577
|
} })
|
|
3974
4578
|
] }),
|
|
3975
|
-
/* @__PURE__ */
|
|
3976
|
-
/* @__PURE__ */
|
|
3977
|
-
/* @__PURE__ */
|
|
3978
|
-
/* @__PURE__ */
|
|
4579
|
+
/* @__PURE__ */ jsxs8("label", { className: "switch-row", children: [
|
|
4580
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
4581
|
+
/* @__PURE__ */ jsx11("strong", { children: "Keep controls visible" }),
|
|
4582
|
+
/* @__PURE__ */ jsx11("small", { children: "Keep the input bar on screen" })
|
|
3979
4583
|
] }),
|
|
3980
|
-
/* @__PURE__ */
|
|
4584
|
+
/* @__PURE__ */ jsx11("input", { type: "checkbox", role: "switch", checked: alwaysShowControls, onChange: (event) => setAlwaysShowControls(event.target.checked) })
|
|
3981
4585
|
] })
|
|
3982
4586
|
] }),
|
|
3983
|
-
/* @__PURE__ */
|
|
3984
|
-
/* @__PURE__ */
|
|
3985
|
-
/* @__PURE__ */
|
|
4587
|
+
branding?.showDeveloperLinks !== false && /* @__PURE__ */ jsxs8("nav", { className: "developer-links", "aria-label": "Build with VanillaSky", children: [
|
|
4588
|
+
/* @__PURE__ */ jsx11("p", { className: "section-label", children: "Build with VanillaSky" }),
|
|
4589
|
+
/* @__PURE__ */ jsxs8("a", { href: "https://github.com/VanillaSkyAi/video/blob/main/docs/getting-started.md", target: "_blank", rel: "noopener noreferrer", children: [
|
|
3986
4590
|
"Docs",
|
|
3987
|
-
/* @__PURE__ */
|
|
4591
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\u2197" })
|
|
3988
4592
|
] }),
|
|
3989
|
-
/* @__PURE__ */
|
|
4593
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", "aria-expanded": aboutOpen, "aria-controls": `${instanceId}-about`, onClick: () => setAboutOpen((open) => !open), children: [
|
|
3990
4594
|
"About",
|
|
3991
|
-
/* @__PURE__ */
|
|
4595
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: aboutOpen ? "\u2212" : "+" })
|
|
3992
4596
|
] }),
|
|
3993
|
-
/* @__PURE__ */
|
|
3994
|
-
/* @__PURE__ */
|
|
4597
|
+
/* @__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." }) }),
|
|
4598
|
+
/* @__PURE__ */ jsxs8("a", { href: "https://github.com/VanillaSkyAi/video", target: "_blank", rel: "noopener noreferrer", children: [
|
|
3995
4599
|
"GitHub",
|
|
3996
|
-
/* @__PURE__ */
|
|
4600
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\u2197" })
|
|
3997
4601
|
] })
|
|
3998
4602
|
] })
|
|
3999
4603
|
]
|
|
4000
4604
|
}
|
|
4001
4605
|
)
|
|
4002
4606
|
] }),
|
|
4003
|
-
/* @__PURE__ */
|
|
4004
|
-
/* @__PURE__ */
|
|
4607
|
+
/* @__PURE__ */ jsx11("div", { ref: panelRef, className: "panel", "data-input-visible": controls.visible || !captionsOn || !line, children: /* @__PURE__ */ jsxs8("div", { className: "panel-inner", children: [
|
|
4608
|
+
/* @__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
4609
|
"Show transcript",
|
|
4006
|
-
/* @__PURE__ */
|
|
4007
|
-
] }) : /* @__PURE__ */
|
|
4610
|
+
/* @__PURE__ */ jsx11(ChevronUp, {})
|
|
4611
|
+
] }) : /* @__PURE__ */ jsxs8(
|
|
4008
4612
|
"div",
|
|
4009
4613
|
{
|
|
4010
4614
|
className: "line-row",
|
|
@@ -4017,44 +4621,44 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
4017
4621
|
onFocusCapture: captionControls.onFocusCapture,
|
|
4018
4622
|
onBlurCapture: captionControls.onBlurCapture,
|
|
4019
4623
|
children: [
|
|
4020
|
-
captionsOn && line && /* @__PURE__ */
|
|
4021
|
-
/* @__PURE__ */
|
|
4624
|
+
captionsOn && line && /* @__PURE__ */ jsxs8("div", { className: "caption-actions", children: [
|
|
4625
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", className: "caption-action", "aria-label": captionsExpanded ? "Collapse subtitles" : "Expand subtitles", "aria-expanded": captionsExpanded, onClick: () => setCaptionsExpanded((open) => !open), children: [
|
|
4022
4626
|
captionsExpanded ? "Collapse" : "Expand",
|
|
4023
|
-
/* @__PURE__ */
|
|
4627
|
+
/* @__PURE__ */ jsx11(ChevronUp, {})
|
|
4024
4628
|
] }),
|
|
4025
|
-
/* @__PURE__ */
|
|
4629
|
+
/* @__PURE__ */ jsx11("button", { type: "button", className: "caption-action", "aria-label": "Hide subtitles", onClick: () => {
|
|
4026
4630
|
setCaptionsOn(false);
|
|
4027
4631
|
setCaptionsExpanded(false);
|
|
4028
|
-
}, children: /* @__PURE__ */
|
|
4632
|
+
}, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
4029
4633
|
] }),
|
|
4030
|
-
captionsExpanded ? /* @__PURE__ */
|
|
4634
|
+
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
4635
|
]
|
|
4032
4636
|
}
|
|
4033
4637
|
) }) }),
|
|
4034
|
-
showRecoveryNotice && chat.shownTurn && dismissedNoticeTurn !== chat.shownTurn.id && chat.warnings.includes(MEDIA_RECOVERY_NOTICE) && !chat.error && /* @__PURE__ */
|
|
4035
|
-
/* @__PURE__ */
|
|
4036
|
-
/* @__PURE__ */
|
|
4638
|
+
showRecoveryNotice && chat.shownTurn && dismissedNoticeTurn !== chat.shownTurn.id && chat.warnings.includes(MEDIA_RECOVERY_NOTICE) && !chat.error && /* @__PURE__ */ jsxs8("div", { className: "recovery-notice", children: [
|
|
4639
|
+
/* @__PURE__ */ jsx11("p", { role: "status", children: MEDIA_RECOVERY_NOTICE }),
|
|
4640
|
+
/* @__PURE__ */ jsx11("button", { type: "button", className: "round", "aria-label": "Dismiss notice", onClick: () => setDismissedNoticeTurn(chat.shownTurn?.id), children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
4037
4641
|
] }),
|
|
4038
|
-
(chat.error || listen.error) && /* @__PURE__ */
|
|
4039
|
-
/* @__PURE__ */
|
|
4040
|
-
/* @__PURE__ */
|
|
4642
|
+
(chat.error || listen.error) && /* @__PURE__ */ jsxs8("p", { className: "error", role: "status", children: [
|
|
4643
|
+
/* @__PURE__ */ jsx11(Warning, {}),
|
|
4644
|
+
/* @__PURE__ */ jsx11("span", { children: chat.error?.message ?? listen.error })
|
|
4041
4645
|
] }),
|
|
4042
|
-
/* @__PURE__ */
|
|
4043
|
-
(listen.listening || listen.thinking) && /* @__PURE__ */
|
|
4044
|
-
/* @__PURE__ */
|
|
4646
|
+
/* @__PURE__ */ jsxs8("div", { ref: composerRef, className: "conversation-composer", "data-editing": editing, ...controlEvents, children: [
|
|
4647
|
+
(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" }) }),
|
|
4648
|
+
/* @__PURE__ */ jsxs8("form", { className: "composer", "aria-label": "Ask a question", onSubmit: (event) => {
|
|
4045
4649
|
event.preventDefault();
|
|
4046
4650
|
ask(draft);
|
|
4047
4651
|
}, children: [
|
|
4048
|
-
transport && /* @__PURE__ */
|
|
4652
|
+
transport && /* @__PURE__ */ jsx11("button", { type: "button", className: "ghost transport", "aria-label": transport.label, onClick: () => {
|
|
4049
4653
|
listen.stop();
|
|
4050
4654
|
setEditing(false);
|
|
4051
4655
|
resumeAfterInput.current = false;
|
|
4052
4656
|
inputRef.current?.blur();
|
|
4053
4657
|
transport.action();
|
|
4054
4658
|
}, children: transport.icon }),
|
|
4055
|
-
/* @__PURE__ */
|
|
4056
|
-
/* @__PURE__ */
|
|
4057
|
-
/* @__PURE__ */
|
|
4659
|
+
/* @__PURE__ */ jsx11(Waveform, { active: listen.listening || chat.speaking && !chat.muted && status !== "paused", listening: listen.listening }),
|
|
4660
|
+
/* @__PURE__ */ jsx11("label", { className: "sr-only", htmlFor: promptId, children: "Prompt" }),
|
|
4661
|
+
/* @__PURE__ */ jsx11(
|
|
4058
4662
|
"textarea",
|
|
4059
4663
|
{
|
|
4060
4664
|
id: promptId,
|
|
@@ -4082,17 +4686,28 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
4082
4686
|
}
|
|
4083
4687
|
}
|
|
4084
4688
|
),
|
|
4085
|
-
listen.supported && /* @__PURE__ */
|
|
4086
|
-
/* @__PURE__ */
|
|
4087
|
-
editing && /* @__PURE__ */
|
|
4689
|
+
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, {}) }),
|
|
4690
|
+
/* @__PURE__ */ jsx11("button", { type: "submit", className: "send", "aria-label": "Ask", disabled: !draft.trim() || listen.thinking, children: /* @__PURE__ */ jsx11(Send, {}) }),
|
|
4691
|
+
editing && /* @__PURE__ */ jsx11("button", { type: "button", className: "ghost", "aria-label": "Cancel question", onClick: cancelInput, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
4088
4692
|
] })
|
|
4089
4693
|
] }),
|
|
4090
|
-
!showing && chat.turns.length === 0 && /* @__PURE__ */
|
|
4694
|
+
!showing && chat.turns.length === 0 && /* @__PURE__ */ jsx11("p", { className: "dock-hint", children: "Speak or type. See where it takes you." })
|
|
4091
4695
|
] }) })
|
|
4092
4696
|
]
|
|
4093
4697
|
}
|
|
4094
4698
|
);
|
|
4095
4699
|
}
|
|
4700
|
+
function safeHomeUrl(value) {
|
|
4701
|
+
const candidate = value?.trim();
|
|
4702
|
+
if (!candidate || !/^(?:https?:\/\/|\/(?![/\\]))/i.test(candidate)) return void 0;
|
|
4703
|
+
try {
|
|
4704
|
+
const url = new URL(candidate, "https://vanillasky.local");
|
|
4705
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password) return void 0;
|
|
4706
|
+
return candidate.startsWith("/") ? `${url.pathname}${url.search}${url.hash}` : url.href;
|
|
4707
|
+
} catch {
|
|
4708
|
+
return void 0;
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4096
4711
|
export {
|
|
4097
4712
|
VideoChat,
|
|
4098
4713
|
VideoError,
|