@vanillaskyai/video 0.4.1 → 0.5.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/PUBLIC-API.md +12 -5
  3. package/README.md +14 -17
  4. package/dist/{builtin-server-BRTZN4Q7.js → builtin-server-YHEZ2JRF.js} +2 -2
  5. package/dist/check-runtime.js +1 -1
  6. package/dist/{chunk-BKF3A357.js → chunk-34O5BY6X.js} +1 -1
  7. package/dist/{chunk-LHFADNWJ.js → chunk-3ROGEU5A.js} +179 -342
  8. package/dist/{chunk-XAMUOSX7.js → chunk-3RV4YKB3.js} +0 -1
  9. package/dist/{chunk-GHJEAP4O.js → chunk-66MNRUCR.js} +1 -1
  10. package/dist/{chunk-EE6PK6XC.js → chunk-AOWSXU2K.js} +1 -1
  11. package/dist/{chunk-GR4I3MN2.js → chunk-EFL34TXF.js} +3 -3
  12. package/dist/{chunk-I5YWVQ5R.js → chunk-FVTMYS6U.js} +1 -1
  13. package/dist/{chunk-STIFILQG.js → chunk-FZHMQFG3.js} +46 -38
  14. package/dist/{chunk-JW47XCRL.js → chunk-G66Z5CWR.js} +30 -72
  15. package/dist/{chunk-RBQN3VSY.js → chunk-HVCPEAQF.js} +83 -1265
  16. package/dist/{chunk-N3KUA7VP.js → chunk-K6YVQODK.js} +4 -1
  17. package/dist/{chunk-WHPZED7X.js → chunk-PNS52FL4.js} +2 -5
  18. package/dist/{chunk-S5ZU5WUU.js → chunk-ZQQQAAKP.js} +2 -2
  19. package/dist/cli.js +20 -14
  20. package/dist/compose-video-QJHKTOQ7.js +13 -0
  21. package/dist/{events-D_1F6gw5.d.ts → events-BBP30j3c.d.ts} +3 -14
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +2 -2
  24. package/dist/{kit-DLeCr3d0.d.ts → kit-DEG3fcaL.d.ts} +1 -1
  25. package/dist/react.d.ts +6 -5
  26. package/dist/react.js +999 -348
  27. package/dist/server.d.ts +3 -3
  28. package/dist/server.js +18 -79
  29. package/dist/{system-prompt-NMYHLAMJ.js → system-prompt-4I6Z5HK3.js} +1 -1
  30. package/dist/template-catalog.js +1 -1
  31. package/dist/templates.d.ts +3 -3
  32. package/dist/templates.js +87 -4
  33. package/dist/test.d.ts +3 -27
  34. package/dist/test.js +3 -3
  35. package/dist/{text-stream-UPGUD2TD.js → text-stream-J6EDDJP4.js} +3 -3
  36. package/dist/{types-Cg0Onj7l.d.ts → types-BV9IqExh.d.ts} +1 -16
  37. package/docs/agent-integration.md +4 -3
  38. package/docs/concepts.md +4 -4
  39. package/docs/custom-templates.md +2 -2
  40. package/docs/getting-started.md +13 -18
  41. package/docs/integrate-nextjs.md +12 -8
  42. package/docs/persistence.md +1 -1
  43. package/docs/prompt-and-input.md +2 -2
  44. package/docs/reference/protocol.md +9 -12
  45. package/docs/reference/provider-adapters.md +5 -5
  46. package/docs/testing.md +1 -1
  47. package/examples/nextjs-quickstart/.env.example +2 -2
  48. package/examples/nextjs-quickstart/README.md +4 -1
  49. package/examples/nextjs-quickstart/package.json +4 -4
  50. package/examples/nextjs-quickstart/src/app/api/video/route.ts +4 -2
  51. package/package.json +8 -8
  52. package/dist/chunk-CR7FE7BW.js +0 -109
  53. package/dist/compose-video-6UQ33DOO.js +0 -13
package/dist/react.js CHANGED
@@ -2,34 +2,29 @@ import {
2
2
  VIDEO_STREAM_CONTENT_TYPE,
3
3
  VIDEO_STREAM_HEADER,
4
4
  decodeVideoSse
5
- } from "./chunk-S5ZU5WUU.js";
5
+ } from "./chunk-ZQQQAAKP.js";
6
6
  import {
7
7
  applyVideoEvent,
8
+ createReplayableStream,
8
9
  createVideoState
9
- } from "./chunk-STIFILQG.js";
10
+ } from "./chunk-FZHMQFG3.js";
10
11
  import {
11
12
  safePublicDiagnostic
12
- } from "./chunk-JW47XCRL.js";
13
+ } from "./chunk-G66Z5CWR.js";
13
14
  import {
14
15
  parseVideo
15
- } from "./chunk-GHJEAP4O.js";
16
+ } from "./chunk-66MNRUCR.js";
16
17
  import {
17
18
  VIDEO_PROTOCOL_VERSION,
18
19
  createVideoRequest
19
- } from "./chunk-EE6PK6XC.js";
20
+ } from "./chunk-AOWSXU2K.js";
20
21
  import {
21
- overlayTemplateRegistry
22
- } from "./chunk-CR7FE7BW.js";
23
- import {
24
- BUILTIN_TEMPLATE_CAPABILITIES,
25
- BUILTIN_TEMPLATE_CATALOG,
26
- getBuiltinTemplateMetadata
27
- } from "./chunk-XAMUOSX7.js";
28
- import "./chunk-2E6T633S.js";
22
+ assertTemplateTransitionMetadata
23
+ } from "./chunk-2E6T633S.js";
29
24
  import {
30
25
  VideoFrame,
31
26
  getDimensions
32
- } from "./chunk-N3KUA7VP.js";
27
+ } from "./chunk-K6YVQODK.js";
33
28
  import {
34
29
  fontStack
35
30
  } from "./chunk-XWWLKRNU.js";
@@ -42,13 +37,41 @@ import "./chunk-73NTSFFI.js";
42
37
 
43
38
  // src/player/video-player.tsx
44
39
  import {
45
- useEffect,
40
+ useEffect as useEffect2,
46
41
  useMemo,
47
42
  useRef,
48
43
  useState
49
44
  } from "react";
50
45
 
51
- // src/visual-system/catalog/builtin.ts
46
+ // src/visual-system/catalog/player-kit.ts
47
+ function createPlayerTemplateRegistry(templates) {
48
+ const byId = /* @__PURE__ */ new Map();
49
+ for (const template of templates) {
50
+ assertTemplateTransitionMetadata(template);
51
+ if (!template.defaults && !template.schema) {
52
+ throw new Error(`${template.id} requires defaults or a schema`);
53
+ }
54
+ if (byId.has(template.id)) throw new Error(`Duplicate template id: ${template.id}`);
55
+ byId.set(template.id, template);
56
+ }
57
+ const frozenTemplates = Object.freeze([...templates]);
58
+ return Object.freeze({
59
+ templates: frozenTemplates,
60
+ capabilities: { templates: frozenTemplates.map(({ id }) => id) },
61
+ getTemplate: (id) => byId.get(id)
62
+ });
63
+ }
64
+ function overlayPlayerTemplateRegistry(defaults, customer) {
65
+ if (!customer) return defaults;
66
+ const customerTemplates = new Map(customer.templates.map((template) => [template.id, template]));
67
+ const defaultIds = new Set(defaults.templates.map(({ id }) => id));
68
+ return createPlayerTemplateRegistry([
69
+ ...defaults.templates.map((template) => customerTemplates.get(template.id) ?? template),
70
+ ...customer.templates.filter(({ id }) => !defaultIds.has(id))
71
+ ]);
72
+ }
73
+
74
+ // src/visual-system/catalog/builtin-player.ts
52
75
  import { createElement } from "react";
53
76
 
54
77
  // src/visual-system/catalog/builtin-loaders.generated.ts
@@ -83,7 +106,551 @@ var GENERATED_BUILTIN_TEMPLATE_LOADERS = {
83
106
  "ctaMedia": () => import("./cta-media-NY4ASBGG.js").then((module) => ({ default: module.CtaMediaTemplate }))
84
107
  };
85
108
 
