@vanillaskyai/video 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.3.4
8
+
9
+ - Mounts a scene that brings a new photo or video 1.2s before it appears,
10
+ instead of 0.3s, so the element has time to decode and the scene arrives
11
+ showing its picture rather than a gradient that pops a beat later. The extra
12
+ time is invisible: the layer stays fully transparent until the existing
13
+ 0.3s cross-fade begins, so every rendered frame is unchanged, and the mounted
14
+ element is handed to the incoming scene rather than rebuilt. Scenes that
15
+ reuse the backdrop already on screen are unaffected.
16
+
17
+ ## 0.3.3
18
+
19
+ - Keeps a soundtrack audible on iPhone Safari when its audio context cannot be
20
+ unlocked. Routing a media element into a context that never reaches
21
+ `running` does not fade it, it silences it, so playback now stays on the
22
+ element unless the context is confirmed running.
23
+ - Actually preloads scene backdrops. The warm elements were unreferenced the
24
+ moment they were created, so a browser was free to collect them and cancel
25
+ the request mid-flight; they are now held until they finish. Video streams
26
+ are warmed too, not just their posters, which is what left a gradient
27
+ flashing between two consecutive media scenes. One video warms at a time and
28
+ is released as soon as its first frame lands.
29
+
7
30
  ## 0.3.2
8
31
 
9
32
  - Rebuilds text legibility over photo and video backdrops. Scrims now ramp off
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Give your AI a video output
2
2
 
