@maravilla-labs/frames 0.3.3 → 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/README.md +14 -4
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +158 -68
package/README.md
CHANGED
|
@@ -115,15 +115,25 @@ a WAAPI `Animation` object via `Element.getAnimations()`, and drives
|
|
|
115
115
|
its `currentTime` exactly like every other slot. The visual is the
|
|
116
116
|
class's native CSS animation; the timing is yours to control.
|
|
117
117
|
|
|
118
|
-
The stylesheet that defines the keyframes must be loaded before
|
|
119
|
-
|
|
120
|
-
`
|
|
118
|
+
The stylesheet that defines the keyframes must be loaded before the
|
|
119
|
+
animations are materialised — but **you don't need to do anything for
|
|
120
|
+
that**. `defineTimeline` waits internally for render-blocking
|
|
121
|
+
stylesheets and fonts to be ready before it reads the keyframes, so you
|
|
122
|
+
can just call it as soon as your module runs:
|
|
121
123
|
|
|
122
124
|
```ts
|
|
123
|
-
await new Promise(r => addEventListener("load", r, { once: true }));
|
|
124
125
|
defineTimeline({ /* ... */ });
|
|
125
126
|
```
|
|
126
127
|
|
|
128
|
+
> **Do not** gate `defineTimeline` behind the window `load` event
|
|
129
|
+
> (`await new Promise(r => addEventListener("load", r))`). `load` is also
|
|
130
|
+
> held open by `<video preload>` and `<img>`, which on a media-heavy page
|
|
131
|
+
> can take long enough to blow past the Maravilla renderer's readiness
|
|
132
|
+
> budget and fail the render with "timeline never became available."
|
|
133
|
+
> `defineTimeline` already waits for exactly what it needs (stylesheets +
|
|
134
|
+
> fonts) and nothing it doesn't (media). If you need that signal yourself,
|
|
135
|
+
> import `stylesheetsReady()`.
|
|
136
|
+
|
|
127
137
|
This works for any library that exposes its motion as CSS classes
|
|
128
138
|
plus shipped `@keyframes` — Animate.css, Magic, your own custom
|
|
129
139
|
keyframes — not just Animate.css specifically.
|
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,13 +68,61 @@ 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
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Declare the page's timeline. **Safe to call as soon as your module runs** —
|
|
108
|
+
* it internally waits for render-blocking stylesheets and fonts to be ready
|
|
109
|
+
* (so CSS keyframes, e.g. Animate.css, are present before animations are
|
|
110
|
+
* materialised) and only then registers `window.__mvFrames`.
|
|
111
|
+
*
|
|
112
|
+
* It deliberately does **NOT** wait for the window `load` event or for media
|
|
113
|
+
* (`<video preload>`, `<img>`). Those can take a long time — long enough to
|
|
114
|
+
* blow past the Maravilla renderer's readiness budget — and aren't needed to
|
|
115
|
+
* build the timeline. **Do not** gate this behind
|
|
116
|
+
* `window.addEventListener('load', …)`; that was an old recommendation and is
|
|
117
|
+
* the classic cause of "renderer timed out waiting for the timeline" on pages
|
|
118
|
+
* with video. `defineTimeline` now owns that readiness so every app gets it
|
|
119
|
+
* right by default.
|
|
120
|
+
*/
|
|
70
121
|
export declare function defineTimeline(schema: TimelineSchema): void;
|
|
122
|
+
/**
|
|
123
|
+
* Resolve once the document's render-blocking stylesheets are applied and web
|
|
124
|
+
* fonts are ready — **without** waiting on the window `load` event or media.
|
|
125
|
+
* Exposed for advanced callers; [`defineTimeline`] already awaits it.
|
|
126
|
+
*/
|
|
127
|
+
export declare function stylesheetsReady(): Promise<void>;
|
|
71
128
|
//# sourceMappingURL=index.d.ts.map
|
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
|
@@ -7,7 +7,60 @@
|
|
|
7
7
|
* - Inside the Maravilla renderer, the timeline is stepped frame by
|
|
8
8
|
* frame to produce deterministic video output.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Declare the page's timeline. **Safe to call as soon as your module runs** —
|
|
12
|
+
* it internally waits for render-blocking stylesheets and fonts to be ready
|
|
13
|
+
* (so CSS keyframes, e.g. Animate.css, are present before animations are
|
|
14
|
+
* materialised) and only then registers `window.__mvFrames`.
|
|
15
|
+
*
|
|
16
|
+
* It deliberately does **NOT** wait for the window `load` event or for media
|
|
17
|
+
* (`<video preload>`, `<img>`). Those can take a long time — long enough to
|
|
18
|
+
* blow past the Maravilla renderer's readiness budget — and aren't needed to
|
|
19
|
+
* build the timeline. **Do not** gate this behind
|
|
20
|
+
* `window.addEventListener('load', …)`; that was an old recommendation and is
|
|
21
|
+
* the classic cause of "renderer timed out waiting for the timeline" on pages
|
|
22
|
+
* with video. `defineTimeline` now owns that readiness so every app gets it
|
|
23
|
+
* right by default.
|
|
24
|
+
*/
|
|
10
25
|
export function defineTimeline(schema) {
|
|
26
|
+
stylesheetsReady()
|
|
27
|
+
.then(() => installTimeline(schema))
|
|
28
|
+
.catch((err) => {
|
|
29
|
+
// Surface in the page console (the renderer captures console output) so
|
|
30
|
+
// a bad selector / missing keyframes is debuggable. On failure
|
|
31
|
+
// `__mvFrames` simply never registers and the renderer reports a clean
|
|
32
|
+
// readiness timeout rather than hanging.
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.error("[@maravilla-labs/frames] defineTimeline failed:", err);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve once the document's render-blocking stylesheets are applied and web
|
|
39
|
+
* fonts are ready — **without** waiting on the window `load` event or media.
|
|
40
|
+
* Exposed for advanced callers; [`defineTimeline`] already awaits it.
|
|
41
|
+
*/
|
|
42
|
+
export function stylesheetsReady() {
|
|
43
|
+
if (typeof document === "undefined")
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
const domReady = document.readyState === "loading"
|
|
46
|
+
? new Promise((resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true }))
|
|
47
|
+
: Promise.resolve();
|
|
48
|
+
return domReady
|
|
49
|
+
.then(() => {
|
|
50
|
+
// A <link rel="stylesheet"> has a null `.sheet` until it has loaded +
|
|
51
|
+
// parsed. Wait for any still-pending ones; `error` resolves too so a
|
|
52
|
+
// missing/broken stylesheet can't hang the timeline.
|
|
53
|
+
const pending = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
54
|
+
.filter((link) => !link.sheet)
|
|
55
|
+
.map((link) => new Promise((resolve) => {
|
|
56
|
+
link.addEventListener("load", () => resolve(), { once: true });
|
|
57
|
+
link.addEventListener("error", () => resolve(), { once: true });
|
|
58
|
+
}));
|
|
59
|
+
return Promise.all(pending);
|
|
60
|
+
})
|
|
61
|
+
.then(() => (document.fonts ? document.fonts.ready.then(() => undefined) : undefined));
|
|
62
|
+
}
|
|
63
|
+
function installTimeline(schema) {
|
|
11
64
|
const rendererPresent = typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
|
|
12
65
|
const slots = schema.instructions.flatMap((i) => {
|
|
13
66
|
const el = document.querySelector(i.selector);
|
|
@@ -21,8 +74,25 @@ export function defineTimeline(schema) {
|
|
|
21
74
|
const naturalMs = isFinite(el.duration) ? el.duration * 1000 : Infinity;
|
|
22
75
|
const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
|
|
23
76
|
el.pause();
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
];
|
|
26
96
|
}
|
|
27
97
|
if (i.kind === "css") {
|
|
28
98
|
if (!(el instanceof HTMLElement)) {
|
|
@@ -78,30 +148,40 @@ export function defineTimeline(schema) {
|
|
|
78
148
|
anim.pause();
|
|
79
149
|
return [{ kind: "animation", at: i.at, duration, anim }];
|
|
80
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
|
+
}));
|
|
81
162
|
window.__mvFrames = {
|
|
163
|
+
videos,
|
|
82
164
|
async applyState(t) {
|
|
83
|
-
const videoWaits = [];
|
|
84
165
|
for (const s of slots) {
|
|
85
166
|
if (s.kind === "css-pending")
|
|
86
167
|
continue; // only relevant in browser preview
|
|
87
|
-
const local = clamp(t - s.at, 0, s.duration);
|
|
88
168
|
if (s.kind === "video") {
|
|
89
|
-
s.
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
else {
|
|
101
|
-
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;
|
|
102
180
|
}
|
|
181
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
182
|
+
s.anim.currentTime = local;
|
|
103
183
|
}
|
|
104
|
-
await
|
|
184
|
+
await document.fonts.ready;
|
|
105
185
|
},
|
|
106
186
|
};
|
|
107
187
|
if (!rendererPresent) {
|
|
@@ -141,51 +221,4 @@ export function defineTimeline(schema) {
|
|
|
141
221
|
function clamp(v, lo, hi) {
|
|
142
222
|
return v < lo ? lo : v > hi ? hi : v;
|
|
143
223
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Seek `el` to `targetSec` and resolve only once the seeked frame has
|
|
146
|
-
* actually decoded — so a subsequent capture grabs the right frame instead
|
|
147
|
-
* of racing an in-flight seek.
|
|
148
|
-
*
|
|
149
|
-
* Resolution waits for the `seeked` event, then for one decoded video frame
|
|
150
|
-
* via `requestVideoFrameCallback` where available (fallback: a microtask, so
|
|
151
|
-
* we still yield to the decode). A timeout guards against `seeked` never
|
|
152
|
-
* firing (e.g. seeking to the exact current position, or an unsupported
|
|
153
|
-
* codec) so `applyState` can't hang the whole render. Already-satisfied
|
|
154
|
-
* seeks (`readyState` high enough and time already at target) resolve fast.
|
|
155
|
-
*/
|
|
156
|
-
function seekVideo(el, targetSec) {
|
|
157
|
-
const HAVE_CURRENT_DATA = 2;
|
|
158
|
-
// If we're already at (or within a frame of) the target with data ready,
|
|
159
|
-
// there's nothing to wait for — assigning currentTime won't fire `seeked`.
|
|
160
|
-
const alreadyThere = Math.abs(el.currentTime - targetSec) < 1e-3 && el.readyState >= HAVE_CURRENT_DATA;
|
|
161
|
-
el.currentTime = targetSec;
|
|
162
|
-
if (alreadyThere)
|
|
163
|
-
return Promise.resolve();
|
|
164
|
-
return new Promise((resolve) => {
|
|
165
|
-
let done = false;
|
|
166
|
-
const finish = () => {
|
|
167
|
-
if (done)
|
|
168
|
-
return;
|
|
169
|
-
done = true;
|
|
170
|
-
el.removeEventListener("seeked", onSeeked);
|
|
171
|
-
clearTimeout(timer);
|
|
172
|
-
resolve();
|
|
173
|
-
};
|
|
174
|
-
const afterSeeked = () => {
|
|
175
|
-
// Wait for one decoded frame to be presented when the API exists.
|
|
176
|
-
const rvfc = el.requestVideoFrameCallback;
|
|
177
|
-
if (typeof rvfc === "function") {
|
|
178
|
-
rvfc.call(el, () => finish());
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
// No rVFC: yield a microtask so the decode can settle, then finish.
|
|
182
|
-
Promise.resolve().then(finish);
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
const onSeeked = () => afterSeeked();
|
|
186
|
-
el.addEventListener("seeked", onSeeked, { once: true });
|
|
187
|
-
// Guard: never let a missing `seeked` stall the render.
|
|
188
|
-
const timer = setTimeout(finish, 2000);
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
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;
|
|
@@ -79,7 +127,69 @@ type CssPendingSlot = {
|
|
|
79
127
|
};
|
|
80
128
|
type Slot = AnimSlot | VideoSlot | CssPendingSlot;
|
|
81
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Declare the page's timeline. **Safe to call as soon as your module runs** —
|
|
132
|
+
* it internally waits for render-blocking stylesheets and fonts to be ready
|
|
133
|
+
* (so CSS keyframes, e.g. Animate.css, are present before animations are
|
|
134
|
+
* materialised) and only then registers `window.__mvFrames`.
|
|
135
|
+
*
|
|
136
|
+
* It deliberately does **NOT** wait for the window `load` event or for media
|
|
137
|
+
* (`<video preload>`, `<img>`). Those can take a long time — long enough to
|
|
138
|
+
* blow past the Maravilla renderer's readiness budget — and aren't needed to
|
|
139
|
+
* build the timeline. **Do not** gate this behind
|
|
140
|
+
* `window.addEventListener('load', …)`; that was an old recommendation and is
|
|
141
|
+
* the classic cause of "renderer timed out waiting for the timeline" on pages
|
|
142
|
+
* with video. `defineTimeline` now owns that readiness so every app gets it
|
|
143
|
+
* right by default.
|
|
144
|
+
*/
|
|
82
145
|
export function defineTimeline(schema: TimelineSchema): void {
|
|
146
|
+
stylesheetsReady()
|
|
147
|
+
.then(() => installTimeline(schema))
|
|
148
|
+
.catch((err) => {
|
|
149
|
+
// Surface in the page console (the renderer captures console output) so
|
|
150
|
+
// a bad selector / missing keyframes is debuggable. On failure
|
|
151
|
+
// `__mvFrames` simply never registers and the renderer reports a clean
|
|
152
|
+
// readiness timeout rather than hanging.
|
|
153
|
+
// eslint-disable-next-line no-console
|
|
154
|
+
console.error("[@maravilla-labs/frames] defineTimeline failed:", err);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve once the document's render-blocking stylesheets are applied and web
|
|
160
|
+
* fonts are ready — **without** waiting on the window `load` event or media.
|
|
161
|
+
* Exposed for advanced callers; [`defineTimeline`] already awaits it.
|
|
162
|
+
*/
|
|
163
|
+
export function stylesheetsReady(): Promise<void> {
|
|
164
|
+
if (typeof document === "undefined") return Promise.resolve();
|
|
165
|
+
const domReady =
|
|
166
|
+
document.readyState === "loading"
|
|
167
|
+
? new Promise<void>((resolve) =>
|
|
168
|
+
document.addEventListener("DOMContentLoaded", () => resolve(), { once: true }),
|
|
169
|
+
)
|
|
170
|
+
: Promise.resolve();
|
|
171
|
+
return domReady
|
|
172
|
+
.then(() => {
|
|
173
|
+
// A <link rel="stylesheet"> has a null `.sheet` until it has loaded +
|
|
174
|
+
// parsed. Wait for any still-pending ones; `error` resolves too so a
|
|
175
|
+
// missing/broken stylesheet can't hang the timeline.
|
|
176
|
+
const pending = Array.from(
|
|
177
|
+
document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]'),
|
|
178
|
+
)
|
|
179
|
+
.filter((link) => !link.sheet)
|
|
180
|
+
.map(
|
|
181
|
+
(link) =>
|
|
182
|
+
new Promise<void>((resolve) => {
|
|
183
|
+
link.addEventListener("load", () => resolve(), { once: true });
|
|
184
|
+
link.addEventListener("error", () => resolve(), { once: true });
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
return Promise.all(pending);
|
|
188
|
+
})
|
|
189
|
+
.then(() => (document.fonts ? document.fonts.ready.then(() => undefined) : undefined));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function installTimeline(schema: TimelineSchema): void {
|
|
83
193
|
const rendererPresent =
|
|
84
194
|
typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
|
|
85
195
|
|
|
@@ -95,8 +205,25 @@ export function defineTimeline(schema: TimelineSchema): void {
|
|
|
95
205
|
const naturalMs = isFinite(el.duration) ? el.duration * 1000 : Infinity;
|
|
96
206
|
const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
|
|
97
207
|
el.pause();
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
];
|
|
100
227
|
}
|
|
101
228
|
|
|
102
229
|
if (i.kind === "css") {
|
|
@@ -157,28 +284,40 @@ export function defineTimeline(schema: TimelineSchema): void {
|
|
|
157
284
|
return [{ kind: "animation", at: i.at, duration, anim }];
|
|
158
285
|
});
|
|
159
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
|
+
|
|
160
299
|
window.__mvFrames = {
|
|
300
|
+
videos,
|
|
161
301
|
async applyState(t: number) {
|
|
162
|
-
const videoWaits: Promise<void>[] = [];
|
|
163
302
|
for (const s of slots) {
|
|
164
303
|
if (s.kind === "css-pending") continue; // only relevant in browser preview
|
|
165
|
-
const local = clamp(t - s.at, 0, s.duration);
|
|
166
304
|
if (s.kind === "video") {
|
|
167
|
-
s.
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
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;
|
|
179
316
|
}
|
|
317
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
318
|
+
s.anim.currentTime = local;
|
|
180
319
|
}
|
|
181
|
-
await
|
|
320
|
+
await document.fonts.ready;
|
|
182
321
|
},
|
|
183
322
|
};
|
|
184
323
|
|
|
@@ -218,52 +357,3 @@ export function defineTimeline(schema: TimelineSchema): void {
|
|
|
218
357
|
function clamp(v: number, lo: number, hi: number): number {
|
|
219
358
|
return v < lo ? lo : v > hi ? hi : v;
|
|
220
359
|
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Seek `el` to `targetSec` and resolve only once the seeked frame has
|
|
224
|
-
* actually decoded — so a subsequent capture grabs the right frame instead
|
|
225
|
-
* of racing an in-flight seek.
|
|
226
|
-
*
|
|
227
|
-
* Resolution waits for the `seeked` event, then for one decoded video frame
|
|
228
|
-
* via `requestVideoFrameCallback` where available (fallback: a microtask, so
|
|
229
|
-
* we still yield to the decode). A timeout guards against `seeked` never
|
|
230
|
-
* firing (e.g. seeking to the exact current position, or an unsupported
|
|
231
|
-
* codec) so `applyState` can't hang the whole render. Already-satisfied
|
|
232
|
-
* seeks (`readyState` high enough and time already at target) resolve fast.
|
|
233
|
-
*/
|
|
234
|
-
function seekVideo(el: HTMLVideoElement, targetSec: number): Promise<void> {
|
|
235
|
-
const HAVE_CURRENT_DATA = 2;
|
|
236
|
-
// If we're already at (or within a frame of) the target with data ready,
|
|
237
|
-
// there's nothing to wait for — assigning currentTime won't fire `seeked`.
|
|
238
|
-
const alreadyThere =
|
|
239
|
-
Math.abs(el.currentTime - targetSec) < 1e-3 && el.readyState >= HAVE_CURRENT_DATA;
|
|
240
|
-
el.currentTime = targetSec;
|
|
241
|
-
if (alreadyThere) return Promise.resolve();
|
|
242
|
-
|
|
243
|
-
return new Promise<void>((resolve) => {
|
|
244
|
-
let done = false;
|
|
245
|
-
const finish = () => {
|
|
246
|
-
if (done) return;
|
|
247
|
-
done = true;
|
|
248
|
-
el.removeEventListener("seeked", onSeeked);
|
|
249
|
-
clearTimeout(timer);
|
|
250
|
-
resolve();
|
|
251
|
-
};
|
|
252
|
-
const afterSeeked = () => {
|
|
253
|
-
// Wait for one decoded frame to be presented when the API exists.
|
|
254
|
-
const rvfc = (el as unknown as {
|
|
255
|
-
requestVideoFrameCallback?: (cb: () => void) => number;
|
|
256
|
-
}).requestVideoFrameCallback;
|
|
257
|
-
if (typeof rvfc === "function") {
|
|
258
|
-
rvfc.call(el, () => finish());
|
|
259
|
-
} else {
|
|
260
|
-
// No rVFC: yield a microtask so the decode can settle, then finish.
|
|
261
|
-
Promise.resolve().then(finish);
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
const onSeeked = () => afterSeeked();
|
|
265
|
-
el.addEventListener("seeked", onSeeked, { once: true });
|
|
266
|
-
// Guard: never let a missing `seeked` stall the render.
|
|
267
|
-
const timer = setTimeout(finish, 2000);
|
|
268
|
-
});
|
|
269
|
-
}
|