86
- // src/visual-system/catalog/builtin.ts
109
+ // src/visual-system/catalog/builtin-player.generated.ts
110
+ var GENERATED_BUILTIN_PLAYER_TEMPLATES = [
111
+ {
112
+ "id": "media",
113
+ "defaults": {
114
+ "texts": "Make an impact.",
115
+ "mediaUrl": "",
116
+ "mediaKeyword": "",
117
+ "mediaType": "auto",
118
+ "mediaPoster": "",
119
+ "mediaPosition": "center",
120
+ "mediaTreatment": "cinematic",
121
+ "confetti": false
122
+ },
123
+ "usesGlobalTransition": true,
124
+ "transitionTiming": {
125
+ "entryReadyProgress": 0.2,
126
+ "holdProgress": 0.7
127
+ }
128
+ },
129
+ {
130
+ "id": "reaction",
131
+ "defaults": {
132
+ "texts": "Still editing manually?",
133
+ "reactionTag": "manual",
134
+ "mediaUrl": "",
135
+ "mediaType": "video",
136
+ "mediaPoster": ""
137
+ },
138
+ "usesGlobalTransition": true,
139
+ "transitionTiming": {
140
+ "entryReadyProgress": 0.2,
141
+ "holdProgress": 0.7
142
+ }
143
+ },
144
+ {
145
+ "id": "confetti",
146
+ "defaults": {
147
+ "texts": "Celebrate."
148
+ },
149
+ "usesGlobalTransition": true,
150
+ "transitionTiming": {
151
+ "entryReadyProgress": 0.2,
152
+ "holdProgress": 0.7
153
+ }
154
+ },
155
+ {
156
+ "id": "emojiBurst",
157
+ "defaults": {
158
+ "texts": "Let's go!"
159
+ },
160
+ "usesGlobalTransition": true,
161
+ "transitionTiming": {
162
+ "entryReadyProgress": 0.2,
163
+ "holdProgress": 0.7
164
+ }
165
+ },
166
+ {
167
+ "id": "bigNumber",
168
+ "defaults": {
169
+ "texts": "Our biggest milestone yet.",
170
+ "value": 1e3,
171
+ "confetti": false,
172
+ "label": "Total users",
173
+ "prefix": "",
174
+ "unit": "",
175
+ "mediaUrl": "",
176
+ "mediaKeyword": "",
177
+ "mediaType": "auto",
178
+ "mediaPoster": "",
179
+ "mediaPosition": "center",
180
+ "mediaTreatment": "cinematic"
181
+ },
182
+ "usesGlobalTransition": true,
183
+ "transitionTiming": {
184
+ "entryReadyProgress": 0.2,
185
+ "holdProgress": 0.7
186
+ }
187
+ },
188
+ {
189
+ "id": "barChart",
190
+ "defaults": {
191
+ "texts": "Revenue up 300%.",
192
+ "bars": [
193
+ {
194
+ "label": "Q1",
195
+ "value": 42
196
+ },
197
+ {
198
+ "label": "Q2",
199
+ "value": 58
200
+ },
201
+ {
202
+ "label": "Q3",
203
+ "value": 76
204
+ },
205
+ {
206
+ "label": "Q4",
207
+ "value": 91
208
+ }
209
+ ],
210
+ "mediaUrl": "",
211
+ "mediaKeyword": "",
212
+ "mediaType": "auto",
213
+ "mediaPoster": "",
214
+ "mediaPosition": "center",
215
+ "mediaTreatment": "cinematic"
216
+ },
217
+ "usesGlobalTransition": true,
218
+ "transitionTiming": {
219
+ "entryReadyProgress": 0.2,
220
+ "holdProgress": 0.7
221
+ }
222
+ },
223
+ {
224
+ "id": "progressRing",
225
+ "defaults": {
226
+ "texts": "Almost there.",
227
+ "value": 75,
228
+ "label": "Complete",
229
+ "unit": "%",
230
+ "mediaUrl": "",
231
+ "mediaKeyword": "",
232
+ "mediaType": "auto",
233
+ "mediaPoster": "",
234
+ "mediaPosition": "center",
235
+ "mediaTreatment": "cinematic"
236
+ },
237
+ "usesGlobalTransition": true,
238
+ "transitionTiming": {
239
+ "entryReadyProgress": 0.2,
240
+ "holdProgress": 0.7
241
+ }
242
+ },
243
+ {
244
+ "id": "phoneMockup",
245
+ "defaults": {
246
+ "texts": "See it in action.",
247
+ "screenMediaUrl": "",
248
+ "screen1Url": "",
249
+ "screen2Url": "",
250
+ "screenFit": "cover",
251
+ "screenFocusX": 50,
252
+ "screenFocusY": 50,
253
+ "screenMotion": "pushIn",
254
+ "screenCalloutText": "",
255
+ "screenCalloutX": 70,
256
+ "screenCalloutY": 35,
257
+ "mediaUrl": "",
258
+ "mediaKeyword": "",
259
+ "mediaType": "auto",
260
+ "mediaPoster": "",
261
+ "mediaPosition": "center",
262
+ "mediaTreatment": "cinematic"
263
+ },
264
+ "usesGlobalTransition": true,
265
+ "transitionTiming": {
266
+ "entryReadyProgress": 0.2,
267
+ "holdProgress": 0.7
268
+ }
269
+ },
270
+ {
271
+ "id": "webMockup",
272
+ "defaults": {
273
+ "frame": "browser",
274
+ "texts": "See it in action.",
275
+ "screenMediaUrl": "",
276
+ "screen1Url": "",
277
+ "screen2Url": "",
278
+ "screenFit": "cover",
279
+ "screenFocusX": 50,
280
+ "screenFocusY": 50,
281
+ "screenMotion": "pushIn",
282
+ "screenCalloutText": "",
283
+ "screenCalloutX": 70,
284
+ "screenCalloutY": 35,
285
+ "addressBarUrl": "",
286
+ "mediaUrl": "",
287
+ "mediaKeyword": "",
288
+ "mediaType": "auto",
289
+ "mediaPoster": "",
290
+ "mediaPosition": "center",
291
+ "mediaTreatment": "cinematic"
292
+ },
293
+ "usesGlobalTransition": true,
294
+ "transitionTiming": {
295
+ "entryReadyProgress": 0.2,
296
+ "holdProgress": 0.7
297
+ }
298
+ },
299
+ {
300
+ "id": "codeEditor",
301
+ "defaults": {
302
+ "texts": "Simple as this.",
303
+ "code": 'import { getVideoDuration } from "@vanillaskyai/video";\nimport type { Video } from "@vanillaskyai/video";\n\nexport function duration(video: Video) {\n return getVideoDuration(video);\n}',
304
+ "filename": "app.ts",
305
+ "mediaUrl": "",
306
+ "mediaKeyword": "",
307
+ "mediaType": "auto",
308
+ "mediaPoster": "",
309
+ "mediaPosition": "center",
310
+ "mediaTreatment": "cinematic"
311
+ },
312
+ "usesGlobalTransition": true,
313
+ "transitionTiming": {
314
+ "entryReadyProgress": 0.2,
315
+ "holdProgress": 0.7
316
+ }
317
+ },
318
+ {
319
+ "id": "terminal",
320
+ "defaults": {
321
+ "texts": "Ship it.",
322
+ "command": "npx vanillasky check",
323
+ "output": [],
324
+ "promptPrefix": "$",
325
+ "mediaUrl": "",
326
+ "mediaKeyword": "",
327
+ "mediaType": "auto",
328
+ "mediaPoster": "",
329
+ "mediaPosition": "center",
330
+ "mediaTreatment": "cinematic"
331
+ },
332
+ "usesGlobalTransition": true,
333
+ "transitionTiming": {
334
+ "entryReadyProgress": 0.2,
335
+ "holdProgress": 0.7
336
+ }
337
+ },
338
+ {
339
+ "id": "tweet",
340
+ "defaults": {
341
+ "authorName": "Your brand",
342
+ "authorHandle": "",
343
+ "authorVerified": false,
344
+ "message": "Just shipped the new feature \u{1F680}",
345
+ "likes": 0,
346
+ "replies": 0,
347
+ "mediaUrl": "",
348
+ "mediaKeyword": "",
349
+ "mediaType": "auto",
350
+ "mediaPoster": "",
351
+ "mediaPosition": "center",
352
+ "mediaTreatment": "cinematic"
353
+ },
354
+ "usesGlobalTransition": true,
355
+ "transitionTiming": {
356
+ "entryReadyProgress": 0.2,
357
+ "holdProgress": 0.7
358
+ }
359
+ },
360
+ {
361
+ "id": "notification",
362
+ "defaults": {
363
+ "appName": "Reminder",
364
+ "appIcon": "\u{1F514}",
365
+ "message": "Believe in your dreams, they will lead you.",
366
+ "mediaUrl": "",
367
+ "mediaKeyword": "",
368
+ "mediaType": "auto",
369
+ "mediaPoster": "",
370
+ "mediaPosition": "center",
371
+ "mediaTreatment": "cinematic"
372
+ },
373
+ "usesGlobalTransition": true,
374
+ "transitionTiming": {
375
+ "entryReadyProgress": 0.35,
376
+ "holdProgress": 0.7
377
+ }
378
+ },
379
+ {
380
+ "id": "chatMessenger",
381
+ "defaults": {
382
+ "msg1": "Have you tried it yet?",
383
+ "msg2": "Yes \u2014 it saves me hours|out",
384
+ "msg3": "Okay, send me the link",
385
+ "msg4": "",
386
+ "dateChip1": "",
387
+ "dateChip2": "",
388
+ "msg5": "",
389
+ "theme": "messenger"
390
+ },
391
+ "usesGlobalTransition": false
392
+ },
393
+ {
394
+ "id": "chatWhatsapp",
395
+ "defaults": {
396
+ "msg1": "Have you tried it yet?",
397
+ "msg2": "Yes \u2014 it saves me hours|out",
398
+ "msg3": "Okay, send me the link",
399
+ "msg4": "",
400
+ "dateChip1": "",
401
+ "dateChip2": "",
402
+ "msg5": "",
403
+ "theme": "whatsapp"
404
+ },
405
+ "usesGlobalTransition": false
406
+ },
407
+ {
408
+ "id": "milestone",
409
+ "defaults": {
410
+ "label": "Followers",
411
+ "targetNumber": 1e4,
412
+ "startNumber": 0,
413
+ "badgeText": "",
414
+ "badgeEmoji": "\u{1F389}",
415
+ "mediaUrl": "",
416
+ "mediaKeyword": "",
417
+ "mediaType": "auto",
418
+ "mediaPoster": "",
419
+ "mediaPosition": "center",
420
+ "mediaTreatment": "cinematic"
421
+ },
422
+ "usesGlobalTransition": false
423
+ },
424
+ {
425
+ "id": "reviewStack",
426
+ "defaults": {
427
+ "review1Title": "Life changing app",
428
+ "review1Body": "",
429
+ "review1Author": "",
430
+ "review2Title": "Best in class",
431
+ "review2Body": "",
432
+ "review2Author": "",
433
+ "review3Title": "Exceeded expectations",
434
+ "review3Body": "",
435
+ "review3Author": "",
436
+ "mediaUrl": "",
437
+ "mediaKeyword": "",
438
+ "mediaType": "auto",
439
+ "mediaPoster": "",
440
+ "mediaPosition": "center",
441
+ "mediaTreatment": "cinematic"
442
+ },
443
+ "usesGlobalTransition": false
444
+ },
445
+ {
446
+ "id": "testimonial",
447
+ "defaults": {
448
+ "quote": "Best decision I made this year. Our conversion rate doubled in the first week.",
449
+ "authorName": "Jessica Torres",
450
+ "authorRole": "",
451
+ "mediaUrl": "",
452
+ "mediaKeyword": "",
453
+ "mediaType": "auto",
454
+ "mediaPoster": "",
455
+ "mediaPosition": "center",
456
+ "mediaTreatment": "cinematic"
457
+ },
458
+ "usesGlobalTransition": false
459
+ },
460
+ {
461
+ "id": "incomingCall",
462
+ "defaults": {
463
+ "callerName": "Your brand",
464
+ "subtitle": "is calling....",
465
+ "declineLabel": "Decline",
466
+ "acceptLabel": "Accept",
467
+ "mediaUrl": "",
468
+ "mediaKeyword": "",
469
+ "mediaType": "auto",
470
+ "mediaPoster": "",
471
+ "mediaPosition": "center",
472
+ "mediaTreatment": "cinematic"
473
+ },
474
+ "usesGlobalTransition": false
475
+ },
476
+ {
477
+ "id": "brandMessage",
478
+ "defaults": {
479
+ "message": "We built this for you.",
480
+ "mediaUrl": "",
481
+ "mediaKeyword": "",
482
+ "mediaType": "auto",
483
+ "mediaPoster": "",
484
+ "mediaPosition": "center",
485
+ "mediaTreatment": "cinematic"
486
+ },
487
+ "usesGlobalTransition": false
488
+ },
489
+ {
490
+ "id": "promptInput",
491
+ "defaults": {
492
+ "promptText": "Make a launch video for our app",
493
+ "mediaUrl": "",
494
+ "mediaKeyword": "",
495
+ "mediaType": "auto",
496
+ "mediaPoster": "",
497
+ "mediaPosition": "center",
498
+ "mediaTreatment": "cinematic"
499
+ },
500
+ "usesGlobalTransition": false
501
+ },
502
+ {
503
+ "id": "beforeAfter",
504
+ "defaults": {
505
+ "problemLabel": "BEFORE",
506
+ "problemHeadline": "Your calendar today.",
507
+ "solutionLabel": "AFTER",
508
+ "solutionHeadline": "Calmly organized.",
509
+ "problemEmojis": [
510
+ "\u{1F4C5}",
511
+ "\u{1F630}",
512
+ "\u{1F4BC}",
513
+ "\u{1F4CA}",
514
+ "\u23F0",
515
+ "\u{1F4AC}",
516
+ "\u{1F4DE}",
517
+ "\u{1F514}"
518
+ ],
519
+ "solutionEmojis": [
520
+ "\u2728",
521
+ "\u{1F4CB}",
522
+ "\u2705",
523
+ "\u{1F3AF}"
524
+ ],
525
+ "showEmojis": true
526
+ },
527
+ "usesGlobalTransition": false
528
+ },
529
+ {
530
+ "id": "tripleStats",
531
+ "defaults": {
532
+ "texts": "By the numbers.",
533
+ "stat1Value": "10K",
534
+ "stat1Label": "Users",
535
+ "stat2Value": "99.9%",
536
+ "stat2Label": "Uptime",
537
+ "stat3Value": "<50ms",
538
+ "stat3Label": "Latency",
539
+ "mediaUrl": "",
540
+ "mediaKeyword": "",
541
+ "mediaType": "auto",
542
+ "mediaPoster": "",
543
+ "mediaPosition": "center",
544
+ "mediaTreatment": "cinematic"
545
+ },
546
+ "usesGlobalTransition": true,
547
+ "transitionTiming": {
548
+ "entryReadyProgress": 0.2,
549
+ "holdProgress": 0.7
550
+ }
551
+ },
552
+ {
553
+ "id": "problemSolution",
554
+ "defaults": {
555
+ "problemLabel": "THE PROBLEM",
556
+ "problemText": "Teams waste 40% of time in meetings",
557
+ "solutionLabel": "THE SOLUTION",
558
+ "solutionText": "AI that summarizes in seconds",
559
+ "mediaUrl": "",
560
+ "mediaKeyword": "",
561
+ "mediaType": "auto",
562
+ "mediaPoster": "",
563
+ "mediaPosition": "center",
564
+ "mediaTreatment": "cinematic"
565
+ },
566
+ "usesGlobalTransition": false
567
+ },
568
+ {
569
+ "id": "cardList",
570
+ "defaults": {
571
+ "texts": "What you get.",
572
+ "items": [
573
+ "Automate your savings on energy bills",
574
+ "Cleaner energy without raising your bill",
575
+ "Same account and service guaranteed"
576
+ ],
577
+ "itemEmojis": [],
578
+ "mediaUrl": "",
579
+ "mediaKeyword": "",
580
+ "mediaType": "auto",
581
+ "mediaPoster": "",
582
+ "mediaPosition": "center",
583
+ "mediaTreatment": "cinematic"
584
+ },
585
+ "usesGlobalTransition": true,
586
+ "transitionTiming": {
587
+ "entryReadyProgress": 0.2,
588
+ "holdProgress": 0.7
589
+ }
590
+ },
591
+ {
592
+ "id": "steps",
593
+ "defaults": {
594
+ "texts": "How it works.",
595
+ "steps": [
596
+ "Describe",
597
+ "Preview",
598
+ "Export"
599
+ ],
600
+ "stepEmojis": [],
601
+ "mediaUrl": "",
602
+ "mediaKeyword": "",
603
+ "mediaType": "auto",
604
+ "mediaPoster": "",
605
+ "mediaPosition": "center",
606
+ "mediaTreatment": "cinematic"
607
+ },
608
+ "usesGlobalTransition": true,
609
+ "transitionTiming": {
610
+ "entryReadyProgress": 0.2,
611
+ "holdProgress": 0.7
612
+ }
613
+ },
614
+ {
615
+ "id": "ctaLogo",
616
+ "defaults": {
617
+ "url": "",
618
+ "cta": "",
619
+ "mediaUrl": "",
620
+ "mediaKeyword": "",
621
+ "mediaType": "auto",
622
+ "mediaPoster": "",
623
+ "mediaPosition": "center",
624
+ "mediaTreatment": "cinematic"
625
+ },
626
+ "usesGlobalTransition": true,
627
+ "transitionTiming": {
628
+ "entryReadyProgress": 0.2,
629
+ "holdProgress": 0.7
630
+ }
631
+ },
632
+ {
633
+ "id": "ctaMedia",
634
+ "defaults": {
635
+ "headline": "Make every moment count",
636
+ "url": "",
637
+ "cta": "",
638
+ "mediaUrl": "",
639
+ "mediaKeyword": "",
640
+ "mediaType": "auto",
641
+ "mediaPoster": "",
642
+ "mediaPosition": "center",
643
+ "mediaTreatment": "cinematic"
644
+ },
645
+ "usesGlobalTransition": true,
646
+ "transitionTiming": {
647
+ "entryReadyProgress": 0.2,
648
+ "holdProgress": 0.7
649
+ }
650
+ }
651
+ ];
652
+
653
+ // src/visual-system/catalog/builtin-player.ts
87
654
  function createPreloadableBuiltinTemplate(loader) {
88
655
  let loaded;
89
656
  let failure;
@@ -109,35 +676,27 @@ function createPreloadableBuiltinTemplate(loader) {
109
676
  };
110
677
  return { component, preload };
111
678
  }
