@motionscript/browser 0.1.0 → 0.1.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/asset-bytes.d.ts +12 -50
- package/dist/asset-bytes.d.ts.map +1 -1
- package/dist/asset-bytes.js +20 -52
- package/dist/asset-bytes.js.map +1 -1
- package/dist/audio/bus-graph.d.ts +4 -39
- package/dist/audio/bus-graph.d.ts.map +1 -1
- package/dist/audio/bus-graph.js +4 -38
- package/dist/audio/bus-graph.js.map +1 -1
- package/dist/audio/effect-graph.d.ts +4 -29
- package/dist/audio/effect-graph.d.ts.map +1 -1
- package/dist/audio/effect-graph.js +4 -31
- package/dist/audio/effect-graph.js.map +1 -1
- package/dist/audio/loop-region.d.ts +3 -21
- package/dist/audio/loop-region.d.ts.map +1 -1
- package/dist/audio/loop-region.js +3 -21
- package/dist/audio/loop-region.js.map +1 -1
- package/dist/audio/mixer.d.ts +9 -41
- package/dist/audio/mixer.d.ts.map +1 -1
- package/dist/audio/mixer.js +11 -44
- package/dist/audio/mixer.js.map +1 -1
- package/dist/audio/player.d.ts +3 -17
- package/dist/audio/player.d.ts.map +1 -1
- package/dist/audio/player.js +4 -23
- package/dist/audio/player.js.map +1 -1
- package/dist/browser/audio.js +1 -1
- package/dist/browser/chunks/chunk-NDJYMKIV.js +2 -0
- package/dist/browser/chunks/chunk-NDJYMKIV.js.map +7 -0
- package/dist/browser/index.js +19 -19
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/manifest.json +1 -1
- package/dist/browser/ms_renderer_bg.wasm +0 -0
- package/dist/concat.d.ts +25 -84
- package/dist/concat.d.ts.map +1 -1
- package/dist/concat.js +19 -69
- package/dist/concat.js.map +1 -1
- package/dist/dynamic-nodes.d.ts +1 -6
- package/dist/dynamic-nodes.d.ts.map +1 -1
- package/dist/dynamic-nodes.js +3 -24
- package/dist/dynamic-nodes.js.map +1 -1
- package/dist/engine.d.ts +9 -42
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +10 -44
- package/dist/engine.js.map +1 -1
- package/dist/export-audio.d.ts +1 -6
- package/dist/export-audio.d.ts.map +1 -1
- package/dist/export-audio.js +1 -6
- package/dist/export-audio.js.map +1 -1
- package/dist/exporter.d.ts +28 -141
- package/dist/exporter.d.ts.map +1 -1
- package/dist/exporter.js +15 -90
- package/dist/exporter.js.map +1 -1
- package/dist/frame-loop.d.ts +19 -105
- package/dist/frame-loop.d.ts.map +1 -1
- package/dist/frame-loop.js +20 -82
- package/dist/frame-loop.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -15
- package/dist/index.js.map +1 -1
- package/dist/mix-timeline-audio.d.ts +7 -24
- package/dist/mix-timeline-audio.d.ts.map +1 -1
- package/dist/mix-timeline-audio.js +7 -23
- package/dist/mix-timeline-audio.js.map +1 -1
- package/dist/native-export.d.ts +29 -0
- package/dist/native-export.d.ts.map +1 -0
- package/dist/native-export.js +99 -0
- package/dist/native-export.js.map +1 -0
- package/dist/parallel/export-parallel.d.ts +2 -16
- package/dist/parallel/export-parallel.d.ts.map +1 -1
- package/dist/parallel/export-parallel.js +1 -11
- package/dist/parallel/export-parallel.js.map +1 -1
- package/dist/parallel/governor.d.ts +2 -15
- package/dist/parallel/governor.d.ts.map +1 -1
- package/dist/parallel/governor.js +1 -5
- package/dist/parallel/governor.js.map +1 -1
- package/dist/parallel/lane.d.ts +1 -5
- package/dist/parallel/lane.d.ts.map +1 -1
- package/dist/parallel/lane.js.map +1 -1
- package/dist/parallel/plan.d.ts +3 -7
- package/dist/parallel/plan.d.ts.map +1 -1
- package/dist/parallel/plan.js +1 -6
- package/dist/parallel/plan.js.map +1 -1
- package/dist/parallel/run-lanes.d.ts +4 -18
- package/dist/parallel/run-lanes.d.ts.map +1 -1
- package/dist/parallel/run-lanes.js +1 -12
- package/dist/parallel/run-lanes.js.map +1 -1
- package/dist/screenshot.d.ts +1 -12
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +1 -12
- package/dist/screenshot.js.map +1 -1
- package/dist/still.d.ts +17 -74
- package/dist/still.d.ts.map +1 -1
- package/dist/still.js +13 -50
- package/dist/still.js.map +1 -1
- package/dist/ticker.d.ts +10 -39
- package/dist/ticker.d.ts.map +1 -1
- package/dist/ticker.js +13 -45
- package/dist/ticker.js.map +1 -1
- package/package.json +4 -4
- package/dist/browser/chunks/chunk-F3AZKPHU.js +0 -2
- package/dist/browser/chunks/chunk-F3AZKPHU.js.map +0 -7
package/dist/frame-loop.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { AssetManager, AudioSink, ClipStore, AssetTimeline, isFatalRenderIssue, RenderBackendError, renderIssueKey, renderSettled, SETTLE_PASSES, setTheme, toRenderIssue, spansCovering, StateEvaluator, yieldToScheduler, } from "@motionscript/core";
|
|
2
|
-
/**
|
|
3
|
-
* Audio is mixed offline at the end of a run (or irrelevant, for a still), so the
|
|
4
|
-
* `AssetManager` just needs an `AudioSink`-shaped sink that does nothing during
|
|
5
|
-
* the per-frame pass.
|
|
6
|
-
*/
|
|
2
|
+
/** Audio is mixed offline at the end, so the per-frame pass needs an `AudioSink` that does nothing. */
|
|
7
3
|
export class NoopAudioSink extends AudioSink {
|
|
8
4
|
/** Nothing here records a request, so `AssetManager` can skip the whole sync. */
|
|
9
5
|
get schedulesAudio() { return false; }
|
|
@@ -15,10 +11,7 @@ export class NoopAudioSink extends AudioSink {
|
|
|
15
11
|
play(_time, _speed, _reverse) { }
|
|
16
12
|
stop() { }
|
|
17
13
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Hand the context's issues since the last call to `onIssue`, once per key in
|
|
20
|
-
* `seen`, and throw on one that means the frames drawn are not to be trusted.
|
|
21
|
-
*/
|
|
14
|
+
/** Hands the context's issues since the last call to `onIssue`, once per key in `seen`; throws on one that makes the drawn frames untrustworthy. */
|
|
22
15
|
function drainIssues(renderContext, seen, onIssue) {
|
|
23
16
|
for (const raw of renderContext.takeRenderIssues()) {
|
|
24
17
|
const issue = toRenderIssue(raw);
|
|
@@ -38,11 +31,7 @@ async function renderWarmFrame({ renderContext, storageAdapter }, draw, frame) {
|
|
|
38
31
|
console.warn(`[export] frame ${frame} still had pending decodes after ${SETTLE_PASSES} renders; it may not be exact.`);
|
|
39
32
|
}
|
|
40
33
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Switch the adapter to decoding exactly what each frame draws, so a frame is
|
|
43
|
-
* the same pixels whatever was rendered on this adapter before it. Returns the
|
|
44
|
-
* restore: an adapter lent by a live preview must not come back pinned to it.
|
|
45
|
-
*/
|
|
34
|
+
/** Makes the adapter decode exactly what each frame draws, so a frame is the same whatever was rendered before. Returns the restore: a live preview's adapter must not come back pinned. */
|
|
46
35
|
function exactDecodes(storageAdapter) {
|
|
47
36
|
const previous = storageAdapter.getDecodePolicy();
|
|
48
37
|
storageAdapter.setDecodePolicy("exact");
|
|
@@ -73,10 +62,7 @@ function prepare(params) {
|
|
|
73
62
|
// have replaced it; applied before the first build, which resolves tokens.
|
|
74
63
|
const applyTheme = () => setTheme(project.theme ?? params.theme);
|
|
75
64
|
applyTheme();
|
|
76
|
-
// The render context *is* the measurer
|
|
77
|
-
// `MeasureContext2D` so a node measures and draws through one object. Named
|
|
78
|
-
// rather than cast so the compiler checks it; this package has no test suite
|
|
79
|
-
// to catch it otherwise.
|
|
65
|
+
// The render context *is* the measurer (it extends `MeasureContext2D`); named rather than cast so the compiler checks it.
|
|
80
66
|
const measurer = renderContext;
|
|
81
67
|
const scope = { registry, viewport, fps, assets: assetCatalog, measurer };
|
|
82
68
|
const loads = {};
|
|
@@ -85,11 +71,7 @@ function prepare(params) {
|
|
|
85
71
|
const stateEvaluator = new StateEvaluator(project, scope, store);
|
|
86
72
|
return { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme };
|
|
87
73
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Load the layout gates of `trackIds`, then rebuild any of them whose compile
|
|
90
|
-
* measured a face before it was resident — every build here runs before a
|
|
91
|
-
* single asset has loaded.
|
|
92
|
-
*/
|
|
74
|
+
/** Loads the layout gates of `trackIds`, then rebuilds any whose compile measured a face before it was resident (every build runs before an asset has loaded). */
|
|
93
75
|
async function settleLayoutGates(store, manager, trackIds) {
|
|
94
76
|
const gates = trackIds.flatMap((id) => store.layoutGates(id).filter((record) => !manager.isReady(record.src)));
|
|
95
77
|
if (gates.length > 0)
|
|
@@ -97,15 +79,10 @@ async function settleLayoutGates(store, manager, trackIds) {
|
|
|
97
79
|
store.settleGates(trackIds);
|
|
98
80
|
}
|
|
99
81
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* alongside its frames: the video is rendered in pieces by separate workers, and
|
|
105
|
-
* concatenating separately-encoded AAC would put the encoder's priming delay at
|
|
106
|
-
* every join. The coordinator mixes once over the whole timeline instead — and
|
|
107
|
-
* must schedule it exactly as this does. A second implementation that drifted by
|
|
108
|
-
* a track offset would produce a file that plays perfectly and is out of sync.
|
|
82
|
+
* Collects every audio request with the absolute offset it plays at ({@link ScheduledAudioRequest.globalOffset}).
|
|
83
|
+
* Exported because a **split** export mixes once over the whole timeline in the coordinator (concatenated AAC would
|
|
84
|
+
* put the encoder's priming delay at every join) and must schedule exactly as this does: a drifted copy would give
|
|
85
|
+
* a file that plays perfectly and is out of sync.
|
|
109
86
|
*/
|
|
110
87
|
export function collectAudio(precomp) {
|
|
111
88
|
const out = [];
|
|
@@ -116,13 +93,7 @@ export function collectAudio(precomp) {
|
|
|
116
93
|
}
|
|
117
94
|
return out;
|
|
118
95
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Drive every frame of `project` (or of `frames`) into `sink`, then mix audio and finalize.
|
|
121
|
-
*
|
|
122
|
-
* The caller owns the render context and its surface — this neither creates nor
|
|
123
|
-
* disposes them, because how a surface comes to exist is exactly the thing that
|
|
124
|
-
* differs between backends.
|
|
125
|
-
*/
|
|
96
|
+
/** Drives every frame of `project` (or `frames`) into `sink`, then mixes audio and finalizes. The caller owns the render context and surface; this neither creates nor disposes them. */
|
|
126
97
|
export async function renderTimeline(params) {
|
|
127
98
|
const { scale = 1, sink, mixer, onProgress, signal, includeAudio = true } = params;
|
|
128
99
|
const harness = params;
|
|
@@ -162,16 +133,10 @@ export async function renderTimeline(params) {
|
|
|
162
133
|
});
|
|
163
134
|
const frameDuration = 1 / fps;
|
|
164
135
|
let globalTime = 0;
|
|
165
|
-
// An export is
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// wait on, followed by a *second* full render of the frame once it lands (see
|
|
170
|
-
// `renderWarmFrame`). Telling the adapter we are playing turns that into one
|
|
171
|
-
// sequential decode pass per clip.
|
|
172
|
-
//
|
|
173
|
-
// Restored in the `finally` so a cancelled export doesn't leave an adapter the
|
|
174
|
-
// caller still owns believing a playhead is running.
|
|
136
|
+
// An export is a perfectly monotonic forward walk, the pattern the adapter's look-ahead decode window is built
|
|
137
|
+
// for. At the default (not playing) every video frame instead takes the random-access branch: a seek + decode
|
|
138
|
+
// plus a second full render (see `renderWarmFrame`). Restored in the `finally` so a cancelled export doesn't
|
|
139
|
+
// leave a caller-owned adapter believing a playhead is running.
|
|
175
140
|
storageAdapter.setPlaying(true);
|
|
176
141
|
try {
|
|
177
142
|
for (let f = start; f < end; f++) {
|
|
@@ -187,9 +152,7 @@ export async function renderTimeline(params) {
|
|
|
187
152
|
const done = f - start + 1;
|
|
188
153
|
// Video occupies most of the range; the audio mix and finalize are the tail.
|
|
189
154
|
onProgress?.((done / frameCount) * (wantsAudio ? 0.85 : 1));
|
|
190
|
-
// Hand the host back its event loop periodically so a long export stays
|
|
191
|
-
// interruptible. Core's yield is feature-detected and prefers a real
|
|
192
|
-
// macrotask over a clamped setTimeout, in the browser and in Node alike.
|
|
155
|
+
// Hand the host back its event loop periodically so a long export stays interruptible.
|
|
193
156
|
if ((f + 1) % 4 === 0)
|
|
194
157
|
await yieldToScheduler();
|
|
195
158
|
}
|
|
@@ -215,38 +178,19 @@ export async function renderTimeline(params) {
|
|
|
215
178
|
store.dispose();
|
|
216
179
|
}
|
|
217
180
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Every track covering {@link frame} failed to build. Drawn anyway, it would be
|
|
220
|
-
* a blank frame — the consequence, with the cause hidden — so a still and an
|
|
221
|
-
* export both refuse it.
|
|
222
|
-
*/
|
|
181
|
+
/** Every track covering {@link frame} failed to build; drawn anyway it would be a blank frame with the cause hidden, so a still and an export both refuse it. */
|
|
223
182
|
export class NothingToDrawError extends Error {
|
|
224
183
|
frame;
|
|
225
184
|
failures;
|
|
226
185
|
name = "NothingToDrawError";
|
|
227
|
-
constructor(frame,
|
|
228
|
-
/** One per track covering the frame. */
|
|
229
|
-
failures) {
|
|
186
|
+
constructor(frame, failures) {
|
|
230
187
|
const causes = failures.map((failure) => ` ${failure.trackId}: ${failure.message}`).join("\n");
|
|
231
188
|
super(`Frame ${frame} has nothing to draw — every track on it failed to build:\n${causes}`);
|
|
232
189
|
this.frame = frame;
|
|
233
190
|
this.failures = failures;
|
|
234
191
|
}
|
|
235
192
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Draw one frame of `project` into the caller's render context and leave it on
|
|
238
|
-
* the surface.
|
|
239
|
-
*
|
|
240
|
-
* Same discovery/evaluator/warm-retry path as {@link renderTimeline}, so a still
|
|
241
|
-
* is the same render the video would produce at that frame — and only the tracks
|
|
242
|
-
* on that frame are discovered, since a track's span is known without it.
|
|
243
|
-
*
|
|
244
|
-
* The drawing is shared while what happens to the frame varies: a still encodes
|
|
245
|
-
* it, a live preview leaves it on a visible canvas. The context is the
|
|
246
|
-
* caller's throughout — this neither creates, resizes nor disposes it — so a host
|
|
247
|
-
* holding one surface can call this repeatedly and keep both the surface and the
|
|
248
|
-
* storage adapter's decode cache warm.
|
|
249
|
-
*/
|
|
193
|
+
/** Draws one frame into the caller's render context and leaves it on the surface, via the same path as {@link renderTimeline} so a still equals the video's frame. Only tracks on that frame are discovered. The context stays the caller's (never created, resized or disposed), so a host can call this repeatedly and keep the decode cache warm. */
|
|
250
194
|
export async function drawFrameAt(params) {
|
|
251
195
|
const { scale = 1, frame, project } = params;
|
|
252
196
|
const harness = params;
|
|
@@ -276,18 +220,12 @@ export async function drawFrameAt(params) {
|
|
|
276
220
|
}
|
|
277
221
|
finally {
|
|
278
222
|
restorePolicy();
|
|
279
|
-
// The pass flushed the surface
|
|
280
|
-
// tree being torn down here. In `finally` so a throw mid-draw doesn't leak
|
|
281
|
-
// a live scene graph.
|
|
223
|
+
// The pass flushed the surface, so the pixels survive teardown; in `finally` so a mid-draw throw doesn't leak a scene graph.
|
|
282
224
|
stateEvaluator.dispose();
|
|
283
225
|
store.dispose();
|
|
284
226
|
}
|
|
285
227
|
}
|
|
286
|
-
/**
|
|
287
|
-
* Throw a {@link NothingToDrawError} for the first frame in `first..last` whose
|
|
288
|
-
* every track failed to build. Only a build that threw counts: `buildErrors` also
|
|
289
|
-
* carries a node's failed declaration, and a track with one still draws.
|
|
290
|
-
*/
|
|
228
|
+
/** Throws {@link NothingToDrawError} for the first frame in `first..last` whose every track failed to build. Only a build that threw counts: `buildErrors` also carries a node's failed declaration, and such a track still draws. */
|
|
291
229
|
function assertDrawable(store, precomp, first, last = first) {
|
|
292
230
|
const failed = new Map(precomp.buildErrors
|
|
293
231
|
.filter((error) => store.failed(error.trackId))
|
package/dist/frame-loop.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frame-loop.js","sourceRoot":"","sources":["../src/frame-loop.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AssetCatalog, AssetManager, AudioSink, type AudioRequest, type BuildError, type CanvasRenderContext2D, ClipStore, MeasureContext2D,\n AssetTimeline, type AssetTimelineResult, type FrameRange, type ProjectDocument, type Registry,\n isFatalRenderIssue, RenderBackendError, type RenderIssue, renderIssueKey, renderSettled, SETTLE_PASSES, setTheme, toRenderIssue,\n type FrameSpec, type Size2D, spansCovering, StateEvaluator, type StorageAdapter, type Theme, yieldToScheduler,\n} from \"@motionscript/core\";\n\n/**\n * Rendering a timeline is portable; *encoding* it is not.\n *\n * Everything about driving the frames — discovery, state evaluation, asset\n * windows, the bounded warm-and-re-render retry that makes a frame accurate,\n * audio scheduling, progress, cancellation — is the same work whatever the\n * output format. Only the codec differs, so it is injected as a `VideoFrameSink`\n * (exporter.ts: mediabunny over WebCodecs).\n */\n\n/** Image formats a still can be encoded to. */\nexport type ScreenshotFormat = \"png\" | \"jpeg\";\n\nexport type { FrameSpec };\n\n/** One scheduled audio request, with the absolute time its track starts at. */\nexport interface ScheduledAudioRequest {\n request: AudioRequest;\n /**\n * Seconds to add to the request's own times, which are clip-relative: its\n * track's start. Getting it wrong produces an export that looks right and\n * drifts out of sync, which no pixel comparison catches.\n */\n globalOffset: number;\n}\n\n/**\n * Where finished frames go.\n *\n * `TAudio` ties the sink to its {@link AudioMixer}: what a mixer produces is\n * platform-specific (a Web Audio `AudioBuffer`, interleaved PCM, a temp file) and\n * only its own sink consumes it, so this module relays it without naming it.\n */\nexport interface VideoFrameSink<TAudio = unknown> {\n /**\n * Called once before the first frame, with the resolved output geometry.\n *\n * `hasAudio` is here rather than left for the sink to infer because a muxer\n * generally has to declare its tracks before writing any, and whether the\n * timeline schedules any audio is only known after discovery — which happens\n * inside the orchestrator, after the sink was constructed.\n */\n start(info: {\n width: number;\n height: number;\n fps: number;\n totalFrames: number;\n hasAudio: boolean;\n }): Promise<void>;\n\n /**\n * One frame has been drawn and the surface flushed. Times are in seconds.\n *\n * **Deliberately does not receive pixels.** A browser sink wraps the canvas\n * the renderer already drew into and lets WebCodecs pull from it directly, with no\n * readback; handing a buffer here instead would force a full GPU→CPU\n * `readPixels` on every frame of every export. A sink that *does* need bytes\n * (ffmpeg over stdin) reads them itself, because it holds the render target it\n * was constructed with. Do not \"simplify\" this signature.\n */\n addFrame(timestamp: number, duration: number): Promise<void>;\n\n /** Mixed audio for the whole timeline, if any was produced. */\n addAudio?(mixed: TAudio): Promise<void>;\n\n /** Finish the container. Returns encoded bytes, or nothing if the sink delivered them itself. */\n finalize(): Promise<Uint8Array | void>;\n}\n\n/** Mixes every scheduled request into one timeline-length buffer. */\nexport interface AudioMixer<TAudio = unknown> {\n mix(requests: readonly ScheduledAudioRequest[], totalDuration: number): Promise<TAudio | null>;\n}\n\n/**\n * Audio is mixed offline at the end of a run (or irrelevant, for a still), so the\n * `AssetManager` just needs an `AudioSink`-shaped sink that does nothing during\n * the per-frame pass.\n */\nexport class NoopAudioSink extends AudioSink {\n /** Nothing here records a request, so `AssetManager` can skip the whole sync. */\n override get schedulesAudio(): boolean { return false; }\n\n has(_src: string): boolean { return true; }\n async append(_src: string, _data: ArrayBuffer): Promise<void> { }\n retain(_keep: ReadonlySet<string>): void { }\n schedule(_requests: readonly AudioRequest[]): void { }\n syncTo(_clipTime: number): void { }\n play(_time: number, _speed: number, _reverse: boolean): void { }\n stop(): void { }\n}\n\n/** The platform pieces both entry points need. */\n/**\n * What the frame loop draws with. Any backend's context and adapter: the loop\n * uses only core's base members, so another renderer exports through it too.\n */\ninterface RenderHarness {\n renderContext: CanvasRenderContext2D & { pixelRatio: number };\n /** The same adapter the context was built with. */\n storageAdapter: StorageAdapter;\n assetCatalog: AssetCatalog;\n}\n\n/** Fields shared by the video and still entry points. */\ninterface CommonParams extends RenderHarness {\n /** Its fps and viewport resolved — see `Session`. */\n project: ProjectDocument;\n /** The vocabulary its clips compile against. */\n registry: Registry;\n /** Theme for a project that carries none of its own. */\n theme?: Theme;\n /**\n * What the backend drew but not as the document says (a feature it lacks,\n * a shader that does not compile, a GPU error), once per kind, feature and\n * node. A lost device or a crashed backend is not reported here: the render\n * throws a {@link RenderBackendError} instead.\n */\n onIssue?: (issue: RenderIssue) => void;\n}\n\n/**\n * Hand the context's issues since the last call to `onIssue`, once per key in\n * `seen`, and throw on one that means the frames drawn are not to be trusted.\n */\nfunction drainIssues(renderContext: CanvasRenderContext2D, seen: Set<string>, onIssue?: (issue: RenderIssue) => void): void {\n for (const raw of renderContext.takeRenderIssues()) {\n const issue = toRenderIssue(raw);\n if (isFatalRenderIssue(issue)) throw new RenderBackendError(issue);\n const key = renderIssueKey(issue);\n if (seen.has(key)) continue;\n seen.add(key);\n onIssue?.(issue);\n }\n}\n\n/** Draw one frame, settling and re-rendering until it is accurate. See `renderSettled`. */\nasync function renderWarmFrame(\n { renderContext, storageAdapter }: RenderHarness,\n draw: () => void,\n frame: number,\n): Promise<void> {\n const outcome = await renderSettled(() => renderContext.execute(draw), storageAdapter, () => true);\n if (outcome === \"exhausted\") {\n console.warn(`[export] frame ${frame} still had pending decodes after ${SETTLE_PASSES} renders; it may not be exact.`);\n }\n}\n\n/**\n * Switch the adapter to decoding exactly what each frame draws, so a frame is\n * the same pixels whatever was rendered on this adapter before it. Returns the\n * restore: an adapter lent by a live preview must not come back pinned to it.\n */\nfunction exactDecodes(storageAdapter: StorageAdapter): () => void {\n const previous = storageAdapter.getDecodePolicy();\n storageAdapter.setDecodePolicy(\"exact\");\n return () => storageAdapter.setDecodePolicy(previous);\n}\n\nfunction outputOf(project: ProjectDocument): { fps: number; viewport: Size2D } {\n const { fps, viewport } = project;\n if (fps === undefined || viewport === undefined) {\n throw new Error(\"Rendering takes a project whose fps and viewport are resolved — see Session.\");\n }\n return { fps, viewport };\n}\n\n/** `frames`, or the whole timeline; a range that is empty or leaves the timeline is refused rather than clamped. */\nfunction checkedRange(frames: FrameRange | undefined, totalFrames: number): FrameRange {\n if (!frames) return { start: 0, end: totalFrames };\n const { start, end } = frames;\n if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start || end > totalFrames) {\n throw new RangeError(`Frames ${start} to ${end} are not a range inside this timeline's ${totalFrames} frames.`);\n }\n return { start, end };\n}\n\n/** Build the discovery + evaluator pair both entry points drive, over one store so each track builds once. */\nfunction prepare(params: CommonParams) {\n const { project, registry, assetCatalog, renderContext } = params;\n const { fps, viewport } = outputOf(project);\n // The theme is process-global, so anything else drawing in this page may\n // have replaced it; applied before the first build, which resolves tokens.\n const applyTheme = () => setTheme(project.theme ?? params.theme);\n applyTheme();\n // The render context *is* the measurer — `RenderContext2D` extends\n // `MeasureContext2D` so a node measures and draws through one object. Named\n // rather than cast so the compiler checks it; this package has no test suite\n // to catch it otherwise.\n const measurer: MeasureContext2D = renderContext;\n const scope = { registry, viewport, fps, assets: assetCatalog, measurer };\n const loads: { manager?: AssetManager } = {};\n const store = new ClipStore(project, scope, { isReady: (key) => loads.manager?.isReady(key) ?? false });\n const precomper = new AssetTimeline(project, scope, { store });\n const stateEvaluator = new StateEvaluator(project, scope, store);\n return { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme };\n}\n\n/**\n * Load the layout gates of `trackIds`, then rebuild any of them whose compile\n * measured a face before it was resident — every build here runs before a\n * single asset has loaded.\n */\nasync function settleLayoutGates(store: ClipStore, manager: AssetManager, trackIds: readonly string[]): Promise<void> {\n const gates = trackIds.flatMap((id) => store.layoutGates(id).filter((record) => !manager.isReady(record.src)));\n if (gates.length > 0) await manager.loadRecords(gates);\n store.settleGates(trackIds);\n}\n\n/**\n * Collect every audio request with the absolute offset it plays at. See\n * {@link ScheduledAudioRequest.globalOffset}.\n *\n * Exported because a **split** export has to mix its audio somewhere other than\n * alongside its frames: the video is rendered in pieces by separate workers, and\n * concatenating separately-encoded AAC would put the encoder's priming delay at\n * every join. The coordinator mixes once over the whole timeline instead — and\n * must schedule it exactly as this does. A second implementation that drifted by\n * a track offset would produce a file that plays perfectly and is out of sync.\n */\nexport function collectAudio(precomp: AssetTimelineResult): ScheduledAudioRequest[] {\n const out: ScheduledAudioRequest[] = [];\n for (const track of precomp.tracks) {\n const globalOffset = track.startFrame / precomp.fps;\n for (const request of track.audioRequests) out.push({ request, globalOffset });\n }\n return out;\n}\n\nexport interface RenderTimelineParams<TAudio> extends CommonParams {\n scale?: number;\n sink: VideoFrameSink<TAudio>;\n mixer?: AudioMixer<TAudio>;\n /**\n * Set false to render a silent file: no mix, and no audio track declared on\n * the container at all.\n *\n * Distinct from muting. The audio is still discovered — the pass that finds\n * it is the one that finds the frames' assets — it simply is not encoded.\n */\n includeAudio?: boolean;\n /**\n * Render only these frames, timestamped from 0, with no audio: one segment of a\n * split export. Only the tracks they cover are discovered.\n */\n frames?: FrameRange;\n onProgress?: (progress: number) => void;\n signal?: AbortSignal;\n}\n\n/**\n * Drive every frame of `project` (or of `frames`) into `sink`, then mix audio and finalize.\n *\n * The caller owns the render context and its surface — this neither creates nor\n * disposes them, because how a surface comes to exist is exactly the thing that\n * differs between backends.\n */\nexport async function renderTimeline<TAudio>(\n params: RenderTimelineParams<TAudio>,\n): Promise<Uint8Array | void> {\n const { scale = 1, sink, mixer, onProgress, signal, includeAudio = true } = params;\n const harness: RenderHarness = params;\n const { renderContext, storageAdapter } = harness;\n\n if (params.project.tracks.length === 0) return;\n signal?.throwIfAborted();\n\n renderContext.pixelRatio = scale;\n const { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme } = prepare(params);\n const restorePolicy = exactDecodes(storageAdapter);\n const seen = new Set<string>();\n try {\n // Not this render's: a device lost before it started was recovered from before it draws.\n renderContext.takeRenderIssues();\n const { start, end } = checkedRange(params.frames, precomper.result().totalFrames);\n const trackIds = params.frames\n ? store.spans.filter((span) => span.startFrame < end && span.endFrame >= start).map((span) => span.id)\n : params.project.tracks.map((track) => track.id);\n if (params.frames) for (const id of trackIds) store.discover(id);\n const precomp = params.frames ? precomper.result() : precomper.run();\n const { totalDuration } = precomp;\n const frameCount = end - start;\n // Before the sink starts, so a refused export encodes nothing.\n assertDrawable(store, precomp, start, end - 1);\n const assetManager = loads.manager = new AssetManager(precomp, storageAdapter, new NoopAudioSink());\n await settleLayoutGates(store, assetManager, trackIds);\n const scheduled = collectAudio(precomp);\n const wantsAudio = includeAudio && !params.frames && scheduled.length > 0 && mixer !== undefined;\n\n await sink.start({\n width: viewport.width * scale,\n height: viewport.height * scale,\n fps,\n totalFrames: frameCount,\n hasAudio: wantsAudio,\n });\n\n const frameDuration = 1 / fps;\n let globalTime = 0;\n // An export is the most perfectly monotonic forward walk of a timeline there\n // is — exactly the access pattern an adapter's look-ahead decode window is\n // built for. Left at the default (`false`, i.e. \"paused\") every video frame\n // instead takes the random-access branch: a seek + decode the render has to\n // wait on, followed by a *second* full render of the frame once it lands (see\n // `renderWarmFrame`). Telling the adapter we are playing turns that into one\n // sequential decode pass per clip.\n //\n // Restored in the `finally` so a cancelled export doesn't leave an adapter the\n // caller still owns believing a playhead is running.\n storageAdapter.setPlaying(true);\n try {\n for (let f = start; f < end; f++) {\n signal?.throwIfAborted();\n\n await assetManager.loadAt(f);\n applyTheme();\n stateEvaluator.stateAt(f);\n stateEvaluator.layout(measurer);\n await renderWarmFrame(harness, () => stateEvaluator.render(renderContext), f);\n drainIssues(renderContext, seen, params.onIssue);\n\n await sink.addFrame(globalTime, frameDuration);\n globalTime += frameDuration;\n const done = f - start + 1;\n // Video occupies most of the range; the audio mix and finalize are the tail.\n onProgress?.((done / frameCount) * (wantsAudio ? 0.85 : 1));\n\n // Hand the host back its event loop periodically so a long export stays\n // interruptible. Core's yield is feature-detected and prefers a real\n // macrotask over a clamped setTimeout, in the browser and in Node alike.\n if ((f + 1) % 4 === 0) await yieldToScheduler();\n }\n } finally {\n storageAdapter.setPlaying(false);\n }\n\n if (wantsAudio) {\n const mixed = await mixer!.mix(scheduled, totalDuration);\n if (mixed !== null && sink.addAudio) await sink.addAudio(mixed);\n onProgress?.(0.97);\n }\n\n // The last frame's device may have been lost after it was drawn.\n drainIssues(renderContext, seen, params.onIssue);\n const bytes = await sink.finalize();\n onProgress?.(1);\n return bytes;\n } finally {\n restorePolicy();\n stateEvaluator.dispose();\n store.dispose();\n }\n}\n\nexport interface DrawFrameParams extends CommonParams {\n scale?: number;\n frame: FrameSpec;\n}\n\nexport interface DrawnFrame {\n /** The frame actually drawn, after clamping the request into range. */\n frame: number;\n totalFrames: number;\n}\n\n/**\n * Every track covering {@link frame} failed to build. Drawn anyway, it would be\n * a blank frame — the consequence, with the cause hidden — so a still and an\n * export both refuse it.\n */\nexport class NothingToDrawError extends Error {\n override readonly name = \"NothingToDrawError\";\n\n constructor(\n readonly frame: number,\n /** One per track covering the frame. */\n readonly failures: readonly BuildError[],\n ) {\n const causes = failures.map((failure) => ` ${failure.trackId}: ${failure.message}`).join(\"\\n\");\n super(`Frame ${frame} has nothing to draw — every track on it failed to build:\\n${causes}`);\n }\n}\n\n/**\n * Draw one frame of `project` into the caller's render context and leave it on\n * the surface.\n *\n * Same discovery/evaluator/warm-retry path as {@link renderTimeline}, so a still\n * is the same render the video would produce at that frame — and only the tracks\n * on that frame are discovered, since a track's span is known without it.\n *\n * The drawing is shared while what happens to the frame varies: a still encodes\n * it, a live preview leaves it on a visible canvas. The context is the\n * caller's throughout — this neither creates, resizes nor disposes it — so a host\n * holding one surface can call this repeatedly and keep both the surface and the\n * storage adapter's decode cache warm.\n */\nexport async function drawFrameAt(params: DrawFrameParams): Promise<DrawnFrame> {\n const { scale = 1, frame, project } = params;\n const harness: RenderHarness = params;\n const { renderContext, storageAdapter } = harness;\n\n if (project.tracks.length === 0) throw new Error(\"The project has no tracks to render.\");\n\n renderContext.pixelRatio = scale;\n const { store, precomper, stateEvaluator, measurer, loads } = prepare(params);\n const restorePolicy = exactDecodes(storageAdapter);\n try {\n const { totalFrames } = precomper.result();\n const requested =\n frame.kind === \"first\" ? 0\n : frame.kind === \"last\" ? totalFrames - 1\n : frame.frame;\n const targetFrame = Math.max(0, Math.min(totalFrames - 1, Math.floor(requested)));\n const precomp = precomper.ensureCovering(targetFrame);\n assertDrawable(store, precomp, targetFrame);\n\n const assetManager = loads.manager = new AssetManager(precomp, storageAdapter, new NoopAudioSink());\n await settleLayoutGates(store, assetManager, spansCovering(store.spans, targetFrame).map((span) => span.id));\n await assetManager.loadAt(targetFrame);\n stateEvaluator.stateAt(targetFrame);\n stateEvaluator.layout(measurer);\n renderContext.takeRenderIssues();\n await renderWarmFrame(harness, () => stateEvaluator.render(renderContext), targetFrame);\n drainIssues(renderContext, new Set(), params.onIssue);\n\n return { frame: targetFrame, totalFrames };\n } finally {\n restorePolicy();\n // The pass flushed the surface before returning, so the pixels survive the\n // tree being torn down here. In `finally` so a throw mid-draw doesn't leak\n // a live scene graph.\n stateEvaluator.dispose();\n store.dispose();\n }\n}\n\n/**\n * Throw a {@link NothingToDrawError} for the first frame in `first..last` whose\n * every track failed to build. Only a build that threw counts: `buildErrors` also\n * carries a node's failed declaration, and a track with one still draws.\n */\nfunction assertDrawable(store: ClipStore, precomp: AssetTimelineResult, first: number, last = first): void {\n const failed = new Map(precomp.buildErrors\n .filter((error) => store.failed(error.trackId))\n .map((error) => [error.trackId, error]));\n if (failed.size === 0) return;\n for (let frame = first; frame <= last; frame++) {\n const covering = spansCovering(store.spans, frame);\n if (covering.length > 0 && covering.every((span) => failed.has(span.id))) {\n throw new NothingToDrawError(frame, covering.map((span) => failed.get(span.id)!));\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"frame-loop.js","sourceRoot":"","sources":["../src/frame-loop.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AssetCatalog, AssetManager, AudioSink, type AudioRequest, type BuildError, type CanvasRenderContext2D, ClipStore, MeasureContext2D,\n AssetTimeline, type AssetTimelineResult, type FrameRange, type ProjectDocument, type Registry,\n isFatalRenderIssue, RenderBackendError, type RenderIssue, renderIssueKey, renderSettled, SETTLE_PASSES, setTheme, toRenderIssue,\n type FrameSpec, type Size2D, spansCovering, StateEvaluator, type StorageAdapter, type Theme, yieldToScheduler,\n} from \"@motionscript/core\";\n\n/** Rendering a timeline is portable; encoding is not, so the codec is injected as a `VideoFrameSink` (exporter.ts: mediabunny over WebCodecs). */\n\nexport type ScreenshotFormat = \"png\" | \"jpeg\";\n\nexport type { FrameSpec };\n\nexport interface ScheduledAudioRequest {\n request: AudioRequest;\n /** Seconds added to the request's clip-relative times (its track's start). A wrong value exports in apparent sync and drifts, which no pixel comparison catches. */\n globalOffset: number;\n}\n\n/** Where finished frames go. `TAudio` ties the sink to its {@link AudioMixer}: the mixed form is platform-specific and only its own sink consumes it. */\nexport interface VideoFrameSink<TAudio = unknown> {\n /** Called once before the first frame. `hasAudio` is passed because a muxer declares its tracks before writing, and whether any audio is scheduled is only known after discovery, which runs after the sink is built. */\n start(info: {\n width: number;\n height: number;\n fps: number;\n totalFrames: number;\n hasAudio: boolean;\n }): Promise<void>;\n\n /**\n * One frame has been drawn and the surface flushed. **Deliberately does not receive pixels**: a browser sink lets\n * WebCodecs pull from the canvas, while a buffer here would force a GPU->CPU `readPixels` on every frame. A sink\n * that needs bytes (ffmpeg over stdin) reads them from its own render target. Do not \"simplify\" this signature.\n */\n addFrame(timestamp: number, duration: number): Promise<void>;\n\n /** Mixed audio for the whole timeline, if any was produced. */\n addAudio?(mixed: TAudio): Promise<void>;\n\n /** Finish the container. Returns encoded bytes, or nothing if the sink delivered them itself. */\n finalize(): Promise<Uint8Array | void>;\n}\n\n/** Mixes every scheduled request into one timeline-length buffer. */\nexport interface AudioMixer<TAudio = unknown> {\n mix(requests: readonly ScheduledAudioRequest[], totalDuration: number): Promise<TAudio | null>;\n}\n\n/** Audio is mixed offline at the end, so the per-frame pass needs an `AudioSink` that does nothing. */\nexport class NoopAudioSink extends AudioSink {\n /** Nothing here records a request, so `AssetManager` can skip the whole sync. */\n override get schedulesAudio(): boolean { return false; }\n\n has(_src: string): boolean { return true; }\n async append(_src: string, _data: ArrayBuffer): Promise<void> { }\n retain(_keep: ReadonlySet<string>): void { }\n schedule(_requests: readonly AudioRequest[]): void { }\n syncTo(_clipTime: number): void { }\n play(_time: number, _speed: number, _reverse: boolean): void { }\n stop(): void { }\n}\n\n/** What the frame loop draws with: any backend's context and adapter (the loop uses only core's base members). */\ninterface RenderHarness {\n renderContext: CanvasRenderContext2D & { pixelRatio: number };\n storageAdapter: StorageAdapter;\n assetCatalog: AssetCatalog;\n}\n\ninterface CommonParams extends RenderHarness {\n /** Its fps and viewport resolved — see `Session`. */\n project: ProjectDocument;\n registry: Registry;\n /** Theme for a project that carries none of its own. */\n theme?: Theme;\n /** What the backend drew but not as the document says, once per kind, feature and node. A lost device or crashed backend throws a {@link RenderBackendError} instead. */\n onIssue?: (issue: RenderIssue) => void;\n}\n\n/** Hands the context's issues since the last call to `onIssue`, once per key in `seen`; throws on one that makes the drawn frames untrustworthy. */\nfunction drainIssues(renderContext: CanvasRenderContext2D, seen: Set<string>, onIssue?: (issue: RenderIssue) => void): void {\n for (const raw of renderContext.takeRenderIssues()) {\n const issue = toRenderIssue(raw);\n if (isFatalRenderIssue(issue)) throw new RenderBackendError(issue);\n const key = renderIssueKey(issue);\n if (seen.has(key)) continue;\n seen.add(key);\n onIssue?.(issue);\n }\n}\n\n/** Draw one frame, settling and re-rendering until it is accurate. See `renderSettled`. */\nasync function renderWarmFrame(\n { renderContext, storageAdapter }: RenderHarness,\n draw: () => void,\n frame: number,\n): Promise<void> {\n const outcome = await renderSettled(() => renderContext.execute(draw), storageAdapter, () => true);\n if (outcome === \"exhausted\") {\n console.warn(`[export] frame ${frame} still had pending decodes after ${SETTLE_PASSES} renders; it may not be exact.`);\n }\n}\n\n/** Makes the adapter decode exactly what each frame draws, so a frame is the same whatever was rendered before. Returns the restore: a live preview's adapter must not come back pinned. */\nfunction exactDecodes(storageAdapter: StorageAdapter): () => void {\n const previous = storageAdapter.getDecodePolicy();\n storageAdapter.setDecodePolicy(\"exact\");\n return () => storageAdapter.setDecodePolicy(previous);\n}\n\nfunction outputOf(project: ProjectDocument): { fps: number; viewport: Size2D } {\n const { fps, viewport } = project;\n if (fps === undefined || viewport === undefined) {\n throw new Error(\"Rendering takes a project whose fps and viewport are resolved — see Session.\");\n }\n return { fps, viewport };\n}\n\n/** `frames`, or the whole timeline; a range that is empty or leaves the timeline is refused rather than clamped. */\nfunction checkedRange(frames: FrameRange | undefined, totalFrames: number): FrameRange {\n if (!frames) return { start: 0, end: totalFrames };\n const { start, end } = frames;\n if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start || end > totalFrames) {\n throw new RangeError(`Frames ${start} to ${end} are not a range inside this timeline's ${totalFrames} frames.`);\n }\n return { start, end };\n}\n\n/** Build the discovery + evaluator pair both entry points drive, over one store so each track builds once. */\nfunction prepare(params: CommonParams) {\n const { project, registry, assetCatalog, renderContext } = params;\n const { fps, viewport } = outputOf(project);\n // The theme is process-global, so anything else drawing in this page may\n // have replaced it; applied before the first build, which resolves tokens.\n const applyTheme = () => setTheme(project.theme ?? params.theme);\n applyTheme();\n // The render context *is* the measurer (it extends `MeasureContext2D`); named rather than cast so the compiler checks it.\n const measurer: MeasureContext2D = renderContext;\n const scope = { registry, viewport, fps, assets: assetCatalog, measurer };\n const loads: { manager?: AssetManager } = {};\n const store = new ClipStore(project, scope, { isReady: (key) => loads.manager?.isReady(key) ?? false });\n const precomper = new AssetTimeline(project, scope, { store });\n const stateEvaluator = new StateEvaluator(project, scope, store);\n return { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme };\n}\n\n/** Loads the layout gates of `trackIds`, then rebuilds any whose compile measured a face before it was resident (every build runs before an asset has loaded). */\nasync function settleLayoutGates(store: ClipStore, manager: AssetManager, trackIds: readonly string[]): Promise<void> {\n const gates = trackIds.flatMap((id) => store.layoutGates(id).filter((record) => !manager.isReady(record.src)));\n if (gates.length > 0) await manager.loadRecords(gates);\n store.settleGates(trackIds);\n}\n\n/**\n * Collects every audio request with the absolute offset it plays at ({@link ScheduledAudioRequest.globalOffset}).\n * Exported because a **split** export mixes once over the whole timeline in the coordinator (concatenated AAC would\n * put the encoder's priming delay at every join) and must schedule exactly as this does: a drifted copy would give\n * a file that plays perfectly and is out of sync.\n */\nexport function collectAudio(precomp: AssetTimelineResult): ScheduledAudioRequest[] {\n const out: ScheduledAudioRequest[] = [];\n for (const track of precomp.tracks) {\n const globalOffset = track.startFrame / precomp.fps;\n for (const request of track.audioRequests) out.push({ request, globalOffset });\n }\n return out;\n}\n\nexport interface RenderTimelineParams<TAudio> extends CommonParams {\n scale?: number;\n sink: VideoFrameSink<TAudio>;\n mixer?: AudioMixer<TAudio>;\n /** Set false for a silent file: no mix and no audio track. Not muting: audio is still discovered (the same pass finds the frames' assets), just not encoded. */\n includeAudio?: boolean;\n /**\n * Render only these frames, timestamped from 0, with no audio: one segment of a\n * split export. Only the tracks they cover are discovered.\n */\n frames?: FrameRange;\n onProgress?: (progress: number) => void;\n signal?: AbortSignal;\n}\n\n/** Drives every frame of `project` (or `frames`) into `sink`, then mixes audio and finalizes. The caller owns the render context and surface; this neither creates nor disposes them. */\nexport async function renderTimeline<TAudio>(\n params: RenderTimelineParams<TAudio>,\n): Promise<Uint8Array | void> {\n const { scale = 1, sink, mixer, onProgress, signal, includeAudio = true } = params;\n const harness: RenderHarness = params;\n const { renderContext, storageAdapter } = harness;\n\n if (params.project.tracks.length === 0) return;\n signal?.throwIfAborted();\n\n renderContext.pixelRatio = scale;\n const { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme } = prepare(params);\n const restorePolicy = exactDecodes(storageAdapter);\n const seen = new Set<string>();\n try {\n // Not this render's: a device lost before it started was recovered from before it draws.\n renderContext.takeRenderIssues();\n const { start, end } = checkedRange(params.frames, precomper.result().totalFrames);\n const trackIds = params.frames\n ? store.spans.filter((span) => span.startFrame < end && span.endFrame >= start).map((span) => span.id)\n : params.project.tracks.map((track) => track.id);\n if (params.frames) for (const id of trackIds) store.discover(id);\n const precomp = params.frames ? precomper.result() : precomper.run();\n const { totalDuration } = precomp;\n const frameCount = end - start;\n // Before the sink starts, so a refused export encodes nothing.\n assertDrawable(store, precomp, start, end - 1);\n const assetManager = loads.manager = new AssetManager(precomp, storageAdapter, new NoopAudioSink());\n await settleLayoutGates(store, assetManager, trackIds);\n const scheduled = collectAudio(precomp);\n const wantsAudio = includeAudio && !params.frames && scheduled.length > 0 && mixer !== undefined;\n\n await sink.start({\n width: viewport.width * scale,\n height: viewport.height * scale,\n fps,\n totalFrames: frameCount,\n hasAudio: wantsAudio,\n });\n\n const frameDuration = 1 / fps;\n let globalTime = 0;\n // An export is a perfectly monotonic forward walk, the pattern the adapter's look-ahead decode window is built\n // for. At the default (not playing) every video frame instead takes the random-access branch: a seek + decode\n // plus a second full render (see `renderWarmFrame`). Restored in the `finally` so a cancelled export doesn't\n // leave a caller-owned adapter believing a playhead is running.\n storageAdapter.setPlaying(true);\n try {\n for (let f = start; f < end; f++) {\n signal?.throwIfAborted();\n\n await assetManager.loadAt(f);\n applyTheme();\n stateEvaluator.stateAt(f);\n stateEvaluator.layout(measurer);\n await renderWarmFrame(harness, () => stateEvaluator.render(renderContext), f);\n drainIssues(renderContext, seen, params.onIssue);\n\n await sink.addFrame(globalTime, frameDuration);\n globalTime += frameDuration;\n const done = f - start + 1;\n // Video occupies most of the range; the audio mix and finalize are the tail.\n onProgress?.((done / frameCount) * (wantsAudio ? 0.85 : 1));\n\n // Hand the host back its event loop periodically so a long export stays interruptible.\n if ((f + 1) % 4 === 0) await yieldToScheduler();\n }\n } finally {\n storageAdapter.setPlaying(false);\n }\n\n if (wantsAudio) {\n const mixed = await mixer!.mix(scheduled, totalDuration);\n if (mixed !== null && sink.addAudio) await sink.addAudio(mixed);\n onProgress?.(0.97);\n }\n\n // The last frame's device may have been lost after it was drawn.\n drainIssues(renderContext, seen, params.onIssue);\n const bytes = await sink.finalize();\n onProgress?.(1);\n return bytes;\n } finally {\n restorePolicy();\n stateEvaluator.dispose();\n store.dispose();\n }\n}\n\nexport interface DrawFrameParams extends CommonParams {\n scale?: number;\n frame: FrameSpec;\n}\n\nexport interface DrawnFrame {\n frame: number;\n totalFrames: number;\n}\n\n/** Every track covering {@link frame} failed to build; drawn anyway it would be a blank frame with the cause hidden, so a still and an export both refuse it. */\nexport class NothingToDrawError extends Error {\n override readonly name = \"NothingToDrawError\";\n\n constructor(\n readonly frame: number,\n readonly failures: readonly BuildError[],\n ) {\n const causes = failures.map((failure) => ` ${failure.trackId}: ${failure.message}`).join(\"\\n\");\n super(`Frame ${frame} has nothing to draw — every track on it failed to build:\\n${causes}`);\n }\n}\n\n/** Draws one frame into the caller's render context and leaves it on the surface, via the same path as {@link renderTimeline} so a still equals the video's frame. Only tracks on that frame are discovered. The context stays the caller's (never created, resized or disposed), so a host can call this repeatedly and keep the decode cache warm. */\nexport async function drawFrameAt(params: DrawFrameParams): Promise<DrawnFrame> {\n const { scale = 1, frame, project } = params;\n const harness: RenderHarness = params;\n const { renderContext, storageAdapter } = harness;\n\n if (project.tracks.length === 0) throw new Error(\"The project has no tracks to render.\");\n\n renderContext.pixelRatio = scale;\n const { store, precomper, stateEvaluator, measurer, loads } = prepare(params);\n const restorePolicy = exactDecodes(storageAdapter);\n try {\n const { totalFrames } = precomper.result();\n const requested =\n frame.kind === \"first\" ? 0\n : frame.kind === \"last\" ? totalFrames - 1\n : frame.frame;\n const targetFrame = Math.max(0, Math.min(totalFrames - 1, Math.floor(requested)));\n const precomp = precomper.ensureCovering(targetFrame);\n assertDrawable(store, precomp, targetFrame);\n\n const assetManager = loads.manager = new AssetManager(precomp, storageAdapter, new NoopAudioSink());\n await settleLayoutGates(store, assetManager, spansCovering(store.spans, targetFrame).map((span) => span.id));\n await assetManager.loadAt(targetFrame);\n stateEvaluator.stateAt(targetFrame);\n stateEvaluator.layout(measurer);\n renderContext.takeRenderIssues();\n await renderWarmFrame(harness, () => stateEvaluator.render(renderContext), targetFrame);\n drainIssues(renderContext, new Set(), params.onIssue);\n\n return { frame: targetFrame, totalFrames };\n } finally {\n restorePolicy();\n // The pass flushed the surface, so the pixels survive teardown; in `finally` so a mid-draw throw doesn't leak a scene graph.\n stateEvaluator.dispose();\n store.dispose();\n }\n}\n\n/** Throws {@link NothingToDrawError} for the first frame in `first..last` whose every track failed to build. Only a build that threw counts: `buildErrors` also carries a node's failed declaration, and such a track still draws. */\nfunction assertDrawable(store: ClipStore, precomp: AssetTimelineResult, first: number, last = first): void {\n const failed = new Map(precomp.buildErrors\n .filter((error) => store.failed(error.trackId))\n .map((error) => [error.trackId, error]));\n if (failed.size === 0) return;\n for (let frame = first; frame <= last; frame++) {\n const covering = spansCovering(store.spans, frame);\n if (covering.length > 0 && covering.every((span) => failed.has(span.id))) {\n throw new NothingToDrawError(frame, covering.map((span) => failed.get(span.id)!));\n }\n }\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { exportScreenshot, type ScreenshotParams, type ScreenshotResult, type Sc
|
|
|
7
7
|
export { NothingToDrawError } from "./frame-loop";
|
|
8
8
|
export { concatVideoSegments, createSegmentJoiner, ConcatMismatchError, type ConcatOptions, type SegmentJoiner } from "./concat";
|
|
9
9
|
export { exportVideoParallel, type ParallelExportParams } from "./parallel/export-parallel";
|
|
10
|
+
export { exportVideoNative, type NativeExportParams, type NativeExportTransport } from "./native-export";
|
|
10
11
|
export { localLane, messagePortLane, serveExportLane, type MessagePortLaneOptions } from "./parallel/lanes";
|
|
11
12
|
export type { ExportLane, LaneEnvironment, LaneReport, LaneSession, SegmentSpec } from "./parallel/lane";
|
|
12
13
|
export type { ExportChunk } from "./parallel/plan";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACzF,OAAO,EACH,WAAW,EACX,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GAC3B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACH,cAAc,EACd,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,WAAW,GACnB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACH,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,KAAK,OAAO,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAErI,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAGjI,OAAO,EAAE,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAE5F,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACzG,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC5G,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACzG,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAErF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC7D,YAAY,EACR,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,QAAQ,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC/C,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAG7E,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACnH,YAAY,EACR,SAAS,EAAE,gBAAgB,EAAE,WAAW,EAAE,kBAAkB,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,GACxG,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
// @motionscript/browser — @motionscript/core in the browser: WebGPU surfaces
|
|
2
2
|
// (through @motionscript/renderer), video export, audio playback and clock.
|
|
3
|
-
//
|
|
4
|
-
// to the network — see `asset-bytes.ts` for why it is module-level.
|
|
3
|
+
// Lets a host answer for an asset's bytes before the adapter goes to the network (see `asset-bytes.ts`).
|
|
5
4
|
export { loadAssetBytes, setAssetByteSource } from "./asset-bytes";
|
|
6
5
|
export { exportVideo, } from "./exporter";
|
|
7
6
|
// Mixes the timeline's audio and encodes it alone, without rendering a frame.
|
|
8
7
|
export { exportAudio, NoAudioError } from "./export-audio";
|
|
9
8
|
export { EXPORT_FORMATS, } from "./formats";
|
|
10
|
-
// mediabunny's resolution-aware quality levels,
|
|
11
|
-
// one for `ExportParams.video.bitrate` without taking a direct dependency on the
|
|
12
|
-
// encoder library this package happens to use.
|
|
9
|
+
// mediabunny's resolution-aware quality levels, for `ExportParams.video.bitrate` without a direct encoder dependency.
|
|
13
10
|
export { QUALITY_VERY_LOW, QUALITY_LOW, QUALITY_MEDIUM, QUALITY_HIGH, QUALITY_VERY_HIGH, } from "mediabunny";
|
|
14
11
|
export { exportScreenshot } from "./screenshot";
|
|
15
12
|
// Thrown by a still and an export alike for a frame whose every track failed to build.
|
|
16
13
|
export { NothingToDrawError } from "./frame-loop";
|
|
17
|
-
// Joins separately-encoded MP4s (or WebMs)
|
|
18
|
-
// worth rendering an export in parallel at all: the parts still have to become
|
|
19
|
-
// one file, and re-encoding to join them gives back everything the parallelism
|
|
20
|
-
// won. See `concat.ts` for the two things a caller has to guarantee.
|
|
14
|
+
// Joins separately-encoded MP4s (or WebMs) without re-encoding, which is what makes parallel export worthwhile; see `concat.ts` for the two things a caller must guarantee.
|
|
21
15
|
export { concatVideoSegments, createSegmentJoiner, ConcatMismatchError } from "./concat";
|
|
22
16
|
// One export across several renderers: chunks of frames on lanes the run admits
|
|
23
17
|
// while they add throughput, joined as they land. See `parallel/export-parallel.ts`.
|
|
24
18
|
export { exportVideoParallel } from "./parallel/export-parallel";
|
|
19
|
+
// Experimental: the same export rendered and encoded by the native sidecar (NVENC), fed a stream through a host transport. See `native-export.ts`.
|
|
20
|
+
export { exportVideoNative } from "./native-export";
|
|
25
21
|
export { localLane, messagePortLane, serveExportLane } from "./parallel/lanes";
|
|
26
|
-
// Mixes a timeline's audio without rendering a frame
|
|
27
|
-
// export, whose video arrives in separately-encoded pieces that AAC cannot be
|
|
28
|
-
// concatenated across. See `mix-timeline-audio.ts`.
|
|
22
|
+
// Mixes a timeline's audio without rendering a frame: the other half of a split export (AAC cannot be concatenated across pieces). See `mix-timeline-audio.ts`.
|
|
29
23
|
export { mixTimelineAudio } from "./mix-timeline-audio";
|
|
30
|
-
// Single frames as a first-class capability: one long-lived renderer that repaints
|
|
31
|
-
// into a canvas the host owns, for thumbnails and still previews. `exportScreenshot`
|
|
32
|
-
// above is the one-shot wrapper over it.
|
|
24
|
+
// Single frames as a first-class capability: one long-lived renderer that repaints into a host-owned canvas (thumbnails, still previews); `exportScreenshot` is its one-shot wrapper.
|
|
33
25
|
export { StillRenderer, createStillRenderer } from "./still";
|
|
34
26
|
export { WebAudioSink as WebAudioPlayer } from "./audio/player";
|
|
35
27
|
// Mixdown. Also on the `@motionscript/browser/audio` subpath, which is the
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4EAA4E;AAE5E,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4EAA4E;AAE5E,yGAAyG;AACzG,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAwB,MAAM,eAAe,CAAC;AACzF,OAAO,EACH,WAAW,GAKd,MAAM,YAAY,CAAC;AACpB,8EAA8E;AAC9E,OAAO,EAAE,WAAW,EAAE,YAAY,EAA0B,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACH,cAAc,GAKjB,MAAM,WAAW,CAAC;AACnB,sHAAsH;AACtH,OAAO,EACH,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,iBAAiB,GAEpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAuF,MAAM,cAAc,CAAC;AACrI,uFAAuF;AACvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,4KAA4K;AAC5K,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAA0C,MAAM,UAAU,CAAC;AACjI,gFAAgF;AAChF,qFAAqF;AACrF,OAAO,EAAE,mBAAmB,EAA6B,MAAM,4BAA4B,CAAC;AAC5F,mJAAmJ;AACnJ,OAAO,EAAE,iBAAiB,EAAuD,MAAM,iBAAiB,CAAC;AACzG,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAA+B,MAAM,kBAAkB,CAAC;AAG5G,gKAAgK;AAChK,OAAO,EAAE,gBAAgB,EAA+B,MAAM,sBAAsB,CAAC;AACrF,sLAAsL;AACtL,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAQ7D,OAAO,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,2EAA2E;AAC3E,iEAAiE;AACjE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAwB,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAE/C,OAAO,EAAE,sBAAsB,EAA2B,MAAM,YAAY,CAAC;AAE7E,wEAAwE;AACxE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC","sourcesContent":["// @motionscript/browser — @motionscript/core in the browser: WebGPU surfaces\n// (through @motionscript/renderer), video export, audio playback and clock.\n\n// Lets a host answer for an asset's bytes before the adapter goes to the network (see `asset-bytes.ts`).\nexport { loadAssetBytes, setAssetByteSource, type AssetByteSource } from \"./asset-bytes\";\nexport {\n exportVideo,\n type ExportParams,\n type ExportProgressCallback,\n type VideoEncoderOptions,\n type AudioEncoderOptions,\n} from \"./exporter\";\n// Mixes the timeline's audio and encodes it alone, without rendering a frame.\nexport { exportAudio, NoAudioError, type ExportAudioParams } from \"./export-audio\";\nexport {\n EXPORT_FORMATS,\n type ExportFormat,\n type ExportFormatInfo,\n type VideoFormat,\n type AudioFormat,\n} from \"./formats\";\n// mediabunny's resolution-aware quality levels, for `ExportParams.video.bitrate` without a direct encoder dependency.\nexport {\n QUALITY_VERY_LOW,\n QUALITY_LOW,\n QUALITY_MEDIUM,\n QUALITY_HIGH,\n QUALITY_VERY_HIGH,\n type Quality,\n} from \"mediabunny\";\nexport { exportScreenshot, type ScreenshotParams, type ScreenshotResult, type ScreenshotFormat, type FrameSpec } from \"./screenshot\";\n// Thrown by a still and an export alike for a frame whose every track failed to build.\nexport { NothingToDrawError } from \"./frame-loop\";\n// Joins separately-encoded MP4s (or WebMs) without re-encoding, which is what makes parallel export worthwhile; see `concat.ts` for the two things a caller must guarantee.\nexport { concatVideoSegments, createSegmentJoiner, ConcatMismatchError, type ConcatOptions, type SegmentJoiner } from \"./concat\";\n// One export across several renderers: chunks of frames on lanes the run admits\n// while they add throughput, joined as they land. See `parallel/export-parallel.ts`.\nexport { exportVideoParallel, type ParallelExportParams } from \"./parallel/export-parallel\";\n// Experimental: the same export rendered and encoded by the native sidecar (NVENC), fed a stream through a host transport. See `native-export.ts`.\nexport { exportVideoNative, type NativeExportParams, type NativeExportTransport } from \"./native-export\";\nexport { localLane, messagePortLane, serveExportLane, type MessagePortLaneOptions } from \"./parallel/lanes\";\nexport type { ExportLane, LaneEnvironment, LaneReport, LaneSession, SegmentSpec } from \"./parallel/lane\";\nexport type { ExportChunk } from \"./parallel/plan\";\n// Mixes a timeline's audio without rendering a frame: the other half of a split export (AAC cannot be concatenated across pieces). See `mix-timeline-audio.ts`.\nexport { mixTimelineAudio, type MixTimelineAudioParams } from \"./mix-timeline-audio\";\n// Single frames as a first-class capability: one long-lived renderer that repaints into a host-owned canvas (thumbnails, still previews); `exportScreenshot` is its one-shot wrapper.\nexport { StillRenderer, createStillRenderer } from \"./still\";\nexport type {\n StillRendererOptions,\n RenderStillOptions,\n EncodeStillOptions,\n StillSource,\n FrameRef,\n} from \"./still\";\nexport { WebAudioSink as WebAudioPlayer } from \"./audio/player\";\n// Mixdown. Also on the `@motionscript/browser/audio` subpath, which is the\n// import to prefer — it reaches it without loading the renderer.\nexport { mixAudio, encodeWav, type MixAudioOptions } from \"./audio/mixer\";\nexport { WebTicker } from \"./ticker\";\n\nexport { createBrowserEngine } from \"./engine\";\nexport type { BrowserEngineOptions, BrowserEncodeOptions } from \"./engine\";\nexport { WebGpuUnavailableError, type RendererWasmOption } from \"./renderer\";\n\n// The renderer's host-facing surface, so a host needs no second import.\nexport { isWgpuSupported, onWgpuEvent, ShaderPreview, validateShader, WGSL_PRELUDE } from \"@motionscript/renderer\";\nexport type {\n WgpuEvent, ShaderDiagnostic, ShaderField, ShaderPreviewInput, ShaderReport, ShaderScope, ShaderStatus,\n} from \"@motionscript/renderer\";\n"]}
|
|
@@ -3,7 +3,6 @@ import { type RendererWasmOption } from "./renderer";
|
|
|
3
3
|
export interface MixTimelineAudioParams extends RendererWasmOption {
|
|
4
4
|
/** A project, or a lone still or composition as the one track of one. */
|
|
5
5
|
document: ProjectDocument | VisualDocument;
|
|
6
|
-
/** The vocabulary its clips compile against. Defaults to what the library ships. */
|
|
7
6
|
registry?: Registry;
|
|
8
7
|
/** Win over the document's own; 1920×1080 at 60 fps when neither says. */
|
|
9
8
|
viewport?: Size2D;
|
|
@@ -13,30 +12,14 @@ export interface MixTimelineAudioParams extends RendererWasmOption {
|
|
|
13
12
|
sampleRate?: number;
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
16
|
-
*
|
|
15
|
+
* Mixes a timeline's audio without rendering a frame. A **split** export cannot mix per piece (separately-encoded AAC
|
|
16
|
+
* puts the encoder's priming delay at every join: a click and drift at each boundary), so the coordinator mixes the
|
|
17
|
+
* whole timeline once here. Returns `null` when no audio is scheduled (the common case, not a failure), so the caller
|
|
18
|
+
* muxes video only rather than declaring an empty track.
|
|
17
19
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* separately-encoded AAC puts the encoder's priming delay at every join — a click
|
|
22
|
-
* at each boundary and a track that drifts further out of sync with every one. So
|
|
23
|
-
* the coordinator mixes the whole timeline once, here, and the joined file gets a
|
|
24
|
-
* single continuous audio track.
|
|
25
|
-
*
|
|
26
|
-
* Returns `null` when the timeline schedules no audio at all, which is the common
|
|
27
|
-
* case and not a failure — the caller should then mux video only rather than
|
|
28
|
-
* declare an empty track.
|
|
29
|
-
*
|
|
30
|
-
* ## Why this discovers
|
|
31
|
-
*
|
|
32
|
-
* Audio requests are an *output of discovery*: each node declares its clips as it
|
|
33
|
-
* enters the tree, and the pass that finds what a clip's frames draw is the one
|
|
34
|
-
* that puts its nodes in the tree, so there is no way to know what plays when
|
|
35
|
-
* without it.
|
|
36
|
-
*
|
|
37
|
-
* Nothing is drawn, so this takes the renderer's measure context rather than a
|
|
38
|
-
* render context: discovery lays each clip out, which needs text measured, not
|
|
39
|
-
* rasterized. No canvas, no surface, no GPU, so WebGPU is not required.
|
|
20
|
+
* It must discover: audio requests are an *output of discovery* (each node declares its clips as it enters the tree), so
|
|
21
|
+
* there is no knowing what plays when without it. Nothing is drawn, so it takes the renderer's measure context, not a
|
|
22
|
+
* render context: text is measured, not rasterized, and WebGPU is not required.
|
|
40
23
|
*/
|
|
41
24
|
export declare function mixTimelineAudio(params: MixTimelineAudioParams): Promise<AudioBuffer | null>;
|
|
42
25
|
//# sourceMappingURL=mix-timeline-audio.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mix-timeline-audio.d.ts","sourceRoot":"","sources":["../src/mix-timeline-audio.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,QAAQ,EAER,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,MAAM,EACd,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAC9D,yEAAyE;IACzE,QAAQ,EAAE,eAAe,GAAG,cAAc,CAAC;IAC3C,
|
|
1
|
+
{"version":3,"file":"mix-timeline-audio.d.ts","sourceRoot":"","sources":["../src/mix-timeline-audio.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,QAAQ,EAER,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,MAAM,EACd,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAC9D,yEAAyE;IACzE,QAAQ,EAAE,eAAe,GAAG,cAAc,CAAC;IAC3C,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAClC,MAAM,EAAE,sBAAsB,GAC/B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA4B7B"}
|
|
@@ -4,30 +4,14 @@ import { mixAudio } from "./audio/mixer";
|
|
|
4
4
|
import { loadRenderer } from "./renderer";
|
|
5
5
|
const EMPTY_MANIFEST = { image: {}, video: {}, audio: {}, font: {} };
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Mixes a timeline's audio without rendering a frame. A **split** export cannot mix per piece (separately-encoded AAC
|
|
8
|
+
* puts the encoder's priming delay at every join: a click and drift at each boundary), so the coordinator mixes the
|
|
9
|
+
* whole timeline once here. Returns `null` when no audio is scheduled (the common case, not a failure), so the caller
|
|
10
|
+
* muxes video only rather than declaring an empty track.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* separately-encoded AAC puts the encoder's priming delay at every join — a click
|
|
13
|
-
* at each boundary and a track that drifts further out of sync with every one. So
|
|
14
|
-
* the coordinator mixes the whole timeline once, here, and the joined file gets a
|
|
15
|
-
* single continuous audio track.
|
|
16
|
-
*
|
|
17
|
-
* Returns `null` when the timeline schedules no audio at all, which is the common
|
|
18
|
-
* case and not a failure — the caller should then mux video only rather than
|
|
19
|
-
* declare an empty track.
|
|
20
|
-
*
|
|
21
|
-
* ## Why this discovers
|
|
22
|
-
*
|
|
23
|
-
* Audio requests are an *output of discovery*: each node declares its clips as it
|
|
24
|
-
* enters the tree, and the pass that finds what a clip's frames draw is the one
|
|
25
|
-
* that puts its nodes in the tree, so there is no way to know what plays when
|
|
26
|
-
* without it.
|
|
27
|
-
*
|
|
28
|
-
* Nothing is drawn, so this takes the renderer's measure context rather than a
|
|
29
|
-
* render context: discovery lays each clip out, which needs text measured, not
|
|
30
|
-
* rasterized. No canvas, no surface, no GPU, so WebGPU is not required.
|
|
12
|
+
* It must discover: audio requests are an *output of discovery* (each node declares its clips as it enters the tree), so
|
|
13
|
+
* there is no knowing what plays when without it. Nothing is drawn, so it takes the renderer's measure context, not a
|
|
14
|
+
* render context: text is measured, not rasterized, and WebGPU is not required.
|
|
31
15
|
*/
|
|
32
16
|
export async function mixTimelineAudio(params) {
|
|
33
17
|
const { registry = new Registry(), manifest = EMPTY_MANIFEST, sampleRate } = params;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mix-timeline-audio.js","sourceRoot":"","sources":["../src/mix-timeline-audio.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,QAAQ,EACR,sBAAsB,GAKzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAA2B,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"mix-timeline-audio.js","sourceRoot":"","sources":["../src/mix-timeline-audio.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,QAAQ,EACR,sBAAsB,GAKzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAA2B,MAAM,YAAY,CAAC;AAcnE,MAAM,cAAc,GAAkB,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AAEpF;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAClC,MAA8B;IAE9B,MAAM,EAAE,QAAQ,GAAG,IAAI,QAAQ,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC;IACpF,MAAM,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC;IAChF,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAS,CAAC;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAI,CAAC;IAEzB,2DAA2D;IAC3D,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,IAAI,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,QAAQ,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAExD,qFAAqF;IACrF,MAAM,SAAS,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACxH,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC;QAEhC,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAExC,OAAO,MAAM,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;IACrF,CAAC;YAAS,CAAC;QACP,SAAS,CAAC,OAAO,EAAE,CAAC;QACpB,0EAA0E;QAC1E,uCAAuC;QACvC,OAAO,CAAC,OAAO,EAAE,CAAC;IACtB,CAAC;AACL,CAAC","sourcesContent":["import {\n DEFAULT_OUTPUT,\n ManifestAssetCatalog,\n AssetTimeline,\n Registry,\n resolveProjectDocument,\n type AssetManifest,\n type ProjectDocument,\n type VisualDocument,\n type Size2D,\n} from \"@motionscript/core\";\nimport { collectAudio } from \"./frame-loop\";\n\nimport { mixAudio } from \"./audio/mixer\";\nimport { loadRenderer, type RendererWasmOption } from \"./renderer\";\n\nexport interface MixTimelineAudioParams extends RendererWasmOption {\n /** A project, or a lone still or composition as the one track of one. */\n document: ProjectDocument | VisualDocument;\n registry?: Registry;\n /** Win over the document's own; 1920×1080 at 60 fps when neither says. */\n viewport?: Size2D;\n fps?: number;\n manifest?: AssetManifest;\n /** Mixdown rate. Defaults to the mixer's own (44100). */\n sampleRate?: number;\n}\n\nconst EMPTY_MANIFEST: AssetManifest = { image: {}, video: {}, audio: {}, font: {} };\n\n/**\n * Mixes a timeline's audio without rendering a frame. A **split** export cannot mix per piece (separately-encoded AAC\n * puts the encoder's priming delay at every join: a click and drift at each boundary), so the coordinator mixes the\n * whole timeline once here. Returns `null` when no audio is scheduled (the common case, not a failure), so the caller\n * muxes video only rather than declaring an empty track.\n *\n * It must discover: audio requests are an *output of discovery* (each node declares its clips as it enters the tree), so\n * there is no knowing what plays when without it. Nothing is drawn, so it takes the renderer's measure context, not a\n * render context: text is measured, not rasterized, and WebGPU is not required.\n */\nexport async function mixTimelineAudio(\n params: MixTimelineAudioParams,\n): Promise<AudioBuffer | null> {\n const { registry = new Registry(), manifest = EMPTY_MANIFEST, sampleRate } = params;\n const project = resolveProjectDocument(params.document, params, DEFAULT_OUTPUT);\n if (project.tracks.length === 0) return null;\n const viewport = project.viewport!;\n const fps = project.fps!;\n\n // The renderer only for its text shaper; nothing is drawn.\n const renderer = await loadRenderer(params, { gpu: false });\n const catalog = new ManifestAssetCatalog(manifest);\n const storage = renderer.createStorageAdapter({ catalog, viewport, fps });\n const measurer = renderer.createMeasureContext(storage);\n\n // Audio is declared from each node's arrival state, so no asset windows are refined.\n const precomper = new AssetTimeline(project, { registry, viewport, fps, assets: catalog, measurer }, { assets: false });\n try {\n const precomp = precomper.run();\n\n const scheduled = collectAudio(precomp);\n if (scheduled.length === 0) return null;\n\n return await mixAudio(scheduled, precomp.totalDuration, { sampleRate, catalog });\n } finally {\n precomper.dispose();\n // The adapter holds decode sessions and cached images; without this a mix\n // leaks them for the life of the page.\n storage.dispose();\n }\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ExportParams } from "./exporter";
|
|
2
|
+
/**
|
|
3
|
+
* Where a native export's stream goes: in a desktop host, a pipe into the
|
|
4
|
+
* `ms-export` sidecar (`packages/renderer/crates/export`), which renders on
|
|
5
|
+
* the native GPU and encodes with NVENC (or libx264). Experimental.
|
|
6
|
+
*/
|
|
7
|
+
export interface NativeExportTransport {
|
|
8
|
+
/** The sidecar's built-in effect names, in its index order (`ms-export info`). */
|
|
9
|
+
readonly effects: readonly string[];
|
|
10
|
+
/** The next bytes of the stream, in order. Resolves when the transport can take more. */
|
|
11
|
+
write(bytes: Uint8Array): Promise<void>;
|
|
12
|
+
/** The stream is complete; resolves with the file's bytes, or nothing when the sidecar wrote it where the host asked. */
|
|
13
|
+
finish(): Promise<Uint8Array | void>;
|
|
14
|
+
/** The export failed or was cancelled: stop the sidecar, keep no file. */
|
|
15
|
+
abort?(reason: unknown): void;
|
|
16
|
+
}
|
|
17
|
+
export type NativeExportParams = Omit<ExportParams, "format" | "returnBytes" | "filename" | "rendererWasmUrl"> & {
|
|
18
|
+
rendererWasmUrl?: string | URL;
|
|
19
|
+
transport: NativeExportTransport;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Experimental: {@link exportVideo} through the native sidecar. The frame
|
|
23
|
+
* loop, adapter, text and assets run here exactly as for a canvas export;
|
|
24
|
+
* what would be drawn is streamed to `transport` instead, and the sidecar
|
|
25
|
+
* draws it with the same renderer on the native GPU and encodes it. MP4
|
|
26
|
+
* only; `video.codec` picks avc (default), hevc or av1.
|
|
27
|
+
*/
|
|
28
|
+
export declare function exportVideoNative(params: NativeExportParams): Promise<Uint8Array | void>;
|
|
29
|
+
//# sourceMappingURL=native-export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-export.d.ts","sourceRoot":"","sources":["../src/native-export.ts"],"names":[],"mappings":"AAGA,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAItF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IAClC,kFAAkF;IAClF,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,yFAAyF;IACzF,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,yHAAyH;IACzH,MAAM,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACrC,0EAA0E;IAC1E,KAAK,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,aAAa,GAAG,UAAU,GAAG,iBAAiB,CAAC,GAAG;IAC7G,eAAe,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAC/B,SAAS,EAAE,qBAAqB,CAAC;CACpC,CAAC;AAmDF;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAsC9F"}
|