@sequio/server 0.1.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.js +166 -0
- package/dist/route-b/env.d.ts +15 -0
- package/dist/route-b/export-node.d.ts +46 -0
- package/dist/route-b/fonts-node.d.ts +24 -0
- package/dist/route-b/frame-node.d.ts +28 -0
- package/dist/route-b/index.d.ts +17 -0
- package/dist/route-b/render-bundle.d.ts +41 -0
- package/dist/route-b.js +326 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/sample-timeline.d.ts +3 -0
- package/dist/src/timeline.d.ts +172 -0
- package/package.json +75 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { TextClip as g, ShapeClip as w, ImageSource as y, ImageClip as b, VideoSource as I, VideoClip as v, BlurEffect as S, ColorEffect as x, Timebase as k, Compositor as C, VisualTrack as O, AudioEngine as p, AudioSource as z, AudioClip as E, easeInOutCubic as F, easeOutCubic as T, easeInCubic as A, easeInOutQuad as H, easeOutQuad as M, easeInQuad as W, hold as Q, linear as R, fonts as d } from "@sequio/engine";
|
|
2
|
+
const V = {
|
|
3
|
+
linear: R,
|
|
4
|
+
hold: Q,
|
|
5
|
+
easeInQuad: W,
|
|
6
|
+
easeOutQuad: M,
|
|
7
|
+
easeInOutQuad: H,
|
|
8
|
+
easeInCubic: A,
|
|
9
|
+
easeOutCubic: T,
|
|
10
|
+
easeInOutCubic: F
|
|
11
|
+
};
|
|
12
|
+
function s(t, e) {
|
|
13
|
+
e !== void 0 && (typeof e == "object" && e !== null && "keyframes" in e ? t.setKeyframes(e.keyframes.map((o) => ({ time: o.time, value: o.value, easing: o.easing ? V[o.easing] : void 0 }))) : t.setStatic(e));
|
|
14
|
+
}
|
|
15
|
+
function c(t) {
|
|
16
|
+
if (t.type === "blur") {
|
|
17
|
+
const o = new S();
|
|
18
|
+
return t.strength !== void 0 && o.strength.setStatic(t.strength), o;
|
|
19
|
+
}
|
|
20
|
+
const e = new x();
|
|
21
|
+
return t.brightness !== void 0 && e.brightness.setStatic(t.brightness), t.contrast !== void 0 && e.contrast.setStatic(t.contrast), t.saturation !== void 0 && e.saturation.setStatic(t.saturation), e;
|
|
22
|
+
}
|
|
23
|
+
function B(t, e) {
|
|
24
|
+
t.start = e.start, t.end = e.end, e.sourceIn !== void 0 && (t.sourceIn = e.sourceIn), e.sourceOut !== void 0 && (t.sourceOut = e.sourceOut), e.speed !== void 0 && (t.speed = e.speed), e.blendMode && (t.blendMode = e.blendMode), s(t.opacity, e.opacity);
|
|
25
|
+
const o = e.transform;
|
|
26
|
+
o && (o.anchor && t.transform.anchor.setStatic(o.anchor), s(t.transform.position, o.position), s(t.transform.scale, o.scale), s(t.transform.rotation, o.rotation));
|
|
27
|
+
for (const i of e.effects ?? []) t.effects.push(c(i));
|
|
28
|
+
}
|
|
29
|
+
async function G(t, e) {
|
|
30
|
+
let o;
|
|
31
|
+
if (t.type === "text")
|
|
32
|
+
o = new g({ text: t.text, fontFamily: t.fontFamily, fill: t.fill }), s(o.fontSize, t.fontSize);
|
|
33
|
+
else if (t.type === "shape")
|
|
34
|
+
o = new w(t.shape);
|
|
35
|
+
else if (t.type === "image") {
|
|
36
|
+
const i = new y({ src: t.src });
|
|
37
|
+
await i.load(), e.push(i), o = new b(i);
|
|
38
|
+
} else {
|
|
39
|
+
const i = new I({ src: t.src, cacheFrames: t.cacheFrames, lookahead: t.lookahead });
|
|
40
|
+
await i.load(), e.push(i), o = new v(i);
|
|
41
|
+
}
|
|
42
|
+
return B(o, t), o;
|
|
43
|
+
}
|
|
44
|
+
function j(t) {
|
|
45
|
+
let e = 0;
|
|
46
|
+
for (const o of t.tracks ?? []) for (const i of o.clips) e = Math.max(e, i.end);
|
|
47
|
+
for (const o of t.audio ?? []) e = Math.max(e, o.end);
|
|
48
|
+
return e;
|
|
49
|
+
}
|
|
50
|
+
async function D(t) {
|
|
51
|
+
for (const e of t ?? [])
|
|
52
|
+
e.google ? await d.loadGoogleFont({ family: e.family, weights: e.google.weights, italic: e.google.italic }) : e.src && await d.load({ family: e.family, src: e.src });
|
|
53
|
+
await d.ready();
|
|
54
|
+
}
|
|
55
|
+
async function N(t, e = {}) {
|
|
56
|
+
const o = new k(t.fps), i = new C({
|
|
57
|
+
width: t.width,
|
|
58
|
+
height: t.height,
|
|
59
|
+
timebase: o,
|
|
60
|
+
background: t.background ?? 0,
|
|
61
|
+
origin: t.origin,
|
|
62
|
+
createRenderer: e.createRenderer,
|
|
63
|
+
resolution: e.resolution
|
|
64
|
+
});
|
|
65
|
+
await i.init(), await (e.loadFonts ?? D)(t.fonts);
|
|
66
|
+
const f = [];
|
|
67
|
+
for (const n of t.tracks ?? []) {
|
|
68
|
+
if (n.clips.length === 0) continue;
|
|
69
|
+
const r = new O();
|
|
70
|
+
r.zIndex = n.zIndex ?? 0;
|
|
71
|
+
for (const a of n.clips) r.add(await G(a, f));
|
|
72
|
+
i.addTrack(r);
|
|
73
|
+
}
|
|
74
|
+
for (const n of t.effects ?? []) i.effects.push(c(n));
|
|
75
|
+
const u = new p(o), l = (t.audio?.length ?? 0) > 0;
|
|
76
|
+
for (const n of t.audio ?? []) {
|
|
77
|
+
const r = new z({ src: n.src });
|
|
78
|
+
await r.load(), f.push(r);
|
|
79
|
+
const a = new E();
|
|
80
|
+
a.start = n.start, a.end = n.end, n.sourceIn !== void 0 && (a.sourceIn = n.sourceIn), n.gain !== void 0 && a.gain.setStatic(n.gain), n.fadeIn !== void 0 && (a.fadeIn = n.fadeIn), n.fadeOut !== void 0 && (a.fadeOut = n.fadeOut), u.schedule(a, r);
|
|
81
|
+
}
|
|
82
|
+
const h = t.range ?? [0, j(t)], m = { audio: l, ...t.export };
|
|
83
|
+
return {
|
|
84
|
+
compositor: i,
|
|
85
|
+
audioEngine: u,
|
|
86
|
+
range: h,
|
|
87
|
+
exportOptions: m,
|
|
88
|
+
hasAudio: l,
|
|
89
|
+
dispose() {
|
|
90
|
+
for (const n of f) n.dispose();
|
|
91
|
+
i.dispose();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function P() {
|
|
96
|
+
return {
|
|
97
|
+
width: 320,
|
|
98
|
+
height: 180,
|
|
99
|
+
fps: 30,
|
|
100
|
+
background: 1052692,
|
|
101
|
+
range: [0, 2],
|
|
102
|
+
tracks: [
|
|
103
|
+
// Background: a full-frame teal rectangle.
|
|
104
|
+
{
|
|
105
|
+
zIndex: 0,
|
|
106
|
+
clips: [
|
|
107
|
+
{
|
|
108
|
+
type: "shape",
|
|
109
|
+
shape: { kind: "rect", width: 320, height: 180, fill: 1013358 },
|
|
110
|
+
start: 0,
|
|
111
|
+
end: 2,
|
|
112
|
+
transform: { anchor: [0, 0], position: [0, 0] }
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
// Foreground: a circle sliding left→right while the title fades in.
|
|
117
|
+
{
|
|
118
|
+
zIndex: 1,
|
|
119
|
+
clips: [
|
|
120
|
+
{
|
|
121
|
+
type: "shape",
|
|
122
|
+
shape: { kind: "ellipse", width: 48, height: 48, fill: 16096779 },
|
|
123
|
+
start: 0,
|
|
124
|
+
end: 2,
|
|
125
|
+
transform: {
|
|
126
|
+
anchor: [0.5, 0.5],
|
|
127
|
+
position: {
|
|
128
|
+
keyframes: [
|
|
129
|
+
{ time: 0, value: [40, 180 / 2] },
|
|
130
|
+
{ time: 2, value: [280, 180 / 2], easing: "easeInOutCubic" }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
// A filter (shader) — renders on the server only via Route B's WebGPU backend.
|
|
135
|
+
effects: [{ type: "blur", strength: 4 }]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: "Server-Side Render",
|
|
140
|
+
fontSize: 26,
|
|
141
|
+
fill: 16766474,
|
|
142
|
+
// coloured text — exercises the BGRA swizzle on the Node path
|
|
143
|
+
start: 0,
|
|
144
|
+
end: 2,
|
|
145
|
+
transform: { anchor: [0.5, 0.5], position: [320 / 2, 40] },
|
|
146
|
+
opacity: {
|
|
147
|
+
keyframes: [
|
|
148
|
+
{ time: 0, value: 0 },
|
|
149
|
+
{ time: 0.6, value: 1, easing: "easeOutQuad" }
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
export: { container: "mp4", videoCodec: "avc", bitrate: 2e6 }
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export {
|
|
160
|
+
G as buildClip,
|
|
161
|
+
c as buildEffect,
|
|
162
|
+
N as buildTimeline,
|
|
163
|
+
P as sampleTimeline,
|
|
164
|
+
j as timelineEnd
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AutoDetectOptions, MediabunnyModule, Renderer } from '@sequio/engine';
|
|
2
|
+
/** The mediabunny instance carrying the node-av encoders. Call after {@link setupNodeEnvironment}. */
|
|
3
|
+
export declare function getMediabunny(): MediabunnyModule;
|
|
4
|
+
/**
|
|
5
|
+
* Install the browser-global shims and return the GPU handle. Idempotent.
|
|
6
|
+
* Throws if no WebGPU adapter is available (no GPU and no software Vulkan
|
|
7
|
+
* driver) — the caller should surface that as "Route B unavailable on this host".
|
|
8
|
+
*/
|
|
9
|
+
export declare function setupNodeEnvironment(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* A {@link CompositorOptions.createRenderer} factory that builds a PixiJS WebGPU
|
|
12
|
+
* renderer in Node. Requires {@link setupNodeEnvironment} to have run.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createNodeWebGPURenderer(options: Partial<AutoDetectOptions>): Promise<Renderer>;
|
|
15
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AudioEngine, Compositor, Renderer } from '@sequio/engine';
|
|
2
|
+
export interface NodeExportOptions {
|
|
3
|
+
fps: number;
|
|
4
|
+
range: [number, number];
|
|
5
|
+
/** Output file path. The extension is corrected to match the encoded container. */
|
|
6
|
+
out: string;
|
|
7
|
+
/** Preferred container; falls back to what node-av can encode. */
|
|
8
|
+
container?: 'mp4' | 'webm';
|
|
9
|
+
videoCodec?: string;
|
|
10
|
+
bitrate?: number;
|
|
11
|
+
/** Mux the {@link AudioEngine}'s offline mix as an audio track. */
|
|
12
|
+
audio?: {
|
|
13
|
+
engine: AudioEngine;
|
|
14
|
+
codec?: string;
|
|
15
|
+
bitrate?: number;
|
|
16
|
+
};
|
|
17
|
+
onProgress?: (p: number) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Render a single frame at `t` and return it as tightly-packed RGBA bytes plus
|
|
21
|
+
* its pixel dimensions. Same await-prepare → render → GPU-readback path as the
|
|
22
|
+
* per-frame loop in {@link renderTimelineToFile}, factored out for the
|
|
23
|
+
* single-frame (image) export that `sequio frame` uses. The `renderer` must be
|
|
24
|
+
* the WebGPU renderer backing `compositor`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderFrameRGBA(compositor: Compositor, renderer: Renderer, t: number): Promise<{
|
|
27
|
+
rgba: Uint8Array;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Render `compositor`'s timeline over `range` at `fps` and write a video file to
|
|
33
|
+
* `out` (extension corrected to the container actually encoded). The `renderer`
|
|
34
|
+
* must be the WebGPU renderer backing the compositor, since we read frames off
|
|
35
|
+
* its GPU. Tries the preferred codec first, falling back through VP9/VP8/WebM if
|
|
36
|
+
* node-av can't encode it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function renderTimelineToFile(compositor: Compositor, renderer: Renderer, opts: NodeExportOptions): Promise<{
|
|
39
|
+
frames: number;
|
|
40
|
+
bytes: number;
|
|
41
|
+
out: string;
|
|
42
|
+
container: string;
|
|
43
|
+
videoCodec: string;
|
|
44
|
+
audio: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
//# sourceMappingURL=export-node.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FontSpec } from '../src/timeline';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the font-file URLs from a Google Fonts css2 stylesheet. Pure and
|
|
4
|
+
* testable — css2 returns one `@font-face { … src: url(X) … }` per subset/weight.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseGoogleFontUrls(css: string): string[];
|
|
7
|
+
/** Register every font a timeline references with `@napi-rs/canvas`'s GlobalFonts. */
|
|
8
|
+
export declare function loadFontsNode(specs: FontSpec[] | undefined): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Bridge the engine's {@link FontManager} to Node so a composition's own font
|
|
11
|
+
* loads reach the text rasterizer.
|
|
12
|
+
*
|
|
13
|
+
* In a browser, `fonts.load(...)` / `fonts.loadGoogleFont(...)` register into
|
|
14
|
+
* `document.fonts`, which PixiJS's text measurement reads. In Node those calls
|
|
15
|
+
* are no-ops (text is drawn via `@napi-rs/canvas`, whose fonts come from
|
|
16
|
+
* `GlobalFonts`). The `TimelineSpec` route sidesteps this by taking a separate
|
|
17
|
+
* `spec.fonts` list; the **code/bundle** route has no such list — the composition
|
|
18
|
+
* loads fonts imperatively — so we retarget `FontManager`'s (overridable) load
|
|
19
|
+
* hooks at {@link loadFontsNode}. After this runs, the same `fonts.load*` call a
|
|
20
|
+
* composition makes renders identically in the browser preview and the Node
|
|
21
|
+
* render (contract #3). Idempotent; call once after {@link setupNodeEnvironment}.
|
|
22
|
+
*/
|
|
23
|
+
export declare function bridgeFontManagerToNode(): void;
|
|
24
|
+
//# sourceMappingURL=fonts-node.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AssetLoader, Externals, RuntimeBundle } from '@sequio/runtime';
|
|
2
|
+
export interface RenderFrameNodeOptions {
|
|
3
|
+
/** Output image path. The extension is corrected to `.png`. */
|
|
4
|
+
out: string;
|
|
5
|
+
/** Time (seconds) to sample. Clamped to `[0, duration]`. @default 0 */
|
|
6
|
+
time?: number;
|
|
7
|
+
/** Output resolution multiplier (N× the composition size). @default 1 */
|
|
8
|
+
scale?: number;
|
|
9
|
+
/** Extra bare modules the composition may `import` (e.g. `gsap`); see {@link renderBundleToFile}. */
|
|
10
|
+
externals?: Externals;
|
|
11
|
+
/** Resolver for local media assets (`loadAsset('./clip.mp4')`); see {@link renderBundleToFile}. */
|
|
12
|
+
loadAsset?: AssetLoader;
|
|
13
|
+
}
|
|
14
|
+
export interface RenderFrameNodeResult {
|
|
15
|
+
/** The written file path (extension corrected to `.png`). */
|
|
16
|
+
out: string;
|
|
17
|
+
/** The time actually sampled (the request, clamped to `[0, duration]`). */
|
|
18
|
+
time: number;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
bytes: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compile + run `bundle`, seek to `opts.time`, and write that single frame to
|
|
25
|
+
* `opts.out` (as PNG). The built graph is disposed before returning.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderBundleFrameToFile(bundle: RuntimeBundle, opts: RenderFrameNodeOptions): Promise<RenderFrameNodeResult>;
|
|
28
|
+
//# sourceMappingURL=frame-node.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sequio/server/route-b` — the pure-Node (PixiJS WebGPU) server-render route,
|
|
3
|
+
* as a programmatic API. **Node-only**: importing this pulls in the Node
|
|
4
|
+
* environment adapter (`env.ts` → pixi.js, jsdom, @napi-rs/canvas, webgpu,
|
|
5
|
+
* @mediabunny/server), so it is deliberately kept out of the browser-safe main
|
|
6
|
+
* barrel (`@sequio/server`) and reached via this subpath, mirroring how the
|
|
7
|
+
* runtime keeps `@sequio/runtime/node-fs` separate.
|
|
8
|
+
*
|
|
9
|
+
* Needs a WebGPU-capable host: a real GPU or a software Vulkan driver (Mesa
|
|
10
|
+
* lavapipe). See `docs/server-side-rendering.md`.
|
|
11
|
+
*/
|
|
12
|
+
export { setupNodeEnvironment, createNodeWebGPURenderer, getMediabunny } from './env';
|
|
13
|
+
export { renderTimelineToFile, renderFrameRGBA, type NodeExportOptions } from './export-node';
|
|
14
|
+
export { loadFontsNode, bridgeFontManagerToNode } from './fonts-node';
|
|
15
|
+
export { renderBundleToFile, type RenderBundleNodeOptions, type RenderBundleNodeResult, } from './render-bundle';
|
|
16
|
+
export { renderBundleFrameToFile, type RenderFrameNodeOptions, type RenderFrameNodeResult, } from './frame-node';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AssetLoader, Externals, RuntimeBundle } from '@sequio/runtime';
|
|
2
|
+
export interface RenderBundleNodeOptions {
|
|
3
|
+
/** Output file path. The extension is corrected to match the encoded container. */
|
|
4
|
+
out: string;
|
|
5
|
+
/** Output resolution multiplier (N× the composition size). @default 1 */
|
|
6
|
+
scale?: number;
|
|
7
|
+
/** Preferred container; falls back to what node-av can encode. */
|
|
8
|
+
container?: 'mp4' | 'webm';
|
|
9
|
+
videoCodec?: string;
|
|
10
|
+
bitrate?: number;
|
|
11
|
+
onProgress?: (p: number) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Extra bare modules the composition may `import` (merged over the built-in
|
|
14
|
+
* `@sequio/engine` / `@sequio/runtime`). This is the seam a host uses to make a
|
|
15
|
+
* third-party library — e.g. `gsap` — resolvable to user code in the Node
|
|
16
|
+
* render, exactly as it is in the browser preview. The server itself pulls in
|
|
17
|
+
* nothing: the caller (`sequio render`) owns the library and passes it here.
|
|
18
|
+
*/
|
|
19
|
+
externals?: Externals;
|
|
20
|
+
/**
|
|
21
|
+
* Resolver for a composition's **local media assets** (`loadAsset('./clip.mp4')`).
|
|
22
|
+
* Passed straight to the {@link Runtime}; `sequio render` supplies a loader that
|
|
23
|
+
* reads the files off disk next to the entry, so a local `image`/`video` renders
|
|
24
|
+
* the same as it previews (contract #3). Assets are never part of the bundle.
|
|
25
|
+
*/
|
|
26
|
+
loadAsset?: AssetLoader;
|
|
27
|
+
}
|
|
28
|
+
export interface RenderBundleNodeResult {
|
|
29
|
+
frames: number;
|
|
30
|
+
bytes: number;
|
|
31
|
+
out: string;
|
|
32
|
+
container: string;
|
|
33
|
+
videoCodec: string;
|
|
34
|
+
audio: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Compile + run `bundle` and render it to `opts.out` via the Node WebGPU pipeline.
|
|
38
|
+
* The caller owns nothing — the built graph is disposed before returning.
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderBundleToFile(bundle: RuntimeBundle, opts: RenderBundleNodeOptions): Promise<RenderBundleNodeResult>;
|
|
41
|
+
//# sourceMappingURL=render-bundle.d.ts.map
|
package/dist/route-b.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { createRequire as J } from "node:module";
|
|
2
|
+
import { exportFrameTimes as Q, FontManager as X, buildGoogleCss2Url as Y } from "@sequio/engine";
|
|
3
|
+
import { Runtime as H } from "@sequio/runtime";
|
|
4
|
+
let W = !1;
|
|
5
|
+
const L = Symbol("napi-canvas");
|
|
6
|
+
let I = null;
|
|
7
|
+
function K() {
|
|
8
|
+
if (!I) throw new Error("getMediabunny() before setupNodeEnvironment()");
|
|
9
|
+
return I;
|
|
10
|
+
}
|
|
11
|
+
async function q() {
|
|
12
|
+
if (W) return;
|
|
13
|
+
const { JSDOM: a } = await import("jsdom"), e = new a("<!doctype html><html><body></body></html>");
|
|
14
|
+
for (const o of ["window", "document", "navigator", "HTMLElement", "Node", "DOMParser"])
|
|
15
|
+
if (globalThis[o] === void 0 && e.window[o] !== void 0)
|
|
16
|
+
try {
|
|
17
|
+
globalThis[o] = e.window[o];
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
const t = globalThis;
|
|
21
|
+
t.requestAnimationFrame ??= (o) => setTimeout(() => o(Date.now()), 16), t.cancelAnimationFrame ??= (o) => clearTimeout(o), t.addEventListener ??= () => {
|
|
22
|
+
}, t.removeEventListener ??= () => {
|
|
23
|
+
};
|
|
24
|
+
const r = await import("webgpu"), d = r.create([]);
|
|
25
|
+
for (const [o, n] of Object.entries(r.globals ?? {}))
|
|
26
|
+
if (globalThis[o] === void 0)
|
|
27
|
+
try {
|
|
28
|
+
globalThis[o] = n;
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
if (Object.defineProperty(globalThis.navigator, "gpu", { value: d, configurable: !0 }), !await d.requestAdapter())
|
|
32
|
+
throw new Error(
|
|
33
|
+
"No WebGPU adapter — Route B needs a GPU or a software Vulkan driver (install Mesa lavapipe: `apt install mesa-vulkan-drivers`)."
|
|
34
|
+
);
|
|
35
|
+
const s = r.globals, f = s?.GPURenderPassEncoder?.prototype;
|
|
36
|
+
if (f?.setBindGroup) {
|
|
37
|
+
const o = f.setBindGroup;
|
|
38
|
+
f.setBindGroup = function(n, ...i) {
|
|
39
|
+
return o.call(this, Number(n), ...i);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const l = s?.GPUQueue?.prototype;
|
|
43
|
+
if (l?.copyExternalImageToTexture && l.writeTexture) {
|
|
44
|
+
const o = l.copyExternalImageToTexture;
|
|
45
|
+
l.copyExternalImageToTexture = function(...n) {
|
|
46
|
+
const i = n[0], g = n[1], m = i?.source;
|
|
47
|
+
if (m && m[L]) {
|
|
48
|
+
const y = m.width, x = m.height, S = m.getContext("2d").getImageData(0, 0, y, x).data, j = String(g.texture?.format ?? "").startsWith("bgra"), _ = g.premultipliedAlpha === !0, A = new Uint8Array(y * x * 4);
|
|
49
|
+
for (let k = 0; k < y * x; k++) {
|
|
50
|
+
const v = k * 4;
|
|
51
|
+
let R = S[v], U = S[v + 1], E = S[v + 2];
|
|
52
|
+
const G = S[v + 3];
|
|
53
|
+
_ && (R = (R * G + 127) / 255 | 0, U = (U * G + 127) / 255 | 0, E = (E * G + 127) / 255 | 0), j ? (A[v] = E, A[v + 1] = U, A[v + 2] = R, A[v + 3] = G) : (A[v] = R, A[v + 1] = U, A[v + 2] = E, A[v + 3] = G);
|
|
54
|
+
}
|
|
55
|
+
return this.writeTexture(
|
|
56
|
+
{ texture: g.texture, mipLevel: g.mipLevel ?? 0, origin: g.origin ?? {}, aspect: g.aspect ?? "all" },
|
|
57
|
+
A,
|
|
58
|
+
{ offset: 0, bytesPerRow: y * 4, rowsPerImage: x },
|
|
59
|
+
{ width: y, height: x, depthOrArrayLayers: 1 }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return o.apply(this, n);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const { createCanvas: h, loadImage: p } = await import("@napi-rs/canvas"), w = (o, n) => {
|
|
66
|
+
const i = h(o || 1, n || 1);
|
|
67
|
+
i[L] = !0;
|
|
68
|
+
const g = i.getContext.bind(i);
|
|
69
|
+
return i.getContext = (m, ...y) => m === "2d" ? g("2d", ...y) : null, i.addEventListener = () => {
|
|
70
|
+
}, i.removeEventListener = () => {
|
|
71
|
+
}, i.style = {}, i.getBoundingClientRect = () => ({
|
|
72
|
+
x: 0,
|
|
73
|
+
y: 0,
|
|
74
|
+
top: 0,
|
|
75
|
+
left: 0,
|
|
76
|
+
right: i.width,
|
|
77
|
+
bottom: i.height,
|
|
78
|
+
width: i.width,
|
|
79
|
+
height: i.height
|
|
80
|
+
}), i;
|
|
81
|
+
}, b = globalThis;
|
|
82
|
+
b.createImageBitmap === void 0 && (b.createImageBitmap = async (o) => {
|
|
83
|
+
const n = o;
|
|
84
|
+
if (!n?.arrayBuffer) return o;
|
|
85
|
+
const { Buffer: i } = await import("node:buffer"), g = await p(i.from(await n.arrayBuffer())), m = w(g.width, g.height);
|
|
86
|
+
return m.getContext("2d").drawImage(g, 0, 0), m;
|
|
87
|
+
});
|
|
88
|
+
const B = await import("pixi.js"), T = globalThis.DOMParser;
|
|
89
|
+
B.DOMAdapter.set({
|
|
90
|
+
createCanvas: (o, n) => w(o, n),
|
|
91
|
+
createImage: () => w(1, 1),
|
|
92
|
+
getCanvasRenderingContext2D: () => h(1, 1).getContext("2d").constructor,
|
|
93
|
+
getWebGLRenderingContext: () => function() {
|
|
94
|
+
},
|
|
95
|
+
getNavigator: () => globalThis.navigator,
|
|
96
|
+
getBaseUrl: () => "http://localhost/",
|
|
97
|
+
getFontFaceSet: () => {
|
|
98
|
+
},
|
|
99
|
+
fetch: (...o) => fetch(...o),
|
|
100
|
+
parseXML: (o) => new T().parseFromString(o, "text/xml")
|
|
101
|
+
});
|
|
102
|
+
const C = B.CanvasSource.test.bind(B.CanvasSource);
|
|
103
|
+
B.CanvasSource.test = ((o) => !!(o && o[L]) || C(o));
|
|
104
|
+
const u = J(import.meta.url), { registerMediabunnyServer: F } = u("@mediabunny/server");
|
|
105
|
+
F(), I = u("mediabunny");
|
|
106
|
+
const { setMediabunnyModule: O, setFrameImageExtractor: P } = await import("@sequio/engine");
|
|
107
|
+
O(I), P(async (o) => {
|
|
108
|
+
const n = o, i = n.codedWidth, g = n.codedHeight, m = new Uint8Array(n.allocationSize({ format: "RGBA" }));
|
|
109
|
+
await n.copyTo(m, { format: "RGBA" });
|
|
110
|
+
const y = w(i, g), x = y.getContext("2d"), D = x.createImageData(i, g);
|
|
111
|
+
return D.data.set(m), x.putImageData(D, 0, 0), y;
|
|
112
|
+
});
|
|
113
|
+
const M = await import("node-web-audio-api");
|
|
114
|
+
for (const o of ["OfflineAudioContext", "AudioContext", "AudioBuffer"])
|
|
115
|
+
globalThis[o] === void 0 && M[o] && (globalThis[o] = M[o]);
|
|
116
|
+
W = !0;
|
|
117
|
+
}
|
|
118
|
+
async function V(a) {
|
|
119
|
+
const { WebGPURenderer: e } = await import("pixi.js"), t = new e();
|
|
120
|
+
return await t.init({
|
|
121
|
+
width: a.width,
|
|
122
|
+
height: a.height,
|
|
123
|
+
background: a.background,
|
|
124
|
+
backgroundAlpha: 1,
|
|
125
|
+
// MSAA for shape/text edges. Export renders into antialiased RenderTextures
|
|
126
|
+
// (RT-level MSAA is what actually smooths the pixels read back), but honor the
|
|
127
|
+
// compositor's choice here too so the renderer and its targets agree.
|
|
128
|
+
antialias: a.antialias ?? !0,
|
|
129
|
+
// Honor the compositor's resolution. Text and other Canvas-rasterized content
|
|
130
|
+
// (PIXI.Text glyph atlases) are generated at the *renderer's* resolution, so
|
|
131
|
+
// dropping it here rasterizes glyphs at 1× and upscales them into a higher-res
|
|
132
|
+
// frame — blurry text at `--scale 2`. Match the render-texture resolution.
|
|
133
|
+
resolution: a.resolution ?? 1
|
|
134
|
+
// No canvas presentation — we render to RenderTextures and read them back.
|
|
135
|
+
}), t;
|
|
136
|
+
}
|
|
137
|
+
const Z = (a) => a === "vp8" || a === "vp9" || a === "av1";
|
|
138
|
+
function ee(a) {
|
|
139
|
+
const e = [];
|
|
140
|
+
a.codec ? e.push({ container: a.container ?? (Z(a.codec) ? "webm" : "mp4"), codec: a.codec }) : a.container === "webm" && e.push({ container: "webm", codec: "vp9" }), e.push(
|
|
141
|
+
{ container: "mp4", codec: "avc" },
|
|
142
|
+
{ container: "webm", codec: "vp9" },
|
|
143
|
+
{ container: "webm", codec: "vp8" },
|
|
144
|
+
{ container: "mp4", codec: "av1" }
|
|
145
|
+
);
|
|
146
|
+
const t = /* @__PURE__ */ new Set();
|
|
147
|
+
return e.filter((r) => t.has(r.codec) ? !1 : (t.add(r.codec), !0));
|
|
148
|
+
}
|
|
149
|
+
async function z(a, e) {
|
|
150
|
+
const t = a.gpu.device, r = a.texture.getGpuSource(e), d = e.pixelWidth, c = e.pixelHeight, s = globalThis, f = Math.ceil(d * 4 / 256) * 256, l = t.createBuffer({ size: f * c, usage: s.GPUBufferUsage.COPY_DST | s.GPUBufferUsage.MAP_READ }), h = t.createCommandEncoder();
|
|
151
|
+
h.copyTextureToBuffer(
|
|
152
|
+
{ texture: r },
|
|
153
|
+
{ buffer: l, bytesPerRow: f, rowsPerImage: c },
|
|
154
|
+
{ width: d, height: c, depthOrArrayLayers: 1 }
|
|
155
|
+
), t.queue.submit([h.finish()]), await l.mapAsync(s.GPUMapMode.READ);
|
|
156
|
+
const p = new Uint8Array(l.getMappedRange()), w = new Uint8Array(d * c * 4);
|
|
157
|
+
for (let b = 0; b < c; b++) {
|
|
158
|
+
const B = b * f;
|
|
159
|
+
for (let T = 0; T < d; T++) {
|
|
160
|
+
const C = B + T * 4, u = (b * d + T) * 4;
|
|
161
|
+
w[u] = p[C + 2], w[u + 1] = p[C + 1], w[u + 2] = p[C], w[u + 3] = p[C + 3];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return l.unmap(), l.destroy(), w;
|
|
165
|
+
}
|
|
166
|
+
async function te(a, e, t) {
|
|
167
|
+
const r = e;
|
|
168
|
+
await a.prepare(t);
|
|
169
|
+
const d = a.renderToTexture(t);
|
|
170
|
+
try {
|
|
171
|
+
const c = d.source;
|
|
172
|
+
return { rgba: await z(r, c), width: c.pixelWidth, height: c.pixelHeight };
|
|
173
|
+
} finally {
|
|
174
|
+
d.destroy(!0);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function oe(a) {
|
|
178
|
+
return /VideoFrame is not defined|not supported by this browser|VideoEncoder is not|Invalid (video|audio) codec|Unknown codec/.test(String(a));
|
|
179
|
+
}
|
|
180
|
+
async function ae(a, e, t) {
|
|
181
|
+
const { Output: r, Mp4OutputFormat: d, WebMOutputFormat: c, FilePathTarget: s, VideoSampleSource: f, VideoSample: l, AudioBufferSource: h } = K(), p = await import("node:fs"), w = await import("node:path"), b = Q(t.range, t.fps), B = e, T = ee({ container: t.container, codec: t.videoCodec });
|
|
182
|
+
let C = null;
|
|
183
|
+
for (const u of T) {
|
|
184
|
+
const F = w.format({ ...w.parse(t.out), base: void 0, ext: `.${u.container}` }), O = u.container === "webm" ? new c() : new d({ fastStart: "in-memory" }), P = new r({ format: O, target: new s(F) });
|
|
185
|
+
let M = null;
|
|
186
|
+
try {
|
|
187
|
+
const o = new f({ codec: u.codec, bitrate: t.bitrate ?? 5e6 });
|
|
188
|
+
if (P.addVideoTrack(o), t.audio) {
|
|
189
|
+
const n = u.container === "webm" ? "opus" : t.audio.codec ?? "aac";
|
|
190
|
+
M = new h({ codec: n, bitrate: t.audio.bitrate ?? 128e3 }), P.addAudioTrack(M);
|
|
191
|
+
}
|
|
192
|
+
await P.start();
|
|
193
|
+
for (let n = 0; n < b.length; n++) {
|
|
194
|
+
const i = b[n];
|
|
195
|
+
await a.prepare(i);
|
|
196
|
+
const g = a.renderToTexture(i);
|
|
197
|
+
try {
|
|
198
|
+
const m = g.source, y = await z(B, m), x = new l(y, { format: "RGBA", codedWidth: m.pixelWidth, codedHeight: m.pixelHeight, timestamp: i, duration: 1 / t.fps });
|
|
199
|
+
await o.add(x), x.close();
|
|
200
|
+
} finally {
|
|
201
|
+
g.destroy(!0);
|
|
202
|
+
}
|
|
203
|
+
t.onProgress?.((n + 1) / b.length);
|
|
204
|
+
}
|
|
205
|
+
if (M && t.audio) {
|
|
206
|
+
const n = await t.audio.engine.renderOffline(Math.max(0, t.range[1] - t.range[0]));
|
|
207
|
+
await M.add(n);
|
|
208
|
+
}
|
|
209
|
+
return await P.finalize(), { frames: b.length, bytes: p.statSync(F).size, out: F, container: u.container, videoCodec: u.codec, audio: !!M };
|
|
210
|
+
} catch (o) {
|
|
211
|
+
C = o, await P.cancel().catch(() => {
|
|
212
|
+
});
|
|
213
|
+
try {
|
|
214
|
+
p.unlinkSync(F);
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
if (oe(o)) {
|
|
218
|
+
console.warn(`⚠ node-av can't encode ${u.container}/${u.codec} here — trying the next codec…`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
throw o;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw new Error(
|
|
225
|
+
"No server-side video encoder available (tried " + T.map((u) => u.codec).join(", ") + "). Route B encodes through @mediabunny/server (node-av / FFmpeg); ensure its native binary is built (`pnpm rebuild node-av`). Last error: " + String(C)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
function re(a) {
|
|
229
|
+
const e = [], t = /src:\s*url\(([^)]+)\)/g;
|
|
230
|
+
let r;
|
|
231
|
+
for (; r = t.exec(a); )
|
|
232
|
+
e.push(r[1].replace(/["']/g, "").trim());
|
|
233
|
+
return e;
|
|
234
|
+
}
|
|
235
|
+
const ne = "Mozilla/5.0";
|
|
236
|
+
async function N(a) {
|
|
237
|
+
if (!a?.length) return;
|
|
238
|
+
const { GlobalFonts: e } = await import("@napi-rs/canvas"), { Buffer: t } = await import("node:buffer");
|
|
239
|
+
for (const r of a)
|
|
240
|
+
if (r.google) {
|
|
241
|
+
const d = Y(r.google.cssBase ?? "https://fonts.googleapis.com/css2", {
|
|
242
|
+
family: r.family,
|
|
243
|
+
weights: r.google.weights,
|
|
244
|
+
italic: r.google.italic
|
|
245
|
+
}), c = await (await fetch(d, { headers: { "User-Agent": ne } })).text();
|
|
246
|
+
for (const s of re(c)) {
|
|
247
|
+
const f = t.from(await (await fetch(s)).arrayBuffer());
|
|
248
|
+
e.register(f, r.family);
|
|
249
|
+
}
|
|
250
|
+
} else if (r.src) {
|
|
251
|
+
const d = t.from(await (await fetch(r.src)).arrayBuffer());
|
|
252
|
+
e.register(d, r.family);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function $() {
|
|
256
|
+
const a = X.prototype;
|
|
257
|
+
a.loadFace = (e) => {
|
|
258
|
+
const t = /^\s*url\(\s*['"]?([^'")]+)['"]?\s*\)\s*$/.exec(e.src)?.[1] ?? (/^\s*(url|local)\s*\(/.test(e.src) ? null : e.src);
|
|
259
|
+
return t ? N([{ family: e.family, src: t }]) : Promise.resolve();
|
|
260
|
+
}, a.loadGoogle = (e) => N([{ family: e.family, google: { weights: e.weights, italic: e.italic, cssBase: e.cssBase } }]);
|
|
261
|
+
}
|
|
262
|
+
async function ge(a, e) {
|
|
263
|
+
await q(), $();
|
|
264
|
+
const t = Math.max(1, e.scale ?? 1);
|
|
265
|
+
let r = null;
|
|
266
|
+
const c = await (await new H({ ...a, externals: e.externals, loadAsset: e.loadAsset }).run()).build({
|
|
267
|
+
target: "server",
|
|
268
|
+
compositorOptions: {
|
|
269
|
+
createRenderer: async (s) => (r = await V(s), r),
|
|
270
|
+
resolution: t
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
try {
|
|
274
|
+
const s = c.compositor.timebase.fps;
|
|
275
|
+
return await ae(c.compositor, r, {
|
|
276
|
+
fps: s,
|
|
277
|
+
range: [0, c.duration],
|
|
278
|
+
out: e.out,
|
|
279
|
+
container: e.container,
|
|
280
|
+
videoCodec: e.videoCodec,
|
|
281
|
+
bitrate: e.bitrate,
|
|
282
|
+
onProgress: e.onProgress
|
|
283
|
+
});
|
|
284
|
+
} finally {
|
|
285
|
+
c.dispose();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function ie(a, e, t, r) {
|
|
289
|
+
const { createCanvas: d } = await import("@napi-rs/canvas"), c = await import("node:fs"), s = d(t, r), f = s.getContext("2d"), l = f.createImageData(t, r);
|
|
290
|
+
l.data.set(e), f.putImageData(l, 0, 0);
|
|
291
|
+
const h = await s.encode("png");
|
|
292
|
+
return c.writeFileSync(a, h), h.length;
|
|
293
|
+
}
|
|
294
|
+
function ce(a) {
|
|
295
|
+
return a.replace(/\.[^./\\]*$/, "") + ".png";
|
|
296
|
+
}
|
|
297
|
+
async function me(a, e) {
|
|
298
|
+
await q(), $();
|
|
299
|
+
const t = Math.max(1, e.scale ?? 1);
|
|
300
|
+
let r = null;
|
|
301
|
+
const c = await (await new H({ ...a, externals: e.externals, loadAsset: e.loadAsset }).run()).build({
|
|
302
|
+
target: "server",
|
|
303
|
+
compositorOptions: {
|
|
304
|
+
createRenderer: async (s) => (r = await V(s), r),
|
|
305
|
+
resolution: t
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
try {
|
|
309
|
+
const s = Math.min(Math.max(e.time ?? 0, 0), c.duration), { rgba: f, width: l, height: h } = await te(c.compositor, r, s), p = ce(e.out), w = await ie(p, f, l, h);
|
|
310
|
+
return { out: p, time: s, width: l, height: h, bytes: w };
|
|
311
|
+
} finally {
|
|
312
|
+
c.dispose();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export {
|
|
316
|
+
$ as bridgeFontManagerToNode,
|
|
317
|
+
V as createNodeWebGPURenderer,
|
|
318
|
+
K as getMediabunny,
|
|
319
|
+
N as loadFontsNode,
|
|
320
|
+
me as renderBundleFrameToFile,
|
|
321
|
+
ge as renderBundleToFile,
|
|
322
|
+
te as renderFrameRGBA,
|
|
323
|
+
ae as renderTimelineToFile,
|
|
324
|
+
q as setupNodeEnvironment
|
|
325
|
+
};
|
|
326
|
+
//# sourceMappingURL=route-b.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sequio/server` — server-side rendering.
|
|
3
|
+
*
|
|
4
|
+
* Public surface: the serializable {@link TimelineSpec} protocol and the
|
|
5
|
+
* {@link buildTimeline} builder that rebuilds the SDK object graph from it. Both
|
|
6
|
+
* SSR routes (Route A headless Chrome in `route-a/`, Route B pure Node in
|
|
7
|
+
* `route-b/`) and the editor's "Server Render" button consume this protocol.
|
|
8
|
+
*
|
|
9
|
+
* The Node-only render workers (`route-b/*`) are intentionally NOT re-exported
|
|
10
|
+
* here so importing this barrel stays browser-safe (no jsdom / napi-canvas).
|
|
11
|
+
*/
|
|
12
|
+
export * from './timeline';
|
|
13
|
+
export { sampleTimeline } from './sample-timeline';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { AudioEngine, AutoDetectOptions, BLEND_MODES, Compositor, Effect, Renderer, ShapeSpec, VisualClip } from '@sequio/engine';
|
|
2
|
+
/** Named easing curves usable in a keyframe spec. */
|
|
3
|
+
export type EasingName = 'linear' | 'hold' | 'easeInQuad' | 'easeOutQuad' | 'easeInOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic';
|
|
4
|
+
/**
|
|
5
|
+
* A filter/effect on a clip or the whole frame. Effects are shaders — they need
|
|
6
|
+
* a GPU renderer (so on the server they require Route B's WebGPU backend, not the
|
|
7
|
+
* Canvas fallback). Params are static here for brevity.
|
|
8
|
+
*/
|
|
9
|
+
export type EffectSpec = {
|
|
10
|
+
type: 'blur';
|
|
11
|
+
strength?: number;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'color';
|
|
14
|
+
brightness?: number;
|
|
15
|
+
contrast?: number;
|
|
16
|
+
saturation?: number;
|
|
17
|
+
};
|
|
18
|
+
/** A property that is either a constant value or a keyframed animation. */
|
|
19
|
+
export type PropSpec<T> = T | {
|
|
20
|
+
keyframes: Array<{
|
|
21
|
+
time: number;
|
|
22
|
+
value: T;
|
|
23
|
+
easing?: EasingName;
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
export interface TransformSpec {
|
|
27
|
+
position?: PropSpec<[number, number]>;
|
|
28
|
+
scale?: PropSpec<[number, number]>;
|
|
29
|
+
rotation?: PropSpec<number>;
|
|
30
|
+
/** Normalized anchor (0..1); static only. Default `[0.5, 0.5]`. */
|
|
31
|
+
anchor?: [number, number];
|
|
32
|
+
}
|
|
33
|
+
interface BaseClipSpec {
|
|
34
|
+
/** Timeline interval, seconds. `end` is exclusive (`[start, end)`). */
|
|
35
|
+
start: number;
|
|
36
|
+
end: number;
|
|
37
|
+
sourceIn?: number;
|
|
38
|
+
sourceOut?: number;
|
|
39
|
+
speed?: number;
|
|
40
|
+
opacity?: PropSpec<number>;
|
|
41
|
+
blendMode?: BLEND_MODES;
|
|
42
|
+
transform?: TransformSpec;
|
|
43
|
+
/** Filters applied to this clip (needs Route B's GPU backend on the server). */
|
|
44
|
+
effects?: EffectSpec[];
|
|
45
|
+
}
|
|
46
|
+
export interface TextClipSpec extends BaseClipSpec {
|
|
47
|
+
type: 'text';
|
|
48
|
+
text: string;
|
|
49
|
+
fontFamily?: string;
|
|
50
|
+
fontSize?: PropSpec<number>;
|
|
51
|
+
fill?: number | string;
|
|
52
|
+
}
|
|
53
|
+
export interface ShapeClipSpec extends BaseClipSpec {
|
|
54
|
+
type: 'shape';
|
|
55
|
+
shape: ShapeSpec;
|
|
56
|
+
}
|
|
57
|
+
export interface ImageClipSpec extends BaseClipSpec {
|
|
58
|
+
type: 'image';
|
|
59
|
+
/** URL/data-URI the headless browser can fetch. */
|
|
60
|
+
src: string;
|
|
61
|
+
}
|
|
62
|
+
export interface VideoClipSpec extends BaseClipSpec {
|
|
63
|
+
type: 'video';
|
|
64
|
+
/** URL/data-URI the headless browser can fetch. */
|
|
65
|
+
src: string;
|
|
66
|
+
/** Decoded-frame ring size (bounds memory for big sources). */
|
|
67
|
+
cacheFrames?: number;
|
|
68
|
+
lookahead?: number;
|
|
69
|
+
}
|
|
70
|
+
export type ClipSpec = TextClipSpec | ShapeClipSpec | ImageClipSpec | VideoClipSpec;
|
|
71
|
+
export interface TrackSpec {
|
|
72
|
+
zIndex?: number;
|
|
73
|
+
clips: ClipSpec[];
|
|
74
|
+
}
|
|
75
|
+
/** An audio clip scheduled into the {@link AudioEngine} for the exported mix. */
|
|
76
|
+
export interface AudioClipSpec {
|
|
77
|
+
src: string;
|
|
78
|
+
start: number;
|
|
79
|
+
end: number;
|
|
80
|
+
sourceIn?: number;
|
|
81
|
+
gain?: number;
|
|
82
|
+
fadeIn?: number;
|
|
83
|
+
fadeOut?: number;
|
|
84
|
+
}
|
|
85
|
+
export interface FontSpec {
|
|
86
|
+
family: string;
|
|
87
|
+
/** Self-hosted URL. Mutually exclusive with {@link google}. */
|
|
88
|
+
src?: string;
|
|
89
|
+
/** Load from Google Fonts instead of a self-hosted URL. */
|
|
90
|
+
google?: {
|
|
91
|
+
weights?: number[];
|
|
92
|
+
italic?: boolean;
|
|
93
|
+
cssBase?: string;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export interface ExportSpec {
|
|
97
|
+
container?: 'mp4' | 'webm';
|
|
98
|
+
videoCodec?: string;
|
|
99
|
+
audioCodec?: string;
|
|
100
|
+
bitrate?: number;
|
|
101
|
+
audioBitrate?: number;
|
|
102
|
+
/** Mux the audio mix. Auto-enabled when the timeline has audio clips. */
|
|
103
|
+
audio?: boolean;
|
|
104
|
+
}
|
|
105
|
+
/** The full, JSON-serializable description of a timeline to render. */
|
|
106
|
+
export interface TimelineSpec {
|
|
107
|
+
width: number;
|
|
108
|
+
height: number;
|
|
109
|
+
fps: number;
|
|
110
|
+
background?: number;
|
|
111
|
+
/** Normalized coordinate origin (`0..1`); default `[0, 0]`. */
|
|
112
|
+
origin?: [number, number];
|
|
113
|
+
/**
|
|
114
|
+
* Explicit render range `[start, end]` in seconds. Defaults to
|
|
115
|
+
* `[0, maxClipEnd]` — the whole timeline.
|
|
116
|
+
*/
|
|
117
|
+
range?: [number, number];
|
|
118
|
+
fonts?: FontSpec[];
|
|
119
|
+
tracks?: TrackSpec[];
|
|
120
|
+
audio?: AudioClipSpec[];
|
|
121
|
+
/** Global filters over the whole composite (a master grade / blur). */
|
|
122
|
+
effects?: EffectSpec[];
|
|
123
|
+
export?: ExportSpec;
|
|
124
|
+
}
|
|
125
|
+
/** Everything the exporter path needs, plus a teardown that frees every source. */
|
|
126
|
+
export interface BuiltTimeline {
|
|
127
|
+
compositor: Compositor;
|
|
128
|
+
audioEngine: AudioEngine;
|
|
129
|
+
range: [number, number];
|
|
130
|
+
exportOptions: ExportSpec;
|
|
131
|
+
hasAudio: boolean;
|
|
132
|
+
/** Dispose the compositor and every source/decoder this build created. */
|
|
133
|
+
dispose(): void;
|
|
134
|
+
}
|
|
135
|
+
/** Construct a built-in effect (filter) from its spec. */
|
|
136
|
+
export declare function buildEffect(spec: EffectSpec): Effect;
|
|
137
|
+
/**
|
|
138
|
+
* Construct one clip from its spec. Image/Video sources are loaded (async) and
|
|
139
|
+
* pushed onto `sources` for later disposal. Text/Shape clips need no source and
|
|
140
|
+
* resolve without touching the browser, so this is unit-testable for them.
|
|
141
|
+
*/
|
|
142
|
+
export declare function buildClip(spec: ClipSpec, sources: {
|
|
143
|
+
dispose(): void;
|
|
144
|
+
}[]): Promise<VisualClip>;
|
|
145
|
+
/** Largest clip end across all visual + audio clips (the timeline duration). */
|
|
146
|
+
export declare function timelineEnd(spec: TimelineSpec): number;
|
|
147
|
+
/**
|
|
148
|
+
* Overrides that let {@link buildTimeline} run outside a browser (server-side
|
|
149
|
+
* rendering). Both default to the browser behaviour.
|
|
150
|
+
*/
|
|
151
|
+
export interface BuildOverrides {
|
|
152
|
+
/** Custom GPU renderer factory (e.g. Node WebGPU via Dawn). */
|
|
153
|
+
createRenderer?: (options: Partial<AutoDetectOptions>) => Promise<Renderer>;
|
|
154
|
+
/** Custom font loader (e.g. Node `GlobalFonts.register`) replacing `document.fonts`. */
|
|
155
|
+
loadFonts?: (specs: FontSpec[] | undefined) => Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Backing-store scale for the output. `2` renders/exports at 2× the timeline's
|
|
158
|
+
* `width`×`height` (crisper text/vectors, larger file). In a browser this
|
|
159
|
+
* defaults to `devicePixelRatio`; in Node there's none, so the default is `1`
|
|
160
|
+
* and a caller (e.g. the SSR worker's `--scale`) can bump it here.
|
|
161
|
+
*/
|
|
162
|
+
resolution?: number;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Rebuild the SDK object graph from a {@link TimelineSpec} and initialize the
|
|
166
|
+
* GPU renderer, ready for {@link Exporter.export}. Loads fonts and every
|
|
167
|
+
* image/video source up front. The caller must {@link BuiltTimeline.dispose}
|
|
168
|
+
* when done (frees the compositor and every source/decoder).
|
|
169
|
+
*/
|
|
170
|
+
export declare function buildTimeline(spec: TimelineSpec, overrides?: BuildOverrides): Promise<BuiltTimeline>;
|
|
171
|
+
export {};
|
|
172
|
+
//# sourceMappingURL=timeline.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sequio/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Server-side rendering for sequio: a serializable TimelineSpec protocol plus Route B — pure Node, PixiJS WebGPU. (Route A, headless Chrome, is a repo-internal harness and not part of the published package.)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "slightc",
|
|
8
|
+
"homepage": "https://sequio-demo.vercel.app/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/slightc/sequio.git",
|
|
12
|
+
"directory": "packages/server"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/slightc/sequio/issues"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/src/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/src/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./route-b": {
|
|
26
|
+
"types": "./dist/route-b/index.d.ts",
|
|
27
|
+
"import": "./dist/route-b.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"!dist/**/*.map"
|
|
33
|
+
],
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"server-side-rendering",
|
|
43
|
+
"pixijs",
|
|
44
|
+
"webgpu",
|
|
45
|
+
"video",
|
|
46
|
+
"headless"
|
|
47
|
+
],
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"mediabunny": "^1.49.0",
|
|
50
|
+
"pixi.js": "^8.6.0",
|
|
51
|
+
"@sequio/engine": "^0.1.1",
|
|
52
|
+
"@sequio/runtime": "^0.1.0"
|
|
53
|
+
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"@mediabunny/server": "^1.50.4",
|
|
56
|
+
"@napi-rs/canvas": "^1.0.2",
|
|
57
|
+
"jsdom": "^29.1.1",
|
|
58
|
+
"node-web-audio-api": "^2.0.0",
|
|
59
|
+
"webgpu": "^0.4.0"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"dev": "vite",
|
|
63
|
+
"build": "vite build",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest",
|
|
67
|
+
"verify:ssr": "node scripts/verify-page.cjs route-a/ssr-render.html __SSR_TEST__ \"server-side render\"",
|
|
68
|
+
"verify:ssr-node": "tsx route-b/render.ts --verify --out .ssr-out/node-demo.mp4",
|
|
69
|
+
"verify:ssr-node-audio": "tsx route-b/verify-audio.ts",
|
|
70
|
+
"verify:ssr-node-font": "tsx route-b/verify-font.ts",
|
|
71
|
+
"verify:ssr-node-media": "tsx route-b/verify-media.ts",
|
|
72
|
+
"ssr:render": "node route-a/ssr-render.cjs",
|
|
73
|
+
"ssr:render-node": "tsx route-b/render.ts"
|
|
74
|
+
}
|
|
75
|
+
}
|