112
- function freezeTemplate(value) {
679
+ function freezeValue(value) {
113
680
  if (value && typeof value === "object" && !Object.isFrozen(value)) {
114
- for (const nested of Object.values(value)) freezeTemplate(nested);
681
+ for (const nested of Object.values(value)) freezeValue(nested);
115
682
  Object.freeze(value);
116
683
  }
117
684
  return value;
118
685
  }
119
686
  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
- })
687
+ GENERATED_BUILTIN_PLAYER_TEMPLATES.map(({ id }) => [
688
+ id,
689
+ createPreloadableBuiltinTemplate(GENERATED_BUILTIN_TEMPLATE_LOADERS[id])
690
+ ])
124
691
  );
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
692
+ var builtinPlayerTemplates = Object.freeze(
693
+ GENERATED_BUILTIN_PLAYER_TEMPLATES.map((template) => Object.freeze({
694
+ ...template,
695
+ defaults: freezeValue({ ...template.defaults }),
696
+ component: preloadableById.get(template.id).component
131
697
  }))
132
698
  );
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
- });
699
+ var BUILTIN_PLAYER_KIT = createPlayerTemplateRegistry(builtinPlayerTemplates);
141
700
  function preloadBuiltinTemplate(id) {
142
701
  return preloadableById.get(id)?.preload();
143
702
  }
