@vanillaskyai/video 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE +201 -0
- package/PUBLIC-API.md +276 -0
- package/README.md +152 -0
- package/SECURITY.md +22 -0
- package/SUPPORT.md +28 -0
- package/bin/vanillasky.js +3 -0
- package/dist/bg-confetti-WNQXS7ZS.js +89 -0
- package/dist/bg-emoji-ETI47GLS.js +150 -0
- package/dist/bg-media-SVUZAMGX.js +99 -0
- package/dist/brand-message-F3RD4P3P.js +253 -0
- package/dist/builtin-server-F7YXRVCH.js +8 -0
- package/dist/catalog-types-BIhSpOWK.d.ts +74 -0
- package/dist/chart-bar-CNSC7BNK.js +304 -0
- package/dist/chart-counter-MCRHKN77.js +299 -0
- package/dist/chart-progress-ring-RMI4A7ZK.js +296 -0
- package/dist/check-runtime.d.ts +15 -0
- package/dist/check-runtime.js +102 -0
- package/dist/chunk-2E6T633S.js +27 -0
- package/dist/chunk-352ZZCET.js +83 -0
- package/dist/chunk-73NTSFFI.js +81 -0
- package/dist/chunk-A2X45FET.js +46 -0
- package/dist/chunk-ABPKM4GK.js +187 -0
- package/dist/chunk-ABZACD4G.js +216 -0
- package/dist/chunk-BCRUUJ6A.js +756 -0
- package/dist/chunk-CHLGW22F.js +3551 -0
- package/dist/chunk-CJ7F2CE6.js +104 -0
- package/dist/chunk-CR7FE7BW.js +109 -0
- package/dist/chunk-E7CL7UPB.js +109 -0
- package/dist/chunk-EE6PK6XC.js +18 -0
- package/dist/chunk-ERY4S6QV.js +385 -0
- package/dist/chunk-EVG2ZYIY.js +125 -0
- package/dist/chunk-FNAEQ7QO.js +155 -0
- package/dist/chunk-FQ47WXAJ.js +38 -0
- package/dist/chunk-FSG2PYWG.js +336 -0
- package/dist/chunk-GHJEAP4O.js +504 -0
- package/dist/chunk-GRMUV7QY.js +262 -0
- package/dist/chunk-IFOW65Z6.js +205 -0
- package/dist/chunk-IXCUY3B3.js +786 -0
- package/dist/chunk-L5BTTCZV.js +97 -0
- package/dist/chunk-LMG3GHLL.js +107 -0
- package/dist/chunk-OOPIETLW.js +101 -0
- package/dist/chunk-P22HHFDK.js +43 -0
- package/dist/chunk-QAAO7OAY.js +19 -0
- package/dist/chunk-S7HFLYNN.js +35 -0
- package/dist/chunk-SPVTJH3F.js +24 -0
- package/dist/chunk-W2XJKG6C.js +3177 -0
- package/dist/chunk-XB7LH4J2.js +301 -0
- package/dist/chunk-XGRA2MUV.js +242 -0
- package/dist/chunk-XTOXFMLM.js +105 -0
- package/dist/chunk-YC5MFXA5.js +132 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1681 -0
- package/dist/compose-video-CN3VSWST.js +13 -0
- package/dist/cta-logo-SVA5GP2A.js +209 -0
- package/dist/cta-media-E7VLLK42.js +215 -0
- package/dist/events-_wfjbmEp.d.ts +81 -0
- package/dist/incoming-call-62MFZL3Z.js +320 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/infographic-before-after-AXCDAWND.js +460 -0
- package/dist/infographic-feature-list-GW5GUE6O.js +266 -0
- package/dist/infographic-problem-solution-7NURJANA.js +340 -0
- package/dist/infographic-stat-row-J7IZR6ZN.js +241 -0
- package/dist/infographic-steps-VSZM6F5M.js +400 -0
- package/dist/kit-BMNcHqa9.d.ts +79 -0
- package/dist/prompt-input-LOUCRXCY.js +236 -0
- package/dist/react.d.ts +81 -0
- package/dist/react.js +835 -0
- package/dist/reaction-QG4CYZLQ.js +119 -0
- package/dist/server.d.ts +141 -0
- package/dist/server.js +498 -0
- package/dist/showcase-code-RUTFZR3H.js +351 -0
- package/dist/showcase-phone-6CP6XEU5.js +272 -0
- package/dist/showcase-terminal-MGAEQWPR.js +306 -0
- package/dist/showcase-web-Y4W2HVRJ.js +390 -0
- package/dist/social-conversation-75SD2637.js +695 -0
- package/dist/social-milestone-XI2QE5BO.js +292 -0
- package/dist/social-notification-2UOHQCUY.js +244 -0
- package/dist/social-review-stack-FDYZGZTJ.js +239 -0
- package/dist/social-testimonial-OQ7N52GJ.js +284 -0
- package/dist/social-tweet-5OLCNPPC.js +295 -0
- package/dist/state-DZcKuS32.d.ts +3 -0
- package/dist/system-prompt-A6UXNTPK.js +8 -0
- package/dist/template-catalog.d.ts +24 -0
- package/dist/template-catalog.js +6 -0
- package/dist/templates.d.ts +25 -0
- package/dist/templates.js +19 -0
- package/dist/test.d.ts +149 -0
- package/dist/test.js +272 -0
- package/dist/text-stream-6XHC7J47.js +10 -0
- package/dist/types-B-PZR6G_.d.ts +208 -0
- package/dist/validate-T7GBU2YF.js +8 -0
- package/docs/agent-integration.md +39 -0
- package/docs/architecture.md +84 -0
- package/docs/branding-and-personalization.md +87 -0
- package/docs/concepts.md +106 -0
- package/docs/custom-templates.md +329 -0
- package/docs/customization.md +141 -0
- package/docs/errors.md +70 -0
- package/docs/getting-started.md +111 -0
- package/docs/input-and-first-scene.md +41 -0
- package/docs/integrate-nextjs.md +79 -0
- package/docs/media-and-audio.md +69 -0
- package/docs/motion-and-effects.md +107 -0
- package/docs/persistence.md +95 -0
- package/docs/production.md +122 -0
- package/docs/prompt-and-input.md +185 -0
- package/docs/provider-integration.md +115 -0
- package/docs/reference/protocol.md +96 -0
- package/docs/reference/provider-adapters.md +158 -0
- package/docs/responsive-orientation.md +37 -0
- package/docs/security.md +38 -0
- package/docs/streaming-protocol.md +15 -0
- package/docs/testing.md +137 -0
- package/docs/use-cases.md +59 -0
- package/examples/custom-template/README.md +19 -0
- package/examples/custom-template/minimal-text.tsx +82 -0
- package/examples/custom-template/structured-data.tsx +104 -0
- package/examples/custom-template/supplied-media.tsx +92 -0
- package/examples/nextjs-quickstart/.env.example +2 -0
- package/examples/nextjs-quickstart/README.md +22 -0
- package/examples/nextjs-quickstart/next-env.d.ts +4 -0
- package/examples/nextjs-quickstart/package.json +24 -0
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +22 -0
- package/examples/nextjs-quickstart/src/app/layout.tsx +5 -0
- package/examples/nextjs-quickstart/src/app/page.tsx +31 -0
- package/examples/nextjs-quickstart/tsconfig.json +26 -0
- package/package.json +167 -0
- package/registry/items/backgrounds.json +52 -0
- package/registry/items/barChart.json +237 -0
- package/registry/items/beforeAfter.json +180 -0
- package/registry/items/bigNumber.json +225 -0
- package/registry/items/brandMessage.json +158 -0
- package/registry/items/cardList.json +206 -0
- package/registry/items/chatMessenger.json +151 -0
- package/registry/items/chatWhatsapp.json +151 -0
- package/registry/items/codeEditor.json +184 -0
- package/registry/items/confetti.json +107 -0
- package/registry/items/ctaLogo.json +160 -0
- package/registry/items/ctaMedia.json +193 -0
- package/registry/items/emojiBurst.json +101 -0
- package/registry/items/incomingCall.json +170 -0
- package/registry/items/media.json +179 -0
- package/registry/items/milestone.json +205 -0
- package/registry/items/motion.json +45 -0
- package/registry/items/notification.json +182 -0
- package/registry/items/phoneMockup.json +254 -0
- package/registry/items/problemSolution.json +186 -0
- package/registry/items/progressRing.json +199 -0
- package/registry/items/promptInput.json +148 -0
- package/registry/items/reaction.json +170 -0
- package/registry/items/reviewStack.json +224 -0
- package/registry/items/steps.json +207 -0
- package/registry/items/template-context.json +31 -0
- package/registry/items/terminal.json +212 -0
- package/registry/items/testimonial.json +164 -0
- package/registry/items/theme.json +47 -0
- package/registry/items/tripleStats.json +228 -0
- package/registry/items/tweet.json +203 -0
- package/registry/items/typography.json +47 -0
- package/registry/items/webMockup.json +273 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VIDEO_STREAM_CONTENT_TYPE,
|
|
3
|
+
VIDEO_STREAM_HEADER,
|
|
4
|
+
decodeVideoSse
|
|
5
|
+
} from "./chunk-OOPIETLW.js";
|
|
6
|
+
import {
|
|
7
|
+
applyVideoEvent,
|
|
8
|
+
createVideoState
|
|
9
|
+
} from "./chunk-ABPKM4GK.js";
|
|
10
|
+
import {
|
|
11
|
+
safePublicDiagnostic
|
|
12
|
+
} from "./chunk-ERY4S6QV.js";
|
|
13
|
+
import {
|
|
14
|
+
parseVideo
|
|
15
|
+
} from "./chunk-GHJEAP4O.js";
|
|
16
|
+
import {
|
|
17
|
+
VIDEO_PROTOCOL_VERSION,
|
|
18
|
+
createVideoRequest
|
|
19
|
+
} from "./chunk-EE6PK6XC.js";
|
|
20
|
+
import {
|
|
21
|
+
overlayTemplateRegistry
|
|
22
|
+
} from "./chunk-CR7FE7BW.js";
|
|
23
|
+
import {
|
|
24
|
+
BUILTIN_TEMPLATE_CAPABILITIES,
|
|
25
|
+
BUILTIN_TEMPLATE_CATALOG,
|
|
26
|
+
getBuiltinTemplateMetadata
|
|
27
|
+
} from "./chunk-CHLGW22F.js";
|
|
28
|
+
import "./chunk-2E6T633S.js";
|
|
29
|
+
import {
|
|
30
|
+
VideoFrame,
|
|
31
|
+
getDimensions
|
|
32
|
+
} from "./chunk-XB7LH4J2.js";
|
|
33
|
+
import {
|
|
34
|
+
fontStack
|
|
35
|
+
} from "./chunk-FNAEQ7QO.js";
|
|
36
|
+
import {
|
|
37
|
+
getVideoDuration,
|
|
38
|
+
resolveVideoTimeline
|
|
39
|
+
} from "./chunk-SPVTJH3F.js";
|
|
40
|
+
import "./chunk-XGRA2MUV.js";
|
|
41
|
+
import "./chunk-73NTSFFI.js";
|
|
42
|
+
|
|
43
|
+
// src/player/video-player.tsx
|
|
44
|
+
import {
|
|
45
|
+
useEffect,
|
|
46
|
+
useMemo,
|
|
47
|
+
useRef,
|
|
48
|
+
useState
|
|
49
|
+
} from "react";
|
|
50
|
+
|
|
51
|
+
// src/visual-system/catalog/builtin.ts
|
|
52
|
+
import { createElement } from "react";
|
|
53
|
+
|
|
54
|
+
// src/visual-system/catalog/builtin-loaders.generated.ts
|
|
55
|
+
var GENERATED_BUILTIN_TEMPLATE_LOADERS = {
|
|
56
|
+
"media": () => import("./bg-media-SVUZAMGX.js").then((module) => ({ default: module.BgMediaTemplate })),
|
|
57
|
+
"reaction": () => import("./reaction-QG4CYZLQ.js").then((module) => ({ default: module.ReactionTemplate })),
|
|
58
|
+
"confetti": () => import("./bg-confetti-WNQXS7ZS.js").then((module) => ({ default: module.BgConfettiTemplate })),
|
|
59
|
+
"emojiBurst": () => import("./bg-emoji-ETI47GLS.js").then((module) => ({ default: module.BgEmojiTemplate })),
|
|
60
|
+
"bigNumber": () => import("./chart-counter-MCRHKN77.js").then((module) => ({ default: module.ChartCounterTemplate })),
|
|
61
|
+
"barChart": () => import("./chart-bar-CNSC7BNK.js").then((module) => ({ default: module.ChartBarTemplate })),
|
|
62
|
+
"progressRing": () => import("./chart-progress-ring-RMI4A7ZK.js").then((module) => ({ default: module.ChartProgressRingTemplate })),
|
|
63
|
+
"phoneMockup": () => import("./showcase-phone-6CP6XEU5.js").then((module) => ({ default: module.ShowcasePhoneTemplate })),
|
|
64
|
+
"webMockup": () => import("./showcase-web-Y4W2HVRJ.js").then((module) => ({ default: module.ShowcaseWebTemplate })),
|
|
65
|
+
"codeEditor": () => import("./showcase-code-RUTFZR3H.js").then((module) => ({ default: module.ShowcaseCodeTemplate })),
|
|
66
|
+
"terminal": () => import("./showcase-terminal-MGAEQWPR.js").then((module) => ({ default: module.ShowcaseTerminalTemplate })),
|
|
67
|
+
"tweet": () => import("./social-tweet-5OLCNPPC.js").then((module) => ({ default: module.SocialTweetTemplate })),
|
|
68
|
+
"notification": () => import("./social-notification-2UOHQCUY.js").then((module) => ({ default: module.SocialNotificationTemplate })),
|
|
69
|
+
"chatMessenger": () => import("./social-conversation-75SD2637.js").then((module) => ({ default: module.SocialConversationTemplate })),
|
|
70
|
+
"chatWhatsapp": () => import("./social-conversation-75SD2637.js").then((module) => ({ default: module.SocialConversationTemplate })),
|
|
71
|
+
"milestone": () => import("./social-milestone-XI2QE5BO.js").then((module) => ({ default: module.SocialMilestoneTemplate })),
|
|
72
|
+
"reviewStack": () => import("./social-review-stack-FDYZGZTJ.js").then((module) => ({ default: module.SocialReviewStackTemplate })),
|
|
73
|
+
"testimonial": () => import("./social-testimonial-OQ7N52GJ.js").then((module) => ({ default: module.SocialTestimonialTemplate })),
|
|
74
|
+
"incomingCall": () => import("./incoming-call-62MFZL3Z.js").then((module) => ({ default: module.IncomingCallTemplate })),
|
|
75
|
+
"brandMessage": () => import("./brand-message-F3RD4P3P.js").then((module) => ({ default: module.BrandMessageTemplate })),
|
|
76
|
+
"promptInput": () => import("./prompt-input-LOUCRXCY.js").then((module) => ({ default: module.PromptInputTemplate })),
|
|
77
|
+
"beforeAfter": () => import("./infographic-before-after-AXCDAWND.js").then((module) => ({ default: module.InfographicBeforeAfterTemplate })),
|
|
78
|
+
"tripleStats": () => import("./infographic-stat-row-J7IZR6ZN.js").then((module) => ({ default: module.InfographicStatRowTemplate })),
|
|
79
|
+
"problemSolution": () => import("./infographic-problem-solution-7NURJANA.js").then((module) => ({ default: module.InfographicProblemSolutionTemplate })),
|
|
80
|
+
"cardList": () => import("./infographic-feature-list-GW5GUE6O.js").then((module) => ({ default: module.InfographicFeatureListTemplate })),
|
|
81
|
+
"steps": () => import("./infographic-steps-VSZM6F5M.js").then((module) => ({ default: module.InfographicStepsTemplate })),
|
|
82
|
+
"ctaLogo": () => import("./cta-logo-SVA5GP2A.js").then((module) => ({ default: module.CtaLogoTemplate })),
|
|
83
|
+
"ctaMedia": () => import("./cta-media-E7VLLK42.js").then((module) => ({ default: module.CtaMediaTemplate }))
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/visual-system/catalog/builtin.ts
|
|
87
|
+
function createPreloadableBuiltinTemplate(loader) {
|
|
88
|
+
let loaded;
|
|
89
|
+
let failure;
|
|
90
|
+
let pending;
|
|
91
|
+
const preload = () => {
|
|
92
|
+
if (loaded) return Promise.resolve();
|
|
93
|
+
if (failure) return Promise.reject(failure);
|
|
94
|
+
pending ??= loader().then(
|
|
95
|
+
(module) => {
|
|
96
|
+
loaded = module;
|
|
97
|
+
},
|
|
98
|
+
(cause) => {
|
|
99
|
+
failure = cause;
|
|
100
|
+
throw cause;
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
return pending;
|
|
104
|
+
};
|
|
105
|
+
const component = (props) => {
|
|
106
|
+
if (loaded) return createElement(loaded.default, props);
|
|
107
|
+
if (failure) throw failure;
|
|
108
|
+
throw preload();
|
|
109
|
+
};
|
|
110
|
+
return { component, preload };
|
|
111
|
+
}
|
|
112
|
+
function freezeTemplate(value) {
|
|
113
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
114
|
+
for (const nested of Object.values(value)) freezeTemplate(nested);
|
|
115
|
+
Object.freeze(value);
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
var preloadableById = new Map(
|
|
120
|
+
BUILTIN_TEMPLATE_CATALOG.map((metadata) => {
|
|
121
|
+
const id = metadata.id;
|
|
122
|
+
return [id, createPreloadableBuiltinTemplate(GENERATED_BUILTIN_TEMPLATE_LOADERS[id])];
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
var BUILTIN_TEMPLATES = Object.freeze(
|
|
126
|
+
BUILTIN_TEMPLATE_CATALOG.map((metadata) => Object.freeze({
|
|
127
|
+
...freezeTemplate({ ...metadata }),
|
|
128
|
+
component: preloadableById.get(
|
|
129
|
+
metadata.id
|
|
130
|
+
).component
|
|
131
|
+
}))
|
|
132
|
+
);
|
|
133
|
+
var templateById = new Map(BUILTIN_TEMPLATES.map((template) => [template.id, template]));
|
|
134
|
+
var BUILTIN_TEMPLATE_KIT = Object.freeze({
|
|
135
|
+
templates: BUILTIN_TEMPLATES,
|
|
136
|
+
capabilities: BUILTIN_TEMPLATE_CAPABILITIES,
|
|
137
|
+
getTemplate: (id) => templateById.get(id),
|
|
138
|
+
getTemplateMetadata: getBuiltinTemplateMetadata,
|
|
139
|
+
listTemplateMetadata: () => BUILTIN_TEMPLATES.map(({ component: _component, ...metadata }) => metadata)
|
|
140
|
+
});
|
|
141
|
+
function preloadBuiltinTemplate(id) {
|
|
142
|
+
return preloadableById.get(id)?.preload();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/player/video-player.tsx
|
|
146
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
147
|
+
function savedVideoState(video) {
|
|
148
|
+
return {
|
|
149
|
+
...createVideoState(),
|
|
150
|
+
status: "complete",
|
|
151
|
+
config: video
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function VideoPlayerRuntime({
|
|
155
|
+
kit,
|
|
156
|
+
stream,
|
|
157
|
+
video,
|
|
158
|
+
autoPlay = true,
|
|
159
|
+
startMuted = true,
|
|
160
|
+
width,
|
|
161
|
+
orientation: orientationOverride,
|
|
162
|
+
responsiveBreakpoint = 520,
|
|
163
|
+
className,
|
|
164
|
+
style,
|
|
165
|
+
ariaLabel = "Video response",
|
|
166
|
+
onComplete,
|
|
167
|
+
onError,
|
|
168
|
+
onStateChange
|
|
169
|
+
}) {
|
|
170
|
+
const [reducedMotion] = useState(
|
|
171
|
+
() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
172
|
+
);
|
|
173
|
+
const [isPlaying, setIsPlaying] = useState(() => autoPlay && !reducedMotion);
|
|
174
|
+
const [isMuted, setIsMuted] = useState(startMuted);
|
|
175
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
176
|
+
const [state, setState] = useState(() => video ? savedVideoState(video) : createVideoState());
|
|
177
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
178
|
+
const [observedWidth, setObservedWidth] = useState(0);
|
|
179
|
+
const containerRef = useRef(null);
|
|
180
|
+
const stateRef = useRef(state);
|
|
181
|
+
const timeRef = useRef(currentTime);
|
|
182
|
+
const audioRef = useRef(null);
|
|
183
|
+
const callbacksRef = useRef({ onComplete, onError, onStateChange });
|
|
184
|
+
stateRef.current = state;
|
|
185
|
+
timeRef.current = currentTime;
|
|
186
|
+
callbacksRef.current = { onComplete, onError, onStateChange };
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
189
|
+
const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
190
|
+
const handleChange = (event) => {
|
|
191
|
+
if (event.matches) setIsPlaying(false);
|
|
192
|
+
};
|
|
193
|
+
preference.addEventListener?.("change", handleChange);
|
|
194
|
+
return () => preference.removeEventListener?.("change", handleChange);
|
|
195
|
+
}, []);
|
|
196
|
+
useEffect(() => setIsMuted(startMuted), [startMuted]);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
const updateFullscreen = () => setIsFullscreen(document.fullscreenElement === containerRef.current);
|
|
199
|
+
document.addEventListener("fullscreenchange", updateFullscreen);
|
|
200
|
+
return () => document.removeEventListener("fullscreenchange", updateFullscreen);
|
|
201
|
+
}, []);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
let cancelled = false;
|
|
204
|
+
let started = false;
|
|
205
|
+
const reset = video ? savedVideoState(video) : createVideoState();
|
|
206
|
+
stateRef.current = reset;
|
|
207
|
+
timeRef.current = 0;
|
|
208
|
+
setState(reset);
|
|
209
|
+
setCurrentTime(0);
|
|
210
|
+
if (video) {
|
|
211
|
+
for (const scene of video.scenes) preloadBuiltinTemplate(scene.templateId);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!stream) return;
|
|
215
|
+
const consume = async () => {
|
|
216
|
+
try {
|
|
217
|
+
for await (const event of stream) {
|
|
218
|
+
if (cancelled) return;
|
|
219
|
+
const current = stateRef.current;
|
|
220
|
+
const mutableSceneIds = new Set(
|
|
221
|
+
current.config ? resolveVideoTimeline(current.config).filter((range) => range.end > timeRef.current + 1e-3).map((range) => range.scene.id) : []
|
|
222
|
+
);
|
|
223
|
+
const next = applyVideoEvent(current, event, {
|
|
224
|
+
...current.config ? { mutableSceneIds } : {}
|
|
225
|
+
});
|
|
226
|
+
stateRef.current = next;
|
|
227
|
+
setState(next);
|
|
228
|
+
callbacksRef.current.onStateChange?.(next);
|
|
229
|
+
if (next.status === "complete" && current.status !== "complete") {
|
|
230
|
+
callbacksRef.current.onComplete?.(next);
|
|
231
|
+
} else if (next.status === "error" && current.status !== "error") {
|
|
232
|
+
callbacksRef.current.onError?.(new Error("Video response could not finish"), next);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
if (!cancelled) {
|
|
237
|
+
callbacksRef.current.onError?.(new Error("Video playback stream failed"), stateRef.current);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
queueMicrotask(() => {
|
|
242
|
+
if (cancelled) return;
|
|
243
|
+
started = true;
|
|
244
|
+
void consume();
|
|
245
|
+
});
|
|
246
|
+
return () => {
|
|
247
|
+
cancelled = true;
|
|
248
|
+
if (started) {
|
|
249
|
+
stream.cancel?.();
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}, [stream, video]);
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (width != null) return;
|
|
255
|
+
const container = containerRef.current;
|
|
256
|
+
if (!container) return;
|
|
257
|
+
const update = () => setObservedWidth(container.clientWidth);
|
|
258
|
+
update();
|
|
259
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
260
|
+
const observer = new ResizeObserver(update);
|
|
261
|
+
observer.observe(container);
|
|
262
|
+
return () => observer.disconnect();
|
|
263
|
+
}, [width]);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (!isPlaying) return;
|
|
266
|
+
let frame = 0;
|
|
267
|
+
let previous = performance.now();
|
|
268
|
+
const tick = (now) => {
|
|
269
|
+
const current = stateRef.current;
|
|
270
|
+
const config2 = current.config;
|
|
271
|
+
const delta = Math.max(0, (now - previous) / 1e3);
|
|
272
|
+
previous = now;
|
|
273
|
+
if (config2?.scenes.length) {
|
|
274
|
+
const duration3 = getVideoDuration(config2);
|
|
275
|
+
const nextTime = Math.min(timeRef.current + delta, duration3);
|
|
276
|
+
if (nextTime !== timeRef.current) {
|
|
277
|
+
timeRef.current = nextTime;
|
|
278
|
+
setCurrentTime(nextTime);
|
|
279
|
+
}
|
|
280
|
+
const audio = audioRef.current;
|
|
281
|
+
if (audio && (current.status === "complete" || current.status === "error")) {
|
|
282
|
+
const fadeSeconds = Math.max(0, (config2.audio?.fadeOutMs ?? 3e3) / 1e3);
|
|
283
|
+
const remaining = Math.max(0, duration3 - nextTime);
|
|
284
|
+
const baseVolume = config2.audio?.volume ?? 1;
|
|
285
|
+
audio.volume = fadeSeconds > 0 ? baseVolume * Math.min(1, remaining / fadeSeconds) : baseVolume;
|
|
286
|
+
if (remaining <= 0) audio.pause();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const terminal2 = current.status === "complete" || current.status === "error" || current.status === "aborted";
|
|
290
|
+
const duration2 = current.config ? getVideoDuration(current.config) : 0;
|
|
291
|
+
if (!terminal2 || timeRef.current < duration2) frame = requestAnimationFrame(tick);
|
|
292
|
+
else setIsPlaying(false);
|
|
293
|
+
};
|
|
294
|
+
frame = requestAnimationFrame(tick);
|
|
295
|
+
return () => cancelAnimationFrame(frame);
|
|
296
|
+
}, [isPlaying]);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const audio = audioRef.current;
|
|
299
|
+
if (!audio) return;
|
|
300
|
+
audio.volume = state.config?.audio?.volume ?? 1;
|
|
301
|
+
}, [state.config?.audio?.audioUrl, state.config?.audio?.volume]);
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
const audio = audioRef.current;
|
|
304
|
+
if (!audio) return;
|
|
305
|
+
if (isPlaying) {
|
|
306
|
+
void audio.play().catch(() => {
|
|
307
|
+
setIsPlaying(false);
|
|
308
|
+
});
|
|
309
|
+
} else {
|
|
310
|
+
audio.pause();
|
|
311
|
+
}
|
|
312
|
+
}, [isPlaying, state.config?.audio?.audioUrl]);
|
|
313
|
+
const streamOrientation = state.config?.orientation ?? "portrait";
|
|
314
|
+
const responsiveWidth = width ?? observedWidth;
|
|
315
|
+
const orientation = orientationOverride === "auto" ? responsiveWidth > 0 ? responsiveWidth <= responsiveBreakpoint ? "portrait" : "landscape" : streamOrientation : orientationOverride ?? streamOrientation;
|
|
316
|
+
const dimensions = getDimensions(orientation);
|
|
317
|
+
const displayWidth = (width ?? observedWidth) || dimensions.width;
|
|
318
|
+
const displayHeight = displayWidth * dimensions.height / dimensions.width;
|
|
319
|
+
const scale = displayWidth / dimensions.width;
|
|
320
|
+
const config = state.config;
|
|
321
|
+
const displayConfig = config && config.orientation !== orientation ? { ...config, orientation } : config;
|
|
322
|
+
const duration = config ? getVideoDuration(config) : 0;
|
|
323
|
+
const terminal = state.status === "complete" || state.status === "error" || state.status === "aborted";
|
|
324
|
+
const ended = terminal && duration > 0 && currentTime >= duration - 1e-3;
|
|
325
|
+
const togglePlayback = () => {
|
|
326
|
+
if (!isPlaying && ended) {
|
|
327
|
+
timeRef.current = 0;
|
|
328
|
+
setCurrentTime(0);
|
|
329
|
+
if (audioRef.current) audioRef.current.currentTime = 0;
|
|
330
|
+
setIsPlaying(true);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
setIsPlaying((playing) => !playing);
|
|
334
|
+
};
|
|
335
|
+
const toggleFullscreen = async () => {
|
|
336
|
+
const container = containerRef.current;
|
|
337
|
+
if (!container) return;
|
|
338
|
+
if (document.fullscreenElement === container) await document.exitFullscreen?.();
|
|
339
|
+
else await container.requestFullscreen?.();
|
|
340
|
+
};
|
|
341
|
+
const controlButtonStyle = {
|
|
342
|
+
display: "inline-grid",
|
|
343
|
+
placeItems: "center",
|
|
344
|
+
flex: "0 0 auto",
|
|
345
|
+
minWidth: 44,
|
|
346
|
+
minHeight: 44,
|
|
347
|
+
padding: "6px 9px",
|
|
348
|
+
border: 0,
|
|
349
|
+
borderRadius: 8,
|
|
350
|
+
backgroundColor: "rgba(9, 7, 18, 0.88)",
|
|
351
|
+
color: "#ffffff",
|
|
352
|
+
font: "600 13px/1 system-ui, sans-serif",
|
|
353
|
+
cursor: "pointer"
|
|
354
|
+
};
|
|
355
|
+
const generationCoverVisible = state.status === "streaming" && !config?.scenes.length;
|
|
356
|
+
const coverBackground = config?.style.brand.background;
|
|
357
|
+
const coverText = config?.style.brand.colors.foreground ?? "#ffffff";
|
|
358
|
+
const coverBackgroundCss = coverBackground?.type === "solid" ? coverBackground.color : `linear-gradient(135deg, ${coverBackground?.colors[0] ?? "#8711C1"}, ${coverBackground?.colors[1] ?? "#2167E3"})`;
|
|
359
|
+
return /* @__PURE__ */ jsxs(
|
|
360
|
+
"div",
|
|
361
|
+
{
|
|
362
|
+
ref: containerRef,
|
|
363
|
+
role: "region",
|
|
364
|
+
"aria-label": ariaLabel,
|
|
365
|
+
tabIndex: 0,
|
|
366
|
+
onKeyDown: (event) => {
|
|
367
|
+
if (event.target === event.currentTarget && (event.key === " " || event.key === "Enter")) {
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
togglePlayback();
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
"data-testid": "video-player",
|
|
373
|
+
"data-status": state.status,
|
|
374
|
+
"data-finish-reason": state.finishReason,
|
|
375
|
+
"data-scenes": config?.scenes.length ?? 0,
|
|
376
|
+
"data-orientation": orientation,
|
|
377
|
+
"data-current-time": currentTime.toFixed(3),
|
|
378
|
+
"data-playing": isPlaying,
|
|
379
|
+
"data-ended": ended,
|
|
380
|
+
className,
|
|
381
|
+
style: {
|
|
382
|
+
width: width ?? "100%",
|
|
383
|
+
height: displayHeight,
|
|
384
|
+
position: "relative",
|
|
385
|
+
overflow: "hidden",
|
|
386
|
+
background: "#090712",
|
|
387
|
+
...style
|
|
388
|
+
},
|
|
389
|
+
children: [
|
|
390
|
+
generationCoverVisible ? /* @__PURE__ */ jsx(
|
|
391
|
+
"div",
|
|
392
|
+
{
|
|
393
|
+
"data-testid": "video-generation-cover",
|
|
394
|
+
role: "status",
|
|
395
|
+
"aria-live": "polite",
|
|
396
|
+
style: {
|
|
397
|
+
position: "absolute",
|
|
398
|
+
inset: 0,
|
|
399
|
+
display: "grid",
|
|
400
|
+
placeItems: "center",
|
|
401
|
+
padding: "12%",
|
|
402
|
+
textAlign: "center",
|
|
403
|
+
color: coverText,
|
|
404
|
+
background: coverBackgroundCss,
|
|
405
|
+
fontFamily: fontStack(config?.style.brand.font)
|
|
406
|
+
},
|
|
407
|
+
children: /* @__PURE__ */ jsxs("div", { children: [
|
|
408
|
+
/* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { fontSize: "clamp(28px, 8vw, 72px)", opacity: 0.9 }, children: "\u2726" }),
|
|
409
|
+
/* @__PURE__ */ jsx("strong", { style: { display: "block", marginTop: 18, fontSize: "clamp(24px, 5vw, 56px)", lineHeight: 1.08 }, children: "Creating your video\u2026" }),
|
|
410
|
+
/* @__PURE__ */ jsx("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." })
|
|
411
|
+
] })
|
|
412
|
+
}
|
|
413
|
+
) : config?.scenes.length ? /* @__PURE__ */ jsx(
|
|
414
|
+
VideoFrame,
|
|
415
|
+
{
|
|
416
|
+
kit,
|
|
417
|
+
config: displayConfig,
|
|
418
|
+
time: currentTime,
|
|
419
|
+
width: dimensions.width,
|
|
420
|
+
height: dimensions.height,
|
|
421
|
+
playing: isPlaying,
|
|
422
|
+
style: {
|
|
423
|
+
position: "absolute",
|
|
424
|
+
inset: 0,
|
|
425
|
+
transform: `scale(${scale})`,
|
|
426
|
+
transformOrigin: "top left"
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
) : null,
|
|
430
|
+
config?.audio ? /* @__PURE__ */ jsx(
|
|
431
|
+
"audio",
|
|
432
|
+
{
|
|
433
|
+
ref: audioRef,
|
|
434
|
+
src: config.audio.audioUrl,
|
|
435
|
+
autoPlay: isPlaying,
|
|
436
|
+
muted: isMuted,
|
|
437
|
+
preload: "auto",
|
|
438
|
+
loop: state.status === "streaming"
|
|
439
|
+
}
|
|
440
|
+
) : null,
|
|
441
|
+
!generationCoverVisible ? /* @__PURE__ */ jsxs(
|
|
442
|
+
"div",
|
|
443
|
+
{
|
|
444
|
+
"data-testid": "video-controls",
|
|
445
|
+
style: {
|
|
446
|
+
position: "absolute",
|
|
447
|
+
right: 10,
|
|
448
|
+
bottom: 10,
|
|
449
|
+
zIndex: 2,
|
|
450
|
+
display: "flex",
|
|
451
|
+
alignItems: "center",
|
|
452
|
+
gap: 6,
|
|
453
|
+
minHeight: 48,
|
|
454
|
+
padding: "6px 8px",
|
|
455
|
+
border: "1px solid rgba(255, 255, 255, 0.2)",
|
|
456
|
+
borderRadius: 12,
|
|
457
|
+
backgroundColor: "rgba(9, 7, 18, 0.82)",
|
|
458
|
+
backdropFilter: "blur(12px)",
|
|
459
|
+
color: "#ffffff",
|
|
460
|
+
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.35)"
|
|
461
|
+
},
|
|
462
|
+
children: [
|
|
463
|
+
/* @__PURE__ */ jsx(
|
|
464
|
+
"button",
|
|
465
|
+
{
|
|
466
|
+
type: "button",
|
|
467
|
+
"aria-label": ended ? "Replay video response" : isPlaying ? "Pause video response" : "Play video response",
|
|
468
|
+
onClick: togglePlayback,
|
|
469
|
+
style: controlButtonStyle,
|
|
470
|
+
children: ended ? "\u21BB" : isPlaying ? "\u2161" : "\u25B6"
|
|
471
|
+
}
|
|
472
|
+
),
|
|
473
|
+
config?.audio ? /* @__PURE__ */ jsx(
|
|
474
|
+
"button",
|
|
475
|
+
{
|
|
476
|
+
type: "button",
|
|
477
|
+
"aria-label": isMuted ? "Unmute video response" : "Mute video response",
|
|
478
|
+
"aria-pressed": !isMuted,
|
|
479
|
+
onClick: () => setIsMuted((muted) => !muted),
|
|
480
|
+
style: controlButtonStyle,
|
|
481
|
+
children: isMuted ? "\u{1F507}" : "\u{1F50A}"
|
|
482
|
+
}
|
|
483
|
+
) : null,
|
|
484
|
+
/* @__PURE__ */ jsx(
|
|
485
|
+
"button",
|
|
486
|
+
{
|
|
487
|
+
type: "button",
|
|
488
|
+
"aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
|
|
489
|
+
onClick: () => void toggleFullscreen(),
|
|
490
|
+
style: controlButtonStyle,
|
|
491
|
+
children: isFullscreen ? "\u2199" : "\u26F6"
|
|
492
|
+
}
|
|
493
|
+
)
|
|
494
|
+
]
|
|
495
|
+
}
|
|
496
|
+
) : null
|
|
497
|
+
]
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
function VideoPlayer({
|
|
502
|
+
templates,
|
|
503
|
+
stream,
|
|
504
|
+
video,
|
|
505
|
+
onComplete,
|
|
506
|
+
onError,
|
|
507
|
+
...props
|
|
508
|
+
}) {
|
|
509
|
+
const savedVideo = useMemo(() => video ? parseVideo(video) : void 0, [video]);
|
|
510
|
+
const kit = useMemo(
|
|
511
|
+
() => templates ? overlayTemplateRegistry(BUILTIN_TEMPLATE_KIT, templates) : BUILTIN_TEMPLATE_KIT,
|
|
512
|
+
[templates]
|
|
513
|
+
);
|
|
514
|
+
if (!stream && !savedVideo) return null;
|
|
515
|
+
return /* @__PURE__ */ jsx(
|
|
516
|
+
VideoPlayerRuntime,
|
|
517
|
+
{
|
|
518
|
+
...props,
|
|
519
|
+
kit,
|
|
520
|
+
stream,
|
|
521
|
+
video: savedVideo,
|
|
522
|
+
onComplete: (state) => {
|
|
523
|
+
if (state.config) onComplete?.(state.config);
|
|
524
|
+
},
|
|
525
|
+
onError: (error) => onError?.(error)
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/player/video-error.ts
|
|
531
|
+
var VideoError = class extends Error {
|
|
532
|
+
code;
|
|
533
|
+
status;
|
|
534
|
+
requestId;
|
|
535
|
+
runId;
|
|
536
|
+
recoverable;
|
|
537
|
+
constructor(message, options) {
|
|
538
|
+
super(safePublicDiagnostic(message, "Video response failed"));
|
|
539
|
+
this.name = "VideoError";
|
|
540
|
+
this.code = /^[a-z][a-z0-9_]{0,63}$/.test(options.code) ? options.code : "video_failed";
|
|
541
|
+
this.status = options.status;
|
|
542
|
+
this.requestId = options.requestId?.slice(0, 128);
|
|
543
|
+
this.runId = options.runId?.slice(0, 128);
|
|
544
|
+
this.recoverable = options.recoverable;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// src/player/use-video.ts
|
|
549
|
+
import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
550
|
+
|
|
551
|
+
// src/player/stream-video.ts
|
|
552
|
+
var HTTP_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
553
|
+
"body_too_large",
|
|
554
|
+
"http_error",
|
|
555
|
+
"invalid_body",
|
|
556
|
+
"invalid_cursor",
|
|
557
|
+
"invalid_input",
|
|
558
|
+
"invalid_json",
|
|
559
|
+
"invalid_request",
|
|
560
|
+
"method_not_allowed",
|
|
561
|
+
"origin_forbidden",
|
|
562
|
+
"resume_unavailable",
|
|
563
|
+
"secret_field",
|
|
564
|
+
"unauthorized"
|
|
565
|
+
]);
|
|
566
|
+
async function responseError(response, requestId) {
|
|
567
|
+
let code = "http_error";
|
|
568
|
+
let message = `Video response endpoint failed (${response.status})`;
|
|
569
|
+
try {
|
|
570
|
+
const body = await response.json();
|
|
571
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
572
|
+
const error = body.error;
|
|
573
|
+
if (error && typeof error === "object") {
|
|
574
|
+
if ("code" in error && typeof error.code === "string" && HTTP_ERROR_CODES.has(error.code)) {
|
|
575
|
+
code = error.code;
|
|
576
|
+
}
|
|
577
|
+
if ("message" in error && typeof error.message === "string") {
|
|
578
|
+
message = safePublicDiagnostic(error.message, message);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
return new VideoError(message, { code, status: response.status, requestId });
|
|
585
|
+
}
|
|
586
|
+
function normalizeStreamError(cause, requestId, state, aborted) {
|
|
587
|
+
if (cause instanceof VideoError) return cause;
|
|
588
|
+
const causeError = cause instanceof Error ? cause : void 0;
|
|
589
|
+
const safeMessage = aborted ? causeError?.name === "TimeoutError" ? "Request timed out" : safePublicDiagnostic(causeError?.message, "Video generation was aborted") : causeError?.message.startsWith("Video response protocol") || causeError?.message.startsWith("Video response endpoint omitted protocol") ? safePublicDiagnostic(causeError.message, "Video response protocol failed") : "Video response stream failed";
|
|
590
|
+
return new VideoError(safeMessage, {
|
|
591
|
+
code: aborted ? "aborted" : "stream_failed",
|
|
592
|
+
requestId: state.requestId ?? requestId,
|
|
593
|
+
runId: state.runId,
|
|
594
|
+
recoverable: false
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
function streamVideo(options) {
|
|
598
|
+
const request = createVideoRequest(options.input, {
|
|
599
|
+
requestId: options.requestId,
|
|
600
|
+
capabilities: options.capabilities,
|
|
601
|
+
...options.resume?.state.runId != null && options.resume.state.lastSequence >= 0 ? {
|
|
602
|
+
resume: {
|
|
603
|
+
runId: options.resume.state.runId,
|
|
604
|
+
afterSequence: options.resume.state.lastSequence
|
|
605
|
+
}
|
|
606
|
+
} : {}
|
|
607
|
+
});
|
|
608
|
+
const controller = new AbortController();
|
|
609
|
+
const forwardAbort = () => controller.abort(options.signal?.reason);
|
|
610
|
+
if (options.signal?.aborted) forwardAbort();
|
|
611
|
+
else options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
612
|
+
let resolveResult;
|
|
613
|
+
let rejectResult;
|
|
614
|
+
let settled = false;
|
|
615
|
+
const result = new Promise((resolve, reject) => {
|
|
616
|
+
resolveResult = resolve;
|
|
617
|
+
rejectResult = reject;
|
|
618
|
+
});
|
|
619
|
+
const buffered = [];
|
|
620
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
621
|
+
let streamDone = false;
|
|
622
|
+
let streamError;
|
|
623
|
+
const notify = () => {
|
|
624
|
+
for (const waiter of waiters) waiter();
|
|
625
|
+
waiters.clear();
|
|
626
|
+
};
|
|
627
|
+
void (async function consume() {
|
|
628
|
+
let state = options.resume?.state ?? createVideoState();
|
|
629
|
+
try {
|
|
630
|
+
const fetcher = options.fetcher ?? fetch;
|
|
631
|
+
const headers = new Headers(options.headers);
|
|
632
|
+
headers.set("Accept", VIDEO_STREAM_CONTENT_TYPE);
|
|
633
|
+
headers.set("Content-Type", "application/json");
|
|
634
|
+
if (request.resume) headers.set("Last-Event-ID", `${request.resume.runId}:${request.resume.afterSequence}`);
|
|
635
|
+
const response = await fetcher(options.endpoint, {
|
|
636
|
+
method: "POST",
|
|
637
|
+
headers,
|
|
638
|
+
body: JSON.stringify(request),
|
|
639
|
+
signal: controller.signal,
|
|
640
|
+
credentials: options.credentials
|
|
641
|
+
});
|
|
642
|
+
if (!response.ok) throw await responseError(response, request.requestId);
|
|
643
|
+
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
644
|
+
throw new Error("Video response endpoint did not return an SSE stream");
|
|
645
|
+
}
|
|
646
|
+
const protocolVersion = response.headers.get(VIDEO_STREAM_HEADER);
|
|
647
|
+
if (protocolVersion == null) {
|
|
648
|
+
throw new Error(`Video response endpoint omitted protocol version header ${VIDEO_STREAM_HEADER}`);
|
|
649
|
+
}
|
|
650
|
+
if (protocolVersion !== VIDEO_PROTOCOL_VERSION) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`Video response protocol mismatch: expected ${VIDEO_PROTOCOL_VERSION}, received ${protocolVersion}`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
for await (const event of decodeVideoSse(response.body)) {
|
|
656
|
+
state = applyVideoEvent(state, event);
|
|
657
|
+
options.onEvent?.(event, state);
|
|
658
|
+
buffered.push(event);
|
|
659
|
+
notify();
|
|
660
|
+
if (state.status === "complete" && !settled) {
|
|
661
|
+
settled = true;
|
|
662
|
+
resolveResult(state);
|
|
663
|
+
} else if (state.status === "error" && !settled) {
|
|
664
|
+
settled = true;
|
|
665
|
+
const error = state.errors.at(-1);
|
|
666
|
+
rejectResult(new VideoError(error?.message ?? "Video response could not finish", {
|
|
667
|
+
code: error?.code ?? "generation_failed",
|
|
668
|
+
status: response.status,
|
|
669
|
+
requestId: state.requestId ?? request.requestId,
|
|
670
|
+
runId: state.runId,
|
|
671
|
+
recoverable: error?.recoverable
|
|
672
|
+
}));
|
|
673
|
+
} else if (state.status === "aborted" && !settled) {
|
|
674
|
+
settled = true;
|
|
675
|
+
rejectResult(new VideoError(state.abortReason ?? "Video generation was aborted", {
|
|
676
|
+
code: "aborted",
|
|
677
|
+
status: response.status,
|
|
678
|
+
requestId: state.requestId ?? request.requestId,
|
|
679
|
+
runId: state.runId,
|
|
680
|
+
recoverable: false
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (!["complete", "error", "aborted"].includes(state.status)) {
|
|
685
|
+
throw new Error("Video response stream ended without a terminal event");
|
|
686
|
+
}
|
|
687
|
+
if (!settled && state.status === "complete") {
|
|
688
|
+
settled = true;
|
|
689
|
+
resolveResult(state);
|
|
690
|
+
}
|
|
691
|
+
} catch (cause) {
|
|
692
|
+
const error = normalizeStreamError(cause, request.requestId, state, controller.signal.aborted);
|
|
693
|
+
if (!settled) {
|
|
694
|
+
settled = true;
|
|
695
|
+
rejectResult(error);
|
|
696
|
+
}
|
|
697
|
+
streamError = error;
|
|
698
|
+
} finally {
|
|
699
|
+
streamDone = true;
|
|
700
|
+
notify();
|
|
701
|
+
options.signal?.removeEventListener("abort", forwardAbort);
|
|
702
|
+
}
|
|
703
|
+
})();
|
|
704
|
+
const stream = {
|
|
705
|
+
async *[Symbol.asyncIterator]() {
|
|
706
|
+
let index = 0;
|
|
707
|
+
while (true) {
|
|
708
|
+
while (index < buffered.length) yield buffered[index++];
|
|
709
|
+
if (streamDone) {
|
|
710
|
+
if (streamError) throw streamError;
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
await new Promise((resolve) => waiters.add(resolve));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
return {
|
|
718
|
+
request,
|
|
719
|
+
stream,
|
|
720
|
+
result,
|
|
721
|
+
abort(reason = "user cancelled") {
|
|
722
|
+
controller.abort(new Error(reason));
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/player/use-video.ts
|
|
728
|
+
function defaultRequestId() {
|
|
729
|
+
const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
730
|
+
return `request-${id}`;
|
|
731
|
+
}
|
|
732
|
+
function errorFrom(cause) {
|
|
733
|
+
if (cause instanceof VideoError) return cause;
|
|
734
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
735
|
+
return new VideoError(error.message, { code: "video_failed" });
|
|
736
|
+
}
|
|
737
|
+
function useVideo(options = {}) {
|
|
738
|
+
const templates = useMemo2(
|
|
739
|
+
() => options.templates ? overlayTemplateRegistry(BUILTIN_TEMPLATE_KIT, options.templates) : BUILTIN_TEMPLATE_KIT,
|
|
740
|
+
[options.templates]
|
|
741
|
+
);
|
|
742
|
+
const [state, setState] = useState2(() => createVideoState());
|
|
743
|
+
const [error, setError] = useState2();
|
|
744
|
+
const [run, setRun] = useState2();
|
|
745
|
+
const activeRun = useRef2();
|
|
746
|
+
const generation = useRef2(0);
|
|
747
|
+
const mounted = useRef2(true);
|
|
748
|
+
const optionsRef = useRef2(options);
|
|
749
|
+
const templatesRef = useRef2(templates);
|
|
750
|
+
optionsRef.current = options;
|
|
751
|
+
templatesRef.current = templates;
|
|
752
|
+
const abort = useCallback((reason = "user cancelled") => {
|
|
753
|
+
generation.current += 1;
|
|
754
|
+
const current = activeRun.current;
|
|
755
|
+
activeRun.current = void 0;
|
|
756
|
+
current?.abort(reason);
|
|
757
|
+
if (current && mounted.current) {
|
|
758
|
+
setError(new VideoError(reason, {
|
|
759
|
+
code: "aborted",
|
|
760
|
+
requestId: current.request.requestId,
|
|
761
|
+
recoverable: false
|
|
762
|
+
}));
|
|
763
|
+
setState((previous) => ({ ...previous, status: "aborted", abortReason: reason }));
|
|
764
|
+
}
|
|
765
|
+
}, []);
|
|
766
|
+
const generate = useCallback((input) => {
|
|
767
|
+
const currentGeneration = generation.current + 1;
|
|
768
|
+
generation.current = currentGeneration;
|
|
769
|
+
activeRun.current?.abort("replaced by a new composition");
|
|
770
|
+
const currentOptions = optionsRef.current;
|
|
771
|
+
const initialState = createVideoState();
|
|
772
|
+
setState({ ...initialState, status: "streaming" });
|
|
773
|
+
setError(void 0);
|
|
774
|
+
const nextRun = streamVideo({
|
|
775
|
+
endpoint: currentOptions.endpoint ?? "/api/video",
|
|
776
|
+
input,
|
|
777
|
+
requestId: (currentOptions.createRequestId ?? defaultRequestId)(),
|
|
778
|
+
capabilities: currentOptions.templateIds ? { ...templatesRef.current.capabilities, templates: [...new Set(currentOptions.templateIds)] } : templatesRef.current.capabilities,
|
|
779
|
+
headers: currentOptions.headers,
|
|
780
|
+
credentials: currentOptions.credentials,
|
|
781
|
+
fetcher: currentOptions.fetcher,
|
|
782
|
+
onEvent: (_event, nextState) => {
|
|
783
|
+
if (_event.type === "scene.add" && !currentOptions.templates?.getTemplate(_event.data.scene.templateId)) {
|
|
784
|
+
preloadBuiltinTemplate(_event.data.scene.templateId);
|
|
785
|
+
}
|
|
786
|
+
if (mounted.current && generation.current === currentGeneration) setState(nextState);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
activeRun.current = nextRun;
|
|
790
|
+
setRun(nextRun);
|
|
791
|
+
void nextRun.result.then(() => {
|
|
792
|
+
if (generation.current === currentGeneration) activeRun.current = void 0;
|
|
793
|
+
}).catch((cause) => {
|
|
794
|
+
if (mounted.current && generation.current === currentGeneration) {
|
|
795
|
+
activeRun.current = void 0;
|
|
796
|
+
const error2 = errorFrom(cause);
|
|
797
|
+
setError(error2);
|
|
798
|
+
setState((previous) => ({
|
|
799
|
+
...previous,
|
|
800
|
+
status: error2.code === "aborted" ? "aborted" : "error",
|
|
801
|
+
...error2.code === "aborted" && previous.abortReason == null ? { abortReason: error2.message } : {}
|
|
802
|
+
}));
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
const result = nextRun.result.then((nextState) => {
|
|
806
|
+
if (!nextState.config) throw new Error("Video generation ended without a video");
|
|
807
|
+
return nextState.config;
|
|
808
|
+
});
|
|
809
|
+
void result.catch(() => void 0);
|
|
810
|
+
return result;
|
|
811
|
+
}, []);
|
|
812
|
+
useEffect2(() => {
|
|
813
|
+
mounted.current = true;
|
|
814
|
+
return () => {
|
|
815
|
+
mounted.current = false;
|
|
816
|
+
generation.current += 1;
|
|
817
|
+
activeRun.current?.abort("component unmounted");
|
|
818
|
+
activeRun.current = void 0;
|
|
819
|
+
};
|
|
820
|
+
}, []);
|
|
821
|
+
return {
|
|
822
|
+
generate,
|
|
823
|
+
abort,
|
|
824
|
+
video: state.config,
|
|
825
|
+
status: state.status,
|
|
826
|
+
error,
|
|
827
|
+
warnings: state.warnings,
|
|
828
|
+
playerProps: { templates, ...run ? { stream: run.stream } : {} }
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
export {
|
|
832
|
+
VideoError,
|
|
833
|
+
VideoPlayer,
|
|
834
|
+
useVideo
|
|
835
|
+
};
|