@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
package/dist/engine.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Engine, } from "@motionscript/core";
|
|
2
|
+
import { loadRenderer } from "./renderer";
|
|
3
|
+
import { resolveDynamicNodes } from "./dynamic-nodes";
|
|
4
|
+
import { WebAudioSink } from "./audio/player";
|
|
5
|
+
import { WebTicker } from "./ticker";
|
|
6
|
+
import { exportVideo as exportVideoFile } from "./exporter";
|
|
7
|
+
import { exportAudio } from "./export-audio";
|
|
8
|
+
import { isAudioFormat } from "./formats";
|
|
9
|
+
import { StillRenderer } from "./still";
|
|
10
|
+
/**
|
|
11
|
+
* The browser's {@link Exporter}: WebCodecs + mediabunny for video, the
|
|
12
|
+
* browser's own image codecs for a still.
|
|
13
|
+
*
|
|
14
|
+
* **It does not reuse a session's storage adapter, and that is deliberate.** An
|
|
15
|
+
* adapter's viewport clamps the resolution images decode at — `StorageAdapter`
|
|
16
|
+
* documents that it has no setter for exactly this reason — so handing an export
|
|
17
|
+
* the preview's adapter would paint a 2× render from decodes sized for 1×, and
|
|
18
|
+
* the result would be soft in a way nothing reports. What is shared is the
|
|
19
|
+
* engine's renderer backend, the same one its sessions draw with.
|
|
20
|
+
*/
|
|
21
|
+
class WebExporter {
|
|
22
|
+
backend;
|
|
23
|
+
defaults;
|
|
24
|
+
still = null;
|
|
25
|
+
stillRegistry = null;
|
|
26
|
+
/** Settles once every still asked for so far has been encoded. */
|
|
27
|
+
stillQueue = Promise.resolve();
|
|
28
|
+
constructor(backend, defaults) {
|
|
29
|
+
this.backend = backend;
|
|
30
|
+
this.defaults = defaults;
|
|
31
|
+
}
|
|
32
|
+
async exportVideo(request) {
|
|
33
|
+
const encode = (request.encode ?? {});
|
|
34
|
+
if (encode.format && isAudioFormat(encode.format)) {
|
|
35
|
+
return exportAudio({
|
|
36
|
+
document: request.project,
|
|
37
|
+
registry: request.registry,
|
|
38
|
+
manifest: request.manifest,
|
|
39
|
+
format: encode.format,
|
|
40
|
+
audio: encode.audio ?? this.defaults.audio,
|
|
41
|
+
filename: encode.filename,
|
|
42
|
+
returnBytes: encode.download !== true,
|
|
43
|
+
onProgress: request.onProgress,
|
|
44
|
+
signal: request.signal,
|
|
45
|
+
rendererWasmUrl: this.defaults.rendererWasmUrl,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return exportVideoFile({
|
|
49
|
+
document: request.project,
|
|
50
|
+
registry: request.registry,
|
|
51
|
+
scale: request.scale,
|
|
52
|
+
manifest: request.manifest,
|
|
53
|
+
includeAudio: request.includeAudio,
|
|
54
|
+
frames: request.frames,
|
|
55
|
+
format: encode.format,
|
|
56
|
+
video: encode.video ?? this.defaults.video,
|
|
57
|
+
audio: encode.audio ?? this.defaults.audio,
|
|
58
|
+
supersample: encode.supersample,
|
|
59
|
+
filename: encode.filename,
|
|
60
|
+
// A `Session` asks for bytes; only an explicit `download: true` takes
|
|
61
|
+
// the DOM anchor path this function offers a page.
|
|
62
|
+
returnBytes: encode.download !== true,
|
|
63
|
+
onProgress: request.onProgress,
|
|
64
|
+
onIssue: request.onIssue,
|
|
65
|
+
signal: request.signal,
|
|
66
|
+
rendererWasmUrl: this.defaults.rendererWasmUrl,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Queued, rather than a renderer per call: the one renderer is what keeps a
|
|
71
|
+
* GPU surface and the decode cache warm between stills, and its draw and the
|
|
72
|
+
* encode of it are two steps another call's draw would land between.
|
|
73
|
+
*/
|
|
74
|
+
exportStill(request) {
|
|
75
|
+
const still = this.stillQueue.then(() => this.drawStill(request));
|
|
76
|
+
this.stillQueue = still.catch(() => undefined);
|
|
77
|
+
return still;
|
|
78
|
+
}
|
|
79
|
+
dispose() {
|
|
80
|
+
this.still?.dispose();
|
|
81
|
+
this.still = null;
|
|
82
|
+
}
|
|
83
|
+
async drawStill(request) {
|
|
84
|
+
const renderer = this.renderer(request);
|
|
85
|
+
const drawn = await renderer.render(request.project, {
|
|
86
|
+
scale: request.scale,
|
|
87
|
+
onIssue: request.onIssue,
|
|
88
|
+
...frameOption(request.frame),
|
|
89
|
+
});
|
|
90
|
+
const bytes = await renderer.toBytes({
|
|
91
|
+
format: request.format,
|
|
92
|
+
quality: request.quality,
|
|
93
|
+
});
|
|
94
|
+
return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* One `StillRenderer`, reused across calls and re-viewported when the ask
|
|
98
|
+
* changes — `setViewport` is a no-op when the size already matches, and a
|
|
99
|
+
* full reset when it does not, which is the same trade the class documents.
|
|
100
|
+
*/
|
|
101
|
+
renderer(request) {
|
|
102
|
+
const { project, registry } = request;
|
|
103
|
+
if (project.viewport === undefined || project.fps === undefined) {
|
|
104
|
+
throw new Error("exportStill takes a project whose fps and viewport are resolved — see Session.");
|
|
105
|
+
}
|
|
106
|
+
// A renderer compiles against the registry it was built with.
|
|
107
|
+
if (this.still && this.stillRegistry !== registry)
|
|
108
|
+
this.dispose();
|
|
109
|
+
this.still ??= new StillRenderer(this.backend, {
|
|
110
|
+
viewport: project.viewport,
|
|
111
|
+
fps: project.fps,
|
|
112
|
+
manifest: request.manifest,
|
|
113
|
+
registry,
|
|
114
|
+
});
|
|
115
|
+
this.stillRegistry = registry;
|
|
116
|
+
this.still.setViewport(project.viewport);
|
|
117
|
+
if (request.manifest)
|
|
118
|
+
this.still.setManifest(request.manifest);
|
|
119
|
+
return this.still;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** `ExportStillRequest.frame` in the shape `StillRenderer.render` accepts. */
|
|
123
|
+
function frameOption(frame) {
|
|
124
|
+
if (frame.kind === "frame")
|
|
125
|
+
return { frame: frame.frame };
|
|
126
|
+
return { frame: frame.kind };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* An engine backed by the browser: WebGPU surfaces, Web Audio, WebCodecs.
|
|
130
|
+
* Rejects with `WebGpuUnavailableError` where this browser has no WebGPU adapter.
|
|
131
|
+
*
|
|
132
|
+
* Async because the renderer is a wasm module and every seam below needs it. Await
|
|
133
|
+
* once, at app start, and share the result — the caches underneath (registered
|
|
134
|
+
* fonts, decoded images, shaped paragraphs) are the point of keeping one engine
|
|
135
|
+
* rather than one per canvas.
|
|
136
|
+
*
|
|
137
|
+
* ```ts
|
|
138
|
+
* const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });
|
|
139
|
+
* const session = engine.open({ document, target: canvasEl });
|
|
140
|
+
* session.playback!.play();
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export async function createBrowserEngine(options = {}) {
|
|
144
|
+
const rawNodes = options.nodes;
|
|
145
|
+
// `Array.isArray` doesn't narrow a `readonly T[] | U` union cleanly against
|
|
146
|
+
// its `arg is any[]` (mutable) predicate — the `.filter` predicate below
|
|
147
|
+
// already guarantees no string survives, so the cast is just recovering
|
|
148
|
+
// what the runtime check already proved.
|
|
149
|
+
const classNodes = (Array.isArray(rawNodes)
|
|
150
|
+
? rawNodes.filter((n) => typeof n !== "string")
|
|
151
|
+
: rawNodes);
|
|
152
|
+
const sourceNodes = Array.isArray(rawNodes)
|
|
153
|
+
? rawNodes.filter((n) => typeof n === "string")
|
|
154
|
+
: [];
|
|
155
|
+
// Cheap — see dynamic-nodes.ts: no WASM import happens here, only on
|
|
156
|
+
// `compile`'s first actual call. Attached unconditionally so a later
|
|
157
|
+
// `engine.registerNode(source)` works even when `nodes` carried no
|
|
158
|
+
// string at boot.
|
|
159
|
+
const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);
|
|
160
|
+
const renderer = await loadRenderer(options);
|
|
161
|
+
const exporter = new WebExporter(renderer, { video: options.video, audio: options.audio, rendererWasmUrl: options.rendererWasmUrl });
|
|
162
|
+
const platform = {
|
|
163
|
+
createStorageAdapter: (deps) => renderer.createStorageAdapter(deps),
|
|
164
|
+
createRenderContext: (storage) => renderer.createRenderContext(storage),
|
|
165
|
+
createMeasureContext: (storage) => renderer.createMeasureContext(storage),
|
|
166
|
+
createAudioSink() {
|
|
167
|
+
return new WebAudioSink();
|
|
168
|
+
},
|
|
169
|
+
createTicker(fps, audioDevice) {
|
|
170
|
+
// Shares the device's own `AudioContext`: two independent timebases
|
|
171
|
+
// in one session drift against each other, and sound is the one that
|
|
172
|
+
// cannot be nudged back. A getter, so neither creates it before it
|
|
173
|
+
// is needed (the first play, or the first sound to decode).
|
|
174
|
+
const sink = audioDevice;
|
|
175
|
+
return new WebTicker({ context: () => sink.getContext(), fps });
|
|
176
|
+
},
|
|
177
|
+
createExporter() {
|
|
178
|
+
return exporter;
|
|
179
|
+
},
|
|
180
|
+
dispose() {
|
|
181
|
+
exporter.dispose();
|
|
182
|
+
renderer.dispose?.();
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
const engine = new Engine(platform, { ...options, nodes: classNodes, dynamicNodes });
|
|
186
|
+
for (const source of sourceNodes) {
|
|
187
|
+
// Sequential — one script's error shouldn't mask another's, and
|
|
188
|
+
// registration order should match array order for determinism.
|
|
189
|
+
await engine.registerNode(source);
|
|
190
|
+
}
|
|
191
|
+
return engine;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,MAAM,GAgBT,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,YAAY,EAA2B,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,WAAW,IAAI,eAAe,EAAsD,MAAM,YAAY,CAAC;AAChH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAqB,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAuExC;;;;;;;;;;GAUG;AACH,MAAM,WAAW;IAOQ;IACA;IAPb,KAAK,GAAyB,IAAI,CAAC;IACnC,aAAa,GAAoB,IAAI,CAAC;IAC9C,kEAAkE;IAC1D,UAAU,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAEzD,YACqB,OAAwB,EACxB,QAA2F;QAD3F,YAAO,GAAP,OAAO,CAAiB;QACxB,aAAQ,GAAR,QAAQ,CAAmF;IAC5G,CAAC;IAEL,KAAK,CAAC,WAAW,CAAC,OAA2B;QACzC,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAyB,CAAC;QAC9D,IAAI,MAAM,CAAC,MAAM,IAAI,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAChD,OAAO,WAAW,CAAC;gBACf,QAAQ,EAAE,OAAO,CAAC,OAAO;gBACzB,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK;gBAC1C,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,WAAW,EAAE,MAAM,CAAC,QAAQ,KAAK,IAAI;gBACrC,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe;aACjD,CAAC,CAAC;QACP,CAAC;QACD,OAAO,eAAe,CAAC;YACnB,QAAQ,EAAE,OAAO,CAAC,OAAO;YACzB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK;YAC1C,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK;YAC1C,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,sEAAsE;YACtE,mDAAmD;YACnD,WAAW,EAAE,MAAM,CAAC,QAAQ,KAAK,IAAI;YACrC,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe;SACjD,CAAC,CAAC;IACP,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,OAA2B;QACnC,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QAClE,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC/C,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,OAAO;QACH,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;QACtB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACtB,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,OAA2B;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE;YACjD,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,GAAG,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;SAChC,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC;YACjC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,OAAO,CAAC,OAAO;SAC3B,CAAC,CAAC;QACH,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;IACzE,CAAC;IAED;;;;OAIG;IACK,QAAQ,CAAC,OAA2B;QACxC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;QACtC,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CAAC,gFAAgF,CAAC,CAAC;QACtG,CAAC;QACD,8DAA8D;QAC9D,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,aAAa,KAAK,QAAQ;YAAE,IAAI,CAAC,OAAO,EAAE,CAAC;QAClE,IAAI,CAAC,KAAK,KAAK,IAAI,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE;YAC3C,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ;SACX,CAAC,CAAC;QACH,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,OAAO,CAAC,QAAQ;YAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;CACJ;AAED,8EAA8E;AAC9E,SAAS,WAAW,CAAC,KAAkC;IACnD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;IAC1D,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACrC,UAAgC,EAAE;IAElC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC;IAC/B,4EAA4E;IAC5E,yEAAyE;IACzE,wEAAwE;IACxE,yCAAyC;IACzC,MAAM,UAAU,GAAG,CACf,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QACnB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAwB,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;QACrE,CAAC,CAAC,QAAQ,CACuC,CAAC;IAC1D,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QACvC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;QAC5D,CAAC,CAAC,EAAE,CAAC;IACT,qEAAqE;IACrE,qEAAqE;IACrE,mEAAmE;IACnE,kBAAkB;IAClB,MAAM,YAAY,GAAG,mBAAmB,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAExF,MAAM,QAAQ,GAAwC,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAErI,MAAM,QAAQ,GAAuC;QACjD,oBAAoB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,CAAC;QACnE,mBAAmB,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,OAAO,CAAC;QACvE,oBAAoB,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC,OAAO,CAAC;QACzE,eAAe;YACX,OAAO,IAAI,YAAY,EAAE,CAAC;QAC9B,CAAC;QACD,YAAY,CAAC,GAAW,EAAE,WAAsB;YAC5C,oEAAoE;YACpE,qEAAqE;YACrE,mEAAmE;YACnE,4DAA4D;YAC5D,MAAM,IAAI,GAAG,WAA2B,CAAC;YACzC,OAAO,IAAI,SAAS,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACpE,CAAC;QACD,cAAc;YACV,OAAO,QAAQ,CAAC;QACpB,CAAC;QACD,OAAO;YACH,QAAQ,CAAC,OAAO,EAAE,CAAC;YACnB,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QACzB,CAAC;KACJ,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,MAAM,CAAqB,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAC;IACzG,KAAK,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC;QAC/B,gEAAgE;QAChE,+DAA+D;QAC/D,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["import {\n AudioSink,\n Engine,\n EngineOptions,\n EnginePlatform,\n EngineRenderContext,\n Exporter,\n ExportStillRequest,\n ExportVideoRequest,\n ExportedStill,\n Registry,\n Ticker,\n MeasureContext2D,\n PlatformDeps,\n StorageAdapter,\n type DynamicNodeConfig,\n type NodeConstructor,\n type NodeTypeMap,\n} from \"@motionscript/core\";\n\nimport type { WgpuStorageAdapter } from \"@motionscript/renderer\";\nimport { loadRenderer, type RendererWasmOption } from \"./renderer\";\nimport { resolveDynamicNodes } from \"./dynamic-nodes\";\nimport { WebAudioSink } from \"./audio/player\";\nimport { WebTicker } from \"./ticker\";\nimport { exportVideo as exportVideoFile, type AudioEncoderOptions, type VideoEncoderOptions } from \"./exporter\";\nimport { exportAudio } from \"./export-audio\";\nimport { isAudioFormat, type ExportFormat } from \"./formats\";\nimport { StillRenderer } from \"./still\";\n\nexport interface BrowserEngineOptions extends Omit<EngineOptions, \"nodes\" | \"dynamicNodes\">, RendererWasmOption {\n /**\n * Node types any document opened here may name — components as an array\n * or a record naming each key outright, same as `EngineOptions`, plus a\n * TS source string in the **array** form only. A string default-exports a\n * component, whose `key` is its key — the record form takes no string, so\n * there is exactly one place a dynamic node's key comes from, not two that\n * could disagree.\n */\n nodes?: readonly (NodeConstructor | string)[] | NodeTypeMap;\n /**\n * Enables a source string in `nodes`, and `engine.registerNode(source)`\n * at any point afterward. Any field left unset falls back to `browser`'s\n * own default — `@motionscript/core/sdk` for `sdk`, a lazily-loaded\n * `esbuild-wasm` for `compile` — so this never needs to be passed at all\n * for the common case. `modules` has no default: only the host knows\n * which loaded packages a script it compiles may reach.\n */\n dynamicNodes?: Partial<DynamicNodeConfig>;\n /**\n * Where `esbuild-wasm`'s own `.wasm` binary is served — only read if the\n * default compiler is actually used.\n */\n compilerWasmUrl?: string;\n /** Default video encoder settings for `session.export()`. Overridable per call. */\n video?: VideoEncoderOptions;\n /** Default audio encoder settings for `session.export()`. Overridable per call. */\n audio?: AudioEncoderOptions;\n}\n\n/** What a session's frames are drawn with, and the storage and measurement it reads from. */\nexport interface BrowserRenderer<TStorage extends StorageAdapter = StorageAdapter> {\n createStorageAdapter(deps: PlatformDeps): TStorage;\n createRenderContext(storage: TStorage): BrowserRenderContext;\n createMeasureContext(storage: TStorage): MeasureContext2D;\n dispose?(): void;\n}\n\n/** A render context as this package drives it: an engine one, plus what exports and stills ask of it. */\nexport type BrowserRenderContext = EngineRenderContext & {\n /**\n * Settles once the surface `mount` began can draw. A backend whose surface\n * comes up asynchronously (a GPU device) must have it, or the first frame\n * draws onto nothing.\n */\n whenMounted?(): Promise<void>;\n /**\n * The frame last drawn, copied into a fresh 2D canvas, or `undefined` before\n * one was. What a still encodes; a WebGPU canvas is readable only in the task\n * that drew it, so a backend may draw it again here.\n */\n snapshotCanvas?(): HTMLCanvasElement | undefined;\n};\n\n/** What `session.export({ encode })` accepts in this environment. */\nexport interface BrowserEncodeOptions {\n /**\n * Defaults to `'mp4'`. An audio format (`'wav'`, `'mp3'`, `'m4a'`) renders no\n * frames and ignores `scale`, `frames`, `includeAudio`, `video` and `supersample`.\n */\n format?: ExportFormat;\n video?: VideoEncoderOptions;\n audio?: AudioEncoderOptions;\n supersample?: number;\n /** Set false to get the bytes back instead of triggering a browser download. */\n download?: boolean;\n filename?: string;\n}\n\n/**\n * The browser's {@link Exporter}: WebCodecs + mediabunny for video, the\n * browser's own image codecs for a still.\n *\n * **It does not reuse a session's storage adapter, and that is deliberate.** An\n * adapter's viewport clamps the resolution images decode at — `StorageAdapter`\n * documents that it has no setter for exactly this reason — so handing an export\n * the preview's adapter would paint a 2× render from decodes sized for 1×, and\n * the result would be soft in a way nothing reports. What is shared is the\n * engine's renderer backend, the same one its sessions draw with.\n */\nclass WebExporter implements Exporter {\n private still: StillRenderer | null = null;\n private stillRegistry: Registry | null = null;\n /** Settles once every still asked for so far has been encoded. */\n private stillQueue: Promise<unknown> = Promise.resolve();\n\n constructor(\n private readonly backend: BrowserRenderer,\n private readonly defaults: { video?: VideoEncoderOptions; audio?: AudioEncoderOptions } & RendererWasmOption,\n ) { }\n\n async exportVideo(request: ExportVideoRequest): Promise<Uint8Array | void> {\n const encode = (request.encode ?? {}) as BrowserEncodeOptions;\n if (encode.format && isAudioFormat(encode.format)) {\n return exportAudio({\n document: request.project,\n registry: request.registry,\n manifest: request.manifest,\n format: encode.format,\n audio: encode.audio ?? this.defaults.audio,\n filename: encode.filename,\n returnBytes: encode.download !== true,\n onProgress: request.onProgress,\n signal: request.signal,\n rendererWasmUrl: this.defaults.rendererWasmUrl,\n });\n }\n return exportVideoFile({\n document: request.project,\n registry: request.registry,\n scale: request.scale,\n manifest: request.manifest,\n includeAudio: request.includeAudio,\n frames: request.frames,\n format: encode.format,\n video: encode.video ?? this.defaults.video,\n audio: encode.audio ?? this.defaults.audio,\n supersample: encode.supersample,\n filename: encode.filename,\n // A `Session` asks for bytes; only an explicit `download: true` takes\n // the DOM anchor path this function offers a page.\n returnBytes: encode.download !== true,\n onProgress: request.onProgress,\n onIssue: request.onIssue,\n signal: request.signal,\n rendererWasmUrl: this.defaults.rendererWasmUrl,\n });\n }\n\n /**\n * Queued, rather than a renderer per call: the one renderer is what keeps a\n * GPU surface and the decode cache warm between stills, and its draw and the\n * encode of it are two steps another call's draw would land between.\n */\n exportStill(request: ExportStillRequest): Promise<ExportedStill> {\n const still = this.stillQueue.then(() => this.drawStill(request));\n this.stillQueue = still.catch(() => undefined);\n return still;\n }\n\n dispose(): void {\n this.still?.dispose();\n this.still = null;\n }\n\n private async drawStill(request: ExportStillRequest): Promise<ExportedStill> {\n const renderer = this.renderer(request);\n const drawn = await renderer.render(request.project, {\n scale: request.scale,\n onIssue: request.onIssue,\n ...frameOption(request.frame),\n });\n const bytes = await renderer.toBytes({\n format: request.format,\n quality: request.quality,\n });\n return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };\n }\n\n /**\n * One `StillRenderer`, reused across calls and re-viewported when the ask\n * changes — `setViewport` is a no-op when the size already matches, and a\n * full reset when it does not, which is the same trade the class documents.\n */\n private renderer(request: ExportStillRequest): StillRenderer {\n const { project, registry } = request;\n if (project.viewport === undefined || project.fps === undefined) {\n throw new Error(\"exportStill takes a project whose fps and viewport are resolved — see Session.\");\n }\n // A renderer compiles against the registry it was built with.\n if (this.still && this.stillRegistry !== registry) this.dispose();\n this.still ??= new StillRenderer(this.backend, {\n viewport: project.viewport,\n fps: project.fps,\n manifest: request.manifest,\n registry,\n });\n this.stillRegistry = registry;\n this.still.setViewport(project.viewport);\n if (request.manifest) this.still.setManifest(request.manifest);\n return this.still;\n }\n}\n\n/** `ExportStillRequest.frame` in the shape `StillRenderer.render` accepts. */\nfunction frameOption(frame: ExportStillRequest[\"frame\"]): { frame?: number | \"first\" | \"last\" } {\n if (frame.kind === \"frame\") return { frame: frame.frame };\n return { frame: frame.kind };\n}\n\n/**\n * An engine backed by the browser: WebGPU surfaces, Web Audio, WebCodecs.\n * Rejects with `WebGpuUnavailableError` where this browser has no WebGPU adapter.\n *\n * Async because the renderer is a wasm module and every seam below needs it. Await\n * once, at app start, and share the result — the caches underneath (registered\n * fonts, decoded images, shaped paragraphs) are the point of keeping one engine\n * rather than one per canvas.\n *\n * ```ts\n * const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });\n * const session = engine.open({ document, target: canvasEl });\n * session.playback!.play();\n * ```\n */\nexport async function createBrowserEngine(\n options: BrowserEngineOptions = {},\n): Promise<Engine<WgpuStorageAdapter>> {\n const rawNodes = options.nodes;\n // `Array.isArray` doesn't narrow a `readonly T[] | U` union cleanly against\n // its `arg is any[]` (mutable) predicate — the `.filter` predicate below\n // already guarantees no string survives, so the cast is just recovering\n // what the runtime check already proved.\n const classNodes = (\n Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is NodeConstructor => typeof n !== \"string\")\n : rawNodes\n ) as NodeTypeMap | readonly NodeConstructor[] | undefined;\n const sourceNodes = Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is string => typeof n === \"string\")\n : [];\n // Cheap — see dynamic-nodes.ts: no WASM import happens here, only on\n // `compile`'s first actual call. Attached unconditionally so a later\n // `engine.registerNode(source)` works even when `nodes` carried no\n // string at boot.\n const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);\n\n const renderer: BrowserRenderer<WgpuStorageAdapter> = await loadRenderer(options);\n const exporter = new WebExporter(renderer, { video: options.video, audio: options.audio, rendererWasmUrl: options.rendererWasmUrl });\n\n const platform: EnginePlatform<WgpuStorageAdapter> = {\n createStorageAdapter: (deps) => renderer.createStorageAdapter(deps),\n createRenderContext: (storage) => renderer.createRenderContext(storage),\n createMeasureContext: (storage) => renderer.createMeasureContext(storage),\n createAudioSink(): AudioSink {\n return new WebAudioSink();\n },\n createTicker(fps: number, audioDevice: AudioSink): Ticker {\n // Shares the device's own `AudioContext`: two independent timebases\n // in one session drift against each other, and sound is the one that\n // cannot be nudged back. A getter, so neither creates it before it\n // is needed (the first play, or the first sound to decode).\n const sink = audioDevice as WebAudioSink;\n return new WebTicker({ context: () => sink.getContext(), fps });\n },\n createExporter(): Exporter {\n return exporter;\n },\n dispose(): void {\n exporter.dispose();\n renderer.dispose?.();\n },\n };\n\n const engine = new Engine<WgpuStorageAdapter>(platform, { ...options, nodes: classNodes, dynamicNodes });\n for (const source of sourceNodes) {\n // Sequential — one script's error shouldn't mask another's, and\n // registration order should match array order for determinism.\n await engine.registerNode(source);\n }\n return engine;\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AssetManifest, ProjectDocument, Registry, Size2D, VisualDocument } from "@motionscript/core";
|
|
2
|
+
import type { RendererWasmOption } from "./renderer";
|
|
3
|
+
import type { AudioEncoderOptions, ExportProgressCallback } from "./exporter";
|
|
4
|
+
import { type AudioFormat } from "./formats";
|
|
5
|
+
export type ExportAudioParams = RendererWasmOption & {
|
|
6
|
+
/** A project, or a lone still or composition as the one track of one. */
|
|
7
|
+
document: ProjectDocument | VisualDocument;
|
|
8
|
+
/** The vocabulary its clips compile against. Defaults to what the library ships. */
|
|
9
|
+
registry?: Registry;
|
|
10
|
+
/** Discovery lays clips out at this size and rate; 1920×1080 at 60 fps when neither the document nor this says. */
|
|
11
|
+
viewport?: Size2D;
|
|
12
|
+
fps?: number;
|
|
13
|
+
manifest?: AssetManifest;
|
|
14
|
+
/** Defaults to `'wav'`, which needs no encoder. */
|
|
15
|
+
format?: AudioFormat;
|
|
16
|
+
audio?: AudioEncoderOptions;
|
|
17
|
+
/** Defaults to `export.<extension>`. */
|
|
18
|
+
filename?: string;
|
|
19
|
+
onProgress?: ExportProgressCallback;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
/** When true, the encoded bytes are returned instead of triggering a browser download. */
|
|
22
|
+
returnBytes?: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** The timeline schedules no sound, so an audio-only export has nothing to write. */
|
|
25
|
+
export declare class NoAudioError extends Error {
|
|
26
|
+
readonly name = "NoAudioError";
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Mix the timeline's audio and encode it alone: WAV, MP3 or M4A (AAC). Nothing
|
|
31
|
+
* is drawn, so no frame is rendered and no GPU surface is made.
|
|
32
|
+
*
|
|
33
|
+
* @throws {NoAudioError} when nothing on the timeline plays, rather than writing a silent file.
|
|
34
|
+
*/
|
|
35
|
+
export declare function exportAudio(params: ExportAudioParams): Promise<Uint8Array | void>;
|
|
36
|
+
//# sourceMappingURL=export-audio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-audio.d.ts","sourceRoot":"","sources":["../src/export-audio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAI3G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAG/F,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG;IACjD,yEAAyE;IACzE,QAAQ,EAAE,eAAe,GAAG,cAAc,CAAC;IAC3C,oFAAoF;IACpF,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mHAAmH;IACnH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mDAAmD;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,qFAAqF;AACrF,qBAAa,YAAa,SAAQ,KAAK;IACnC,SAAkB,IAAI,kBAAkB;;CAK3C;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqCvF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BufferTarget, Output } from "mediabunny";
|
|
2
|
+
import { deliver } from "./deliver";
|
|
3
|
+
import { audioTrackSource, ensureAudioEncoder, formatSpec } from "./formats";
|
|
4
|
+
import { mixTimelineAudio } from "./mix-timeline-audio";
|
|
5
|
+
/** The timeline schedules no sound, so an audio-only export has nothing to write. */
|
|
6
|
+
export class NoAudioError extends Error {
|
|
7
|
+
name = "NoAudioError";
|
|
8
|
+
constructor() {
|
|
9
|
+
super("The timeline has no audio to export.");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mix the timeline's audio and encode it alone: WAV, MP3 or M4A (AAC). Nothing
|
|
14
|
+
* is drawn, so no frame is rendered and no GPU surface is made.
|
|
15
|
+
*
|
|
16
|
+
* @throws {NoAudioError} when nothing on the timeline plays, rather than writing a silent file.
|
|
17
|
+
*/
|
|
18
|
+
export async function exportAudio(params) {
|
|
19
|
+
const { format = "wav", audio = {}, onProgress, signal, returnBytes = false } = params;
|
|
20
|
+
const spec = formatSpec(format);
|
|
21
|
+
if (spec.kind !== "audio")
|
|
22
|
+
throw new RangeError(`exportAudio writes audio; "${format}" is video, which exportVideo writes.`);
|
|
23
|
+
signal?.throwIfAborted();
|
|
24
|
+
// Before the mix, so a browser that cannot encode the format fails fast.
|
|
25
|
+
await ensureAudioEncoder(format);
|
|
26
|
+
const mixed = await mixTimelineAudio({
|
|
27
|
+
document: params.document,
|
|
28
|
+
registry: params.registry,
|
|
29
|
+
viewport: params.viewport,
|
|
30
|
+
fps: params.fps,
|
|
31
|
+
manifest: params.manifest,
|
|
32
|
+
sampleRate: audio.sampleRate,
|
|
33
|
+
rendererWasmUrl: params.rendererWasmUrl,
|
|
34
|
+
});
|
|
35
|
+
if (mixed === null)
|
|
36
|
+
throw new NoAudioError();
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
onProgress?.(0.5);
|
|
39
|
+
const target = new BufferTarget();
|
|
40
|
+
const output = new Output({ format: spec.container(), target });
|
|
41
|
+
const source = audioTrackSource(format, audio.bitrate);
|
|
42
|
+
output.addAudioTrack(source);
|
|
43
|
+
await output.start();
|
|
44
|
+
await source.add(mixed);
|
|
45
|
+
source.close();
|
|
46
|
+
await output.finalize();
|
|
47
|
+
if (!target.buffer)
|
|
48
|
+
throw new Error("Export produced no data");
|
|
49
|
+
onProgress?.(1);
|
|
50
|
+
return deliver(new Uint8Array(target.buffer), {
|
|
51
|
+
returnBytes,
|
|
52
|
+
filename: params.filename ?? `export.${spec.extension}`,
|
|
53
|
+
mimeType: spec.mimeType,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=export-audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-audio.js","sourceRoot":"","sources":["../src/export-audio.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAoB,MAAM,WAAW,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAsBxD,qFAAqF;AACrF,MAAM,OAAO,YAAa,SAAQ,KAAK;IACjB,IAAI,GAAG,cAAc,CAAC;IAExC;QACI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC;CACJ;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAyB;IACvD,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,KAAK,GAAG,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,EAAE,GAAG,MAAM,CAAC;IACvF,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,MAAM,IAAI,UAAU,CAAC,8BAA8B,MAAM,uCAAuC,CAAC,CAAC;IAC7H,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,yEAAyE;IACzE,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAEjC,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,eAAe,EAAE,MAAM,CAAC,eAAe;KAC1C,CAAC,CAAC;IACH,IAAI,KAAK,KAAK,IAAI;QAAE,MAAM,IAAI,YAAY,EAAE,CAAC;IAC7C,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;IAElB,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACvD,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC7B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACrB,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxB,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IAEhB,OAAO,OAAO,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;QAC1C,WAAW;QACX,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,UAAU,IAAI,CAAC,SAAS,EAAE;QACvD,QAAQ,EAAE,IAAI,CAAC,QAAQ;KAC1B,CAAC,CAAC;AACP,CAAC","sourcesContent":["import type { AssetManifest, ProjectDocument, Registry, Size2D, VisualDocument } from \"@motionscript/core\";\nimport { BufferTarget, Output } from \"mediabunny\";\n\nimport { deliver } from \"./deliver\";\nimport type { RendererWasmOption } from \"./renderer\";\nimport type { AudioEncoderOptions, ExportProgressCallback } from \"./exporter\";\nimport { audioTrackSource, ensureAudioEncoder, formatSpec, type AudioFormat } from \"./formats\";\nimport { mixTimelineAudio } from \"./mix-timeline-audio\";\n\nexport type ExportAudioParams = RendererWasmOption & {\n /** A project, or a lone still or composition as the one track of one. */\n document: ProjectDocument | VisualDocument;\n /** The vocabulary its clips compile against. Defaults to what the library ships. */\n registry?: Registry;\n /** Discovery lays clips out at this size and rate; 1920×1080 at 60 fps when neither the document nor this says. */\n viewport?: Size2D;\n fps?: number;\n manifest?: AssetManifest;\n /** Defaults to `'wav'`, which needs no encoder. */\n format?: AudioFormat;\n audio?: AudioEncoderOptions;\n /** Defaults to `export.<extension>`. */\n filename?: string;\n onProgress?: ExportProgressCallback;\n signal?: AbortSignal;\n /** When true, the encoded bytes are returned instead of triggering a browser download. */\n returnBytes?: boolean;\n};\n\n/** The timeline schedules no sound, so an audio-only export has nothing to write. */\nexport class NoAudioError extends Error {\n override readonly name = \"NoAudioError\";\n\n constructor() {\n super(\"The timeline has no audio to export.\");\n }\n}\n\n/**\n * Mix the timeline's audio and encode it alone: WAV, MP3 or M4A (AAC). Nothing\n * is drawn, so no frame is rendered and no GPU surface is made.\n *\n * @throws {NoAudioError} when nothing on the timeline plays, rather than writing a silent file.\n */\nexport async function exportAudio(params: ExportAudioParams): Promise<Uint8Array | void> {\n const { format = \"wav\", audio = {}, onProgress, signal, returnBytes = false } = params;\n const spec = formatSpec(format);\n if (spec.kind !== \"audio\") throw new RangeError(`exportAudio writes audio; \"${format}\" is video, which exportVideo writes.`);\n signal?.throwIfAborted();\n // Before the mix, so a browser that cannot encode the format fails fast.\n await ensureAudioEncoder(format);\n\n const mixed = await mixTimelineAudio({\n document: params.document,\n registry: params.registry,\n viewport: params.viewport,\n fps: params.fps,\n manifest: params.manifest,\n sampleRate: audio.sampleRate,\n rendererWasmUrl: params.rendererWasmUrl,\n });\n if (mixed === null) throw new NoAudioError();\n signal?.throwIfAborted();\n onProgress?.(0.5);\n\n const target = new BufferTarget();\n const output = new Output({ format: spec.container(), target });\n const source = audioTrackSource(format, audio.bitrate);\n output.addAudioTrack(source);\n await output.start();\n await source.add(mixed);\n source.close();\n await output.finalize();\n if (!target.buffer) throw new Error(\"Export produced no data\");\n onProgress?.(1);\n\n return deliver(new Uint8Array(target.buffer), {\n returnBytes,\n filename: params.filename ?? `export.${spec.extension}`,\n mimeType: spec.mimeType,\n });\n}\n"]}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { ManifestAssetCatalog, Registry, type AssetManifest, type FrameRange, type ProjectDocument, type RenderIssue, type VisualDocument, type Size2D, type StorageAdapter, type Theme } from "@motionscript/core";
|
|
2
|
+
import { type VideoFrameSink } from "./frame-loop";
|
|
3
|
+
import { type Quality, type VideoCodec } from 'mediabunny';
|
|
4
|
+
import type { BrowserRenderContext } from "./engine";
|
|
5
|
+
import { type RendererWasmOption } from "./renderer";
|
|
6
|
+
import { type VideoFormat } from "./formats";
|
|
7
|
+
/** Reports export progress in [0, 1]; video encoding occupies most of the range, audio mixing/finalize the tail. */
|
|
8
|
+
export type ExportProgressCallback = (progress: number) => void;
|
|
9
|
+
/**
|
|
10
|
+
* How the video track is encoded.
|
|
11
|
+
*
|
|
12
|
+
* Every field is a straight pass-through to mediabunny's `VideoEncodingConfig`,
|
|
13
|
+
* defaulted here rather than there so an export has one obvious place to look.
|
|
14
|
+
*/
|
|
15
|
+
export type VideoEncoderOptions = {
|
|
16
|
+
/**
|
|
17
|
+
* Which codec the video track is encoded with. Defaults to the format's own:
|
|
18
|
+
* `'avc'` (H.264) for MP4, which every player, editor and upload target
|
|
19
|
+
* accepts, and `'vp9'` for WebM.
|
|
20
|
+
*
|
|
21
|
+
* `'hevc'` and `'av1'` reconstruct sharp edges far better at the same
|
|
22
|
+
* bitrate, which is what a listing of syntax-highlighted code or a hairline
|
|
23
|
+
* stroke is made of — worth reaching for when the file is going somewhere
|
|
24
|
+
* that decodes them. Both are still 4:2:0, so they narrow the gap rather
|
|
25
|
+
* than close it. Availability depends on the platform's encoders; an
|
|
26
|
+
* unsupported choice, or one the container cannot hold (`'avc'` in WebM),
|
|
27
|
+
* falls back to the format's default rather than failing the export.
|
|
28
|
+
*/
|
|
29
|
+
codec?: 'avc' | 'hevc' | 'av1' | 'vp9' | 'vp8';
|
|
30
|
+
/**
|
|
31
|
+
* Which encoder implementation WebCodecs should prefer.
|
|
32
|
+
*
|
|
33
|
+
* Defaults to `'no-preference'`, which lets each platform use its own
|
|
34
|
+
* hardware encoder (VideoToolbox on macOS, Media Foundation/NVENC on
|
|
35
|
+
* Windows, VA-API on Linux). That is typically several times faster than the
|
|
36
|
+
* software path *and* it is the only configuration in which this sink's
|
|
37
|
+
* zero-readback design actually holds: a software encoder needs the pixels
|
|
38
|
+
* on the CPU, so it forces a full GPU→CPU copy of every frame that a
|
|
39
|
+
* hardware encoder reads directly off the canvas.
|
|
40
|
+
*
|
|
41
|
+
* Pass `'prefer-software'` when byte-identical output across machines
|
|
42
|
+
* matters more than speed — different hardware encoders tune quality and
|
|
43
|
+
* colour differently at the same nominal bitrate, so the same settings do
|
|
44
|
+
* not produce the same file.
|
|
45
|
+
*/
|
|
46
|
+
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
|
|
47
|
+
/**
|
|
48
|
+
* Target bitrate in bits per second, or one of mediabunny's subjective
|
|
49
|
+
* `Quality` levels.
|
|
50
|
+
*
|
|
51
|
+
* Defaults to {@link motionGraphicsBitrate}, which is deliberately *not*
|
|
52
|
+
* mediabunny's `QUALITY_VERY_HIGH`: that level is frame-rate blind, so a
|
|
53
|
+
* 60 fps export got the same ~12 Mbps at 1080p as a 30 fps one and paid for
|
|
54
|
+
* it with ringing along every glyph edge and banding across every gradient.
|
|
55
|
+
* See that function for the reasoning.
|
|
56
|
+
*/
|
|
57
|
+
bitrate?: number | Quality;
|
|
58
|
+
/** Defaults to mediabunny's `'variable'`. `'constant'` is the reproducible-output choice. */
|
|
59
|
+
bitrateMode?: 'constant' | 'variable';
|
|
60
|
+
/**
|
|
61
|
+
* Seconds between key frames. Defaults to 1.
|
|
62
|
+
*
|
|
63
|
+
* Shorter than mediabunny's 2, because a frame's error is carried by every
|
|
64
|
+
* inter frame that references it: on synthetic content, where a flat
|
|
65
|
+
* background makes a shimmer obvious, a keyframe twice as often visibly
|
|
66
|
+
* resets the drift. It also halves the seek granularity, which matters for a
|
|
67
|
+
* clip an editor is going to scrub.
|
|
68
|
+
*/
|
|
69
|
+
keyFrameInterval?: number;
|
|
70
|
+
/**
|
|
71
|
+
* WebCodecs performance/quality trade-off. Defaults to `'quality'`, which
|
|
72
|
+
* also guarantees no frame is dropped — the only correct setting for a file
|
|
73
|
+
* being written rather than streamed.
|
|
74
|
+
*/
|
|
75
|
+
latencyMode?: 'quality' | 'realtime';
|
|
76
|
+
/**
|
|
77
|
+
* A hint about what the frames contain, per
|
|
78
|
+
* [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).
|
|
79
|
+
*
|
|
80
|
+
* Defaults to `'detail'`: encoders that honour it bias towards preserving
|
|
81
|
+
* sharp edges and fine texture over smoothing motion, which is what motion
|
|
82
|
+
* graphics are. Ignored where it is not implemented.
|
|
83
|
+
*/
|
|
84
|
+
contentHint?: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The default video bitrate for an export of this size and frame rate.
|
|
88
|
+
*
|
|
89
|
+
* Scaling with **both** axes is the point. mediabunny's `Quality` levels scale
|
|
90
|
+
* with resolution only, so doubling the frame rate halved the bits available per
|
|
91
|
+
* frame while leaving the number unchanged — which is exactly the configuration
|
|
92
|
+
* that produced clean stills and a mushy video from the very same renderer.
|
|
93
|
+
*/
|
|
94
|
+
export declare function motionGraphicsBitrate(width: number, height: number, fps: number): number;
|
|
95
|
+
/** How the mixed audio track is encoded. */
|
|
96
|
+
export type AudioEncoderOptions = {
|
|
97
|
+
/** Bitrate in bits per second for AAC, Opus and MP3; WAV ignores it. Defaults to 192 kbps. */
|
|
98
|
+
bitrate?: number;
|
|
99
|
+
/** Mixdown sample rate in Hz. Defaults to the mixer's own (44100); Opus is resampled to 48 kHz. */
|
|
100
|
+
sampleRate?: number;
|
|
101
|
+
};
|
|
102
|
+
export type ExportParams = RendererWasmOption & {
|
|
103
|
+
/** A project, or a lone still or composition as the one track of one. */
|
|
104
|
+
document: ProjectDocument | VisualDocument;
|
|
105
|
+
/** The vocabulary its clips compile against. Defaults to what the library ships. */
|
|
106
|
+
registry?: Registry;
|
|
107
|
+
/** Win over the document's own; 1920×1080 at 60 fps when neither says. */
|
|
108
|
+
viewport?: Size2D;
|
|
109
|
+
fps?: number;
|
|
110
|
+
scale?: number;
|
|
111
|
+
/** The container, and with it the default codecs: H.264 and AAC for `'mp4'` (the default), VP9 and Opus for `'webm'`. */
|
|
112
|
+
format?: VideoFormat;
|
|
113
|
+
/** Defaults to `export.<extension>`. */
|
|
114
|
+
filename?: string;
|
|
115
|
+
manifest?: AssetManifest;
|
|
116
|
+
/** Theme for a document that carries none of its own. */
|
|
117
|
+
theme?: Theme;
|
|
118
|
+
/**
|
|
119
|
+
* Set false to produce a silent file — no audio track is added to the
|
|
120
|
+
* container and the mix is skipped entirely. Defaults to true.
|
|
121
|
+
*/
|
|
122
|
+
includeAudio?: boolean;
|
|
123
|
+
/** Video encoder settings; see {@link VideoEncoderOptions} for the defaults. */
|
|
124
|
+
video?: VideoEncoderOptions;
|
|
125
|
+
/** Audio encoder settings; ignored when `includeAudio` is false. */
|
|
126
|
+
audio?: AudioEncoderOptions;
|
|
127
|
+
/**
|
|
128
|
+
* Encode only these frames (`end` excluded), timestamped from 0 and silent:
|
|
129
|
+
* one segment of a split export, for `concatVideoSegments` to join. Mix
|
|
130
|
+
* the audio once with `mixTimelineAudio` instead.
|
|
131
|
+
*/
|
|
132
|
+
frames?: FrameRange;
|
|
133
|
+
onProgress?: ExportProgressCallback;
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
/**
|
|
136
|
+
* What the renderer drew but not as the document says, once per kind,
|
|
137
|
+
* feature and node. A lost GPU device fails the export with a
|
|
138
|
+
* `RenderBackendError` instead.
|
|
139
|
+
*/
|
|
140
|
+
onIssue?: (issue: RenderIssue) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Render each frame at this multiple of the output resolution and downsample
|
|
143
|
+
* it before encoding. **Defaults to 2**; pass 1 to turn it off.
|
|
144
|
+
*
|
|
145
|
+
* The one lever that improves an export at a *fixed* output size. Every
|
|
146
|
+
* codec a browser can encode is 4:2:0 — colour is stored at half resolution
|
|
147
|
+
* on both axes, and no bitrate buys that back — so the colour fringing along
|
|
148
|
+
* a saturated glyph or a hard edge is a property of the format, not of the
|
|
149
|
+
* settings. Handing the encoder an already-downsampled frame does not undo
|
|
150
|
+
* the subsampling, but it does mean every chroma sample averages four real
|
|
151
|
+
* pixels instead of one, which is what removes most of the crawl on fine
|
|
152
|
+
* detail and much of the banding on gradients.
|
|
153
|
+
*
|
|
154
|
+
* Measured against a correctly-sampled reference frame of a syntax-highlighted
|
|
155
|
+
* listing, `2` lands 2.7 dB closer than a 1× encode — and 1.3 dB closer than
|
|
156
|
+
* an *unencoded* 1× still, which is why it is the default despite costing
|
|
157
|
+
* `supersample²` times the render work per frame plus a rescale. Pass 1 when
|
|
158
|
+
* turnaround matters more than the file, and note that exporting at
|
|
159
|
+
* `scale: 2` is cheaper than supersampling *and* better whenever the extra
|
|
160
|
+
* resolution is wanted in the file itself.
|
|
161
|
+
*
|
|
162
|
+
* Clamped so the render surface stays inside {@link MAX_RENDER_AXIS}, since a
|
|
163
|
+
* supersampled 4K export is already drawing 8K frames.
|
|
164
|
+
*/
|
|
165
|
+
supersample?: number;
|
|
166
|
+
/**
|
|
167
|
+
* When true, the encoded bytes are returned instead of triggering a
|
|
168
|
+
* browser download. Used by headless drivers (e.g. the CLI) that capture
|
|
169
|
+
* the output themselves and write it to disk; leave unset for the
|
|
170
|
+
* interactive player, which downloads via an `<a>` element.
|
|
171
|
+
*/
|
|
172
|
+
returnBytes?: boolean;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* mediabunny/WebCodecs implementation of the frame sink, writing whichever
|
|
176
|
+
* container `format` names.
|
|
177
|
+
*
|
|
178
|
+
* Wraps the very `<canvas>` the renderer draws into, so `addFrame` costs nothing but
|
|
179
|
+
* a timestamp — WebCodecs pulls the pixels from the canvas itself, with no
|
|
180
|
+
* GPU→CPU readback. That is why the seam signals a frame rather than passing one.
|
|
181
|
+
*
|
|
182
|
+
* The readback claim holds only while the encoder is a hardware one, which is
|
|
183
|
+
* why {@link VideoEncoderOptions.hardwareAcceleration} defaults to letting the
|
|
184
|
+
* platform choose: a software encoder has to be handed CPU pixels, so it drags
|
|
185
|
+
* a full-frame copy off the GPU on every single frame.
|
|
186
|
+
*/
|
|
187
|
+
export declare class MediabunnyVideoSink implements VideoFrameSink<AudioBuffer> {
|
|
188
|
+
private readonly format;
|
|
189
|
+
private readonly audioOptions;
|
|
190
|
+
private readonly target;
|
|
191
|
+
private readonly output;
|
|
192
|
+
private readonly video;
|
|
193
|
+
private audio;
|
|
194
|
+
constructor(canvas: HTMLCanvasElement,
|
|
195
|
+
/** Encoded output size, which is the canvas' own unless supersampling is on. */
|
|
196
|
+
output: {
|
|
197
|
+
width: number;
|
|
198
|
+
height: number;
|
|
199
|
+
}, format: VideoFormat, encoding: ResolvedVideoEncoding, audioOptions?: AudioEncoderOptions);
|
|
200
|
+
start(info: {
|
|
201
|
+
fps: number;
|
|
202
|
+
hasAudio: boolean;
|
|
203
|
+
}): Promise<void>;
|
|
204
|
+
addFrame(timestamp: number, duration: number): Promise<void>;
|
|
205
|
+
addAudio(mixed: AudioBuffer): Promise<void>;
|
|
206
|
+
finalize(): Promise<Uint8Array>;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Every encoder setting, decided. A split export pins one of these across its
|
|
210
|
+
* segments, since `concatVideoSegments` refuses parts encoded differently. With
|
|
211
|
+
* a numeric `bitrate` (see {@link plainVideoEncoding}) it is plain data and
|
|
212
|
+
* crosses a process boundary.
|
|
213
|
+
*/
|
|
214
|
+
export interface ResolvedVideoEncoding {
|
|
215
|
+
codec: VideoCodec;
|
|
216
|
+
/** A `Quality` is left for the encoder to size, as an export always has. */
|
|
217
|
+
bitrate: number | Quality;
|
|
218
|
+
hardwareAcceleration: NonNullable<VideoEncoderOptions['hardwareAcceleration']>;
|
|
219
|
+
keyFrameInterval: number;
|
|
220
|
+
latencyMode: NonNullable<VideoEncoderOptions['latencyMode']>;
|
|
221
|
+
contentHint: string;
|
|
222
|
+
bitrateMode?: VideoEncoderOptions['bitrateMode'];
|
|
223
|
+
}
|
|
224
|
+
/** {@link VideoEncoderOptions} with its defaults applied against the encoded size and rate. */
|
|
225
|
+
export declare function resolveVideoEncoding(video: VideoEncoderOptions | undefined, format: VideoFormat, output: {
|
|
226
|
+
width: number;
|
|
227
|
+
height: number;
|
|
228
|
+
}, fps: number): Promise<ResolvedVideoEncoding>;
|
|
229
|
+
/**
|
|
230
|
+
* The encoding with a `Quality` turned into the bits per second mediabunny would
|
|
231
|
+
* give it at this size, since a class instance cannot cross a `MessagePort`.
|
|
232
|
+
*/
|
|
233
|
+
export declare function plainVideoEncoding(encoding: ResolvedVideoEncoding, output: {
|
|
234
|
+
width: number;
|
|
235
|
+
height: number;
|
|
236
|
+
}): ResolvedVideoEncoding & {
|
|
237
|
+
bitrate: number;
|
|
238
|
+
};
|
|
239
|
+
/** The encoded size, rounded up to even, and how far each frame is supersampled. */
|
|
240
|
+
export declare function exportGeometry(viewport: Size2D, scale: number, supersample: number): {
|
|
241
|
+
output: {
|
|
242
|
+
width: number;
|
|
243
|
+
height: number;
|
|
244
|
+
};
|
|
245
|
+
oversample: number;
|
|
246
|
+
renderScale: number;
|
|
247
|
+
};
|
|
248
|
+
/** A hidden canvas with a mounted render context, reusable across renders of one project. */
|
|
249
|
+
export interface ExportSurface {
|
|
250
|
+
canvas: HTMLCanvasElement;
|
|
251
|
+
renderContext: BrowserRenderContext;
|
|
252
|
+
storageAdapter: StorageAdapter;
|
|
253
|
+
assetCatalog: ManifestAssetCatalog;
|
|
254
|
+
dispose(): void;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Open the canvas, renderer, storage and render context an export draws with,
|
|
258
|
+
* resolving once the surface can draw (a frame drawn before an asynchronously
|
|
259
|
+
* started surface is ready encodes blank). The canvas is a whole multiple of the
|
|
260
|
+
* output, so the downsample is exact.
|
|
261
|
+
*/
|
|
262
|
+
export declare function openExportSurface(params: {
|
|
263
|
+
canvasSize: {
|
|
264
|
+
width: number;
|
|
265
|
+
height: number;
|
|
266
|
+
};
|
|
267
|
+
viewport: Size2D;
|
|
268
|
+
fps: number;
|
|
269
|
+
manifest: AssetManifest;
|
|
270
|
+
} & RendererWasmOption): Promise<ExportSurface>;
|
|
271
|
+
/**
|
|
272
|
+
* Renders a document frame-by-frame to an offscreen canvas and muxes the result
|
|
273
|
+
* into an MP4 or WebM via mediabunny.
|
|
274
|
+
*
|
|
275
|
+
* The frame loop itself — discovery, state evaluation, the warm-and-re-render retry
|
|
276
|
+
* that keeps every frame accurate, audio scheduling, progress and cancellation —
|
|
277
|
+
* lives in `renderTimeline` (frame-loop.ts). What this function
|
|
278
|
+
* supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,
|
|
279
|
+
* a Web Audio mixer, and the `<a download>` delivery.
|
|
280
|
+
*/
|
|
281
|
+
export declare function exportVideo(params: ExportParams): Promise<Uint8Array | void>;
|
|
282
|
+
//# sourceMappingURL=exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"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"}
|