@maravilla-labs/frames 0.4.0 → 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.
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +96 -68
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
|
@@ -74,8 +74,25 @@ function installTimeline(schema) {
|
|
|
74
74
|
const naturalMs = isFinite(el.duration) ? el.duration * 1000 : Infinity;
|
|
75
75
|
const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
|
|
76
76
|
el.pause();
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// In renderer mode the engine drives video by swapping in pre-extracted
|
|
78
|
+
// frame images; we must NOT seek the live <video> (seeking under
|
|
79
|
+
// begin-frame-control crashes headless Chromium). In browser preview the
|
|
80
|
+
// setTimeout scheduler below plays it normally.
|
|
81
|
+
if (!rendererPresent) {
|
|
82
|
+
el.currentTime = seek / 1000;
|
|
83
|
+
}
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
kind: "video",
|
|
87
|
+
at: i.at,
|
|
88
|
+
duration,
|
|
89
|
+
seek,
|
|
90
|
+
el,
|
|
91
|
+
selector: i.selector,
|
|
92
|
+
src: el.currentSrc || el.src,
|
|
93
|
+
frameset: i.frameset,
|
|
94
|
+
},
|
|
95
|
+
];
|
|
79
96
|
}
|
|
80
97
|
if (i.kind === "css") {
|
|
81
98
|
if (!(el instanceof HTMLElement)) {
|
|
@@ -131,30 +148,40 @@ function installTimeline(schema) {
|
|
|
131
148
|
anim.pause();
|
|
132
149
|
return [{ kind: "animation", at: i.at, duration, anim }];
|
|
133
150
|
});
|
|
151
|
+
// Metadata for the renderer engine: which `<video>`s to pre-extract + swap.
|
|
152
|
+
const videos = slots
|
|
153
|
+
.filter((s) => s.kind === "video")
|
|
154
|
+
.map((s) => ({
|
|
155
|
+
selector: s.selector,
|
|
156
|
+
at: s.at,
|
|
157
|
+
duration: s.duration,
|
|
158
|
+
seek: s.seek,
|
|
159
|
+
src: s.src,
|
|
160
|
+
frameset: s.frameset,
|
|
161
|
+
}));
|
|
134
162
|
window.__mvFrames = {
|
|
163
|
+
videos,
|
|
135
164
|
async applyState(t) {
|
|
136
|
-
const videoWaits = [];
|
|
137
165
|
for (const s of slots) {
|
|
138
166
|
if (s.kind === "css-pending")
|
|
139
167
|
continue; // only relevant in browser preview
|
|
140
|
-
const local = clamp(t - s.at, 0, s.duration);
|
|
141
168
|
if (s.kind === "video") {
|
|
142
|
-
s.
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
else {
|
|
154
|
-
s.anim.currentTime = local;
|
|
169
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
170
|
+
// Seek the live <video> EXCEPT on the renderer's begin-frame-control
|
|
171
|
+
// capture path (where seeking crashes the headless tab — the engine
|
|
172
|
+
// swaps in pre-extracted frame images instead, flagged via
|
|
173
|
+
// `__mvFramesNoVideoSeek`). So the editor, live preview, AND the
|
|
174
|
+
// renderer's screenshot path all seek normally — frame-accurate
|
|
175
|
+
// "time travel" (StagePreview drives this via applyState).
|
|
176
|
+
if (!window.__mvFramesNoVideoSeek) {
|
|
177
|
+
s.el.currentTime = (s.seek + local) / 1000;
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
155
180
|
}
|
|
181
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
182
|
+
s.anim.currentTime = local;
|
|
156
183
|
}
|
|
157
|
-
await
|
|
184
|
+
await document.fonts.ready;
|
|
158
185
|
},
|
|
159
186
|
};
|
|
160
187
|
if (!rendererPresent) {
|
|
@@ -194,51 +221,4 @@ function installTimeline(schema) {
|
|
|
194
221
|
function clamp(v, lo, hi) {
|
|
195
222
|
return v < lo ? lo : v > hi ? hi : v;
|
|
196
223
|
}
|
|
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
224
|
//# 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.0",
|
|
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;
|
|
@@ -157,8 +205,25 @@ function installTimeline(schema: TimelineSchema): void {
|
|
|
157
205
|
const naturalMs = isFinite(el.duration) ? el.duration * 1000 : Infinity;
|
|
158
206
|
const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
|
|
159
207
|
el.pause();
|
|
160
|
-
|
|
161
|
-
|
|
208
|
+
// In renderer mode the engine drives video by swapping in pre-extracted
|
|
209
|
+
// frame images; we must NOT seek the live <video> (seeking under
|
|
210
|
+
// begin-frame-control crashes headless Chromium). In browser preview the
|
|
211
|
+
// setTimeout scheduler below plays it normally.
|
|
212
|
+
if (!rendererPresent) {
|
|
213
|
+
el.currentTime = seek / 1000;
|
|
214
|
+
}
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
kind: "video",
|
|
218
|
+
at: i.at,
|
|
219
|
+
duration,
|
|
220
|
+
seek,
|
|
221
|
+
el,
|
|
222
|
+
selector: i.selector,
|
|
223
|
+
src: el.currentSrc || el.src,
|
|
224
|
+
frameset: i.frameset,
|
|
225
|
+
},
|
|
226
|
+
];
|
|
162
227
|
}
|
|
163
228
|
|
|
164
229
|
if (i.kind === "css") {
|
|
@@ -219,28 +284,40 @@ function installTimeline(schema: TimelineSchema): void {
|
|
|
219
284
|
return [{ kind: "animation", at: i.at, duration, anim }];
|
|
220
285
|
});
|
|
221
286
|
|
|
287
|
+
// Metadata for the renderer engine: which `<video>`s to pre-extract + swap.
|
|
288
|
+
const videos: VideoSlotInfo[] = slots
|
|
289
|
+
.filter((s): s is VideoSlot => s.kind === "video")
|
|
290
|
+
.map((s) => ({
|
|
291
|
+
selector: s.selector,
|
|
292
|
+
at: s.at,
|
|
293
|
+
duration: s.duration,
|
|
294
|
+
seek: s.seek,
|
|
295
|
+
src: s.src,
|
|
296
|
+
frameset: s.frameset,
|
|
297
|
+
}));
|
|
298
|
+
|
|
222
299
|
window.__mvFrames = {
|
|
300
|
+
videos,
|
|
223
301
|
async applyState(t: number) {
|
|
224
|
-
const videoWaits: Promise<void>[] = [];
|
|
225
302
|
for (const s of slots) {
|
|
226
303
|
if (s.kind === "css-pending") continue; // only relevant in browser preview
|
|
227
|
-
const local = clamp(t - s.at, 0, s.duration);
|
|
228
304
|
if (s.kind === "video") {
|
|
229
|
-
s.
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
s.anim.currentTime = local;
|
|
305
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
306
|
+
// Seek the live <video> EXCEPT on the renderer's begin-frame-control
|
|
307
|
+
// capture path (where seeking crashes the headless tab — the engine
|
|
308
|
+
// swaps in pre-extracted frame images instead, flagged via
|
|
309
|
+
// `__mvFramesNoVideoSeek`). So the editor, live preview, AND the
|
|
310
|
+
// renderer's screenshot path all seek normally — frame-accurate
|
|
311
|
+
// "time travel" (StagePreview drives this via applyState).
|
|
312
|
+
if (!window.__mvFramesNoVideoSeek) {
|
|
313
|
+
s.el.currentTime = (s.seek + local) / 1000;
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
241
316
|
}
|
|
317
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
318
|
+
s.anim.currentTime = local;
|
|
242
319
|
}
|
|
243
|
-
await
|
|
320
|
+
await document.fonts.ready;
|
|
244
321
|
},
|
|
245
322
|
};
|
|
246
323
|
|
|
@@ -280,52 +357,3 @@ function installTimeline(schema: TimelineSchema): void {
|
|
|
280
357
|
function clamp(v: number, lo: number, hi: number): number {
|
|
281
358
|
return v < lo ? lo : v > hi ? hi : v;
|
|
282
359
|
}
|
|
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
|
-
}
|