@vanillaskyai/video 0.10.22 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/PUBLIC-API.md +45 -438
  3. package/README.md +57 -96
  4. package/dist/builtin-metadata-OT6V7TB4.js +8 -0
  5. package/dist/{chapter-title-2JVDU62E.js → chapter-title-2RCXX7SN.js} +1 -2
  6. package/dist/{chunk-PDQFQIQW.js → chunk-3EQ6PVWL.js} +1 -1
  7. package/dist/chunk-4G4JBMCM.js +37 -0
  8. package/dist/{chunk-7AA2JWHZ.js → chunk-5DOQTIMD.js} +6 -6
  9. package/dist/chunk-666HGTVZ.js +41 -0
  10. package/dist/{chunk-7M56IUUX.js → chunk-6Z3ID54H.js} +0 -18
  11. package/dist/{chunk-5BSJLT6H.js → chunk-E7FHBLAC.js} +16 -29
  12. package/dist/chunk-HIQM4J3U.js +30 -0
  13. package/dist/chunk-ISWQ6T5X.js +36 -0
  14. package/dist/{chunk-224QNWRA.js → chunk-K5J7ESRO.js} +1 -10
  15. package/dist/chunk-KMVRBUL5.js +50 -0
  16. package/dist/chunk-NM4CXXZY.js +25 -0
  17. package/dist/{chunk-44WND2VP.js → chunk-V2CP7PVY.js} +19 -63
  18. package/dist/{chunk-3O7OMMMF.js → chunk-WZESNEPT.js} +0 -1
  19. package/dist/{chunk-RXTN2CW6.js → chunk-Z3DLSLAJ.js} +1 -1
  20. package/dist/cinema-media-YZ2UANJG.js +365 -0
  21. package/dist/cli.js +154 -1753
  22. package/dist/{compose-video-BH5K6XWT.js → compose-video-H2YUQDUY.js} +4 -4
  23. package/dist/{events-B4YCc4vc.d.ts → events-BZAfl0Dm.d.ts} +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +4 -6
  26. package/dist/preload-media-23JXUFCF.js +77 -0
  27. package/dist/react.d.ts +39 -15
  28. package/dist/react.js +1350 -739
  29. package/dist/scene-validation-SGLLLFY5.js +9 -0
  30. package/dist/server.d.ts +67 -117
  31. package/dist/server.js +262 -614
  32. package/dist/test.d.ts +2 -2
  33. package/dist/test.js +21 -21
  34. package/dist/{text-stream-LD364KBI.js → text-stream-SOVLYR2L.js} +2 -2
  35. package/dist/{types-BqB8zC9u.d.ts → types-CG-kPI81.d.ts} +3 -1
  36. package/dist/{types-DdZw4GRQ.d.ts → types-ht-Zw3Wv.d.ts} +1 -1
  37. package/docs/agent-integration.md +19 -64
  38. package/docs/architecture.md +82 -87
  39. package/docs/customization.md +49 -66
  40. package/docs/development.md +18 -17
  41. package/docs/errors.md +4 -4
  42. package/docs/getting-started.md +74 -93
  43. package/docs/media-and-audio.md +9 -7
  44. package/docs/performance.md +11 -6
  45. package/docs/persistence.md +3 -7
  46. package/docs/production.md +4 -15
  47. package/docs/prompt-and-input.md +1 -2
  48. package/docs/provider-integration.md +134 -181
  49. package/docs/reference/protocol.md +5 -14
  50. package/docs/reference/provider-adapters.md +21 -13
  51. package/docs/security.md +1 -14
  52. package/docs/testing.md +56 -123
  53. package/package.json +5 -27
  54. package/starters/video-chat/.env.example +12 -4
  55. package/starters/video-chat/.env.native.example +13 -0
  56. package/starters/video-chat/README.md +72 -11
  57. package/starters/video-chat/package.json +1 -1
  58. package/starters/video-chat/providers/text-native.ts +94 -0
  59. package/starters/video-chat/providers/text.ts +19 -0
  60. package/starters/video-chat/providers/transcription.ts +32 -0
  61. package/starters/video-chat/providers/video-custom.ts +45 -0
  62. package/starters/video-chat/providers/video-delivery.ts +57 -0
  63. package/starters/video-chat/providers/video-google.ts +48 -0
  64. package/starters/video-chat/providers/video-job.ts +103 -0
  65. package/starters/video-chat/providers/video-runway.ts +45 -0
  66. package/starters/video-chat/providers/video.ts +39 -63
  67. package/starters/video-chat/server.ts +3 -29
  68. package/starters/video-chat/vite.config.ts +2 -8
  69. package/styles/video-chat.css +8 -105
  70. package/dist/builtin-server-W4PLJUZ5.js +0 -8
  71. package/dist/catalog-types-WTbLP6Jh.d.ts +0 -78
  72. package/dist/check-runtime.d.ts +0 -15
  73. package/dist/check-runtime.js +0 -96
  74. package/dist/chunk-2E6T633S.js +0 -27
  75. package/dist/chunk-4YM2M62S.js +0 -13
  76. package/dist/chunk-4ZJLPHBV.js +0 -684
  77. package/dist/chunk-5JBMYQP6.js +0 -156
  78. package/dist/chunk-73NTSFFI.js +0 -81
  79. package/dist/chunk-EGVQODKU.js +0 -83
  80. package/dist/chunk-HFVNAPHZ.js +0 -35
  81. package/dist/chunk-IIN5M5HW.js +0 -697
  82. package/dist/chunk-IQMYK5DX.js +0 -133
  83. package/dist/chunk-IR44XKBI.js +0 -46
  84. package/dist/chunk-JKVOBTRO.js +0 -145
  85. package/dist/chunk-LVM5Q2DL.js +0 -68
  86. package/dist/chunk-M4QTEJTK.js +0 -709
  87. package/dist/chunk-QSBDB4J2.js +0 -16
  88. package/dist/chunk-R3XAOMKP.js +0 -29
  89. package/dist/chunk-SPVTJH3F.js +0 -24
  90. package/dist/chunk-YAHT3LST.js +0 -663
  91. package/dist/chunk-ZD2VTUYR.js +0 -28
  92. package/dist/cinema-media-HYCDG65Z.js +0 -11
  93. package/dist/comparison-XXAP4S4J.js +0 -36
  94. package/dist/editorial-timeline-VUQC6KPA.js +0 -42
  95. package/dist/key-figure-4TJK7HLT.js +0 -29
  96. package/dist/kit-DrRpdn0p.d.ts +0 -79
  97. package/dist/mobile-message-FIHAO6XC.js +0 -49
  98. package/dist/preload-media-LJXWKTWG.js +0 -57
  99. package/dist/quote-GRPIYKQJ.js +0 -32
  100. package/dist/system-prompt-AG26KJAA.js +0 -12
  101. package/dist/template-catalog.d.ts +0 -400
  102. package/dist/template-catalog.js +0 -6
  103. package/dist/templates.d.ts +0 -25
  104. package/dist/templates.js +0 -102
  105. package/dist/validate-4OUD2CLX.js +0 -10
  106. package/docs/concepts.md +0 -107
  107. package/docs/custom-templates.md +0 -346
  108. package/docs/immersive-interface.md +0 -86
  109. package/docs/maintainers/cinematic-migration.md +0 -38
  110. package/docs/motion-and-effects.md +0 -109
  111. package/docs/reference/design-system.html +0 -125
  112. package/docs/responsive-orientation.md +0 -37
  113. package/docs/streaming-protocol.md +0 -15
  114. package/examples/custom-template/README.md +0 -19
  115. package/examples/custom-template/minimal-text.tsx +0 -82
  116. package/examples/custom-template/structured-data.tsx +0 -104
  117. package/registry/items/backgrounds.json +0 -70
  118. package/registry/items/chapterTitle.json +0 -80
  119. package/registry/items/cinemaMedia.json +0 -136
  120. package/registry/items/comparison.json +0 -168
  121. package/registry/items/editorialTimeline.json +0 -172
  122. package/registry/items/keyFigure.json +0 -162
  123. package/registry/items/mobileMessage.json +0 -153
  124. package/registry/items/motion.json +0 -45
  125. package/registry/items/quote.json +0 -161
  126. package/registry/items/template-context.json +0 -31
  127. package/registry/items/theme.json +0 -47
  128. package/registry/items/typography.json +0 -47