@@ -148,16 +707,26 @@ function warmSceneMedia(variables) {
148
707
  });
149
708
  }
150
709
 
151
- // src/player/video-player.tsx
152
- import { jsx, jsxs } from "react/jsx-runtime";
153
- var MINIMUM_GENERATION_INTRO_MS = 3e3;
154
- function savedVideoState(video) {
710
+ // src/player/playback-policy.ts
711
+ function resolvePlaybackPolicy({
712
+ playbackMode,
713
+ autoPlay,
714
+ startMuted,
715
+ audioUnlocked,
716
+ reducedMotion,
717
+ hasStream
718
+ }) {
719
+ const resolvedStartMuted = playbackMode === "muted-autoplay" ? true : playbackMode ? false : startMuted;
720
+ const shouldAutoPlay = playbackMode === "manual" ? false : playbackMode === "autoplay-after-interaction" ? audioUnlocked : playbackMode === "muted-autoplay" || playbackMode === "autoplay-with-sound" ? true : autoPlay;
155
721
  return {
156
- ...createVideoState(),
157
- status: "complete",
158
- config: video
722
+ resolvedStartMuted,
723
+ shouldAutoPlay,
724
+ autoStartGeneration: Boolean(playbackMode && hasStream && shouldAutoPlay && !reducedMotion)
159
725
  };
160
726
  }
727
+
728
+ // src/player/player-controls.tsx
729
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
161
730
  function PlayerIcon({ name, size = 22 }) {
162
731
  const shared = {
163
732
  width: size,
@@ -198,6 +767,312 @@ function PlayerIcon({ name, size = 22 }) {
198
767
  }
199
768
  return /* @__PURE__ */ jsx("svg", { ...shared, children: /* @__PURE__ */ jsx("path", { d: "M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" }) });
200
769
  }
770
+ var startButtonStyle = {
771
+ display: "inline-flex",
772
+ alignItems: "center",
773
+ gap: 10,
774
+ minHeight: 60,
775
+ padding: "14px 22px",
776
+ border: "1px solid rgba(9, 7, 18, 0.08)",
777
+ borderRadius: 999,
778
+ backgroundColor: "#ffffff",
779
+ color: "#090712",
780
+ boxShadow: "0 8px 28px rgba(0, 0, 0, 0.24)",
781
+ font: "700 16px/1 system-ui, sans-serif",
782
+ whiteSpace: "nowrap",
783
+ cursor: "pointer"
784
+ };
785
+ var startButtonPositionStyle = {
786
+ position: "absolute",
787
+ left: "50%",
788
+ top: "50%",
789
+ zIndex: 3,
790
+ transform: "translate(-50%, -50%)"
791
+ };
792
+ function playLabel(config, isMuted) {
793
+ return config?.audio && !isMuted ? "Play with sound" : "Play video";
794
+ }
795
+ function playAriaLabel(config, isMuted) {
796
+ return config?.audio && !isMuted ? "Play video with sound" : "Play video response";
797
+ }
798
+ function GenerationCover({
799
+ visible,
800
+ config,
801
+ isMuted,
802
+ startRequested,
803
+ isPlaying,
804
+ onStart
805
+ }) {
806
+ if (!visible) return null;
807
+ const background = config?.style.brand.background;
808
+ const backgroundCss = background?.type === "solid" ? background.color : `linear-gradient(135deg, ${background?.colors[0] ?? "#8711C1"}, ${background?.colors[1] ?? "#2167E3"})`;
809
+ return /* @__PURE__ */ jsxs(
810
+ "div",
811
+ {
812
+ "data-testid": "video-generation-cover",
813
+ role: "status",
814
+ "aria-live": "polite",
815
+ style: {
816
+ position: "absolute",
817
+ inset: 0,
818
+ display: "grid",
819
+ placeItems: "center",
820
+ padding: "12%",
821
+ textAlign: "center",
822
+ color: config?.style.brand.colors.foreground ?? "#ffffff",
823
+ background: backgroundCss,
824
+ fontFamily: fontStack(config?.style.brand.font)
825
+ },
826
+ children: [
827
+ /* @__PURE__ */ jsxs("div", { style: { position: "absolute", top: "28%", left: "10%", right: "10%" }, children: [
828
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { fontSize: "clamp(28px, 8vw, 72px)", opacity: 0.9 }, children: "\u2726" }),
829
+ /* @__PURE__ */ jsx("strong", { style: { display: "block", marginTop: 18, fontSize: "clamp(24px, 5vw, 56px)", lineHeight: 1.08 }, children: "Creating your video\u2026" }),
830
+ /* @__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." })
831
+ ] }),
832
+ !startRequested && !isPlaying ? /* @__PURE__ */ jsxs(
833
+ "button",
834
+ {
835
+ type: "button",
836
+ "aria-label": playAriaLabel(config, isMuted),
837
+ onClick: onStart,
838
+ style: { ...startButtonStyle, ...startButtonPositionStyle },
839
+ children: [
840
+ /* @__PURE__ */ jsx(PlayerIcon, { name: "play", size: 20 }),
841
+ playLabel(config, isMuted)
842
+ ]
843
+ }
844
+ ) : null
845
+ ]
846
+ }
847
+ );
848
+ }
849
+ function StartPosterButton({
850
+ visible,
851
+ config,
852
+ isMuted,
853
+ onStart
854
+ }) {
855
+ if (!visible) return null;
856
+ return /* @__PURE__ */ jsxs(
857
+ "button",
858
+ {
859
+ type: "button",
860
+ "data-testid": "video-start-button",
861
+ "aria-label": playAriaLabel(config, isMuted),
862
+ onClick: onStart,
863
+ style: { ...startButtonStyle, ...startButtonPositionStyle },
864
+ children: [
865
+ /* @__PURE__ */ jsx(PlayerIcon, { name: "play", size: 20 }),
866
+ playLabel(config, isMuted)
867
+ ]
868
+ }
869
+ );
870
+ }
871
+ function EndedOverlay({ ended, onReplay }) {
872
+ if (!ended) return null;
873
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
874
+ /* @__PURE__ */ jsx(
875
+ "div",
876
+ {
877
+ "data-testid": "video-ended-scrim",
878
+ "aria-hidden": "true",
879
+ style: {
880
+ position: "absolute",
881
+ inset: 0,
882
+ zIndex: 1,
883
+ pointerEvents: "none",
884
+ background: "rgba(4, 3, 18, 0.52)",
885
+ backdropFilter: "blur(4px)"
886
+ }
887
+ }
888
+ ),
889
+ /* @__PURE__ */ jsxs(
890
+ "button",
891
+ {
892
+ type: "button",
893
+ "data-testid": "video-replay-button",
894
+ "aria-label": "Replay video response",
895
+ onClick: onReplay,
896
+ style: { ...startButtonStyle, ...startButtonPositionStyle, zIndex: 3 },
897
+ children: [
898
+ /* @__PURE__ */ jsx(PlayerIcon, { name: "replay", size: 21 }),
899
+ "Replay"
900
+ ]
901
+ }
902
+ )
903
+ ] });
904
+ }
905
+ function PlayerControls({
906
+ visible,
907
+ displayWidth,
908
+ ended,
909
+ isPlaying,
910
+ isMuted,
911
+ isFullscreen,
912
+ hasAudio,
913
+ onTogglePlayback,
914
+ onToggleMuted,
915
+ onToggleFullscreen
916
+ }) {
917
+ if (!visible) return null;
918
+ const size = Math.max(40, Math.min(52, Math.round(displayWidth * 0.15)));
919
+ const inset = Math.max(10, Math.min(20, Math.round(displayWidth * 0.056)));
920
+ const buttonStyle = {
921
+ display: "inline-grid",
922
+ placeItems: "center",
923
+ width: size,
924
+ height: size,
925
+ minWidth: size,
926
+ minHeight: size,
927
+ padding: 0,
928
+ border: "1px solid rgba(255, 255, 255, 0.08)",
929
+ borderRadius: 999,
930
+ backgroundColor: "rgba(255, 255, 255, 0.16)",
931
+ color: "#ffffff",
932
+ boxShadow: "0 5px 18px rgba(0, 0, 0, 0.16)",
933
+ backdropFilter: "blur(12px)",
934
+ cursor: "pointer"
935
+ };
936
+ return /* @__PURE__ */ jsxs(
937
+ "div",
938
+ {
939
+ "data-testid": "video-controls",
940
+ style: { position: "absolute", inset: 0, zIndex: 4, pointerEvents: "none" },
941
+ children: [
942
+ /* @__PURE__ */ jsx(
943
+ "div",
944
+ {
945
+ "data-testid": "video-primary-controls",
946
+ style: { position: "absolute", left: inset, bottom: inset, display: "flex", pointerEvents: "auto" },
947
+ children: /* @__PURE__ */ jsx(
948
+ "button",
949
+ {
950
+ type: "button",
951
+ "aria-label": ended ? "Play video response from beginning" : isPlaying ? "Pause video response" : "Play video response",
952
+ onClick: onTogglePlayback,
953
+ style: buttonStyle,
954
+ children: /* @__PURE__ */ jsx(PlayerIcon, { name: isPlaying ? "pause" : "play" })
955
+ }
956
+ )
957
+ }
958
+ ),
959
+ /* @__PURE__ */ jsxs(
960
+ "div",
961
+ {
962
+ "data-testid": "video-secondary-controls",
963
+ style: { position: "absolute", right: inset, bottom: inset, display: "flex", gap: 10, pointerEvents: "auto" },
964
+ children: [
965
+ hasAudio ? /* @__PURE__ */ jsx(
966
+ "button",
967
+ {
968
+ type: "button",
969
+ "aria-label": isMuted ? "Unmute video response" : "Mute video response",
970
+ "aria-pressed": !isMuted,
971
+ onClick: onToggleMuted,
972
+ style: buttonStyle,
973
+ children: /* @__PURE__ */ jsx(PlayerIcon, { name: isMuted ? "volume-off" : "volume" })
974
+ }
975
+ ) : null,
976
+ /* @__PURE__ */ jsx(
977
+ "button",
978
+ {
979
+ type: "button",
980
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
981
+ onClick: onToggleFullscreen,
982
+ style: buttonStyle,
983
+ children: /* @__PURE__ */ jsx(PlayerIcon, { name: isFullscreen ? "exit-fullscreen" : "enter-fullscreen" })
984
+ }
985
+ )
986
+ ]
987
+ }
988
+ )
989
+ ]
990
+ }
991
+ );
992
+ }
993
+
994
+ // src/player/use-playback-clock.ts
995
+ import { useEffect } from "react";
996
+ function usePlaybackClock({
997
+ isPlaying,
998
+ stateRef,
999
+ timeRef,
1000
+ audioRef,
1001
+ loopRef,
1002
+ sceneIndexRef,
1003
+ callbacksRef,
1004
+ setCurrentTime,
1005
+ setIsPlaying
1006
+ }) {
1007
+ useEffect(() => {
1008
+ if (!isPlaying) return;
1009
+ let frame = 0;
1010
+ let previous = performance.now();
1011
+ const tick = (now) => {
1012
+ const current = stateRef.current;
1013
+ const config = current.config;
1014
+ const delta = Math.max(0, (now - previous) / 1e3);
1015
+ previous = now;
1016
+ const settled = current.status === "complete" || current.status === "error" || current.status === "aborted";
1017
+ const looping = loopRef.current && settled;
1018
+ let wrapped = false;
1019
+ if (config?.scenes.length) {
1020
+ const duration2 = getVideoDuration(config);
1021
+ const raw = timeRef.current + delta;
1022
+ let nextTime;
1023
+ if (looping && duration2 > 0 && raw >= duration2) {
1024
+ nextTime = raw % duration2;
1025
+ wrapped = true;
1026
+ } else {
1027
+ nextTime = Math.min(raw, duration2);
1028
+ }
1029
+ if (nextTime !== timeRef.current) {
1030
+ timeRef.current = nextTime;
1031
+ setCurrentTime(nextTime);
1032
+ }
1033
+ const audio = audioRef.current;
1034
+ if (audio && (current.status === "complete" || current.status === "error")) {
1035
+ if (wrapped) {
1036
+ audio.currentTime = 0;
1037
+ if (audio.paused) void audio.play().catch(Boolean);
1038
+ }
1039
+ const fadeSeconds = Math.max(0, (config.audio?.fadeOutMs ?? 3e3) / 1e3);
1040
+ const remaining = Math.max(0, duration2 - nextTime);
1041
+ const baseVolume = config.audio?.volume ?? 1;
1042
+ audio.volume = fadeSeconds > 0 ? baseVolume * Math.min(1, remaining / fadeSeconds) : baseVolume;
1043
+ if (remaining <= 0 && !looping) audio.pause();
1044
+ }
1045
+ const notify = callbacksRef.current.onSceneChange;
1046
+ if (notify) {
1047
+ if (wrapped) sceneIndexRef.current = -1;
1048
+ const ranges = resolveVideoTimeline(config);
1049
+ const index = ranges.findIndex((range) => nextTime >= range.start && nextTime < range.end);
1050
+ const resolved = index === -1 && nextTime >= duration2 ? ranges.length - 1 : index;
1051
+ if (resolved !== -1 && resolved !== sceneIndexRef.current) {
1052
+ sceneIndexRef.current = resolved;
1053
+ notify(ranges[resolved].scene, resolved);
1054
+ }
1055
+ }
1056
+ }
1057
+ const duration = current.config ? getVideoDuration(current.config) : 0;
1058
+ if (!settled || looping || timeRef.current < duration) frame = requestAnimationFrame(tick);
1059
+ else setIsPlaying(false);
1060
+ };
1061
+ frame = requestAnimationFrame(tick);
1062
+ return () => cancelAnimationFrame(frame);
1063
+ }, [isPlaying]);
1064
+ }
1065
+
1066
+ // src/player/video-player.tsx
1067
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1068
+ var MINIMUM_GENERATION_INTRO_MS = 3e3;
1069
+ function savedVideoState(video) {
1070
+ return {
1071
+ ...createVideoState(),
1072
+ status: "complete",
1073
+ config: video
1074
+ };
1075
+ }
201
1076
  function VideoPlayerRuntime({
202
1077
  kit,
203
1078
  stream,
@@ -221,9 +1096,14 @@ function VideoPlayerRuntime({
221
1096
  () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
222
1097
  );
223
1098
  const [audioUnlocked, setAudioUnlocked] = useState(false);
224
- const resolvedStartMuted = playbackMode === "muted-autoplay" ? true : playbackMode ? false : startMuted;
225
- const shouldAutoPlay = playbackMode === "manual" ? false : playbackMode === "autoplay-after-interaction" ? audioUnlocked : playbackMode === "muted-autoplay" || playbackMode === "autoplay-with-sound" ? true : autoPlay;
226
- const autoStartGeneration = Boolean(playbackMode && stream && shouldAutoPlay && !reducedMotion);
1099
+ const { resolvedStartMuted, shouldAutoPlay, autoStartGeneration } = resolvePlaybackPolicy({
1100
+ playbackMode,
1101
+ autoPlay,
1102
+ startMuted,
1103
+ audioUnlocked,
1104
+ reducedMotion,
1105
+ hasStream: Boolean(stream)
1106
+ });
227
1107
  const [isPlaying, setIsPlaying] = useState(() => shouldAutoPlay && !reducedMotion && !autoStartGeneration);
228
1108
  const [isMuted, setIsMuted] = useState(resolvedStartMuted);
229
1109
  const [fullscreenMode, setFullscreenMode] = useState("none");
@@ -276,7 +1156,7 @@ function VideoPlayerRuntime({
276
1156
  setGenerationIntroComplete(!playbackMode || !stream);
277
1157
  introStartedAtRef.current = autoStartReplacement ? performance.now() : null;
278
1158
  }
279
- useEffect(() => {
1159
+ useEffect2(() => {
280
1160
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
281
1161
  const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
282
1162
  const handleChange = (event) => {
@@ -291,11 +1171,11 @@ function VideoPlayerRuntime({
291
1171
  preference.addEventListener?.("change", handleChange);
292
1172
  return () => preference.removeEventListener?.("change", handleChange);
293
1173
  }, []);
294
- useEffect(() => {
1174
+ useEffect2(() => {
295
1175
  import("./control-visibility-NQOWTKOQ.js");
296
1176
  }, []);
297
- useEffect(() => setIsMuted(resolvedStartMuted), [resolvedStartMuted]);
298
- useEffect(() => {
1177
+ useEffect2(() => setIsMuted(resolvedStartMuted), [resolvedStartMuted]);
1178
+ useEffect2(() => {
299
1179
  let cancelled = false;
300
1180
  let started = false;
301
1181
  const reset = video ? savedVideoState(video) : createVideoState();
@@ -316,12 +1196,7 @@ function VideoPlayerRuntime({
316
1196
  for await (const event of stream) {
317
1197
  if (cancelled) return;
318
1198
  const current = stateRef.current;
319
- const mutableSceneIds = new Set(
320
- current.config ? resolveVideoTimeline(current.config).filter((range) => range.end > timeRef.current + 1e-3).map((range) => range.scene.id) : []
321
- );
322
- const next = applyVideoEvent(current, event, {
323
- ...current.config ? { mutableSceneIds } : {}
324
- });
1199
+ const next = applyVideoEvent(current, event);
325
1200
  stateRef.current = next;
326
1201
  setReplacementPending(false);
327
1202
  setState(next);
@@ -350,7 +1225,7 @@ function VideoPlayerRuntime({
350
1225
  }
351
1226
  };
352
1227
  }, [stream, video]);
353
- useEffect(() => {
1228
+ useEffect2(() => {
354
1229
  if (width != null) return;
355
1230
  const container = containerRef.current;
356
1231
  if (!container) return;
@@ -363,70 +1238,23 @@ function VideoPlayerRuntime({
363
1238
  observer.observe(container);
364
1239
  return () => observer.disconnect();
365
1240
  }, [width]);
366
- useEffect(() => {
367
- if (!isPlaying) return;
368
- let frame = 0;
369
- let previous = performance.now();
370
- const tick = (now) => {
371
- const current = stateRef.current;
372
- const config2 = current.config;
373
- const delta = Math.max(0, (now - previous) / 1e3);
374
- previous = now;
375
- const settled = current.status === "complete" || current.status === "error" || current.status === "aborted";
376
- const looping = loopRef.current && settled;
377
- let wrapped = false;
378
- if (config2?.scenes.length) {
379
- const duration3 = getVideoDuration(config2);
380
- const raw = timeRef.current + delta;
381
- let nextTime;
382
- if (looping && duration3 > 0 && raw >= duration3) {
383
- nextTime = raw % duration3;
384
- wrapped = true;
385
- } else {
386
- nextTime = Math.min(raw, duration3);
387
- }
388
- if (nextTime !== timeRef.current) {
389
- timeRef.current = nextTime;
390
- setCurrentTime(nextTime);
391
- }
392
- const audio = audioRef.current;
393
- if (audio && (current.status === "complete" || current.status === "error")) {
394
- if (wrapped) {
395
- audio.currentTime = 0;
396
- if (audio.paused) void audio.play().catch(Boolean);
397
- }
398
- const fadeSeconds = Math.max(0, (config2.audio?.fadeOutMs ?? 3e3) / 1e3);
399
- const remaining = Math.max(0, duration3 - nextTime);
400
- const baseVolume = config2.audio?.volume ?? 1;
401
- audio.volume = fadeSeconds > 0 ? baseVolume * Math.min(1, remaining / fadeSeconds) : baseVolume;
402
- if (remaining <= 0 && !looping) audio.pause();
403
- }
404
- const notify = callbacksRef.current.onSceneChange;
405
- if (notify) {
406
- if (wrapped) sceneIndexRef.current = -1;
407
- const ranges = resolveVideoTimeline(config2);
408
- const index = ranges.findIndex((range) => nextTime >= range.start && nextTime < range.end);
409
- const resolved = index === -1 && nextTime >= duration3 ? ranges.length - 1 : index;
410
- if (resolved !== -1 && resolved !== sceneIndexRef.current) {
411
- sceneIndexRef.current = resolved;
412
- notify(ranges[resolved].scene, resolved);
413
- }
414
- }
415
- }
416
- const terminal2 = settled;
417
- const duration2 = current.config ? getVideoDuration(current.config) : 0;
418
- if (!terminal2 || looping || timeRef.current < duration2) frame = requestAnimationFrame(tick);
419
- else setIsPlaying(false);
420
- };
421
- frame = requestAnimationFrame(tick);
422
- return () => cancelAnimationFrame(frame);
423
- }, [isPlaying]);
424
- useEffect(() => {
1241
+ usePlaybackClock({
1242
+ isPlaying,
1243
+ stateRef,
1244
+ timeRef,
1245
+ audioRef,
1246
+ loopRef,
1247
+ sceneIndexRef,
1248
+ callbacksRef,
1249
+ setCurrentTime,
1250
+ setIsPlaying
1251
+ });
1252
+ useEffect2(() => {
425
1253
  const audio = audioRef.current;
426
1254
  if (!audio) return;
427
1255
  audio.volume = state.config?.audio?.volume ?? 1;
428
1256
  }, [state.config?.audio?.audioUrl, state.config?.audio?.volume]);
429
- useEffect(() => {
1257
+ useEffect2(() => {
430
1258
  const audio = audioRef.current;
431
1259
  if (!audio) return;
432
1260
  if (isPlaying || introPlaying) {
@@ -444,7 +1272,7 @@ function VideoPlayerRuntime({
444
1272
  audio.pause();
445
1273
  }
446
1274
  }, [introPlaying, isPlaying, state.config?.audio?.audioUrl]);
447
- useEffect(() => {
1275
+ useEffect2(() => {
448
1276
  if (!state.config?.scenes.length) {
449
1277
  const terminalWithoutVideo = state.status === "complete" || state.status === "error" || state.status === "aborted";
450
1278
  if (terminalWithoutVideo) {
@@ -505,7 +1333,7 @@ function VideoPlayerRuntime({
505
1333
  const firstSceneRange = config ? resolveVideoTimeline(config)[0] : void 0;
506
1334
  const hasSuppliedOpening = firstSceneRange?.scene.id === "supplied-opening";
507
1335
  const showStartPoster = (!generationIntroWaiting || hasSuppliedOpening) && !startRequested && !isPlaying && !ended && currentTime <= 1e-3 && Boolean(config?.scenes.length);
508
- const firstSceneHoldProgress = firstSceneRange ? kit.getTemplateMetadata(firstSceneRange.scene.templateId)?.transitionTiming?.holdProgress ?? 0.7 : 0;
1336
+ const firstSceneHoldProgress = firstSceneRange ? kit.getTemplate(firstSceneRange.scene.templateId)?.transitionTiming?.holdProgress ?? 0.7 : 0;
509
1337
  const posterTime = firstSceneRange ? firstSceneRange.start + Math.max(0, firstSceneRange.end - firstSceneRange.start) * firstSceneHoldProgress : currentTime;
510
1338
  const startPlayback = () => {
511
1339
  setGenerationIntroComplete(true);
@@ -565,53 +1393,10 @@ function VideoPlayerRuntime({
565
1393
  if (!container) return;
566
1394
  void import("./control-visibility-NQOWTKOQ.js").then((output) => output.togglePlayerFullscreen(container, setFullscreenMode));
567
1395
  };
568
- const controlSize = Math.max(40, Math.min(52, Math.round(displayWidth * 0.15)));
569
- const controlInset = Math.max(10, Math.min(20, Math.round(displayWidth * 0.056)));
570
- const controlButtonStyle = {
571
- display: "inline-grid",
572
- placeItems: "center",
573
- width: controlSize,
574
- height: controlSize,
575
- minWidth: controlSize,
576
- minHeight: controlSize,
577
- padding: 0,
578
- border: "1px solid rgba(255, 255, 255, 0.08)",
579
- borderRadius: 999,
580
- backgroundColor: "rgba(255, 255, 255, 0.16)",
581
- color: "#ffffff",
582
- boxShadow: "0 5px 18px rgba(0, 0, 0, 0.16)",
583
- backdropFilter: "blur(12px)",
584
- cursor: "pointer"
585
- };
586
- const startButtonStyle = {
587
- display: "inline-flex",
588
- alignItems: "center",
589
- gap: 10,
590
- minHeight: 60,
591
- padding: "14px 22px",
592
- border: "1px solid rgba(9, 7, 18, 0.08)",
593
- borderRadius: 999,
594
- backgroundColor: "#ffffff",
595
- color: "#090712",
596
- boxShadow: "0 8px 28px rgba(0, 0, 0, 0.24)",
597
- font: "700 16px/1 system-ui, sans-serif",
598
- whiteSpace: "nowrap",
599
- cursor: "pointer"
600
- };
601
- const startButtonPositionStyle = {
602
- position: "absolute",
603
- left: "50%",
604
- top: "50%",
605
- zIndex: 3,
606
- transform: "translate(-50%, -50%)"
607
- };
608
1396
  const pendingReplacement = replacementPending && state.status === "idle";
609
1397
  const displayedStatus = pendingReplacement ? "streaming" : state.status;
610
1398
  const generationCoverVisible = generationIntroWaiting && !hasSuppliedOpening || displayedStatus === "streaming" && !config?.scenes.length;
611
- const coverBackground = config?.style.brand.background;
612
- const coverText = config?.style.brand.colors.foreground ?? "#ffffff";
613
- const coverBackgroundCss = coverBackground?.type === "solid" ? coverBackground.color : `linear-gradient(135deg, ${coverBackground?.colors[0] ?? "#8711C1"}, ${coverBackground?.colors[1] ?? "#2167E3"})`;
614
- return /* @__PURE__ */ jsxs(
1399
+ return /* @__PURE__ */ jsxs2(
615
1400
  "div",
616
1401
  {
617
1402
  ref: containerRef,
@@ -651,45 +1436,18 @@ function VideoPlayerRuntime({
651
1436
  ...style
652
1437
  },
653
1438
  children: [
654
- generationCoverVisible ? /* @__PURE__ */ jsxs(
655
- "div",
1439
+ /* @__PURE__ */ jsx2(
1440
+ GenerationCover,
656
1441
  {
657
- "data-testid": "video-generation-cover",
658
- role: "status",
659
- "aria-live": "polite",
660
- style: {
661
- position: "absolute",
662
- inset: 0,
663
- display: "grid",
664
- placeItems: "center",
665
- padding: "12%",
666
- textAlign: "center",
667
- color: coverText,
668
- background: coverBackgroundCss,
669
- fontFamily: fontStack(config?.style.brand.font)
670
- },
671
- children: [
672
- /* @__PURE__ */ jsxs("div", { style: { position: "absolute", top: "28%", left: "10%", right: "10%" }, children: [
673
- /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { fontSize: "clamp(28px, 8vw, 72px)", opacity: 0.9 }, children: "\u2726" }),
674
- /* @__PURE__ */ jsx("strong", { style: { display: "block", marginTop: 18, fontSize: "clamp(24px, 5vw, 56px)", lineHeight: 1.08 }, children: "Creating your video\u2026" }),
675
- /* @__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." })
676
- ] }),
677
- !startRequested && !isPlaying ? /* @__PURE__ */ jsxs(
678
- "button",
679
- {
680
- type: "button",
681
- "aria-label": config?.audio && !isMuted ? "Play video with sound" : "Play video response",
682
- onClick: armPlayback,
683
- style: { ...startButtonStyle, ...startButtonPositionStyle },
684
- children: [
685
- /* @__PURE__ */ jsx(PlayerIcon, { name: "play", size: 20 }),
686
- config?.audio && !isMuted ? "Play with sound" : "Play video"
687
- ]
688
- }
689
- ) : null
690
- ]
1442
+ visible: generationCoverVisible,
1443
+ config,
1444
+ isMuted,
1445
+ startRequested,
1446
+ isPlaying,
1447
+ onStart: armPlayback
691
1448
  }
692
- ) : config?.scenes.length ? /* @__PURE__ */ jsx(
1449
+ ),
1450
+ !generationCoverVisible && config?.scenes.length ? /* @__PURE__ */ jsx2(
693
1451
  VideoFrame,
694
1452
  {
695
1453
  kit,
@@ -707,24 +1465,16 @@ function VideoPlayerRuntime({
707
1465
  }
708
1466
  }
709
1467
  ) : null,
710
- showStartPoster ? /* @__PURE__ */ jsxs(
711
- "button",
1468
+ /* @__PURE__ */ jsx2(
1469
+ StartPosterButton,
712
1470
  {
713
- type: "button",
714
- "data-testid": "video-start-button",
715
- "aria-label": config?.audio && !isMuted ? "Play video with sound" : "Play video response",
716
- onClick: startPlayback,
717
- style: {
718
- ...startButtonStyle,
719
- ...startButtonPositionStyle
720
- },
721
- children: [
722
- /* @__PURE__ */ jsx(PlayerIcon, { name: "play", size: 20 }),
723
- config?.audio && !isMuted ? "Play with sound" : "Play video"
724
- ]
1471
+ visible: showStartPoster,
1472
+ config,
1473
+ isMuted,
1474
+ onStart: startPlayback
725
1475
  }
726
- ) : null,
727
- config?.audio ? /* @__PURE__ */ jsx(
1476
+ ),
1477
+ config?.audio ? /* @__PURE__ */ jsx2(
728
1478
  "audio",
729
1479
  {
730
1480
  ref: audioRef,
@@ -737,103 +1487,25 @@ function VideoPlayerRuntime({
737
1487
  },
738
1488
  config.audio.audioUrl
739
1489
  ) : null,
740
- ended ? /* @__PURE__ */ jsx(
741
- "div",
1490
+ /* @__PURE__ */ jsx2(EndedOverlay, { ended, onReplay: togglePlayback }),
1491
+ /* @__PURE__ */ jsx2(
1492
+ PlayerControls,
742
1493
  {
743
- "data-testid": "video-ended-scrim",
744
- "aria-hidden": "true",
745
- style: {
746
- position: "absolute",
747
- inset: 0,
748
- zIndex: 1,
749
- pointerEvents: "none",
750
- background: "rgba(4, 3, 18, 0.52)",
751
- backdropFilter: "blur(4px)"
752
- }
1494
+ visible: !generationCoverVisible && !showStartPoster && Boolean(config?.scenes.length),
1495
+ displayWidth,
1496
+ ended,
1497
+ isPlaying,
1498
+ isMuted,
1499
+ isFullscreen,
1500
+ hasAudio: Boolean(config?.audio),
1501
+ onTogglePlayback: togglePlayback,
1502
+ onToggleMuted: () => setIsMuted((muted) => {
1503
+ if (muted) setAudioUnlocked(true);
1504
+ return !muted;
1505
+ }),
1506
+ onToggleFullscreen: toggleFullscreen
753
1507
  }
754
- ) : null,
755
- ended ? /* @__PURE__ */ jsxs(
756
- "button",
757
- {
758
- type: "button",
759
- "data-testid": "video-replay-button",
760
- "aria-label": "Replay video response",
761
- onClick: togglePlayback,
762
- style: {
763
- ...startButtonStyle,
764
- ...startButtonPositionStyle,
765
- zIndex: 3
766
- },
767
- children: [
768
- /* @__PURE__ */ jsx(PlayerIcon, { name: "replay", size: 21 }),
769
- "Replay"
770
- ]
771
- }
772
- ) : null,
773
- !generationCoverVisible && !showStartPoster && config?.scenes.length ? /* @__PURE__ */ jsxs(
774
- "div",
775
- {
776
- "data-testid": "video-controls",
777
- style: {
778
- position: "absolute",
779
- inset: 0,
780
- zIndex: 4,
781
- pointerEvents: "none"
782
- },
783
- children: [
784
- /* @__PURE__ */ jsx(
785
- "div",
786
- {
787
- "data-testid": "video-primary-controls",
788
- style: { position: "absolute", left: controlInset, bottom: controlInset, display: "flex", pointerEvents: "auto" },
789
- children: /* @__PURE__ */ jsx(
790
- "button",
791
- {
792
- type: "button",
793
- "aria-label": ended ? "Play video response from beginning" : isPlaying ? "Pause video response" : "Play video response",
794
- onClick: togglePlayback,
795
- style: controlButtonStyle,
796
- children: /* @__PURE__ */ jsx(PlayerIcon, { name: isPlaying ? "pause" : "play" })
797
- }
798
- )
799
- }
800
- ),
801
- /* @__PURE__ */ jsxs(
802
- "div",
803
- {
804
- "data-testid": "video-secondary-controls",
805
- style: { position: "absolute", right: controlInset, bottom: controlInset, display: "flex", gap: 10, pointerEvents: "auto" },
806
- children: [
807
- config?.audio ? /* @__PURE__ */ jsx(
808
- "button",
809
- {
810
- type: "button",
811
- "aria-label": isMuted ? "Unmute video response" : "Mute video response",
812
- "aria-pressed": !isMuted,
813
- onClick: () => setIsMuted((muted) => {
814
- if (muted) setAudioUnlocked(true);
815
- return !muted;
816
- }),
817
- style: controlButtonStyle,
818
- children: /* @__PURE__ */ jsx(PlayerIcon, { name: isMuted ? "volume-off" : "volume" })
819
- }
820
- ) : null,
821
- /* @__PURE__ */ jsx(
822
- "button",
823
- {
824
- type: "button",
825
- "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
826
- onClick: () => void toggleFullscreen(),
827
- style: controlButtonStyle,
828
- children: /* @__PURE__ */ jsx(PlayerIcon, { name: isFullscreen ? "exit-fullscreen" : "enter-fullscreen" })
829
- }
830
- )
831
- ]
832
- }
833
- )
834
- ]
835
- }
836
- ) : null
1508
+ )
837
1509
  ]
838
1510
  }
839
1511
  );
@@ -848,11 +1520,11 @@ function VideoPlayer({
848
1520
  }) {
849
1521
  const savedVideo = useMemo(() => video ? parseVideo(video) : void 0, [video]);
850
1522
  const kit = useMemo(
851
- () => templates ? overlayTemplateRegistry(BUILTIN_TEMPLATE_KIT, templates) : BUILTIN_TEMPLATE_KIT,
1523
+ () => overlayPlayerTemplateRegistry(BUILTIN_PLAYER_KIT, templates),
852
1524
  [templates]
853
1525
  );
854
1526
  if (!stream && !savedVideo) return null;
855
- return /* @__PURE__ */ jsx(
1527
+ return /* @__PURE__ */ jsx2(
856
1528
  VideoPlayerRuntime,
857
1529
  {
858
1530
  ...props,
@@ -886,7 +1558,7 @@ var VideoError = class extends Error {
886
1558
  };
887
1559
 
888
1560
  // src/player/use-video.ts
889
- import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
1561
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
890
1562
 
891
1563
  // src/player/stream-video.ts
892
1564
  var HTTP_ERROR_CODES = /* @__PURE__ */ new Set([
@@ -956,15 +1628,7 @@ function streamVideo(options) {
956
1628
  resolveResult = resolve;
957
1629
  rejectResult = reject;
958
1630
  });
959
- const buffered = [];
960
- const waiters = /* @__PURE__ */ new Set();
961
- let streamDone = false;
962
- let streamError;
963
- const notify = () => {
964
- for (const waiter of waiters) waiter();
965
- waiters.clear();
966
- };
967
- void (async function consume() {
1631
+ const source = (async function* consume() {
968
1632
  let state = options.resume?.state ?? createVideoState();
969
1633
  try {
970
1634
  const fetcher = options.fetcher ?? fetch;
@@ -995,8 +1659,6 @@ function streamVideo(options) {
995
1659
  for await (const event of decodeVideoSse(response.body)) {
996
1660
  state = applyVideoEvent(state, event);
997
1661
  options.onEvent?.(event, state);
998
- buffered.push(event);
999
- notify();
1000
1662
  if (state.status === "complete" && !settled) {
1001
1663
  settled = true;
1002
1664
  resolveResult(state);
@@ -1020,6 +1682,7 @@ function streamVideo(options) {
1020
1682
  recoverable: false
1021
1683
  }));
1022
1684
  }
1685
+ yield event;
1023
1686
  }
1024
1687
  if (!["complete", "error", "aborted"].includes(state.status)) {
1025
1688
  throw new Error("Video response stream ended without a terminal event");
@@ -1034,26 +1697,12 @@ function streamVideo(options) {
1034
1697
  settled = true;
1035
1698
  rejectResult(error);
1036
1699
  }
1037
- streamError = error;
1700
+ throw error;
1038
1701
  } finally {
1039
- streamDone = true;
1040
- notify();
1041
1702
  options.signal?.removeEventListener("abort", forwardAbort);
1042
1703
  }
1043
1704
  })();
1044
- const stream = {
1045
- async *[Symbol.asyncIterator]() {
1046
- let index = 0;
1047
- while (true) {
1048
- while (index < buffered.length) yield buffered[index++];
1049
- if (streamDone) {
1050
- if (streamError) throw streamError;
1051
- return;
1052
- }
1053
- await new Promise((resolve) => waiters.add(resolve));
1054
- }
1055
- }
1056
- };
1705
+ const stream = createReplayableStream(source, () => void 0);
1057
1706
  return {
1058
1707
  request,
1059
1708
  stream,
@@ -1076,13 +1725,13 @@ function errorFrom(cause) {
1076
1725
  }
1077
1726
  function useVideo(options = {}) {
1078
1727
  const templates = useMemo2(
1079
- () => options.templates ? overlayTemplateRegistry(BUILTIN_TEMPLATE_KIT, options.templates) : BUILTIN_TEMPLATE_KIT,
1728
+ () => overlayPlayerTemplateRegistry(BUILTIN_PLAYER_KIT, options.templates),
1080
1729
  [options.templates]
1081
1730
  );
1082
1731
  const [state, setState] = useState2(() => createVideoState());
1083
1732
  const [error, setError] = useState2();
1084
1733
  const [run, setRun] = useState2();
1085
- const activeRun = useRef2();
1734
+ const activeRun = useRef2(void 0);
1086
1735
  const generation = useRef2(0);
1087
1736
  const mounted = useRef2(true);
1088
1737
  const optionsRef = useRef2(options);
@@ -1124,9 +1773,8 @@ function useVideo(options = {}) {
1124
1773
  if (!currentOptions.templates?.getTemplate(_event.data.scene.templateId)) {
1125
1774
  preloadBuiltinTemplate(_event.data.scene.templateId);
1126
1775
  }
1776
+ warmSceneMedia(_event.data.scene.variables);
1127
1777
  }
1128
- const backdrop = _event.type === "scene.add" ? _event.data.scene.variables : _event.type === "asset.patch" ? _event.data.variables : _event.type === "scene.patch" ? _event.data.patch.variables : void 0;
1129
- if (backdrop) warmSceneMedia(backdrop);
1130
1778
  if (mounted.current && generation.current === currentGeneration) setState(nextState);
1131
1779
  }
1132
1780
  });
@@ -1153,7 +1801,7 @@ function useVideo(options = {}) {
1153
1801
  void result.catch(() => void 0);
1154
1802
  return result;
1155
1803
  }, []);
1156
- useEffect2(() => {
1804
+ useEffect3(() => {
1157
1805
  mounted.current = true;
1158
1806
  return () => {
1159
1807
  mounted.current = false;
@@ -1169,7 +1817,10 @@ function useVideo(options = {}) {
1169
1817
  status: state.status,
1170
1818
  error,
1171
1819
  warnings: state.warnings,
1172
- playerProps: { templates, ...run ? { stream: run.stream } : {} }
1820
+ playerProps: {
1821
+ ...options.templates ? { templates: options.templates } : {},
1822
+ ...run ? { stream: run.stream } : {}
1823
+ }
1173
1824
  };
1174
1825
  }
1175
1826
  export {