@maravilla-labs/frames 0.4.0 → 0.5.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.
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +153 -67
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,15 @@ export type VideoInstr = {
|
|
|
30
30
|
seek?: number;
|
|
31
31
|
/** CSS selector for the `<video>` element. */
|
|
32
32
|
selector: string;
|
|
33
|
+
/**
|
|
34
|
+
* Storage key of a pre-extracted frame-set manifest for this clip (the
|
|
35
|
+
* `output_key` returned by `platform.env.TRANSFORMS.extractFrames`). When
|
|
36
|
+
* present, the headless renderer swaps the live `<video>` for the matching
|
|
37
|
+
* frame image per captured frame (required under begin-frame-control, which
|
|
38
|
+
* can't seek a live video). Optional — without it the renderer falls back to
|
|
39
|
+
* extracting frames at render time, and the screenshot path seeks live video.
|
|
40
|
+
*/
|
|
41
|
+
frameset?: string;
|
|
33
42
|
};
|
|
34
43
|
/**
|
|
35
44
|
* Drive a CSS-class-triggered animation (Animate.css, Magic, your own
|
|
@@ -59,12 +68,39 @@ export type TimelineSchema = {
|
|
|
59
68
|
duration: number;
|
|
60
69
|
instructions: Instr[];
|
|
61
70
|
};
|
|
71
|
+
/** A `<video>` slot as exposed to the renderer engine (worker). The engine
|
|
72
|
+
* pre-extracts each clip to frames with ffmpeg and swaps the live `<video>`
|
|
73
|
+
* for the matching frame image per captured frame — so the live video is never
|
|
74
|
+
* seeked under begin-frame-control (which crashes headless Chromium). */
|
|
75
|
+
export type VideoSlotInfo = {
|
|
76
|
+
/** CSS selector for the `<video>`. */
|
|
77
|
+
selector: string;
|
|
78
|
+
/** Timeline start, ms. */
|
|
79
|
+
at: number;
|
|
80
|
+
/** How long the clip plays on the timeline, ms. */
|
|
81
|
+
duration: number;
|
|
82
|
+
/** Start offset within the clip, ms. */
|
|
83
|
+
seek: number;
|
|
84
|
+
/** Resolved clip URL (`currentSrc` || `src`). */
|
|
85
|
+
src: string;
|
|
86
|
+
/** Pre-extracted frame-set manifest storage key, if the app provided one. */
|
|
87
|
+
frameset?: string;
|
|
88
|
+
};
|
|
62
89
|
declare global {
|
|
63
90
|
interface Window {
|
|
64
91
|
__mvFrames: {
|
|
65
92
|
applyState(t: number): Promise<void>;
|
|
93
|
+
/** Video slots in this timeline — read by the renderer engine to
|
|
94
|
+
* swap video frames out-of-band. Empty when no video. */
|
|
95
|
+
videos: VideoSlotInfo[];
|
|
66
96
|
};
|
|
97
|
+
/** Set true by the headless renderer when it captures (`__mvFramesRendererPresent`). */
|
|
67
98
|
__mvFramesRendererPresent?: boolean;
|
|
99
|
+
/** Set true by the renderer ONLY on the begin-frame-control capture path,
|
|
100
|
+
* where the live `<video>` must not be seeked (the engine swaps in
|
|
101
|
+
* pre-extracted frame images instead). When unset — the screenshot capture
|
|
102
|
+
* path and the live editor/preview — `applyState` seeks the live video. */
|
|
103
|
+
__mvFramesNoVideoSeek?: boolean;
|
|
68
104
|
}
|
|
69
105
|
}
|
|
70
106
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,yDAAyD;IACzD,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,mEAAmE;IACnE,EAAE,EAAE,MAAM,CAAC;IACX,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,uDAAuD;IACvD,IAAI,EAAE,OAAO,CAAC;IACd,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,yDAAyD;IACzD,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,mEAAmE;IACnE,EAAE,EAAE,MAAM,CAAC;IACX,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,uDAAuD;IACvD,IAAI,EAAE,OAAO,CAAC;IACd,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,gCAAgC;IAChC,IAAI,EAAE,KAAK,CAAC;IACZ,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,cAAc,GAAG,UAAU,GAAG,aAAa,CAAC;AAChE,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC;AAEzE;;;yEAGyE;AACzE,MAAM,MAAM,aAAa,GAAG;IAC1B,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,UAAU,EAAE;YACV,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YACrC;qEACyD;YACzD,MAAM,EAAE,aAAa,EAAE,CAAC;SACzB,CAAC;QACF,wFAAwF;QACxF,yBAAyB,CAAC,EAAE,OAAO,CAAC;QACpC;;;mFAG2E;QAC3E,qBAAqB,CAAC,EAAE,OAAO,CAAC;KACjC;CACF;AAsBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAW3D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CA2BhD"}
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,53 @@ export function stylesheetsReady() {
|
|
|
60
60
|
})
|
|
61
61
|
.then(() => (document.fonts ? document.fonts.ready.then(() => undefined) : undefined));
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Seek `el` to `targetSec` and resolve only once the seeked frame has
|
|
65
|
+
* actually decoded — so a subsequent capture grabs the right frame instead
|
|
66
|
+
* of racing an in-flight seek.
|
|
67
|
+
*
|
|
68
|
+
* Resolution waits for the `seeked` event, then for one decoded video frame
|
|
69
|
+
* via `requestVideoFrameCallback` where available (fallback: a microtask, so
|
|
70
|
+
* we still yield to the decode). A timeout guards against `seeked` never
|
|
71
|
+
* firing (e.g. seeking to the exact current position, or an unsupported
|
|
72
|
+
* codec) so `applyState` can't hang the whole render. Already-satisfied
|
|
73
|
+
* seeks (`readyState` high enough and time already at target) resolve fast.
|
|
74
|
+
*/
|
|
75
|
+
function seekVideo(el, targetSec) {
|
|
76
|
+
const HAVE_CURRENT_DATA = 2;
|
|
77
|
+
// If we're already at (or within a frame of) the target with data ready,
|
|
78
|
+
// there's nothing to wait for — assigning currentTime won't fire `seeked`.
|
|
79
|
+
const alreadyThere = Math.abs(el.currentTime - targetSec) < 1e-3 && el.readyState >= HAVE_CURRENT_DATA;
|
|
80
|
+
el.currentTime = targetSec;
|
|
81
|
+
if (alreadyThere)
|
|
82
|
+
return Promise.resolve();
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
let done = false;
|
|
85
|
+
const finish = () => {
|
|
86
|
+
if (done)
|
|
87
|
+
return;
|
|
88
|
+
done = true;
|
|
89
|
+
el.removeEventListener("seeked", onSeeked);
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
resolve();
|
|
92
|
+
};
|
|
93
|
+
const afterSeeked = () => {
|
|
94
|
+
// Wait for one decoded frame to be presented when the API exists.
|
|
95
|
+
const rvfc = el.requestVideoFrameCallback;
|
|
96
|
+
if (typeof rvfc === "function") {
|
|
97
|
+
rvfc.call(el, () => finish());
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// No rVFC: yield a microtask so the decode can settle, then finish.
|
|
101
|
+
Promise.resolve().then(finish);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const onSeeked = () => afterSeeked();
|
|
105
|
+
el.addEventListener("seeked", onSeeked, { once: true });
|
|
106
|
+
// Guard: never let a missing `seeked` stall the render.
|
|
107
|
+
const timer = setTimeout(finish, 2000);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
63
110
|
function installTimeline(schema) {
|
|
64
111
|
const rendererPresent = typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
|
|
65
112
|
const slots = schema.instructions.flatMap((i) => {
|
|
@@ -74,8 +121,25 @@ function installTimeline(schema) {
|
|
|
74
121
|
const naturalMs = isFinite(el.duration) ? el.duration * 1000 : Infinity;
|
|
75
122
|
const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
|
|
76
123
|
el.pause();
|
|
77
|
-
|
|
78
|
-
|
|
124
|
+
// In renderer mode the engine drives video by swapping in pre-extracted
|
|
125
|
+
// frame images; we must NOT seek the live <video> (seeking under
|
|
126
|
+
// begin-frame-control crashes headless Chromium). In browser preview the
|
|
127
|
+
// setTimeout scheduler below plays it normally.
|
|
128
|
+
if (!rendererPresent) {
|
|
129
|
+
el.currentTime = seek / 1000;
|
|
130
|
+
}
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
kind: "video",
|
|
134
|
+
at: i.at,
|
|
135
|
+
duration,
|
|
136
|
+
seek,
|
|
137
|
+
el,
|
|
138
|
+
selector: i.selector,
|
|
139
|
+
src: el.currentSrc || el.src,
|
|
140
|
+
frameset: i.frameset,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
79
143
|
}
|
|
80
144
|
if (i.kind === "css") {
|
|
81
145
|
if (!(el instanceof HTMLElement)) {
|
|
@@ -131,30 +195,47 @@ function installTimeline(schema) {
|
|
|
131
195
|
anim.pause();
|
|
132
196
|
return [{ kind: "animation", at: i.at, duration, anim }];
|
|
133
197
|
});
|
|
198
|
+
// Metadata for the renderer engine: which `<video>`s to pre-extract + swap.
|
|
199
|
+
const videos = slots
|
|
200
|
+
.filter((s) => s.kind === "video")
|
|
201
|
+
.map((s) => ({
|
|
202
|
+
selector: s.selector,
|
|
203
|
+
at: s.at,
|
|
204
|
+
duration: s.duration,
|
|
205
|
+
seek: s.seek,
|
|
206
|
+
src: s.src,
|
|
207
|
+
frameset: s.frameset,
|
|
208
|
+
}));
|
|
134
209
|
window.__mvFrames = {
|
|
210
|
+
videos,
|
|
135
211
|
async applyState(t) {
|
|
136
212
|
const videoWaits = [];
|
|
137
213
|
for (const s of slots) {
|
|
138
214
|
if (s.kind === "css-pending")
|
|
139
215
|
continue; // only relevant in browser preview
|
|
140
|
-
const local = clamp(t - s.at, 0, s.duration);
|
|
141
216
|
if (s.kind === "video") {
|
|
142
|
-
s.
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// decode — otherwise
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
217
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
218
|
+
// Seek the live <video> EXCEPT on the renderer's begin-frame-control
|
|
219
|
+
// capture path (where seeking crashes the headless tab — the engine
|
|
220
|
+
// swaps in pre-extracted frame images instead, flagged via
|
|
221
|
+
// `__mvFramesNoVideoSeek`). On every live-seek path (editor, live
|
|
222
|
+
// preview, AND the renderer's screenshot capture) we must AWAIT the
|
|
223
|
+
// seeked frame's decode before resolving — otherwise a capture races
|
|
224
|
+
// an in-flight seek and grabs a stale/blank frame (and the per-frame
|
|
225
|
+
// loop spins, hitting the render wall-clock budget). `seekVideo`
|
|
226
|
+
// guards with a timeout so a missing `seeked` can't hang the render.
|
|
227
|
+
if (!window.__mvFramesNoVideoSeek) {
|
|
228
|
+
videoWaits.push(seekVideo(s.el, (s.seek + local) / 1000));
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
155
231
|
}
|
|
232
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
233
|
+
s.anim.currentTime = local;
|
|
156
234
|
}
|
|
157
|
-
await Promise.all([
|
|
235
|
+
await Promise.all([
|
|
236
|
+
document.fonts ? document.fonts.ready.then(() => undefined) : Promise.resolve(),
|
|
237
|
+
...videoWaits,
|
|
238
|
+
]);
|
|
158
239
|
},
|
|
159
240
|
};
|
|
160
241
|
if (!rendererPresent) {
|
|
@@ -194,51 +275,4 @@ function installTimeline(schema) {
|
|
|
194
275
|
function clamp(v, lo, hi) {
|
|
195
276
|
return v < lo ? lo : v > hi ? hi : v;
|
|
196
277
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Seek `el` to `targetSec` and resolve only once the seeked frame has
|
|
199
|
-
* actually decoded — so a subsequent capture grabs the right frame instead
|
|
200
|
-
* of racing an in-flight seek.
|
|
201
|
-
*
|
|
202
|
-
* Resolution waits for the `seeked` event, then for one decoded video frame
|
|
203
|
-
* via `requestVideoFrameCallback` where available (fallback: a microtask, so
|
|
204
|
-
* we still yield to the decode). A timeout guards against `seeked` never
|
|
205
|
-
* firing (e.g. seeking to the exact current position, or an unsupported
|
|
206
|
-
* codec) so `applyState` can't hang the whole render. Already-satisfied
|
|
207
|
-
* seeks (`readyState` high enough and time already at target) resolve fast.
|
|
208
|
-
*/
|
|
209
|
-
function seekVideo(el, targetSec) {
|
|
210
|
-
const HAVE_CURRENT_DATA = 2;
|
|
211
|
-
// If we're already at (or within a frame of) the target with data ready,
|
|
212
|
-
// there's nothing to wait for — assigning currentTime won't fire `seeked`.
|
|
213
|
-
const alreadyThere = Math.abs(el.currentTime - targetSec) < 1e-3 && el.readyState >= HAVE_CURRENT_DATA;
|
|
214
|
-
el.currentTime = targetSec;
|
|
215
|
-
if (alreadyThere)
|
|
216
|
-
return Promise.resolve();
|
|
217
|
-
return new Promise((resolve) => {
|
|
218
|
-
let done = false;
|
|
219
|
-
const finish = () => {
|
|
220
|
-
if (done)
|
|
221
|
-
return;
|
|
222
|
-
done = true;
|
|
223
|
-
el.removeEventListener("seeked", onSeeked);
|
|
224
|
-
clearTimeout(timer);
|
|
225
|
-
resolve();
|
|
226
|
-
};
|
|
227
|
-
const afterSeeked = () => {
|
|
228
|
-
// Wait for one decoded frame to be presented when the API exists.
|
|
229
|
-
const rvfc = el.requestVideoFrameCallback;
|
|
230
|
-
if (typeof rvfc === "function") {
|
|
231
|
-
rvfc.call(el, () => finish());
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
// No rVFC: yield a microtask so the decode can settle, then finish.
|
|
235
|
-
Promise.resolve().then(finish);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
const onSeeked = () => afterSeeked();
|
|
239
|
-
el.addEventListener("seeked", onSeeked, { once: true });
|
|
240
|
-
// Guard: never let a missing `seeked` stall the render.
|
|
241
|
-
const timer = setTimeout(finish, 2000);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
278
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maravilla-labs/frames",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Tiny vanilla helper for declaring Web Animations API timelines that the Maravilla runtime renderer drives frame-accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,15 @@ export type VideoInstr = {
|
|
|
32
32
|
seek?: number;
|
|
33
33
|
/** CSS selector for the `<video>` element. */
|
|
34
34
|
selector: string;
|
|
35
|
+
/**
|
|
36
|
+
* Storage key of a pre-extracted frame-set manifest for this clip (the
|
|
37
|
+
* `output_key` returned by `platform.env.TRANSFORMS.extractFrames`). When
|
|
38
|
+
* present, the headless renderer swaps the live `<video>` for the matching
|
|
39
|
+
* frame image per captured frame (required under begin-frame-control, which
|
|
40
|
+
* can't seek a live video). Optional — without it the renderer falls back to
|
|
41
|
+
* extracting frames at render time, and the screenshot path seeks live video.
|
|
42
|
+
*/
|
|
43
|
+
frameset?: string;
|
|
35
44
|
};
|
|
36
45
|
|
|
37
46
|
/**
|
|
@@ -61,15 +70,54 @@ export type CssClassInstr = {
|
|
|
61
70
|
export type Instr = AnimationInstr | VideoInstr | CssClassInstr;
|
|
62
71
|
export type TimelineSchema = { duration: number; instructions: Instr[] };
|
|
63
72
|
|
|
73
|
+
/** A `<video>` slot as exposed to the renderer engine (worker). The engine
|
|
74
|
+
* pre-extracts each clip to frames with ffmpeg and swaps the live `<video>`
|
|
75
|
+
* for the matching frame image per captured frame — so the live video is never
|
|
76
|
+
* seeked under begin-frame-control (which crashes headless Chromium). */
|
|
77
|
+
export type VideoSlotInfo = {
|
|
78
|
+
/** CSS selector for the `<video>`. */
|
|
79
|
+
selector: string;
|
|
80
|
+
/** Timeline start, ms. */
|
|
81
|
+
at: number;
|
|
82
|
+
/** How long the clip plays on the timeline, ms. */
|
|
83
|
+
duration: number;
|
|
84
|
+
/** Start offset within the clip, ms. */
|
|
85
|
+
seek: number;
|
|
86
|
+
/** Resolved clip URL (`currentSrc` || `src`). */
|
|
87
|
+
src: string;
|
|
88
|
+
/** Pre-extracted frame-set manifest storage key, if the app provided one. */
|
|
89
|
+
frameset?: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
64
92
|
declare global {
|
|
65
93
|
interface Window {
|
|
66
|
-
__mvFrames: {
|
|
94
|
+
__mvFrames: {
|
|
95
|
+
applyState(t: number): Promise<void>;
|
|
96
|
+
/** Video slots in this timeline — read by the renderer engine to
|
|
97
|
+
* swap video frames out-of-band. Empty when no video. */
|
|
98
|
+
videos: VideoSlotInfo[];
|
|
99
|
+
};
|
|
100
|
+
/** Set true by the headless renderer when it captures (`__mvFramesRendererPresent`). */
|
|
67
101
|
__mvFramesRendererPresent?: boolean;
|
|
102
|
+
/** Set true by the renderer ONLY on the begin-frame-control capture path,
|
|
103
|
+
* where the live `<video>` must not be seeked (the engine swaps in
|
|
104
|
+
* pre-extracted frame images instead). When unset — the screenshot capture
|
|
105
|
+
* path and the live editor/preview — `applyState` seeks the live video. */
|
|
106
|
+
__mvFramesNoVideoSeek?: boolean;
|
|
68
107
|
}
|
|
69
108
|
}
|
|
70
109
|
|
|
71
110
|
type AnimSlot = { kind: "animation"; at: number; duration: number; anim: Animation };
|
|
72
|
-
type VideoSlot = {
|
|
111
|
+
type VideoSlot = {
|
|
112
|
+
kind: "video";
|
|
113
|
+
at: number;
|
|
114
|
+
duration: number;
|
|
115
|
+
seek: number;
|
|
116
|
+
el: HTMLVideoElement;
|
|
117
|
+
selector: string;
|
|
118
|
+
src: string;
|
|
119
|
+
frameset?: string;
|
|
120
|
+
};
|
|
73
121
|
type CssPendingSlot = {
|
|
74
122
|
kind: "css-pending";
|
|
75
123
|
at: number;
|
|
@@ -141,6 +189,57 @@ export function stylesheetsReady(): Promise<void> {
|
|
|
141
189
|
.then(() => (document.fonts ? document.fonts.ready.then(() => undefined) : undefined));
|
|
142
190
|
}
|
|
143
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Seek `el` to `targetSec` and resolve only once the seeked frame has
|
|
194
|
+
* actually decoded — so a subsequent capture grabs the right frame instead
|
|
195
|
+
* of racing an in-flight seek.
|
|
196
|
+
*
|
|
197
|
+
* Resolution waits for the `seeked` event, then for one decoded video frame
|
|
198
|
+
* via `requestVideoFrameCallback` where available (fallback: a microtask, so
|
|
199
|
+
* we still yield to the decode). A timeout guards against `seeked` never
|
|
200
|
+
* firing (e.g. seeking to the exact current position, or an unsupported
|
|
201
|
+
* codec) so `applyState` can't hang the whole render. Already-satisfied
|
|
202
|
+
* seeks (`readyState` high enough and time already at target) resolve fast.
|
|
203
|
+
*/
|
|
204
|
+
function seekVideo(el: HTMLVideoElement, targetSec: number): Promise<void> {
|
|
205
|
+
const HAVE_CURRENT_DATA = 2;
|
|
206
|
+
// If we're already at (or within a frame of) the target with data ready,
|
|
207
|
+
// there's nothing to wait for — assigning currentTime won't fire `seeked`.
|
|
208
|
+
const alreadyThere =
|
|
209
|
+
Math.abs(el.currentTime - targetSec) < 1e-3 && el.readyState >= HAVE_CURRENT_DATA;
|
|
210
|
+
el.currentTime = targetSec;
|
|
211
|
+
if (alreadyThere) return Promise.resolve();
|
|
212
|
+
|
|
213
|
+
return new Promise<void>((resolve) => {
|
|
214
|
+
let done = false;
|
|
215
|
+
const finish = () => {
|
|
216
|
+
if (done) return;
|
|
217
|
+
done = true;
|
|
218
|
+
el.removeEventListener("seeked", onSeeked);
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
resolve();
|
|
221
|
+
};
|
|
222
|
+
const afterSeeked = () => {
|
|
223
|
+
// Wait for one decoded frame to be presented when the API exists.
|
|
224
|
+
const rvfc = (
|
|
225
|
+
el as unknown as {
|
|
226
|
+
requestVideoFrameCallback?: (cb: () => void) => number;
|
|
227
|
+
}
|
|
228
|
+
).requestVideoFrameCallback;
|
|
229
|
+
if (typeof rvfc === "function") {
|
|
230
|
+
rvfc.call(el, () => finish());
|
|
231
|
+
} else {
|
|
232
|
+
// No rVFC: yield a microtask so the decode can settle, then finish.
|
|
233
|
+
Promise.resolve().then(finish);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const onSeeked = () => afterSeeked();
|
|
237
|
+
el.addEventListener("seeked", onSeeked, { once: true });
|
|
238
|
+
// Guard: never let a missing `seeked` stall the render.
|
|
239
|
+
const timer = setTimeout(finish, 2000);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
144
243
|
function installTimeline(schema: TimelineSchema): void {
|
|
145
244
|
const rendererPresent =
|
|
146
245
|
typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
|
|
@@ -157,8 +256,25 @@ function installTimeline(schema: TimelineSchema): void {
|
|
|
157
256
|
const naturalMs = isFinite(el.duration) ? el.duration * 1000 : Infinity;
|
|
158
257
|
const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
|
|
159
258
|
el.pause();
|
|
160
|
-
|
|
161
|
-
|
|
259
|
+
// In renderer mode the engine drives video by swapping in pre-extracted
|
|
260
|
+
// frame images; we must NOT seek the live <video> (seeking under
|
|
261
|
+
// begin-frame-control crashes headless Chromium). In browser preview the
|
|
262
|
+
// setTimeout scheduler below plays it normally.
|
|
263
|
+
if (!rendererPresent) {
|
|
264
|
+
el.currentTime = seek / 1000;
|
|
265
|
+
}
|
|
266
|
+
return [
|
|
267
|
+
{
|
|
268
|
+
kind: "video",
|
|
269
|
+
at: i.at,
|
|
270
|
+
duration,
|
|
271
|
+
seek,
|
|
272
|
+
el,
|
|
273
|
+
selector: i.selector,
|
|
274
|
+
src: el.currentSrc || el.src,
|
|
275
|
+
frameset: i.frameset,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
162
278
|
}
|
|
163
279
|
|
|
164
280
|
if (i.kind === "css") {
|
|
@@ -219,28 +335,47 @@ function installTimeline(schema: TimelineSchema): void {
|
|
|
219
335
|
return [{ kind: "animation", at: i.at, duration, anim }];
|
|
220
336
|
});
|
|
221
337
|
|
|
338
|
+
// Metadata for the renderer engine: which `<video>`s to pre-extract + swap.
|
|
339
|
+
const videos: VideoSlotInfo[] = slots
|
|
340
|
+
.filter((s): s is VideoSlot => s.kind === "video")
|
|
341
|
+
.map((s) => ({
|
|
342
|
+
selector: s.selector,
|
|
343
|
+
at: s.at,
|
|
344
|
+
duration: s.duration,
|
|
345
|
+
seek: s.seek,
|
|
346
|
+
src: s.src,
|
|
347
|
+
frameset: s.frameset,
|
|
348
|
+
}));
|
|
349
|
+
|
|
222
350
|
window.__mvFrames = {
|
|
351
|
+
videos,
|
|
223
352
|
async applyState(t: number) {
|
|
224
353
|
const videoWaits: Promise<void>[] = [];
|
|
225
354
|
for (const s of slots) {
|
|
226
355
|
if (s.kind === "css-pending") continue; // only relevant in browser preview
|
|
227
|
-
const local = clamp(t - s.at, 0, s.duration);
|
|
228
356
|
if (s.kind === "video") {
|
|
229
|
-
s.
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// decode — otherwise
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
357
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
358
|
+
// Seek the live <video> EXCEPT on the renderer's begin-frame-control
|
|
359
|
+
// capture path (where seeking crashes the headless tab — the engine
|
|
360
|
+
// swaps in pre-extracted frame images instead, flagged via
|
|
361
|
+
// `__mvFramesNoVideoSeek`). On every live-seek path (editor, live
|
|
362
|
+
// preview, AND the renderer's screenshot capture) we must AWAIT the
|
|
363
|
+
// seeked frame's decode before resolving — otherwise a capture races
|
|
364
|
+
// an in-flight seek and grabs a stale/blank frame (and the per-frame
|
|
365
|
+
// loop spins, hitting the render wall-clock budget). `seekVideo`
|
|
366
|
+
// guards with a timeout so a missing `seeked` can't hang the render.
|
|
367
|
+
if (!window.__mvFramesNoVideoSeek) {
|
|
368
|
+
videoWaits.push(seekVideo(s.el, (s.seek + local) / 1000));
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
241
371
|
}
|
|
372
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
373
|
+
s.anim.currentTime = local;
|
|
242
374
|
}
|
|
243
|
-
await Promise.all([
|
|
375
|
+
await Promise.all([
|
|
376
|
+
document.fonts ? document.fonts.ready.then(() => undefined) : Promise.resolve(),
|
|
377
|
+
...videoWaits,
|
|
378
|
+
]);
|
|
244
379
|
},
|
|
245
380
|
};
|
|
246
381
|
|
|
@@ -280,52 +415,3 @@ function installTimeline(schema: TimelineSchema): void {
|
|
|
280
415
|
function clamp(v: number, lo: number, hi: number): number {
|
|
281
416
|
return v < lo ? lo : v > hi ? hi : v;
|
|
282
417
|
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Seek `el` to `targetSec` and resolve only once the seeked frame has
|
|
286
|
-
* actually decoded — so a subsequent capture grabs the right frame instead
|
|
287
|
-
* of racing an in-flight seek.
|
|
288
|
-
*
|
|
289
|
-
* Resolution waits for the `seeked` event, then for one decoded video frame
|
|
290
|
-
* via `requestVideoFrameCallback` where available (fallback: a microtask, so
|
|
291
|
-
* we still yield to the decode). A timeout guards against `seeked` never
|
|
292
|
-
* firing (e.g. seeking to the exact current position, or an unsupported
|
|
293
|
-
* codec) so `applyState` can't hang the whole render. Already-satisfied
|
|
294
|
-
* seeks (`readyState` high enough and time already at target) resolve fast.
|
|
295
|
-
*/
|
|
296
|
-
function seekVideo(el: HTMLVideoElement, targetSec: number): Promise<void> {
|
|
297
|
-
const HAVE_CURRENT_DATA = 2;
|
|
298
|
-
// If we're already at (or within a frame of) the target with data ready,
|
|
299
|
-
// there's nothing to wait for — assigning currentTime won't fire `seeked`.
|
|
300
|
-
const alreadyThere =
|
|
301
|
-
Math.abs(el.currentTime - targetSec) < 1e-3 && el.readyState >= HAVE_CURRENT_DATA;
|
|
302
|
-
el.currentTime = targetSec;
|
|
303
|
-
if (alreadyThere) return Promise.resolve();
|
|
304
|
-
|
|
305
|
-
return new Promise<void>((resolve) => {
|
|
306
|
-
let done = false;
|
|
307
|
-
const finish = () => {
|
|
308
|
-
if (done) return;
|
|
309
|
-
done = true;
|
|
310
|
-
el.removeEventListener("seeked", onSeeked);
|
|
311
|
-
clearTimeout(timer);
|
|
312
|
-
resolve();
|
|
313
|
-
};
|
|
314
|
-
const afterSeeked = () => {
|
|
315
|
-
// Wait for one decoded frame to be presented when the API exists.
|
|
316
|
-
const rvfc = (el as unknown as {
|
|
317
|
-
requestVideoFrameCallback?: (cb: () => void) => number;
|
|
318
|
-
}).requestVideoFrameCallback;
|
|
319
|
-
if (typeof rvfc === "function") {
|
|
320
|
-
rvfc.call(el, () => finish());
|
|
321
|
-
} else {
|
|
322
|
-
// No rVFC: yield a microtask so the decode can settle, then finish.
|
|
323
|
-
Promise.resolve().then(finish);
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
const onSeeked = () => afterSeeked();
|
|
327
|
-
el.addEventListener("seeked", onSeeked, { once: true });
|
|
328
|
-
// Guard: never let a missing `seeked` stall the render.
|
|
329
|
-
const timer = setTimeout(finish, 2000);
|
|
330
|
-
});
|
|
331
|
-
}
|