@@ -1,9 +1,6 @@
1
1
  import {
2
2
  parseVideoEvent
3
- } from "./chunk-7M56IUUX.js";
4
- import {
5
- MAX_RETAINED_MEDIA_URL_LENGTH
6
- } from "./chunk-AUPC6MDK.js";
3
+ } from "./chunk-6Z3ID54H.js";
7
4
  import {
8
5
  VIDEO_PROTOCOL_VERSION
9
6
  } from "./chunk-7NZOMTAL.js";
@@ -139,65 +136,22 @@ function videoSseHeaders(headers) {
139
136
  return result;
140
137
  }
141
138
 
142
- // src/video-chat/deadline.ts
143
- function withDeadline(operation, timeoutMs, parent) {
144
- return new Promise((resolve, reject) => {
145
- const controller = new AbortController();
146
- let settled = false;
147
- const timer = setTimeout(() => finish({ error: new DOMException("Optional work exceeded its deadline", "TimeoutError") }), timeoutMs);
148
- const finish = (result) => {
149
- if (settled) return;
150
- settled = true;
151
- clearTimeout(timer);
152
- parent?.removeEventListener("abort", cancel);
153
- if ("error" in result) {
154
- reject(result.error);
155
- controller.abort(result.error);
156
- } else {
157
- resolve(result.value);
158
- controller.abort();
159
- }
160
- };
161
- const cancel = () => finish({ error: parent?.reason ?? new DOMException("Cancelled", "AbortError") });
162
- if (parent?.aborted) {
163
- cancel();
164
- return;
165
- }
166
- parent?.addEventListener("abort", cancel, { once: true });
167
- try {
168
- const result = operation(controller.signal);
169
- Promise.resolve(result).then((value) => finish({ value }), (error) => finish({ error }));
170
- } catch (error) {
171
- finish({ error });
172
- }
173
- });
139
+ // src/protocol/clip-budget.ts
140
+ var CLIP_NARRATION_TAIL_SEC = 0.8;
141
+ var UNSPACED_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/gu;
142
+ function estimateNarrationSeconds(text) {
143
+ const normalized = text.trim();
144
+ if (!normalized) return 0;
145
+ const characters = normalized.match(UNSPACED_SCRIPT)?.length ?? 0;
146
+ const words = normalized.replace(UNSPACED_SCRIPT, " ").match(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu)?.length ?? 0;
147
+ const pauses = normalized.match(/[.!?。!?;;::]/gu)?.length ?? 0;
148
+ return words / 2.2 + characters / 3.5 + pauses * 0.15;
174
149
  }
175
-
176
- // src/video-chat/media.ts
177
- function sanitizeVideoChatMedia(value) {
178
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
179
- const media = value;
180
- if (media.type !== "image" && media.type !== "video") return null;
181
- if (typeof media.url !== "string") return null;
182
- const url = media.url.trim();
183
- if (!safeMediaUrl(url)) return null;
184
- if (media.posterUrl != null && typeof media.posterUrl !== "string") return null;
185
- const posterUrl = typeof media.posterUrl === "string" ? media.posterUrl.trim() : "";
186
- if (posterUrl && !safeMediaUrl(posterUrl)) return null;
187
- return {
188
- url,
189
- type: media.type,
190
- ...posterUrl ? { posterUrl } : {}
191
- };
150
+ function speechFitsClip(seconds, durationSec) {
151
+ return Number.isFinite(seconds) && seconds >= 0 && Number.isFinite(durationSec) && durationSec > 0 && seconds + CLIP_NARRATION_TAIL_SEC <= durationSec + 1e-6;
192
152
  }
193
- function safeMediaUrl(value) {
194
- if (!value || value.length > MAX_RETAINED_MEDIA_URL_LENGTH) return false;
195
- try {
196
- const protocol = new URL(value).protocol;
197
- return protocol === "https:" || protocol === "http:";
198
- } catch {
199
- return false;
200
- }
153
+ function narrationFitsClip(text, durationSec) {
154
+ return speechFitsClip(estimateNarrationSeconds(text), durationSec);
201
155
  }
202
156
 
203
157
  export {
@@ -208,6 +162,8 @@ export {
208
162
  encodeVideoSseEvent,
209
163
  decodeVideoSse,
210
164
  videoSseHeaders,
211
- withDeadline,
212
- sanitizeVideoChatMedia
165
+ CLIP_NARRATION_TAIL_SEC,
166
+ estimateNarrationSeconds,
167
+ speechFitsClip,
168
+ narrationFitsClip
213
169
  };
@@ -121,7 +121,6 @@ function paceScene(scene, options) {
121
121
  }
122
122
 
123
123
  export {
124
- PACING_PLANNER_RULES,
125
124
  getReadableSceneDuration,
126
125
  getCloserReserve,
127
126
  paceScene
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseVideoEvent
3
- } from "./chunk-7M56IUUX.js";
3
+ } from "./chunk-6Z3ID54H.js";
4
4
  import {
5
5
  VIDEO_PROTOCOL_VERSION,
6
6
  VIDEO_SCHEMA_VERSION
@@ -0,0 +1,365 @@
1
+ import {
2
+ useExternalVideoBackdrop,
3
+ useMediaAudio,
4
+ useMediaFailure,
5
+ useNarrationPreroll
6
+ } from "./chunk-666HGTVZ.js";
7
+ import {
8
+ resolveMediaType
9
+ } from "./chunk-K5J7ESRO.js";
10
+
11
+ // src/visual-system/scene-templates/scene-background.tsx
12
+ import { useEffect as useEffect2, useState as useState2 } from "react";
13
+
14
+ // src/visual-system/scene-templates/media-position.ts
15
+ var MEDIA_POSITIONS = {
16
+ center: "center center",
17
+ top: "center top",
18
+ bottom: "center bottom",
19
+ left: "left center",
20
+ right: "right center"
21
+ };
22
+ function resolveMediaPosition(value) {
23
+ return MEDIA_POSITIONS[value] ?? MEDIA_POSITIONS.center;
24
+ }
25
+
26
+ // src/visual-system/scene-templates/scene-video-backdrop.tsx
27
+ import { useCallback, useEffect, useRef, useState } from "react";
28
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
29
+ var SceneVideoBackdrop = ({
30
+ mediaUrl,
31
+ mediaPoster,
32
+ mediaPosition = "center",
33
+ progress,
34
+ sceneDuration,
35
+ preparingNarration = false,
36
+ isPlaying,
37
+ muted,
38
+ volume,
39
+ playbackId = mediaUrl,
40
+ onReady,
41
+ onError
42
+ }) => {
43
+ const inheritedAudio = useMediaAudio();
44
+ const reportMediaFailure = useMediaFailure();
45
+ const inheritedPreroll = useNarrationPreroll();
46
+ const rewindPreroll = preparingNarration || inheritedPreroll;
47
+ const resolvedMuted = muted ?? inheritedAudio.muted;
48
+ const resolvedVolume = volume ?? inheritedAudio.volume;
49
+ const resolvedPosition = resolveMediaPosition(mediaPosition);
50
+ const [decodedVideoUrl, setDecodedVideoUrl] = useState();
51
+ const [waitingKey, setWaitingKey] = useState();
52
+ const [exhaustedKey, setExhaustedKey] = useState();
53
+ const videoRef = useRef(null);
54
+ const playableVideoUrl = useRef(void 0);
55
+ const startedVideoUrl = useRef(void 0);
56
+ const startedPlaybackId = useRef(void 0);
57
+ const videoPresentationKey = `${playbackId}\0${mediaUrl}`;
58
+ const presentationRef = useRef({ key: videoPresentationKey, playing: isPlaying });
59
+ const failedPresentationRef = useRef(void 0);
60
+ const previousProgressRef = useRef({ key: videoPresentationKey, progress });
61
+ presentationRef.current = { key: videoPresentationKey, playing: isPlaying };
62
+ const unavailable = (reason = "playback-error") => {
63
+ if (presentationRef.current.key === videoPresentationKey && failedPresentationRef.current !== videoPresentationKey && (presentationRef.current.playing || reason === "duration-mismatch")) {
64
+ failedPresentationRef.current = videoPresentationKey;
65
+ setExhaustedKey(videoPresentationKey);
66
+ onError?.();
67
+ reportMediaFailure?.(reason);
68
+ }
69
+ };
70
+ useEffect(() => {
71
+ if (!isPlaying || rewindPreroll || waitingKey !== videoPresentationKey) {
72
+ if (waitingKey) setWaitingKey(void 0);
73
+ return;
74
+ }
75
+ const video = videoRef.current;
76
+ if (!video) return;
77
+ const expectedSource = video.getAttribute("src") === mediaUrl ? video.src : void 0;
78
+ let awaitingPlayback = playableVideoUrl.current !== mediaUrl || video.currentSrc !== expectedSource;
79
+ let previousTime = video.currentTime;
80
+ let forwardFrames = 0;
81
+ let stopped = false;
82
+ let frame;
83
+ let poll;
84
+ const observe = (_now, metadata) => {
85
+ if (stopped) return;
86
+ const currentSource = video.currentSrc === expectedSource;
87
+ if (currentSource && awaitingPlayback && video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
88
+ playableVideoUrl.current = mediaUrl;
89
+ awaitingPlayback = false;
90
+ clearTimeout(deadline);
91
+ deadline = setTimeout(fail, 1e3);
92
+ }
93
+ const time = metadata?.mediaTime ?? video.currentTime;
94
+ if (!currentSource || video.seeking || time < previousTime) forwardFrames = 0;
95
+ else if (time > previousTime + 1e-3) forwardFrames++;
96
+ previousTime = time;
97
+ if (forwardFrames >= 2) {
98
+ playableVideoUrl.current = mediaUrl;
99
+ stopped = true;
100
+ clearTimeout(deadline);
101
+ setWaitingKey(void 0);
102
+ return;
103
+ }
104
+ if (video.requestVideoFrameCallback) frame = video.requestVideoFrameCallback(observe);
105
+ else poll = setTimeout(observe, 50);
106
+ };
107
+ const fail = () => {
108
+ if (!stopped) {
109
+ stopped = true;
110
+ unavailable(awaitingPlayback ? "frame-readiness-timeout" : "stalled-media");
111
+ }
112
+ };
113
+ let deadline = setTimeout(fail, awaitingPlayback ? 8e3 : 1e3);
114
+ observe();
115
+ return () => {
116
+ stopped = true;
117
+ clearTimeout(deadline);
118
+ clearTimeout(poll);
119
+ if (frame !== void 0) video.cancelVideoFrameCallback?.(frame);
120
+ };
121
+ }, [waitingKey, videoPresentationKey, isPlaying, rewindPreroll]);
122
+ const onReadyRef = useRef(onReady);
123
+ onReadyRef.current = onReady;
124
+ useEffect(() => {
125
+ const video = videoRef.current;
126
+ if (!video) return;
127
+ let stopped = false;
128
+ let frame;
129
+ const markPresented = () => {
130
+ if (stopped || !video.isConnected || presentationRef.current.key !== videoPresentationKey || video.getAttribute("src") !== mediaUrl || video.currentSrc !== video.src || failedPresentationRef.current === videoPresentationKey) return false;
131
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) playableVideoUrl.current = mediaUrl;
132
+ video.dispatchEvent(new Event("vanillasky:video-frame-presented", { bubbles: true }));
133
+ onReadyRef.current?.();
134
+ setDecodedVideoUrl(mediaUrl);
135
+ stopped = true;
136
+ return true;
137
+ };
138
+ const observe = () => {
139
+ if (stopped || frame !== void 0) return;
140
+ if (video.requestVideoFrameCallback) {
141
+ frame = video.requestVideoFrameCallback(() => {
142
+ frame = void 0;
143
+ if (!markPresented()) observe();
144
+ });
145
+ } else if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) markPresented();
146
+ };
147
+ video.addEventListener("loadeddata", observe);
148
+ observe();
149
+ return () => {
150
+ stopped = true;
151
+ video.removeEventListener("loadeddata", observe);
152
+ if (frame !== void 0) video.cancelVideoFrameCallback?.(frame);
153
+ };
154
+ }, [mediaUrl, videoPresentationKey]);
155
+ const fitDuration = useCallback((video) => {
156
+ video.playbackRate = 1;
157
+ if (sceneDuration && Number.isFinite(video.duration) && video.duration > 0 && sceneDuration > video.duration + 0.05) {
158
+ video.pause();
159
+ unavailable("duration-mismatch");
160
+ return false;
161
+ }
162
+ return true;
163
+ }, [sceneDuration, videoPresentationKey]);
164
+ useEffect(() => {
165
+ if (videoRef.current) fitDuration(videoRef.current);
166
+ }, [fitDuration]);
167
+ const finishMotion = () => {
168
+ if (!isPlaying) return;
169
+ unavailable();
170
+ };
171
+ useEffect(() => {
172
+ const previous = previousProgressRef.current;
173
+ previousProgressRef.current = { key: videoPresentationKey, progress };
174
+ const video = videoRef.current;
175
+ if (!video || rewindPreroll || previous.key !== videoPresentationKey || progress >= previous.progress - 0.05 || !sceneDuration || !Number.isFinite(sceneDuration)) return;
176
+ video.currentTime = Math.max(0, progress * sceneDuration);
177
+ failedPresentationRef.current = void 0;
178
+ setExhaustedKey(void 0);
179
+ if (isPlaying && video.paused) void video.play().catch(() => unavailable());
180
+ }, [progress, videoPresentationKey, sceneDuration, rewindPreroll, isPlaying]);
181
+ useEffect(() => {
182
+ const video = videoRef.current;
183
+ if (!video) return;
184
+ if (video.getAttribute("src") !== mediaUrl) {
185
+ video.setAttribute("src", mediaUrl);
186
+ video.load();
187
+ }
188
+ }, [mediaUrl]);
189
+ useEffect(() => {
190
+ const video = videoRef.current;
191
+ if (!video) return;
192
+ return () => {
193
+ video.pause();
194
+ video.removeAttribute("src");
195
+ video.load();
196
+ startedVideoUrl.current = void 0;
197
+ startedPlaybackId.current = void 0;
198
+ };
199
+ }, []);
200
+ useEffect(() => {
201
+ const video = videoRef.current;
202
+ if (video) video.volume = resolvedVolume;
203
+ }, [resolvedVolume]);
204
+ useEffect(() => {
205
+ const video = videoRef.current;
206
+ if (!video) return;
207
+ if (!isPlaying) {
208
+ video.pause();
209
+ if (rewindPreroll && !resolvedMuted && video.currentTime > 0) video.currentTime = 0;
210
+ return;
211
+ }
212
+ if (startedPlaybackId.current === playbackId) {
213
+ if (video.ended) finishMotion();
214
+ else void video.play().catch(() => unavailable());
215
+ return;
216
+ }
217
+ const changingSource = startedVideoUrl.current !== void 0 && startedVideoUrl.current !== mediaUrl;
218
+ if (!fitDuration(video)) return;
219
+ if (!changingSource && video.currentTime > 0) video.currentTime = 0;
220
+ video.play().catch(() => unavailable());
221
+ startedVideoUrl.current = mediaUrl;
222
+ startedPlaybackId.current = playbackId;
223
+ }, [isPlaying, mediaUrl, playbackId, rewindPreroll]);
224
+ const enforceRequestedPause = (event) => {
225
+ if (presentationRef.current.playing) return;
226
+ event.currentTarget.pause();
227
+ if (rewindPreroll && event.currentTarget.currentTime > 0) event.currentTarget.currentTime = 0;
228
+ };
229
+ const mediaStyle = {
230
+ position: "absolute",
231
+ inset: 0,
232
+ width: "100%",
233
+ height: "100%",
234
+ objectFit: "cover",
235
+ objectPosition: resolvedPosition
236
+ };
237
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
238
+ exhaustedKey === videoPresentationKey && /* @__PURE__ */ jsx(
239
+ "div",
240
+ {
241
+ role: "status",
242
+ "data-media-continuity": "exhausted",
243
+ style: { position: "absolute", inset: 0, zIndex: 3, background: "#000", color: "#bbb", display: "grid", placeContent: "center", font: "14px system-ui" },
244
+ children: "Visual unavailable"
245
+ }
246
+ ),
247
+ /* @__PURE__ */ jsx(
248
+ "video",
249
+ {
250
+ ref: videoRef,
251
+ src: mediaUrl,
252
+ poster: decodedVideoUrl !== mediaUrl ? mediaPoster || void 0 : void 0,
253
+ muted: resolvedMuted,
254
+ loop: false,
255
+ playsInline: true,
256
+ preload: "auto",
257
+ onLoadedMetadata: (event) => fitDuration(event.currentTarget),
258
+ onEnded: finishMotion,
259
+ onPlay: enforceRequestedPause,
260
+ onPlaying: (event) => {
261
+ if (event.currentTarget.currentSrc === event.currentTarget.src && event.currentTarget.getAttribute("src") === mediaUrl && event.currentTarget.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) playableVideoUrl.current = mediaUrl;
262
+ enforceRequestedPause(event);
263
+ },
264
+ onWaiting: () => {
265
+ if (isPlaying) setWaitingKey(videoPresentationKey);
266
+ },
267
+ onError,
268
+ "data-media-position": mediaPosition,
269
+ "data-video-backdrop": "scene",
270
+ style: { ...mediaStyle, visibility: exhaustedKey === videoPresentationKey ? "hidden" : void 0 }
271
+ }
272
+ )
273
+ ] });
274
+ };
275
+
276
+ // src/visual-system/scene-templates/scene-background.tsx
277
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
278
+ function getMediaBackgroundProps(variables) {
279
+ return {
280
+ mediaUrl: String(variables.mediaUrl || ""),
281
+ mediaType: String(variables.mediaType || "auto"),
282
+ mediaPoster: String(variables.mediaPoster || ""),
283
+ mediaPosition: String(variables.mediaPosition || "center")
284
+ };
285
+ }
286
+ function SceneBackground({
287
+ progress,
288
+ sceneDuration,
289
+ mediaUrl = "",
290
+ mediaType = "auto",
291
+ mediaPoster,
292
+ mediaPosition = "center",
293
+ isPlaying = true
294
+ }) {
295
+ const resolved = resolveMediaType(mediaType, mediaUrl);
296
+ const wantsMedia = resolved !== "gradient" && !!mediaUrl;
297
+ const backdrop = useExternalVideoBackdrop();
298
+ const external = backdrop !== false && resolved === "video";
299
+ const [failedUrl, setFailedUrl] = useState2();
300
+ useEffect2(() => {
301
+ if (!wantsMedia || resolved !== "photo" || typeof Image === "undefined") return;
302
+ let cancelled = false;
303
+ const probe = new Image();
304
+ const fail = () => {
305
+ if (!cancelled) setFailedUrl(mediaUrl);
306
+ };
307
+ probe.onerror = fail;
308
+ probe.src = mediaUrl;
309
+ if (probe.complete && probe.naturalWidth === 0) fail();
310
+ return () => {
311
+ cancelled = true;
312
+ probe.onerror = null;
313
+ };
314
+ }, [mediaUrl, resolved, wantsMedia]);
315
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
316
+ (!external || backdrop === "fallback") && /* @__PURE__ */ jsx2(
317
+ "div",
318
+ {
319
+ "data-scene-background": "black",
320
+ style: { position: "absolute", inset: 0, background: "#000", pointerEvents: "none" }
321
+ }
322
+ ),
323
+ wantsMedia && failedUrl !== mediaUrl && !external && (resolved === "video" ? /* @__PURE__ */ jsx2(
324
+ SceneVideoBackdrop,
325
+ {
326
+ mediaUrl,
327
+ mediaPoster,
328
+ mediaPosition,
329
+ progress,
330
+ sceneDuration,
331
+ isPlaying,
332
+ onError: () => setFailedUrl(mediaUrl)
333
+ }
334
+ ) : /* @__PURE__ */ jsx2(
335
+ "img",
336
+ {
337
+ src: mediaUrl,
338
+ alt: "",
339
+ "aria-hidden": "true",
340
+ draggable: false,
341
+ "data-media-position": mediaPosition,
342
+ onError: () => setFailedUrl(mediaUrl),
343
+ style: {
344
+ position: "absolute",
345
+ inset: 0,
346
+ width: "100%",
347
+ height: "100%",
348
+ objectFit: "cover",
349
+ objectPosition: resolveMediaPosition(mediaPosition)
350
+ }
351
+ }
352
+ ))
353
+ ] });
354
+ }
355
+
356
+ // src/visual-system/scene-templates/cinema-media.tsx
357
+ import { jsx as jsx3 } from "react/jsx-runtime";
358
+ function MediaScene({ variables, width, height, progress, sceneDuration, isPlaying }) {
359
+ return /* @__PURE__ */ jsx3("div", { "data-template": "cinemaMedia", style: { width, height, position: "relative", overflow: "hidden" }, children: /* @__PURE__ */ jsx3(SceneBackground, { progress, sceneDuration, ...getMediaBackgroundProps(variables), isPlaying }) });
360
+ }
361
+ var MediaSceneTemplate = MediaScene;
362
+ export {
363
+ MediaScene,
364
+ MediaSceneTemplate
365
+ };