3
- ![Version 0.3.2 beta](https://img.shields.io/badge/version-0.3.2_beta-7c3aed)
3
+ ![Version 0.3.4 beta](https://img.shields.io/badge/version-0.3.4_beta-7c3aed)
4
4
 
5
5
  **VanillaSky is the open-source video response layer.** Turn text, structured
6
6
  data, and live application context into personalized video responses that start
@@ -17,7 +17,7 @@ the planning prompt, trusted templates, validation, streaming, and player.
17
17
  For humans:
18
18
 
19
19
  ```bash
20
- npm install @vanillaskyai/video@0.3.2 ai @ai-sdk/openai
20
+ npm install @vanillaskyai/video@0.3.4 ai @ai-sdk/openai
21
21
  ```
22
22
 
23
23
  For coding agents:
@@ -106,7 +106,7 @@ shows each complete, validated scene as soon as it is ready and returns a
106
106
  deterministic `Video` object when generation finishes.
107
107
 
108
108
  A copy-and-run app is in
109
- [`examples/nextjs-quickstart`](https://github.com/VanillaSkyAi/video/tree/v0.3.2/examples/nextjs-quickstart).
109
+ [`examples/nextjs-quickstart`](https://github.com/VanillaSkyAi/video/tree/v0.3.4/examples/nextjs-quickstart).
110
110
 
111
111
  ## Shape the response
112
112
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VideoFrame
3
- } from "./chunk-HXSV6RHF.js";
3
+ } from "./chunk-N3KUA7VP.js";
4
4
  import "./chunk-XWWLKRNU.js";
5
5
  import "./chunk-SPVTJH3F.js";
6
6
  import {
@@ -31,6 +31,7 @@ function scaleSafeZone(zone, factor) {
31
31
  // src/player/video-frame.tsx
32
32
  import { jsx, jsxs } from "react/jsx-runtime";
33
33
  var SCENE_TRANSITION_SECONDS = 0.3;
34
+ var MEDIA_PREROLL_SECONDS = 1.2;
34
35
  var CONTIGUITY_ULP_FACTOR = 4;
35
36
  function clamp01(value) {
36
37
  return Math.max(0, Math.min(1, value));
@@ -51,6 +52,9 @@ function brandBackgroundFallback(config) {
51
52
  const background = config.style.brand.background;
52
53
  return background.type === "solid" ? background.color : background.colors[0];
53
54
  }
55
+ function sceneHasBackdrop(range) {
56
+ return String(range.scene.variables.mediaType || "auto") !== "gradient" && String(range.scene.variables.mediaUrl || "").trim() !== "";
57
+ }
54
58
  function sceneBackgroundChanges(left, right) {
55
59
  const mediaUrl = (range) => String(range.scene.variables.mediaType || "auto") === "gradient" ? "" : String(range.scene.variables.mediaUrl || "");
56
60
  return mediaUrl(left) !== mediaUrl(right);
@@ -178,6 +182,14 @@ function VideoFrame({
178
182
  );
179
183
  const blendStart = active.end - blendDuration;
180
184
  const blendEnd = blendStart + blendDuration;
185
+ const prerollsNext = Boolean(
186
+ contiguousNext && sceneHasBackdrop(contiguousNext) && sceneBackgroundChanges(active, contiguousNext)
187
+ );
188
+ const prerollDuration = prerollsNext ? Math.min(Math.max(MEDIA_PREROLL_SECONDS, blendDuration), Math.max(0, duration)) : blendDuration;
189
+ const prerollStart = active.end - prerollDuration;
190
+ const mountingNext = Boolean(
191
+ contiguousNext && time >= prerollStart && time < blendEnd && (eligibleNextTransition || prerollsNext)
192
+ );
181
193
  const previewingNext = Boolean(
182
194
  eligibleNextTransition && time >= blendStart && time < blendEnd
183
195
  );
@@ -232,7 +244,7 @@ function VideoFrame({
232
244
  }
233
245
  }
234
246
  ),
235
- previewingNext && contiguousNext && nextTiming ? [
247
+ mountingNext && contiguousNext ? [
236
248
  /* @__PURE__ */ jsx(
237
249
  SceneLayer,
238
250
  {
@@ -244,7 +256,7 @@ function VideoFrame({
244
256
  width: canvas.width,
245
257
  height: canvas.height,
246
258
  playing,
247
- layer: "outgoing",
259
+ layer: previewingNext ? "outgoing" : "active",
248
260
  opacity: 1 - blendProgress,
249
261
  interactive: true,
250
262
  zIndex: 1
@@ -0,0 +1,55 @@
1
+ import {
2
+ resolveMediaType
3
+ } from "./chunk-35LIYILY.js";
4
+
5
+ // src/player/preload-media.ts
6
+ var warmed = /* @__PURE__ */ new Set();
7
+ var inFlight = /* @__PURE__ */ new Set();
8
+ function warmImage(url) {
9
+ const trimmed = url.trim();
10
+ if (!trimmed || warmed.has(trimmed)) return;
11
+ warmed.add(trimmed);
12
+ const image = new Image();
13
+ const release = () => inFlight.delete(image);
14
+ image.onload = release;
15
+ image.onerror = release;
16
+ inFlight.add(image);
17
+ image.src = trimmed;
18
+ }
19
+ var videoWarmInFlight = false;
20
+ function warmVideo(url) {
21
+ const trimmed = url.trim();
22
+ if (!trimmed || warmed.has(trimmed) || videoWarmInFlight) return;
23
+ warmed.add(trimmed);
24
+ videoWarmInFlight = true;
25
+ const video = document.createElement("video");
26
+ video.preload = "auto";
27
+ video.muted = true;
28
+ video.playsInline = true;
29
+ const release = () => {
30
+ videoWarmInFlight = false;
31
+ inFlight.delete(video);
32
+ video.removeAttribute("src");
33
+ video.load();
34
+ };
35
+ video.onloadeddata = release;
36
+ video.onerror = release;
37
+ inFlight.add(video);
38
+ video.src = trimmed;
39
+ }
40
+ function preloadSceneMedia(variables) {
41
+ if (typeof window === "undefined" || typeof Image === "undefined") return;
42
+ const mediaUrl = String(variables.mediaUrl || "").trim();
43
+ if (!mediaUrl) return;
44
+ const resolved = resolveMediaType(String(variables.mediaType || "auto"), mediaUrl);
45
+ if (resolved === "gradient") return;
46
+ if (resolved === "video") {
47
+ warmImage(String(variables.mediaPoster || ""));
48
+ warmVideo(mediaUrl);
49
+ return;
50
+ }
51
+ warmImage(mediaUrl);
52
+ }
53
+ export {
54
+ preloadSceneMedia
55
+ };
package/dist/react.js CHANGED
@@ -29,7 +29,7 @@ import "./chunk-2E6T633S.js";
29
29
  import {
30
30
  VideoFrame,
31
31
  getDimensions
32
- } from "./chunk-HXSV6RHF.js";
32
+ } from "./chunk-N3KUA7VP.js";
33
33
  import {
34
34
  fontStack
35
35
  } from "./chunk-XWWLKRNU.js";
@@ -144,7 +144,7 @@ function preloadBuiltinTemplate(id) {
144
144
 
145
145
  // src/player/warm-scene-media.ts
146
146
  function warmSceneMedia(variables) {
147
- void import("./preload-media-KIYBFYRH.js").then((module) => module.preloadSceneMedia(variables)).catch(() => {
147
+ void import("./preload-media-NKQ6JA3T.js").then((module) => module.preloadSceneMedia(variables)).catch(() => {
148
148
  });
149
149
  }
150
150
 
@@ -248,8 +248,15 @@ function VideoPlayerRuntime({
248
248
  const Context = window.AudioContext ?? window.webkitAudioContext;
249
249
  if (!Context) return;
250
250
  const context = new Context();
251
- context.resume().catch(Boolean);
252
- import("./control-visibility-NQOWTKOQ.js").then((output) => output.default(audio, context)).catch(() => context.close());
251
+ const unlocked = context.resume().catch(Boolean);
252
+ void import("./control-visibility-NQOWTKOQ.js").then(async (output) => {
253
+ await unlocked;
254
+ if (context.state !== "running") {
255
+ void context.close();
256
+ return;
257
+ }
258
+ output.default(audio, context);
259
+ }).catch(() => context.close());
253
260
  };
254
261
  if (stream !== activeStream) {
255
262
  const autoStartReplacement = Boolean(playbackMode && stream && shouldAutoPlay && !reducedMotion);
@@ -5,7 +5,7 @@
5
5
  Install VanillaSky:
6
6
 
7
7
  ```bash
8
- npm install @vanillaskyai/video@0.3.2 ai @ai-sdk/openai
8
+ npm install @vanillaskyai/video@0.3.4 ai @ai-sdk/openai
9
9
  ```
10
10
 
11
11
  Set your provider key in `.env.local` (never commit it):
@@ -50,7 +50,7 @@ sets its marker only for `next dev`, and it accepts only localhost. Every
50
50
  production request is denied. Replace it with your real session validation
51
51
  before deploying. For literal files and commands,
52
52
  use the tested
53
- [`examples/nextjs-quickstart` directory](https://github.com/VanillaSkyAi/video/tree/v0.3.2/examples/nextjs-quickstart).
53
+ [`examples/nextjs-quickstart` directory](https://github.com/VanillaSkyAi/video/tree/v0.3.4/examples/nextjs-quickstart).
54
54
 
55
55
  `model` can come from any AI SDK provider, registry, gateway, compatible API,
56
56
  or custom implementation. The application can choose a cheaper or faster model
@@ -5,7 +5,7 @@
5
5
  Install VanillaSky and one AI SDK provider:
6
6
 
7
7
  ```bash
8
- npm install @vanillaskyai/video@0.3.2 ai @ai-sdk/openai
8
+ npm install @vanillaskyai/video@0.3.4 ai @ai-sdk/openai
9
9
  ```
10
10
 
11
11
  Create an ignored `.env.local`:
@@ -74,7 +74,7 @@ requests; replace it with your application's session validation before
74
74
  deploying.
75
75
 
76
76
  The copy-and-run app is in the
77
- [`examples/nextjs-quickstart` directory](https://github.com/VanillaSkyAi/video/tree/v0.3.2/examples/nextjs-quickstart).
77
+ [`examples/nextjs-quickstart` directory](https://github.com/VanillaSkyAi/video/tree/v0.3.4/examples/nextjs-quickstart).
78
78
 
79
79
  For another LLM, replace `openai(...)` with the matching AI SDK model. The route
80
80
  shape and React code stay the same. See [Provider integration](provider-integration.md)
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@ai-sdk/openai": "^4.0.42",
12
- "@vanillaskyai/video": "0.3.2",
12
+ "@vanillaskyai/video": "0.3.4",
13
13
  "ai": "^7.0.66",
14
14
  "next": "16.3.1",
15
15
  "react": "19.2.8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillaskyai/video",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Open-source video response SDK for personalized AI applications.",
5
5
  "keywords": [
6
6
  "generative-video",
@@ -1,30 +0,0 @@
1
- import {
2
- resolveMediaType
3
- } from "./chunk-35LIYILY.js";
4
-
5
- // src/player/preload-media.ts
6
- var warmed = /* @__PURE__ */ new Set();
7
- function warm(url) {
8
- const trimmed = url.trim();
9
- if (!trimmed || warmed.has(trimmed)) return;
10
- warmed.add(trimmed);
11
- const image = new Image();
12
- image.onerror = () => {
13
- };
14
- image.src = trimmed;
15
- }
16
- function preloadSceneMedia(variables) {
17
- if (typeof window === "undefined" || typeof Image === "undefined") return;
18
- const mediaUrl = String(variables.mediaUrl || "").trim();
19
- if (!mediaUrl) return;
20
- const resolved = resolveMediaType(String(variables.mediaType || "auto"), mediaUrl);
21
- if (resolved === "gradient") return;
22
- if (resolved === "video") {
23
- warm(String(variables.mediaPoster || ""));
24
- return;
25
- }
26
- warm(mediaUrl);
27
- }
28
- export {
29
- preloadSceneMedia
30
- };