@motionscript/browser 0.0.0-stage → 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +192 -0
- package/LICENSE +201 -0
- package/NOTICE +49 -0
- package/README.md +140 -3
- package/dist/asset-bytes.d.ts +59 -0
- package/dist/asset-bytes.d.ts.map +1 -0
- package/dist/asset-bytes.js +78 -0
- package/dist/asset-bytes.js.map +1 -0
- package/dist/audio/bus-graph.d.ts +52 -0
- package/dist/audio/bus-graph.d.ts.map +1 -0
- package/dist/audio/bus-graph.js +108 -0
- package/dist/audio/bus-graph.js.map +1 -0
- package/dist/audio/effect-graph.d.ts +62 -0
- package/dist/audio/effect-graph.d.ts.map +1 -0
- package/dist/audio/effect-graph.js +234 -0
- package/dist/audio/effect-graph.js.map +1 -0
- package/dist/audio/index.d.ts +5 -0
- package/dist/audio/index.d.ts.map +1 -0
- package/dist/audio/index.js +11 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/audio/loop-region.d.ts +31 -0
- package/dist/audio/loop-region.d.ts.map +1 -0
- package/dist/audio/loop-region.js +43 -0
- package/dist/audio/loop-region.js.map +1 -0
- package/dist/audio/mixer.d.ts +59 -0
- package/dist/audio/mixer.d.ts.map +1 -0
- package/dist/audio/mixer.js +192 -0
- package/dist/audio/mixer.js.map +1 -0
- package/dist/audio/player.d.ts +55 -0
- package/dist/audio/player.d.ts.map +1 -0
- package/dist/audio/player.js +217 -0
- package/dist/audio/player.js.map +1 -0
- package/dist/browser/THIRD_PARTY_LICENSES.txt +784 -0
- package/dist/browser/audio.js +2 -0
- package/dist/browser/audio.js.map +7 -0
- package/dist/browser/chunks/chunk-F3AZKPHU.js +2 -0
- package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
- package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
- package/dist/browser/chunks/chunk-KLXMRKTL.js.map +7 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
- package/dist/browser/esbuild.wasm +0 -0
- package/dist/browser/index.js +734 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/manifest.json +32 -0
- package/dist/browser/ms_renderer_bg.wasm +0 -0
- package/dist/concat.d.ts +121 -0
- package/dist/concat.d.ts.map +1 -0
- package/dist/concat.js +219 -0
- package/dist/concat.js.map +1 -0
- package/dist/deliver.d.ts +10 -0
- package/dist/deliver.d.ts.map +1 -0
- package/dist/deliver.js +22 -0
- package/dist/deliver.js.map +1 -0
- package/dist/dynamic-nodes.d.ts +9 -0
- package/dist/dynamic-nodes.d.ts.map +1 -0
- package/dist/dynamic-nodes.js +53 -0
- package/dist/dynamic-nodes.js.map +1 -0
- package/dist/engine.d.ts +87 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +193 -0
- package/dist/engine.js.map +1 -0
- package/dist/export-audio.d.ts +36 -0
- package/dist/export-audio.d.ts.map +1 -0
- package/dist/export-audio.js +56 -0
- package/dist/export-audio.js.map +1 -0
- package/dist/exporter.d.ts +282 -0
- package/dist/exporter.d.ts.map +1 -0
- package/dist/exporter.js +308 -0
- package/dist/exporter.js.map +1 -0
- package/dist/formats.d.ts +36 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +83 -0
- package/dist/formats.js.map +1 -0
- package/dist/frame-loop.d.ts +190 -0
- package/dist/frame-loop.d.ts.map +1 -0
- package/dist/frame-loop.js +304 -0
- package/dist/frame-loop.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/mix-timeline-audio.d.ts +42 -0
- package/dist/mix-timeline-audio.d.ts.map +1 -0
- package/dist/mix-timeline-audio.js +60 -0
- package/dist/mix-timeline-audio.js.map +1 -0
- package/dist/parallel/export-parallel.d.ts +32 -0
- package/dist/parallel/export-parallel.d.ts.map +1 -0
- package/dist/parallel/export-parallel.js +114 -0
- package/dist/parallel/export-parallel.js.map +1 -0
- package/dist/parallel/governor.d.ts +57 -0
- package/dist/parallel/governor.d.ts.map +1 -0
- package/dist/parallel/governor.js +79 -0
- package/dist/parallel/governor.js.map +1 -0
- package/dist/parallel/lane.d.ts +45 -0
- package/dist/parallel/lane.d.ts.map +1 -0
- package/dist/parallel/lane.js +2 -0
- package/dist/parallel/lane.js.map +1 -0
- package/dist/parallel/lanes.d.ts +20 -0
- package/dist/parallel/lanes.d.ts.map +1 -0
- package/dist/parallel/lanes.js +152 -0
- package/dist/parallel/lanes.js.map +1 -0
- package/dist/parallel/plan.d.ts +23 -0
- package/dist/parallel/plan.d.ts.map +1 -0
- package/dist/parallel/plan.js +30 -0
- package/dist/parallel/plan.js.map +1 -0
- package/dist/parallel/run-lanes.d.ts +38 -0
- package/dist/parallel/run-lanes.d.ts.map +1 -0
- package/dist/parallel/run-lanes.js +164 -0
- package/dist/parallel/run-lanes.js.map +1 -0
- package/dist/parallel/session.d.ts +9 -0
- package/dist/parallel/session.d.ts.map +1 -0
- package/dist/parallel/session.js +79 -0
- package/dist/parallel/session.js.map +1 -0
- package/dist/renderer.d.ts +22 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +23 -0
- package/dist/renderer.js.map +1 -0
- package/dist/screenshot.d.ts +45 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +31 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/still.d.ts +159 -0
- package/dist/still.d.ts.map +1 -0
- package/dist/still.js +222 -0
- package/dist/still.js.map +1 -0
- package/dist/ticker.d.ts +74 -0
- package/dist/ticker.d.ts.map +1 -0
- package/dist/ticker.js +215 -0
- package/dist/ticker.js.map +1 -0
- package/package.json +75 -3
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { openSegmentSession } from "./session";
|
|
2
|
+
/** A lane that renders in this realm, with its renderer, registry and warm caches. */
|
|
3
|
+
export function localLane(env = {}) {
|
|
4
|
+
return { open: (spec) => openSegmentSession(spec, env) };
|
|
5
|
+
}
|
|
6
|
+
/** The error a lane sent back, keeping its name so a host can tell a cancel from a failure. */
|
|
7
|
+
function laneError(message, name) {
|
|
8
|
+
const error = new Error(message);
|
|
9
|
+
error.name = name;
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A lane on the far side of `port`, which {@link serveExportLane} answers. The
|
|
14
|
+
* finished segment comes back as a transferred buffer, never a copy.
|
|
15
|
+
*/
|
|
16
|
+
export function messagePortLane(port, options = {}) {
|
|
17
|
+
let next = 1;
|
|
18
|
+
let gone = null;
|
|
19
|
+
const pending = new Map();
|
|
20
|
+
port.addEventListener("message", (event) => {
|
|
21
|
+
const msg = event.data;
|
|
22
|
+
const entry = pending.get(msg.id);
|
|
23
|
+
if (!entry)
|
|
24
|
+
return;
|
|
25
|
+
if (msg.t === "report") {
|
|
26
|
+
entry.onReport?.(msg.report);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
pending.delete(msg.id);
|
|
30
|
+
if (msg.t === "error")
|
|
31
|
+
entry.reject(laneError(msg.message, msg.name));
|
|
32
|
+
else
|
|
33
|
+
entry.resolve(msg);
|
|
34
|
+
});
|
|
35
|
+
port.start();
|
|
36
|
+
const fail = (reason) => {
|
|
37
|
+
gone ??= new Error(reason);
|
|
38
|
+
for (const entry of pending.values())
|
|
39
|
+
entry.reject(gone);
|
|
40
|
+
pending.clear();
|
|
41
|
+
};
|
|
42
|
+
port.addEventListener("close", () => fail("The export lane closed."));
|
|
43
|
+
void options.lost?.then(() => fail("The export lane was lost."), () => fail("The export lane was lost."));
|
|
44
|
+
const call = (msg, signal, onReport) => new Promise((resolve, reject) => {
|
|
45
|
+
if (gone)
|
|
46
|
+
return reject(gone);
|
|
47
|
+
if (signal.aborted)
|
|
48
|
+
return reject(signal.reason);
|
|
49
|
+
const onAbort = () => {
|
|
50
|
+
pending.delete(msg.id);
|
|
51
|
+
port.postMessage({ t: "abort", id: msg.id });
|
|
52
|
+
reject(signal.reason);
|
|
53
|
+
};
|
|
54
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
55
|
+
pending.set(msg.id, {
|
|
56
|
+
resolve: (m) => { signal.removeEventListener("abort", onAbort); resolve(m); },
|
|
57
|
+
reject: (e) => { signal.removeEventListener("abort", onAbort); reject(e); },
|
|
58
|
+
onReport,
|
|
59
|
+
});
|
|
60
|
+
port.postMessage(msg);
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
async open(spec, signal) {
|
|
64
|
+
const session = next++;
|
|
65
|
+
await call({ t: "open", id: session, spec }, signal);
|
|
66
|
+
return {
|
|
67
|
+
async render(chunk, onReport, renderSignal) {
|
|
68
|
+
const reply = await call({ t: "render", id: next++, session, chunk }, renderSignal, onReport);
|
|
69
|
+
return reply.bytes;
|
|
70
|
+
},
|
|
71
|
+
close() {
|
|
72
|
+
if (!gone)
|
|
73
|
+
port.postMessage({ t: "close", id: session });
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Answer a {@link messagePortLane} from this realm. `resolve` supplies what the
|
|
81
|
+
* realm renders with, given the spec (its `host` field carries anything the
|
|
82
|
+
* coordinator's host wanted this side to know). Returns a function that stops
|
|
83
|
+
* serving and closes every open session.
|
|
84
|
+
*/
|
|
85
|
+
export function serveExportLane(port, resolve) {
|
|
86
|
+
const sessions = new Map();
|
|
87
|
+
const renders = new Map();
|
|
88
|
+
const reply = (msg, transfer = []) => port.postMessage(msg, transfer);
|
|
89
|
+
const replyError = (id, error) => reply({
|
|
90
|
+
t: "error", id,
|
|
91
|
+
message: error instanceof Error ? error.message : String(error),
|
|
92
|
+
name: error instanceof Error ? error.name : "Error",
|
|
93
|
+
});
|
|
94
|
+
const onMessage = async (event) => {
|
|
95
|
+
const msg = event.data;
|
|
96
|
+
switch (msg.t) {
|
|
97
|
+
case "open": {
|
|
98
|
+
const opening = Promise.resolve(resolve(msg.spec)).then((env) => openSegmentSession(msg.spec, env));
|
|
99
|
+
sessions.set(msg.id, opening);
|
|
100
|
+
try {
|
|
101
|
+
await opening;
|
|
102
|
+
reply({ t: "opened", id: msg.id });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
sessions.delete(msg.id);
|
|
106
|
+
replyError(msg.id, error);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
case "render": {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
renders.set(msg.id, controller);
|
|
113
|
+
try {
|
|
114
|
+
const session = await sessions.get(msg.session);
|
|
115
|
+
if (!session)
|
|
116
|
+
throw new Error(`No open export session ${msg.session}.`);
|
|
117
|
+
const bytes = await session.render(msg.chunk, (report) => reply({ t: "report", id: msg.id, report }), controller.signal);
|
|
118
|
+
const whole = bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ? bytes : bytes.slice();
|
|
119
|
+
reply({ t: "done", id: msg.id, bytes: whole }, [whole.buffer]);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!controller.signal.aborted)
|
|
123
|
+
replyError(msg.id, error);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
renders.delete(msg.id);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
case "abort":
|
|
131
|
+
renders.get(msg.id)?.abort(new DOMException("Export cancelled.", "AbortError"));
|
|
132
|
+
return;
|
|
133
|
+
case "close": {
|
|
134
|
+
const session = sessions.get(msg.id);
|
|
135
|
+
sessions.delete(msg.id);
|
|
136
|
+
void session?.then((s) => s.close(), () => { });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
port.addEventListener("message", onMessage);
|
|
142
|
+
port.start();
|
|
143
|
+
return () => {
|
|
144
|
+
port.removeEventListener("message", onMessage);
|
|
145
|
+
for (const controller of renders.values())
|
|
146
|
+
controller.abort(new DOMException("Export cancelled.", "AbortError"));
|
|
147
|
+
for (const session of sessions.values())
|
|
148
|
+
void session.then((s) => s.close(), () => { });
|
|
149
|
+
sessions.clear();
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=lanes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lanes.js","sourceRoot":"","sources":["../../src/parallel/lanes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ExportLane, LaneEnvironment, LaneReport, LaneSession, SegmentSpec } from \"./lane\";\nimport type { ExportChunk } from \"./plan\";\nimport { openSegmentSession } from \"./session\";\n\n/** A lane that renders in this realm, with its renderer, registry and warm caches. */\nexport function localLane(env: LaneEnvironment = {}): ExportLane {\n return { open: (spec) => openSegmentSession(spec, env) };\n}\n\ntype ToLane =\n | { t: \"open\"; id: number; spec: SegmentSpec }\n | { t: \"render\"; id: number; session: number; chunk: ExportChunk }\n | { t: \"abort\"; id: number }\n | { t: \"close\"; id: number };\n\ntype FromLane =\n | { t: \"opened\"; id: number }\n | { t: \"report\"; id: number; report: LaneReport }\n | { t: \"done\"; id: number; bytes: Uint8Array }\n | { t: \"error\"; id: number; message: string; name: string };\n\n/** The error a lane sent back, keeping its name so a host can tell a cancel from a failure. */\nfunction laneError(message: string, name: string): Error {\n const error = new Error(message);\n error.name = name;\n return error;\n}\n\nexport interface MessagePortLaneOptions {\n /** Settles when the realm behind the port is gone, failing whatever it was doing. */\n lost?: Promise<unknown>;\n}\n\n/**\n * A lane on the far side of `port`, which {@link serveExportLane} answers. The\n * finished segment comes back as a transferred buffer, never a copy.\n */\nexport function messagePortLane(port: MessagePort, options: MessagePortLaneOptions = {}): ExportLane {\n let next = 1;\n let gone: Error | null = null;\n const pending = new Map<number, { resolve: (msg: FromLane) => void; reject: (error: Error) => void; onReport?: (r: LaneReport) => void }>();\n\n port.addEventListener(\"message\", (event: MessageEvent<FromLane>) => {\n const msg = event.data;\n const entry = pending.get(msg.id);\n if (!entry) return;\n if (msg.t === \"report\") {\n entry.onReport?.(msg.report);\n return;\n }\n pending.delete(msg.id);\n if (msg.t === \"error\") entry.reject(laneError(msg.message, msg.name));\n else entry.resolve(msg);\n });\n port.start();\n\n const fail = (reason: string) => {\n gone ??= new Error(reason);\n for (const entry of pending.values()) entry.reject(gone);\n pending.clear();\n };\n port.addEventListener(\"close\", () => fail(\"The export lane closed.\"));\n void options.lost?.then(() => fail(\"The export lane was lost.\"), () => fail(\"The export lane was lost.\"));\n\n const call = (msg: ToLane, signal: AbortSignal, onReport?: (r: LaneReport) => void) =>\n new Promise<FromLane>((resolve, reject) => {\n if (gone) return reject(gone);\n if (signal.aborted) return reject(signal.reason);\n const onAbort = () => {\n pending.delete(msg.id);\n port.postMessage({ t: \"abort\", id: msg.id } satisfies ToLane);\n reject(signal.reason);\n };\n signal.addEventListener(\"abort\", onAbort, { once: true });\n pending.set(msg.id, {\n resolve: (m) => { signal.removeEventListener(\"abort\", onAbort); resolve(m); },\n reject: (e) => { signal.removeEventListener(\"abort\", onAbort); reject(e); },\n onReport,\n });\n port.postMessage(msg);\n });\n\n return {\n async open(spec, signal) {\n const session = next++;\n await call({ t: \"open\", id: session, spec }, signal);\n return {\n async render(chunk, onReport, renderSignal) {\n const reply = await call({ t: \"render\", id: next++, session, chunk }, renderSignal, onReport);\n return (reply as Extract<FromLane, { t: \"done\" }>).bytes;\n },\n close() {\n if (!gone) port.postMessage({ t: \"close\", id: session } satisfies ToLane);\n },\n } satisfies LaneSession;\n },\n };\n}\n\n/**\n * Answer a {@link messagePortLane} from this realm. `resolve` supplies what the\n * realm renders with, given the spec (its `host` field carries anything the\n * coordinator's host wanted this side to know). Returns a function that stops\n * serving and closes every open session.\n */\nexport function serveExportLane(\n port: MessagePort,\n resolve: (spec: SegmentSpec) => LaneEnvironment | Promise<LaneEnvironment>,\n): () => void {\n const sessions = new Map<number, Promise<LaneSession>>();\n const renders = new Map<number, AbortController>();\n const reply = (msg: FromLane, transfer: Transferable[] = []) => port.postMessage(msg, transfer);\n const replyError = (id: number, error: unknown) => reply({\n t: \"error\", id,\n message: error instanceof Error ? error.message : String(error),\n name: error instanceof Error ? error.name : \"Error\",\n });\n\n const onMessage = async (event: MessageEvent<ToLane>) => {\n const msg = event.data;\n switch (msg.t) {\n case \"open\": {\n const opening = Promise.resolve(resolve(msg.spec)).then((env) => openSegmentSession(msg.spec, env));\n sessions.set(msg.id, opening);\n try {\n await opening;\n reply({ t: \"opened\", id: msg.id });\n } catch (error) {\n sessions.delete(msg.id);\n replyError(msg.id, error);\n }\n return;\n }\n case \"render\": {\n const controller = new AbortController();\n renders.set(msg.id, controller);\n try {\n const session = await sessions.get(msg.session);\n if (!session) throw new Error(`No open export session ${msg.session}.`);\n const bytes = await session.render(msg.chunk, (report) => reply({ t: \"report\", id: msg.id, report }), controller.signal);\n const whole = bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ? bytes : bytes.slice();\n reply({ t: \"done\", id: msg.id, bytes: whole }, [whole.buffer]);\n } catch (error) {\n if (!controller.signal.aborted) replyError(msg.id, error);\n } finally {\n renders.delete(msg.id);\n }\n return;\n }\n case \"abort\":\n renders.get(msg.id)?.abort(new DOMException(\"Export cancelled.\", \"AbortError\"));\n return;\n case \"close\": {\n const session = sessions.get(msg.id);\n sessions.delete(msg.id);\n void session?.then((s) => s.close(), () => {});\n return;\n }\n }\n };\n\n port.addEventListener(\"message\", onMessage);\n port.start();\n return () => {\n port.removeEventListener(\"message\", onMessage);\n for (const controller of renders.values()) controller.abort(new DOMException(\"Export cancelled.\", \"AbortError\"));\n for (const session of sessions.values()) void session.then((s) => s.close(), () => {});\n sessions.clear();\n };\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Frames `[start, end)` of the timeline, rendered as one complete segment file. */
|
|
2
|
+
export interface ExportChunk {
|
|
3
|
+
index: number;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ChunkPlanOptions {
|
|
8
|
+
fps: number;
|
|
9
|
+
/** Seconds between key frames; every chunk but the last is a whole number of them. */
|
|
10
|
+
keyFrameInterval: number;
|
|
11
|
+
/** The most lanes that may render at once. */
|
|
12
|
+
lanes: number;
|
|
13
|
+
/** Overrides the chunk length the plan would pick, in seconds. */
|
|
14
|
+
chunkSeconds?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Cut the timeline into chunks in timeline order.
|
|
18
|
+
*
|
|
19
|
+
* A chunk starts on a key-frame boundary of the whole timeline, so the joined
|
|
20
|
+
* file keeps the key-frame rhythm a single export would have.
|
|
21
|
+
*/
|
|
22
|
+
export declare function planChunks(totalFrames: number, options: ChunkPlanOptions): ExportChunk[];
|
|
23
|
+
//# sourceMappingURL=plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/parallel/plan.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAOD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,WAAW,EAAE,CAkBxF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const MIN_CHUNK_SECONDS = 2;
|
|
2
|
+
const MAX_CHUNK_SECONDS = 6;
|
|
3
|
+
/** Chunks per lane: enough that lanes finish close together, few enough that a chunk's setup stays small. */
|
|
4
|
+
const CHUNKS_PER_LANE = 4;
|
|
5
|
+
/**
|
|
6
|
+
* Cut the timeline into chunks in timeline order.
|
|
7
|
+
*
|
|
8
|
+
* A chunk starts on a key-frame boundary of the whole timeline, so the joined
|
|
9
|
+
* file keeps the key-frame rhythm a single export would have.
|
|
10
|
+
*/
|
|
11
|
+
export function planChunks(totalFrames, options) {
|
|
12
|
+
if (!Number.isInteger(totalFrames) || totalFrames <= 0)
|
|
13
|
+
return [];
|
|
14
|
+
const unit = Math.max(1, Math.round(options.fps * (options.keyFrameInterval > 0 ? options.keyFrameInterval : 1)));
|
|
15
|
+
const seconds = options.chunkSeconds
|
|
16
|
+
?? Math.min(MAX_CHUNK_SECONDS, Math.max(MIN_CHUNK_SECONDS, totalFrames / options.fps / (Math.max(1, options.lanes) * CHUNKS_PER_LANE)));
|
|
17
|
+
const size = Math.max(unit, Math.round((seconds * options.fps) / unit) * unit);
|
|
18
|
+
const chunks = [];
|
|
19
|
+
for (let start = 0; start < totalFrames; start += size) {
|
|
20
|
+
chunks.push({ index: chunks.length, start, end: Math.min(totalFrames, start + size) });
|
|
21
|
+
}
|
|
22
|
+
// A sliver of a last chunk costs a whole chunk's setup; fold it into the one before.
|
|
23
|
+
const last = chunks[chunks.length - 1];
|
|
24
|
+
if (chunks.length > 1 && last.end - last.start < unit) {
|
|
25
|
+
chunks.pop();
|
|
26
|
+
chunks[chunks.length - 1].end = last.end;
|
|
27
|
+
}
|
|
28
|
+
return chunks;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/parallel/plan.ts"],"names":[],"mappings":"AAiBA,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,6GAA6G;AAC7G,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,WAAmB,EAAE,OAAyB;IACrE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClH,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY;WAC7B,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAC5I,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;IAE/E,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,WAAW,EAAE,KAAK,IAAI,IAAI,EAAE,CAAC;QACrD,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3F,CAAC;IACD,qFAAqF;IACrF,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACvC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC;QACpD,MAAM,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;IAC7C,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/** Frames `[start, end)` of the timeline, rendered as one complete segment file. */\nexport interface ExportChunk {\n index: number;\n start: number;\n end: number;\n}\n\nexport interface ChunkPlanOptions {\n fps: number;\n /** Seconds between key frames; every chunk but the last is a whole number of them. */\n keyFrameInterval: number;\n /** The most lanes that may render at once. */\n lanes: number;\n /** Overrides the chunk length the plan would pick, in seconds. */\n chunkSeconds?: number;\n}\n\nconst MIN_CHUNK_SECONDS = 2;\nconst MAX_CHUNK_SECONDS = 6;\n/** Chunks per lane: enough that lanes finish close together, few enough that a chunk's setup stays small. */\nconst CHUNKS_PER_LANE = 4;\n\n/**\n * Cut the timeline into chunks in timeline order.\n *\n * A chunk starts on a key-frame boundary of the whole timeline, so the joined\n * file keeps the key-frame rhythm a single export would have.\n */\nexport function planChunks(totalFrames: number, options: ChunkPlanOptions): ExportChunk[] {\n if (!Number.isInteger(totalFrames) || totalFrames <= 0) return [];\n const unit = Math.max(1, Math.round(options.fps * (options.keyFrameInterval > 0 ? options.keyFrameInterval : 1)));\n const seconds = options.chunkSeconds\n ?? Math.min(MAX_CHUNK_SECONDS, Math.max(MIN_CHUNK_SECONDS, totalFrames / options.fps / (Math.max(1, options.lanes) * CHUNKS_PER_LANE)));\n const size = Math.max(unit, Math.round((seconds * options.fps) / unit) * unit);\n\n const chunks: ExportChunk[] = [];\n for (let start = 0; start < totalFrames; start += size) {\n chunks.push({ index: chunks.length, start, end: Math.min(totalFrames, start + size) });\n }\n // A sliver of a last chunk costs a whole chunk's setup; fold it into the one before.\n const last = chunks[chunks.length - 1];\n if (chunks.length > 1 && last.end - last.start < unit) {\n chunks.pop();\n chunks[chunks.length - 1].end = last.end;\n }\n return chunks;\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RenderIssue } from "@motionscript/core";
|
|
2
|
+
import { type GovernorOptions } from "./governor";
|
|
3
|
+
import type { ExportLane, SegmentSpec } from "./lane";
|
|
4
|
+
import type { ExportChunk } from "./plan";
|
|
5
|
+
export interface RunLanesOptions {
|
|
6
|
+
lanes: readonly ExportLane[];
|
|
7
|
+
spec: SegmentSpec;
|
|
8
|
+
chunks: readonly ExportChunk[];
|
|
9
|
+
/**
|
|
10
|
+
* A finished range, in whatever order ranges finish. Once the governor
|
|
11
|
+
* settles, adjacent chunks still queued are merged, so a range can span
|
|
12
|
+
* several of the planned chunks; it is named by the first.
|
|
13
|
+
*/
|
|
14
|
+
onChunk: (chunk: ExportChunk, bytes: Uint8Array) => void;
|
|
15
|
+
/** Frames drawn so far across every lane. Drops when a failed range's frames are discarded. */
|
|
16
|
+
onFrames?: (frames: number) => void;
|
|
17
|
+
/** The active lane count, each time it changes. */
|
|
18
|
+
onActive?: (active: number) => void;
|
|
19
|
+
/** A render issue a lane reported (a range retried elsewhere may report one twice). */
|
|
20
|
+
onIssue?: (issue: RenderIssue) => void;
|
|
21
|
+
governor?: Omit<GovernorOptions, "lanes">;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Render every chunk across the lanes the governor admits, pulling from one queue
|
|
27
|
+
* in timeline order, so the earliest finish first and the join rarely waits.
|
|
28
|
+
*
|
|
29
|
+
* Small chunks are what let the governor measure early and lanes share the work,
|
|
30
|
+
* but each costs a fresh encoder and scene build; once the governor settles, a
|
|
31
|
+
* lane takes its share of what is left as one merged range instead (all of it,
|
|
32
|
+
* when one lane is running). A lane opens only once admitted, so a window that
|
|
33
|
+
* is not needed never competes with the first for the GPU. A range that fails is
|
|
34
|
+
* retried once on another lane and its lane is retired; a second failure, or the
|
|
35
|
+
* caller's abort, ends the run.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runLanes(options: RunLanesOptions): Promise<void>;
|
|
38
|
+
//# sourceMappingURL=run-lanes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-lanes.d.ts","sourceRoot":"","sources":["../../src/parallel/run-lanes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAA2B,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IAC/B;;;;OAIG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACzD,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,mDAAmD;IACnD,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,uFAAuF;IACvF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAUD;;;;;;;;;;;GAWG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqItE"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { LaneGovernor } from "./governor";
|
|
2
|
+
/** Tries a range gets before the run fails: once on its lane, once more on another. */
|
|
3
|
+
const ATTEMPTS = 2;
|
|
4
|
+
/**
|
|
5
|
+
* Render every chunk across the lanes the governor admits, pulling from one queue
|
|
6
|
+
* in timeline order, so the earliest finish first and the join rarely waits.
|
|
7
|
+
*
|
|
8
|
+
* Small chunks are what let the governor measure early and lanes share the work,
|
|
9
|
+
* but each costs a fresh encoder and scene build; once the governor settles, a
|
|
10
|
+
* lane takes its share of what is left as one merged range instead (all of it,
|
|
11
|
+
* when one lane is running). A lane opens only once admitted, so a window that
|
|
12
|
+
* is not needed never competes with the first for the GPU. A range that fails is
|
|
13
|
+
* retried once on another lane and its lane is retired; a second failure, or the
|
|
14
|
+
* caller's abort, ends the run.
|
|
15
|
+
*/
|
|
16
|
+
export async function runLanes(options) {
|
|
17
|
+
const { lanes, spec, chunks, signal } = options;
|
|
18
|
+
const now = options.now ?? (() => performance.now());
|
|
19
|
+
if (lanes.length === 0)
|
|
20
|
+
throw new Error("runLanes: no lanes");
|
|
21
|
+
if (chunks.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const abort = () => controller.abort(signal?.reason);
|
|
25
|
+
if (signal?.aborted)
|
|
26
|
+
abort();
|
|
27
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
28
|
+
const governor = new LaneGovernor({ lanes: lanes.length, ...options.governor });
|
|
29
|
+
const queue = chunks.map((chunk) => ({ chunk, attempts: 0 }));
|
|
30
|
+
const inFlight = new Map();
|
|
31
|
+
const dead = new Set();
|
|
32
|
+
let doneFrames = 0;
|
|
33
|
+
let remaining = chunks.reduce((sum, c) => sum + (c.end - c.start), 0);
|
|
34
|
+
let failure = null;
|
|
35
|
+
let wake = () => { };
|
|
36
|
+
let woken = new Promise((resolve) => (wake = resolve));
|
|
37
|
+
const notify = () => {
|
|
38
|
+
const resolve = wake;
|
|
39
|
+
woken = new Promise((r) => (wake = r));
|
|
40
|
+
resolve();
|
|
41
|
+
};
|
|
42
|
+
controller.signal.addEventListener("abort", notify, { once: true });
|
|
43
|
+
const alive = () => lanes.length - dead.size;
|
|
44
|
+
const admitted = (i) => {
|
|
45
|
+
let rank = 0;
|
|
46
|
+
for (let j = 0; j < i; j++)
|
|
47
|
+
if (!dead.has(j))
|
|
48
|
+
rank++;
|
|
49
|
+
return rank < governor.active;
|
|
50
|
+
};
|
|
51
|
+
const retire = (i) => {
|
|
52
|
+
dead.add(i);
|
|
53
|
+
governor.laneLost(alive());
|
|
54
|
+
options.onActive?.(governor.active);
|
|
55
|
+
notify();
|
|
56
|
+
};
|
|
57
|
+
const framesDrawn = () => {
|
|
58
|
+
let frames = doneFrames;
|
|
59
|
+
for (const f of inFlight.values())
|
|
60
|
+
frames += f;
|
|
61
|
+
return frames;
|
|
62
|
+
};
|
|
63
|
+
const take = () => {
|
|
64
|
+
const job = queue.shift();
|
|
65
|
+
if (!governor.settled || job.attempts > 0)
|
|
66
|
+
return job;
|
|
67
|
+
const queued = queue.reduce((sum, j) => sum + (j.chunk.end - j.chunk.start), job.chunk.end - job.chunk.start);
|
|
68
|
+
const share = Math.ceil(queued / (governor.active === 1 ? 1 : governor.active * 2));
|
|
69
|
+
let { end } = job.chunk;
|
|
70
|
+
while (queue.length > 0 && queue[0].attempts === 0 && queue[0].chunk.start === end && end - job.chunk.start < share) {
|
|
71
|
+
end = queue.shift().chunk.end;
|
|
72
|
+
}
|
|
73
|
+
return { chunk: { ...job.chunk, end }, attempts: 0 };
|
|
74
|
+
};
|
|
75
|
+
const runLane = async (i) => {
|
|
76
|
+
while (!admitted(i) && remaining > 0 && !controller.signal.aborted && !dead.has(i))
|
|
77
|
+
await woken;
|
|
78
|
+
if (remaining === 0 || controller.signal.aborted || dead.has(i))
|
|
79
|
+
return;
|
|
80
|
+
let opened;
|
|
81
|
+
try {
|
|
82
|
+
opened = await lanes[i].open(spec, controller.signal);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (controller.signal.aborted)
|
|
86
|
+
return;
|
|
87
|
+
if (alive() <= 1)
|
|
88
|
+
throw error;
|
|
89
|
+
console.warn(`[motionscript] export lane ${i} failed to open; continuing without it.`, error);
|
|
90
|
+
retire(i);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
for (;;) {
|
|
95
|
+
controller.signal.throwIfAborted();
|
|
96
|
+
if (remaining === 0)
|
|
97
|
+
return;
|
|
98
|
+
if (!admitted(i) || queue.length === 0) {
|
|
99
|
+
await woken;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const job = take();
|
|
103
|
+
const frames = job.chunk.end - job.chunk.start;
|
|
104
|
+
const reported = { frames: 0, busyMs: 0, waitMs: 0 };
|
|
105
|
+
inFlight.set(job, 0);
|
|
106
|
+
try {
|
|
107
|
+
const bytes = await opened.render(job.chunk, (report) => {
|
|
108
|
+
const changed = governor.record({
|
|
109
|
+
lane: i,
|
|
110
|
+
frames: report.frames - reported.frames,
|
|
111
|
+
busyMs: report.busyMs - reported.busyMs,
|
|
112
|
+
waitMs: report.waitMs - reported.waitMs,
|
|
113
|
+
at: now(),
|
|
114
|
+
});
|
|
115
|
+
for (const issue of report.issues ?? [])
|
|
116
|
+
options.onIssue?.(issue);
|
|
117
|
+
Object.assign(reported, { frames: report.frames, busyMs: report.busyMs, waitMs: report.waitMs });
|
|
118
|
+
inFlight.set(job, report.frames);
|
|
119
|
+
options.onFrames?.(framesDrawn());
|
|
120
|
+
if (changed !== null) {
|
|
121
|
+
options.onActive?.(changed);
|
|
122
|
+
notify();
|
|
123
|
+
}
|
|
124
|
+
}, controller.signal);
|
|
125
|
+
inFlight.delete(job);
|
|
126
|
+
doneFrames += frames;
|
|
127
|
+
remaining -= frames;
|
|
128
|
+
options.onFrames?.(framesDrawn());
|
|
129
|
+
options.onChunk(job.chunk, bytes);
|
|
130
|
+
notify();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
inFlight.delete(job);
|
|
134
|
+
if (controller.signal.aborted)
|
|
135
|
+
throw error;
|
|
136
|
+
if (++job.attempts >= ATTEMPTS || alive() <= 1)
|
|
137
|
+
throw error;
|
|
138
|
+
console.warn(`[motionscript] export lane ${i} failed frames ${job.chunk.start} to ${job.chunk.end}; retrying on another lane.`, error);
|
|
139
|
+
queue.unshift(job);
|
|
140
|
+
options.onFrames?.(framesDrawn());
|
|
141
|
+
retire(i);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
opened.close();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
await Promise.all(lanes.map((_, i) => runLane(i).catch((error) => {
|
|
152
|
+
failure ??= error;
|
|
153
|
+
controller.abort(error);
|
|
154
|
+
})));
|
|
155
|
+
if (failure)
|
|
156
|
+
throw failure;
|
|
157
|
+
if (remaining > 0)
|
|
158
|
+
throw new Error(`runLanes: ${remaining} frames were never rendered.`);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
signal?.removeEventListener("abort", abort);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=run-lanes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-lanes.js","sourceRoot":"","sources":["../../src/parallel/run-lanes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RenderIssue } from \"@motionscript/core\";\nimport { LaneGovernor, type GovernorOptions } from \"./governor\";\nimport type { ExportLane, LaneReport, LaneSession, SegmentSpec } from \"./lane\";\nimport type { ExportChunk } from \"./plan\";\n\nexport interface RunLanesOptions {\n lanes: readonly ExportLane[];\n spec: SegmentSpec;\n chunks: readonly ExportChunk[];\n /**\n * A finished range, in whatever order ranges finish. Once the governor\n * settles, adjacent chunks still queued are merged, so a range can span\n * several of the planned chunks; it is named by the first.\n */\n onChunk: (chunk: ExportChunk, bytes: Uint8Array) => void;\n /** Frames drawn so far across every lane. Drops when a failed range's frames are discarded. */\n onFrames?: (frames: number) => void;\n /** The active lane count, each time it changes. */\n onActive?: (active: number) => void;\n /** A render issue a lane reported (a range retried elsewhere may report one twice). */\n onIssue?: (issue: RenderIssue) => void;\n governor?: Omit<GovernorOptions, \"lanes\">;\n now?: () => number;\n signal?: AbortSignal;\n}\n\n/** Tries a range gets before the run fails: once on its lane, once more on another. */\nconst ATTEMPTS = 2;\n\ninterface Job {\n chunk: ExportChunk;\n attempts: number;\n}\n\n/**\n * Render every chunk across the lanes the governor admits, pulling from one queue\n * in timeline order, so the earliest finish first and the join rarely waits.\n *\n * Small chunks are what let the governor measure early and lanes share the work,\n * but each costs a fresh encoder and scene build; once the governor settles, a\n * lane takes its share of what is left as one merged range instead (all of it,\n * when one lane is running). A lane opens only once admitted, so a window that\n * is not needed never competes with the first for the GPU. A range that fails is\n * retried once on another lane and its lane is retired; a second failure, or the\n * caller's abort, ends the run.\n */\nexport async function runLanes(options: RunLanesOptions): Promise<void> {\n const { lanes, spec, chunks, signal } = options;\n const now = options.now ?? (() => performance.now());\n if (lanes.length === 0) throw new Error(\"runLanes: no lanes\");\n if (chunks.length === 0) return;\n\n const controller = new AbortController();\n const abort = () => controller.abort(signal?.reason);\n if (signal?.aborted) abort();\n signal?.addEventListener(\"abort\", abort, { once: true });\n\n const governor = new LaneGovernor({ lanes: lanes.length, ...options.governor });\n const queue: Job[] = chunks.map((chunk) => ({ chunk, attempts: 0 }));\n const inFlight = new Map<Job, number>();\n const dead = new Set<number>();\n let doneFrames = 0;\n let remaining = chunks.reduce((sum, c) => sum + (c.end - c.start), 0);\n let failure: unknown = null;\n\n let wake: () => void = () => {};\n let woken = new Promise<void>((resolve) => (wake = resolve));\n const notify = () => {\n const resolve = wake;\n woken = new Promise<void>((r) => (wake = r));\n resolve();\n };\n controller.signal.addEventListener(\"abort\", notify, { once: true });\n\n const alive = () => lanes.length - dead.size;\n const admitted = (i: number) => {\n let rank = 0;\n for (let j = 0; j < i; j++) if (!dead.has(j)) rank++;\n return rank < governor.active;\n };\n const retire = (i: number) => {\n dead.add(i);\n governor.laneLost(alive());\n options.onActive?.(governor.active);\n notify();\n };\n const framesDrawn = () => {\n let frames = doneFrames;\n for (const f of inFlight.values()) frames += f;\n return frames;\n };\n const take = (): Job => {\n const job = queue.shift()!;\n if (!governor.settled || job.attempts > 0) return job;\n const queued = queue.reduce((sum, j) => sum + (j.chunk.end - j.chunk.start), job.chunk.end - job.chunk.start);\n const share = Math.ceil(queued / (governor.active === 1 ? 1 : governor.active * 2));\n let { end } = job.chunk;\n while (queue.length > 0 && queue[0].attempts === 0 && queue[0].chunk.start === end && end - job.chunk.start < share) {\n end = queue.shift()!.chunk.end;\n }\n return { chunk: { ...job.chunk, end }, attempts: 0 };\n };\n\n const runLane = async (i: number): Promise<void> => {\n while (!admitted(i) && remaining > 0 && !controller.signal.aborted && !dead.has(i)) await woken;\n if (remaining === 0 || controller.signal.aborted || dead.has(i)) return;\n\n let opened: LaneSession;\n try {\n opened = await lanes[i].open(spec, controller.signal);\n } catch (error) {\n if (controller.signal.aborted) return;\n if (alive() <= 1) throw error;\n console.warn(`[motionscript] export lane ${i} failed to open; continuing without it.`, error);\n retire(i);\n return;\n }\n try {\n for (;;) {\n controller.signal.throwIfAborted();\n if (remaining === 0) return;\n if (!admitted(i) || queue.length === 0) {\n await woken;\n continue;\n }\n const job = take();\n const frames = job.chunk.end - job.chunk.start;\n const reported: LaneReport = { frames: 0, busyMs: 0, waitMs: 0 };\n inFlight.set(job, 0);\n try {\n const bytes = await opened.render(job.chunk, (report) => {\n const changed = governor.record({\n lane: i,\n frames: report.frames - reported.frames,\n busyMs: report.busyMs - reported.busyMs,\n waitMs: report.waitMs - reported.waitMs,\n at: now(),\n });\n for (const issue of report.issues ?? []) options.onIssue?.(issue);\n Object.assign(reported, { frames: report.frames, busyMs: report.busyMs, waitMs: report.waitMs });\n inFlight.set(job, report.frames);\n options.onFrames?.(framesDrawn());\n if (changed !== null) {\n options.onActive?.(changed);\n notify();\n }\n }, controller.signal);\n inFlight.delete(job);\n doneFrames += frames;\n remaining -= frames;\n options.onFrames?.(framesDrawn());\n options.onChunk(job.chunk, bytes);\n notify();\n } catch (error) {\n inFlight.delete(job);\n if (controller.signal.aborted) throw error;\n if (++job.attempts >= ATTEMPTS || alive() <= 1) throw error;\n console.warn(`[motionscript] export lane ${i} failed frames ${job.chunk.start} to ${job.chunk.end}; retrying on another lane.`, error);\n queue.unshift(job);\n options.onFrames?.(framesDrawn());\n retire(i);\n return;\n }\n }\n } finally {\n opened.close();\n }\n };\n\n try {\n await Promise.all(lanes.map((_, i) => runLane(i).catch((error: unknown) => {\n failure ??= error;\n controller.abort(error);\n })));\n if (failure) throw failure;\n if (remaining > 0) throw new Error(`runLanes: ${remaining} frames were never rendered.`);\n } finally {\n signal?.removeEventListener(\"abort\", abort);\n }\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LaneEnvironment, LaneSession, SegmentSpec } from "./lane";
|
|
2
|
+
/**
|
|
3
|
+
* Open one surface for the whole export and render chunk after chunk onto it.
|
|
4
|
+
* The surface is the expensive part (a WebGPU one takes up to a second and a
|
|
5
|
+
* half to start), and decoded fonts and pictures stay in its storage between
|
|
6
|
+
* chunks; each chunk still gets its own encoder, so it is its own file.
|
|
7
|
+
*/
|
|
8
|
+
export declare function openSegmentSession(spec: SegmentSpec, env: LaneEnvironment): Promise<LaneSession>;
|
|
9
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/parallel/session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAc,WAAW,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAKpF;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CA2BtG"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Registry } from "@motionscript/core";
|
|
2
|
+
import { renderTimeline } from "../frame-loop";
|
|
3
|
+
import { exportGeometry, MediabunnyVideoSink, openExportSurface } from "../exporter";
|
|
4
|
+
/** How often a lane reports its frames, in ms. */
|
|
5
|
+
const REPORT_EVERY_MS = 100;
|
|
6
|
+
/**
|
|
7
|
+
* Open one surface for the whole export and render chunk after chunk onto it.
|
|
8
|
+
* The surface is the expensive part (a WebGPU one takes up to a second and a
|
|
9
|
+
* half to start), and decoded fonts and pictures stay in its storage between
|
|
10
|
+
* chunks; each chunk still gets its own encoder, so it is its own file.
|
|
11
|
+
*/
|
|
12
|
+
export async function openSegmentSession(spec, env) {
|
|
13
|
+
const { project } = spec;
|
|
14
|
+
const viewport = project.viewport;
|
|
15
|
+
const fps = project.fps;
|
|
16
|
+
const registry = env.registry ?? new Registry();
|
|
17
|
+
const { output, oversample, renderScale } = exportGeometry(viewport, spec.scale, spec.supersample);
|
|
18
|
+
const surface = await openExportSurface({
|
|
19
|
+
canvasSize: { width: output.width * oversample, height: output.height * oversample },
|
|
20
|
+
viewport, fps, manifest: spec.manifest, rendererWasmUrl: env.rendererWasmUrl,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
async render(chunk, onReport, signal) {
|
|
24
|
+
const sink = new TimedSink(new MediabunnyVideoSink(surface.canvas, output, spec.format, spec.encoding), onReport);
|
|
25
|
+
const bytes = await renderTimeline({
|
|
26
|
+
project, registry, theme: spec.theme, scale: renderScale, includeAudio: false,
|
|
27
|
+
frames: { start: chunk.start, end: chunk.end },
|
|
28
|
+
renderContext: surface.renderContext, storageAdapter: surface.storageAdapter, assetCatalog: surface.assetCatalog,
|
|
29
|
+
sink, signal,
|
|
30
|
+
onIssue: (issue) => sink.issues.push(issue),
|
|
31
|
+
});
|
|
32
|
+
sink.flush();
|
|
33
|
+
if (!bytes)
|
|
34
|
+
throw new Error(`Frames ${chunk.start} to ${chunk.end} produced no bytes.`);
|
|
35
|
+
return bytes;
|
|
36
|
+
},
|
|
37
|
+
close: () => surface.dispose(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Times each frame: drawing is the gap between frames, waiting is the encoder's backpressure. */
|
|
41
|
+
class TimedSink {
|
|
42
|
+
_inner;
|
|
43
|
+
_onReport;
|
|
44
|
+
issues = [];
|
|
45
|
+
_frames = 0;
|
|
46
|
+
_busyMs = 0;
|
|
47
|
+
_waitMs = 0;
|
|
48
|
+
_lastEnd = 0;
|
|
49
|
+
_lastReport = 0;
|
|
50
|
+
constructor(_inner, _onReport) {
|
|
51
|
+
this._inner = _inner;
|
|
52
|
+
this._onReport = _onReport;
|
|
53
|
+
}
|
|
54
|
+
async start(info) {
|
|
55
|
+
await this._inner.start(info);
|
|
56
|
+
this._lastEnd = this._lastReport = performance.now();
|
|
57
|
+
}
|
|
58
|
+
async addFrame(timestamp, duration) {
|
|
59
|
+
const before = performance.now();
|
|
60
|
+
this._busyMs += before - this._lastEnd;
|
|
61
|
+
await this._inner.addFrame(timestamp, duration);
|
|
62
|
+
const after = performance.now();
|
|
63
|
+
this._waitMs += after - before;
|
|
64
|
+
this._lastEnd = after;
|
|
65
|
+
this._frames++;
|
|
66
|
+
if (after - this._lastReport >= REPORT_EVERY_MS) {
|
|
67
|
+
this._lastReport = after;
|
|
68
|
+
this.flush();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
finalize() {
|
|
72
|
+
return this._inner.finalize();
|
|
73
|
+
}
|
|
74
|
+
flush() {
|
|
75
|
+
const issues = this.issues.splice(0);
|
|
76
|
+
this._onReport({ frames: this._frames, busyMs: this._busyMs, waitMs: this._waitMs, ...(issues.length > 0 && { issues }) });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/parallel/session.ts"],"names":[],"mappings":"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","sourcesContent":["import { Registry, type RenderIssue } from \"@motionscript/core\";\nimport { renderTimeline, type VideoFrameSink } from \"../frame-loop\";\nimport { exportGeometry, MediabunnyVideoSink, openExportSurface } from \"../exporter\";\nimport type { LaneEnvironment, LaneReport, LaneSession, SegmentSpec } from \"./lane\";\n\n/** How often a lane reports its frames, in ms. */\nconst REPORT_EVERY_MS = 100;\n\n/**\n * Open one surface for the whole export and render chunk after chunk onto it.\n * The surface is the expensive part (a WebGPU one takes up to a second and a\n * half to start), and decoded fonts and pictures stay in its storage between\n * chunks; each chunk still gets its own encoder, so it is its own file.\n */\nexport async function openSegmentSession(spec: SegmentSpec, env: LaneEnvironment): Promise<LaneSession> {\n const { project } = spec;\n const viewport = project.viewport!;\n const fps = project.fps!;\n const registry = env.registry ?? new Registry();\n const { output, oversample, renderScale } = exportGeometry(viewport, spec.scale, spec.supersample);\n const surface = await openExportSurface({\n canvasSize: { width: output.width * oversample, height: output.height * oversample },\n viewport, fps, manifest: spec.manifest, rendererWasmUrl: env.rendererWasmUrl,\n });\n\n return {\n async render(chunk, onReport, signal) {\n const sink = new TimedSink(new MediabunnyVideoSink(surface.canvas, output, spec.format, spec.encoding), onReport);\n const bytes = await renderTimeline<AudioBuffer>({\n project, registry, theme: spec.theme, scale: renderScale, includeAudio: false,\n frames: { start: chunk.start, end: chunk.end },\n renderContext: surface.renderContext, storageAdapter: surface.storageAdapter, assetCatalog: surface.assetCatalog,\n sink, signal,\n onIssue: (issue) => sink.issues.push(issue),\n });\n sink.flush();\n if (!bytes) throw new Error(`Frames ${chunk.start} to ${chunk.end} produced no bytes.`);\n return bytes;\n },\n close: () => surface.dispose(),\n };\n}\n\n/** Times each frame: drawing is the gap between frames, waiting is the encoder's backpressure. */\nclass TimedSink implements VideoFrameSink<AudioBuffer> {\n readonly issues: RenderIssue[] = [];\n private _frames = 0;\n private _busyMs = 0;\n private _waitMs = 0;\n private _lastEnd = 0;\n private _lastReport = 0;\n\n constructor(\n private readonly _inner: MediabunnyVideoSink,\n private readonly _onReport: (report: LaneReport) => void,\n ) { }\n\n async start(info: Parameters<MediabunnyVideoSink[\"start\"]>[0]): Promise<void> {\n await this._inner.start(info);\n this._lastEnd = this._lastReport = performance.now();\n }\n\n async addFrame(timestamp: number, duration: number): Promise<void> {\n const before = performance.now();\n this._busyMs += before - this._lastEnd;\n await this._inner.addFrame(timestamp, duration);\n const after = performance.now();\n this._waitMs += after - before;\n this._lastEnd = after;\n this._frames++;\n if (after - this._lastReport >= REPORT_EVERY_MS) {\n this._lastReport = after;\n this.flush();\n }\n }\n\n finalize(): Promise<Uint8Array> {\n return this._inner.finalize();\n }\n\n flush(): void {\n const issues = this.issues.splice(0);\n this._onReport({ frames: this._frames, busyMs: this._busyMs, waitMs: this._waitMs, ...(issues.length > 0 && { issues }) });\n }\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type WgpuRenderer } from "@motionscript/renderer";
|
|
2
|
+
/** Where the renderer's WebAssembly is served, for every entry that draws. */
|
|
3
|
+
export interface RendererWasmOption {
|
|
4
|
+
/** URL of `ms_renderer_bg.wasm`; defaults to beside `@motionscript/renderer`'s module, which a bundler resolves. */
|
|
5
|
+
rendererWasmUrl?: string | URL;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* This browser cannot run the WebGPU renderer: no `navigator.gpu`, or no
|
|
9
|
+
* adapter behind it. Every entry that draws (an engine, a still, an export,
|
|
10
|
+
* a timeline's audio mix) rejects with it before drawing anything.
|
|
11
|
+
*/
|
|
12
|
+
export declare class WebGpuUnavailableError extends Error {
|
|
13
|
+
constructor();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The renderer every entry draws with, or {@link WebGpuUnavailableError}.
|
|
17
|
+
* `gpu: false` is for an entry that only measures text, which needs no device.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadRenderer(options?: RendererWasmOption, { gpu }?: {
|
|
20
|
+
gpu?: boolean | undefined;
|
|
21
|
+
}): Promise<WgpuRenderer>;
|
|
22
|
+
//# sourceMappingURL=renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGhG,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IAC/B,oHAAoH;IACpH,eAAe,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CAClC;AAED;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;;CAKhD;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,kBAAuB,EAAE,EAAE,GAAU,EAAE;;CAAK,GAAG,OAAO,CAAC,YAAY,CAAC,CAG/G"}
|