@vanillaskyai/video 0.10.23 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/PUBLIC-API.md +47 -438
  3. package/README.md +53 -97
  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-5BSJLT6H.js → chunk-2WIETEL7.js} +16 -29
  7. package/dist/{chunk-3O7OMMMF.js → chunk-35K6IKB2.js} +3 -3
  8. package/dist/{chunk-PDQFQIQW.js → chunk-3EQ6PVWL.js} +1 -1
  9. package/dist/chunk-4G4JBMCM.js +37 -0
  10. package/dist/{chunk-7AA2JWHZ.js → chunk-5DOQTIMD.js} +6 -6
  11. package/dist/chunk-666HGTVZ.js +41 -0
  12. package/dist/{chunk-7M56IUUX.js → chunk-6Z3ID54H.js} +0 -18
  13. package/dist/chunk-AUN4S3YW.js +50 -0
  14. package/dist/chunk-HIQM4J3U.js +30 -0
  15. package/dist/chunk-ISWQ6T5X.js +36 -0
  16. package/dist/{chunk-224QNWRA.js → chunk-K5J7ESRO.js} +1 -10
  17. package/dist/chunk-NM4CXXZY.js +25 -0
  18. package/dist/{chunk-44WND2VP.js → chunk-RFXHLLYU.js} +34 -62
  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 -1748
  22. package/dist/{compose-video-BH5K6XWT.js → compose-video-RSLI4CVP.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 +1355 -740
  29. package/dist/scene-validation-SGLLLFY5.js +9 -0
  30. package/dist/server.d.ts +67 -117
  31. package/dist/server.js +296 -626
  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 +80 -106
  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 +156 -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 +4 -25
  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/motion-and-effects.md +0 -109
  110. package/docs/reference/design-system.html +0 -125
  111. package/docs/responsive-orientation.md +0 -37
  112. package/docs/streaming-protocol.md +0 -15
  113. package/examples/custom-template/README.md +0 -19
  114. package/examples/custom-template/minimal-text.tsx +0 -82
  115. package/examples/custom-template/structured-data.tsx +0 -104
  116. package/registry/items/backgrounds.json +0 -70
  117. package/registry/items/chapterTitle.json +0 -80
  118. package/registry/items/cinemaMedia.json +0 -136
  119. package/registry/items/comparison.json +0 -168
  120. package/registry/items/editorialTimeline.json +0 -172
  121. package/registry/items/keyFigure.json +0 -162
  122. package/registry/items/mobileMessage.json +0 -153
  123. package/registry/items/motion.json +0 -45
  124. package/registry/items/quote.json +0 -161
  125. package/registry/items/template-context.json +0 -31
  126. package/registry/items/theme.json +0 -47
  127. 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,37 @@ 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
- });
174
- }
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;
139
+ // src/protocol/clip-budget.ts
140
+ var CLIP_NARRATION_TAIL_SEC = 0.8;
141
+ function clipNarrationBudget(clipDurationSec) {
142
+ const maxSpeechSec = Math.max(0, clipDurationSec - CLIP_NARRATION_TAIL_SEC);
143
+ const maxWords = Math.floor(maxSpeechSec * 2);
144
+ const maxUnspacedCharacters = Math.floor(maxSpeechSec * 3);
187
145
  return {
188
- url,
189
- type: media.type,
190
- ...posterUrl ? { posterUrl } : {}
146
+ clipDurationSec,
147
+ maxSpeechSec,
148
+ targetWords: Math.floor(maxWords * 0.8),
149
+ targetUnspacedCharacters: Math.floor(maxUnspacedCharacters * 0.8),
150
+ maxWords,
151
+ maxUnspacedCharacters
191
152
  };
192
153
  }
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
- }
154
+ 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;
155
+ function estimateNarrationSeconds(text) {
156
+ const normalized = text.trim();
157
+ if (!normalized) return 0;
158
+ const characters = normalized.match(UNSPACED_SCRIPT)?.length ?? 0;
159
+ const words = normalized.replace(UNSPACED_SCRIPT, " ").match(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu)?.length ?? 0;
160
+ const numericExpansion = (normalized.match(/\p{Nd}+/gu) ?? []).reduce((sum, digits) => sum + Math.max(0, digits.length - 1) + (digits.length >= 3 ? 1 : 0), 0);
161
+ const symbols = normalized.match(/[%°\p{Sc}]/gu)?.length ?? 0;
162
+ const pauses = normalized.match(/[.!?。!?;;::]/gu)?.length ?? 0;
163
+ return (words + numericExpansion + symbols) / 2.2 + characters / 3.5 + pauses * 0.15;
164
+ }
165
+ function speechFitsClip(seconds, durationSec) {
166
+ return Number.isFinite(seconds) && seconds >= 0 && Number.isFinite(durationSec) && durationSec > 0 && seconds + CLIP_NARRATION_TAIL_SEC <= durationSec + 1e-6;
167
+ }
168
+ function narrationFitsClip(text, durationSec) {
169
+ return speechFitsClip(estimateNarrationSeconds(text), durationSec);
201
170
  }
202
171
 
203
172
  export {
@@ -208,6 +177,9 @@ export {
208
177
  encodeVideoSseEvent,
209
178
  decodeVideoSse,
210
179
  videoSseHeaders,
211
- withDeadline,
212
- sanitizeVideoChatMedia
180
+ CLIP_NARRATION_TAIL_SEC,
181
+ clipNarrationBudget,
182
+ estimateNarrationSeconds,
183
+ speechFitsClip,
184
+ narrationFitsClip
213
185
  };
@@ -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
+ };