@motionscript/browser 0.1.0-alpha.7 → 0.1.0-alpha.9
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 +12 -0
- package/LICENSE +201 -201
- package/NOTICE +49 -48
- package/README.md +141 -118
- package/dist/asset-bytes.d.ts +7 -4
- package/dist/asset-bytes.d.ts.map +1 -1
- package/dist/asset-bytes.js +9 -4
- package/dist/asset-bytes.js.map +1 -1
- package/dist/audio/bus-graph.d.ts.map +1 -1
- package/dist/audio/bus-graph.js +15 -6
- package/dist/audio/bus-graph.js.map +1 -1
- 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.map +1 -1
- package/dist/audio/index.js +2 -3
- package/dist/audio/index.js.map +1 -1
- package/dist/audio/loop-region.js.map +1 -1
- package/dist/audio/mixer.d.ts +4 -4
- package/dist/audio/mixer.d.ts.map +1 -1
- package/dist/audio/mixer.js +18 -38
- package/dist/audio/mixer.js.map +1 -1
- package/dist/audio/player.d.ts +12 -6
- package/dist/audio/player.d.ts.map +1 -1
- package/dist/audio/player.js +49 -62
- package/dist/audio/player.js.map +1 -1
- package/dist/browser/THIRD_PARTY_LICENSES.txt +379 -27
- package/dist/browser/audio.js +1 -1
- 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-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/index.js +51 -1343
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/manifest.json +8 -8
- package/dist/browser/ms_renderer_bg.wasm +0 -0
- package/dist/concat.d.ts +33 -7
- package/dist/concat.d.ts.map +1 -1
- package/dist/concat.js +117 -72
- package/dist/concat.js.map +1 -1
- 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.map +1 -1
- package/dist/dynamic-nodes.js +2 -3
- package/dist/dynamic-nodes.js.map +1 -1
- package/dist/engine.d.ts +37 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +41 -32
- package/dist/engine.js.map +1 -1
- 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 +125 -14
- package/dist/exporter.d.ts.map +1 -1
- package/dist/exporter.js +133 -89
- package/dist/exporter.js.map +1 -1
- 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 +12 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -21
- package/dist/index.js.map +1 -1
- package/dist/mix-timeline-audio.d.ts +5 -5
- package/dist/mix-timeline-audio.d.ts.map +1 -1
- package/dist/mix-timeline-audio.js +10 -12
- package/dist/mix-timeline-audio.js.map +1 -1
- 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 +4 -4
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +3 -3
- package/dist/screenshot.js.map +1 -1
- package/dist/still.d.ts +17 -19
- package/dist/still.d.ts.map +1 -1
- package/dist/still.js +27 -34
- package/dist/still.js.map +1 -1
- package/dist/ticker.d.ts +22 -8
- package/dist/ticker.d.ts.map +1 -1
- package/dist/ticker.js +55 -20
- package/dist/ticker.js.map +1 -1
- package/package.json +9 -10
- package/dist/audio/filter-graph.d.ts +0 -59
- package/dist/audio/filter-graph.d.ts.map +0 -1
- package/dist/audio/filter-graph.js +0 -241
- package/dist/audio/filter-graph.js.map +0 -1
- package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +0 -2
- package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +0 -7
- package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +0 -2
- package/dist/browser/chunks/chunk-2MAFJWBZ.js +0 -2
- package/dist/browser/chunks/chunk-2MAFJWBZ.js.map +0 -7
- package/dist/browser/chunks/chunk-CTLBXZSU.js +0 -4119
- package/dist/browser/chunks/chunk-CTLBXZSU.js.map +0 -7
- package/dist/browser/chunks/chunk-FQ32PDIU.js +0 -2
- package/dist/browser/chunks/chunk-FQ32PDIU.js.map +0 -7
- package/dist/browser/chunks/chunk-FY3BTKY4.js +0 -2
- package/dist/browser/chunks/chunk-FY3BTKY4.js.map +0 -7
- package/dist/browser/chunks/three.module-Z4EOVUWF.js +0 -2
- package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +0 -7
- package/dist/decode/image-levels.d.ts +0 -137
- package/dist/decode/image-levels.d.ts.map +0 -1
- package/dist/decode/image-levels.js +0 -477
- package/dist/decode/image-levels.js.map +0 -1
- package/dist/decode/levels.d.ts +0 -37
- package/dist/decode/levels.d.ts.map +0 -1
- package/dist/decode/levels.js +0 -68
- package/dist/decode/levels.js.map +0 -1
- package/dist/decode/video-levels.d.ts +0 -37
- package/dist/decode/video-levels.d.ts.map +0 -1
- package/dist/decode/video-levels.js +0 -95
- package/dist/decode/video-levels.js.map +0 -1
- package/dist/getter.d.ts +0 -10
- package/dist/getter.d.ts.map +0 -1
- package/dist/getter.js +0 -38
- package/dist/getter.js.map +0 -1
- package/dist/measure-context.d.ts +0 -12
- package/dist/measure-context.d.ts.map +0 -1
- package/dist/measure-context.js +0 -12
- package/dist/measure-context.js.map +0 -1
- package/dist/render-context.d.ts +0 -32
- package/dist/render-context.d.ts.map +0 -1
- package/dist/render-context.js +0 -59
- package/dist/render-context.js.map +0 -1
- package/dist/storage-adapter.d.ts +0 -367
- package/dist/storage-adapter.d.ts.map +0 -1
- package/dist/storage-adapter.js +0 -1280
- package/dist/storage-adapter.js.map +0 -1
- package/dist/three/renderer.d.ts +0 -47
- package/dist/three/renderer.d.ts.map +0 -1
- package/dist/three/renderer.js +0 -182
- package/dist/three/renderer.js.map +0 -1
- /package/dist/browser/chunks/{SkeletonUtils-XYDMTGTM.js.map → chunk-KLXMRKTL.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
// @motionscript/browser —
|
|
2
|
-
//
|
|
3
|
-
export { WebRenderContext } from "./render-context";
|
|
4
|
-
export { WebStorageAdapter } from "./storage-adapter";
|
|
1
|
+
// @motionscript/browser — @motionscript/core in the browser: WebGPU surfaces
|
|
2
|
+
// (through @motionscript/renderer), video export, audio playback and clock.
|
|
5
3
|
// The seam letting a host answer for an asset's bytes before the adapter goes
|
|
6
4
|
// to the network — see `asset-bytes.ts` for why it is module-level.
|
|
7
|
-
export { setAssetByteSource } from "./asset-bytes";
|
|
8
|
-
export { getCanvasKit } from "./getter";
|
|
9
|
-
// Re-exported from @motionscript/skia-render, whose canonical home these are —
|
|
10
|
-
// kept on this barrel so the specifier consumers already use keeps resolving.
|
|
11
|
-
export { EffectRegistry } from "@motionscript/skia-render";
|
|
5
|
+
export { loadAssetBytes, setAssetByteSource } from "./asset-bytes";
|
|
12
6
|
export { exportVideo, } from "./exporter";
|
|
7
|
+
// Mixes the timeline's audio and encodes it alone, without rendering a frame.
|
|
8
|
+
export { exportAudio, NoAudioError } from "./export-audio";
|
|
9
|
+
export { EXPORT_FORMATS, } from "./formats";
|
|
13
10
|
// mediabunny's resolution-aware quality levels, re-exported so a host can pick
|
|
14
11
|
// one for `ExportParams.video.bitrate` without taking a direct dependency on the
|
|
15
12
|
// encoder library this package happens to use.
|
|
16
13
|
export { QUALITY_VERY_LOW, QUALITY_LOW, QUALITY_MEDIUM, QUALITY_HIGH, QUALITY_VERY_HIGH, } from "mediabunny";
|
|
17
14
|
export { exportScreenshot } from "./screenshot";
|
|
18
15
|
// Thrown by a still and an export alike for a frame whose every track failed to build.
|
|
19
|
-
export { NothingToDrawError } from "
|
|
20
|
-
// Joins separately-encoded MP4s end to end without re-encoding. What makes it
|
|
16
|
+
export { NothingToDrawError } from "./frame-loop";
|
|
17
|
+
// Joins separately-encoded MP4s (or WebMs) end to end without re-encoding. What makes it
|
|
21
18
|
// worth rendering an export in parallel at all: the parts still have to become
|
|
22
19
|
// one file, and re-encoding to join them gives back everything the parallelism
|
|
23
20
|
// won. See `concat.ts` for the two things a caller has to guarantee.
|
|
24
|
-
export { concatVideoSegments, ConcatMismatchError } from "./concat";
|
|
21
|
+
export { concatVideoSegments, createSegmentJoiner, ConcatMismatchError } from "./concat";
|
|
22
|
+
// One export across several renderers: chunks of frames on lanes the run admits
|
|
23
|
+
// while they add throughput, joined as they land. See `parallel/export-parallel.ts`.
|
|
24
|
+
export { exportVideoParallel } from "./parallel/export-parallel";
|
|
25
|
+
export { localLane, messagePortLane, serveExportLane } from "./parallel/lanes";
|
|
25
26
|
// Mixes a timeline's audio without rendering a frame — the other half of a split
|
|
26
27
|
// export, whose video arrives in separately-encoded pieces that AAC cannot be
|
|
27
28
|
// concatenated across. See `mix-timeline-audio.ts`.
|
|
@@ -32,16 +33,11 @@ export { mixTimelineAudio } from "./mix-timeline-audio";
|
|
|
32
33
|
export { StillRenderer, createStillRenderer } from "./still";
|
|
33
34
|
export { WebAudioSink as WebAudioPlayer } from "./audio/player";
|
|
34
35
|
// Mixdown. Also on the `@motionscript/browser/audio` subpath, which is the
|
|
35
|
-
// import to prefer — it reaches it without loading
|
|
36
|
-
// draws.
|
|
36
|
+
// import to prefer — it reaches it without loading the renderer.
|
|
37
37
|
export { mixAudio, encodeWav } from "./audio/mixer";
|
|
38
|
-
export { WebMeasureContext } from "./measure-context";
|
|
39
38
|
export { WebTicker } from "./ticker";
|
|
40
39
|
export { createBrowserEngine } from "./engine";
|
|
41
|
-
|
|
42
|
-
// The
|
|
43
|
-
|
|
44
|
-
// that rasterizes what they build. Re-exported here so the specifiers consumers
|
|
45
|
-
// already use keep resolving.
|
|
46
|
-
export { Canvas3DBackend, canvas3DBackend, loadCanvas3D, disposeCanvas3DBackend } from "@motionscript/skia-render";
|
|
40
|
+
export { WebGpuUnavailableError } from "./renderer";
|
|
41
|
+
// The renderer's host-facing surface, so a host needs no second import.
|
|
42
|
+
export { isWgpuSupported, onWgpuEvent, ShaderPreview, validateShader, WGSL_PRELUDE } from "@motionscript/renderer";
|
|
47
43
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4EAA4E;AAE5E,8EAA8E;AAC9E,oEAAoE;AACpE,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,+EAA+E;AAC/E,iFAAiF;AACjF,+CAA+C;AAC/C,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,yFAAyF;AACzF,+EAA+E;AAC/E,+EAA+E;AAC/E,qEAAqE;AACrE,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAA0C,MAAM,UAAU,CAAC;AACjI,gFAAgF;AAChF,qFAAqF;AACrF,OAAO,EAAE,mBAAmB,EAA6B,MAAM,4BAA4B,CAAC;AAC5F,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAA+B,MAAM,kBAAkB,CAAC;AAG5G,iFAAiF;AACjF,8EAA8E;AAC9E,oDAAoD;AACpD,OAAO,EAAE,gBAAgB,EAA+B,MAAM,sBAAsB,CAAC;AACrF,mFAAmF;AACnF,qFAAqF;AACrF,yCAAyC;AACzC,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// The seam letting a host answer for an asset's bytes before the adapter goes\n// to the network — see `asset-bytes.ts` for why it is module-level.\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, re-exported so a host can pick\n// one for `ExportParams.video.bitrate` without taking a direct dependency on the\n// encoder library this package happens to use.\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) end to end without re-encoding. What makes it\n// worth rendering an export in parallel at all: the parts still have to become\n// one file, and re-encoding to join them gives back everything the parallelism\n// won. See `concat.ts` for the two things a caller has to 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\";\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\n// export, whose video arrives in separately-encoded pieces that AAC cannot be\n// concatenated across. 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\n// into a canvas the host owns, for thumbnails and still previews. `exportScreenshot`\n// above is the one-shot wrapper over it.\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"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Registry, type AssetManifest, type ProjectDocument, type VisualDocument, type Size2D } from "@motionscript/core";
|
|
2
|
-
|
|
2
|
+
import { type RendererWasmOption } from "./renderer";
|
|
3
|
+
export interface MixTimelineAudioParams extends RendererWasmOption {
|
|
3
4
|
/** A project, or a lone still or composition as the one track of one. */
|
|
4
5
|
document: ProjectDocument | VisualDocument;
|
|
5
6
|
/** The vocabulary its clips compile against. Defaults to what the library ships. */
|
|
@@ -10,7 +11,6 @@ export interface MixTimelineAudioParams {
|
|
|
10
11
|
manifest?: AssetManifest;
|
|
11
12
|
/** Mixdown rate. Defaults to the mixer's own (44100). */
|
|
12
13
|
sampleRate?: number;
|
|
13
|
-
wasmUrl?: string;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Mix a timeline's audio without rendering a single frame.
|
|
@@ -34,9 +34,9 @@ export interface MixTimelineAudioParams {
|
|
|
34
34
|
* that puts its nodes in the tree, so there is no way to know what plays when
|
|
35
35
|
* without it.
|
|
36
36
|
*
|
|
37
|
-
* Nothing is drawn, so this takes
|
|
38
|
-
* context: discovery lays each clip out, which needs text measured, not
|
|
39
|
-
* rasterized. No canvas, no surface, no GPU.
|
|
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.
|
|
40
40
|
*/
|
|
41
41
|
export declare function mixTimelineAudio(params: MixTimelineAudioParams): Promise<AudioBuffer | null>;
|
|
42
42
|
//# 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;
|
|
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,oFAAoF;IACpF,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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,gBAAgB,CAClC,MAAM,EAAE,sBAAsB,GAC/B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA4B7B"}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_OUTPUT, ManifestAssetCatalog, AssetTimeline, Registry, resolveProjectDocument, } from "@motionscript/core";
|
|
2
|
-
import { collectAudio } from "
|
|
2
|
+
import { collectAudio } from "./frame-loop";
|
|
3
3
|
import { mixAudio } from "./audio/mixer";
|
|
4
|
-
import {
|
|
5
|
-
import { WebMeasureContext } from "./measure-context";
|
|
6
|
-
import { WebStorageAdapter } from "./storage-adapter";
|
|
4
|
+
import { loadRenderer } from "./renderer";
|
|
7
5
|
const EMPTY_MANIFEST = { image: {}, video: {}, audio: {}, font: {} };
|
|
8
6
|
/**
|
|
9
7
|
* Mix a timeline's audio without rendering a single frame.
|
|
@@ -27,22 +25,22 @@ const EMPTY_MANIFEST = { image: {}, video: {}, audio: {}, font: {} };
|
|
|
27
25
|
* that puts its nodes in the tree, so there is no way to know what plays when
|
|
28
26
|
* without it.
|
|
29
27
|
*
|
|
30
|
-
* Nothing is drawn, so this takes
|
|
31
|
-
* context: discovery lays each clip out, which needs text measured, not
|
|
32
|
-
* rasterized. No canvas, no surface, no GPU.
|
|
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.
|
|
33
31
|
*/
|
|
34
32
|
export async function mixTimelineAudio(params) {
|
|
35
|
-
const { registry = new Registry(), manifest = EMPTY_MANIFEST, sampleRate
|
|
33
|
+
const { registry = new Registry(), manifest = EMPTY_MANIFEST, sampleRate } = params;
|
|
36
34
|
const project = resolveProjectDocument(params.document, params, DEFAULT_OUTPUT);
|
|
37
35
|
if (project.tracks.length === 0)
|
|
38
36
|
return null;
|
|
39
37
|
const viewport = project.viewport;
|
|
40
38
|
const fps = project.fps;
|
|
41
|
-
//
|
|
42
|
-
const
|
|
39
|
+
// The renderer only for its text shaper; nothing is drawn.
|
|
40
|
+
const renderer = await loadRenderer(params, { gpu: false });
|
|
43
41
|
const catalog = new ManifestAssetCatalog(manifest);
|
|
44
|
-
const storage =
|
|
45
|
-
const measurer =
|
|
42
|
+
const storage = renderer.createStorageAdapter({ catalog, viewport, fps });
|
|
43
|
+
const measurer = renderer.createMeasureContext(storage);
|
|
46
44
|
// Audio is declared from each node's arrival state, so no asset windows are refined.
|
|
47
45
|
const precomper = new AssetTimeline(project, { registry, viewport, fps, assets: catalog, measurer }, { assets: false });
|
|
48
46
|
try {
|
|
@@ -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,
|
|
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;AAenE,MAAM,cAAc,GAAkB,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;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 /** The vocabulary its clips compile against. Defaults to what the library ships. */\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 * Mix a timeline's audio without rendering a single frame.\n *\n * `exportVideo` normally does this as part of the render, and for a\n * single-threaded export that is exactly right. A **split** export cannot: its\n * video is rendered in pieces by separate workers, and concatenating\n * separately-encoded AAC puts the encoder's priming delay at every join — a click\n * at each boundary and a track that drifts further out of sync with every one. So\n * the coordinator mixes the whole timeline once, here, and the joined file gets a\n * single continuous audio track.\n *\n * Returns `null` when the timeline schedules no audio at all, which is the common\n * case and not a failure — the caller should then mux video only rather than\n * declare an empty track.\n *\n * ## Why this discovers\n *\n * Audio requests are an *output of discovery*: each node declares its clips as it\n * enters the tree, and the pass that finds what a clip's frames draw is the one\n * that puts its nodes in the tree, so there is no way to know what plays when\n * without it.\n *\n * Nothing is drawn, so this takes the renderer's measure context rather than a\n * render context: discovery lays each clip out, which needs text measured, not\n * rasterized. No canvas, no surface, no GPU, so 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,32 @@
|
|
|
1
|
+
import { type ConcatOptions } from "../concat";
|
|
2
|
+
import { type ExportParams } from "../exporter";
|
|
3
|
+
import type { ExportLane } from "./lane";
|
|
4
|
+
export interface ParallelExportParams extends Omit<ExportParams, "frames" | "returnBytes" | "filename"> {
|
|
5
|
+
/**
|
|
6
|
+
* Where the frames are drawn, most preferred first. Every run starts on the
|
|
7
|
+
* first alone and admits the next only while that raises throughput (see
|
|
8
|
+
* `LaneGovernor`), so the first should be the warmest: usually `localLane()`.
|
|
9
|
+
*/
|
|
10
|
+
lanes: readonly ExportLane[];
|
|
11
|
+
/** Write the file here as it is joined; the call then resolves to an empty array. See `ConcatOptions.stream`. */
|
|
12
|
+
stream?: ConcatOptions["stream"];
|
|
13
|
+
/** Seconds per chunk; by default from the timeline's length and the lane count. */
|
|
14
|
+
chunkSeconds?: number;
|
|
15
|
+
/** Handed to every lane in its spec, for what only the host knows (see `serveExportLane`). */
|
|
16
|
+
host?: unknown;
|
|
17
|
+
/** How many lanes are rendering, each time that changes. */
|
|
18
|
+
onLanes?: (active: number) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* {@link exportVideo}, rendered in chunks across `lanes` and joined into one
|
|
22
|
+
* file without re-encoding.
|
|
23
|
+
*
|
|
24
|
+
* Each chunk is a frame range of the **whole** project, so layered and gapped
|
|
25
|
+
* timelines split like any other. Every chunk is encoded with one pinned
|
|
26
|
+
* configuration, the audio is mixed once over the whole timeline while the frames
|
|
27
|
+
* render (joined AAC would click at every seam), and each chunk is muxed the
|
|
28
|
+
* moment it and the ones before it are done. Progress is frames drawn across
|
|
29
|
+
* every lane, so it climbs smoothly and never goes back.
|
|
30
|
+
*/
|
|
31
|
+
export declare function exportVideoParallel(params: ParallelExportParams): Promise<Uint8Array>;
|
|
32
|
+
//# sourceMappingURL=export-parallel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-parallel.d.ts","sourceRoot":"","sources":["../../src/parallel/export-parallel.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAA4D,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAG1G,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,QAAQ,CAAC;AAItD,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,aAAa,GAAG,UAAU,CAAC;IACnG;;;;OAIG;IACH,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IAC7B,iHAAiH;IACjH,MAAM,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACjC,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8FAA8F;IAC9F,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAOD;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4F3F"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { DEFAULT_OUTPUT, placeTracks, Registry, renderIssueKey, resolveProjectDocument, totalFramesOf } from "@motionscript/core";
|
|
2
|
+
import { createSegmentJoiner } from "../concat";
|
|
3
|
+
import { exportGeometry, plainVideoEncoding, resolveVideoEncoding } from "../exporter";
|
|
4
|
+
import { formatSpec } from "../formats";
|
|
5
|
+
import { mixTimelineAudio } from "../mix-timeline-audio";
|
|
6
|
+
import { planChunks } from "./plan";
|
|
7
|
+
import { runLanes } from "./run-lanes";
|
|
8
|
+
const EMPTY_MANIFEST = { image: {}, video: {}, audio: {}, font: {} };
|
|
9
|
+
const DEFAULT_SUPERSAMPLE = 2;
|
|
10
|
+
/** The share of progress drawing takes; finishing the file is the rest. */
|
|
11
|
+
const DRAWN = 0.97;
|
|
12
|
+
/**
|
|
13
|
+
* {@link exportVideo}, rendered in chunks across `lanes` and joined into one
|
|
14
|
+
* file without re-encoding.
|
|
15
|
+
*
|
|
16
|
+
* Each chunk is a frame range of the **whole** project, so layered and gapped
|
|
17
|
+
* timelines split like any other. Every chunk is encoded with one pinned
|
|
18
|
+
* configuration, the audio is mixed once over the whole timeline while the frames
|
|
19
|
+
* render (joined AAC would click at every seam), and each chunk is muxed the
|
|
20
|
+
* moment it and the ones before it are done. Progress is frames drawn across
|
|
21
|
+
* every lane, so it climbs smoothly and never goes back.
|
|
22
|
+
*/
|
|
23
|
+
export async function exportVideoParallel(params) {
|
|
24
|
+
const { registry = new Registry(), scale = 1, format = "mp4", manifest = EMPTY_MANIFEST, includeAudio = true, audio = {}, signal, onProgress, supersample = DEFAULT_SUPERSAMPLE, } = params;
|
|
25
|
+
if (formatSpec(format).kind !== "video") {
|
|
26
|
+
throw new RangeError(`exportVideoParallel writes video; "${format}" is audio, which exportAudio writes.`);
|
|
27
|
+
}
|
|
28
|
+
const project = resolveProjectDocument(params.document, params, DEFAULT_OUTPUT);
|
|
29
|
+
if (project.tracks.length === 0)
|
|
30
|
+
throw new Error("exportVideoParallel: the project has no tracks.");
|
|
31
|
+
signal?.throwIfAborted();
|
|
32
|
+
const fps = project.fps;
|
|
33
|
+
const totalFrames = totalFramesOf(placeTracks(project.tracks, fps));
|
|
34
|
+
const { output } = exportGeometry(project.viewport, scale, supersample);
|
|
35
|
+
const encoding = plainVideoEncoding(await resolveVideoEncoding(params.video, format, output, fps), output);
|
|
36
|
+
const spec = {
|
|
37
|
+
project, manifest, theme: params.theme, scale, supersample, format, encoding, host: params.host,
|
|
38
|
+
};
|
|
39
|
+
const chunks = planChunks(totalFrames, {
|
|
40
|
+
fps, keyFrameInterval: encoding.keyFrameInterval, lanes: params.lanes.length, chunkSeconds: params.chunkSeconds,
|
|
41
|
+
});
|
|
42
|
+
// Started now so it is ready by the first join; failures surface there.
|
|
43
|
+
const mixed = includeAudio
|
|
44
|
+
? mixTimelineAudio({
|
|
45
|
+
document: project, registry, manifest, sampleRate: audio.sampleRate,
|
|
46
|
+
rendererWasmUrl: params.rendererWasmUrl,
|
|
47
|
+
})
|
|
48
|
+
: Promise.resolve(null);
|
|
49
|
+
mixed.catch(() => { });
|
|
50
|
+
let reported = 0;
|
|
51
|
+
const seenIssues = new Set();
|
|
52
|
+
const report = (p) => {
|
|
53
|
+
if (p <= reported)
|
|
54
|
+
return;
|
|
55
|
+
reported = p;
|
|
56
|
+
onProgress?.(p);
|
|
57
|
+
};
|
|
58
|
+
// One signal for both ways a run ends early: the caller cancelling, and the join failing.
|
|
59
|
+
const run = new AbortController();
|
|
60
|
+
const abortRun = () => run.abort(signal?.reason);
|
|
61
|
+
if (signal?.aborted)
|
|
62
|
+
abortRun();
|
|
63
|
+
signal?.addEventListener("abort", abortRun, { once: true });
|
|
64
|
+
// Keyed by first frame: a merged range spans several planned chunks.
|
|
65
|
+
const ready = new Map();
|
|
66
|
+
let joinedTo = 0;
|
|
67
|
+
let joining = Promise.resolve();
|
|
68
|
+
let joiner = null;
|
|
69
|
+
const pump = () => {
|
|
70
|
+
joining = joining.then(async () => {
|
|
71
|
+
while (ready.has(joinedTo)) {
|
|
72
|
+
joiner ??= createSegmentJoiner({ stream: params.stream, audio: await mixed, audioBitrate: audio.bitrate });
|
|
73
|
+
const { end, bytes } = ready.get(joinedTo);
|
|
74
|
+
ready.delete(joinedTo);
|
|
75
|
+
joinedTo = end;
|
|
76
|
+
await joiner.append(bytes);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return joining;
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
await runLanes({
|
|
83
|
+
lanes: params.lanes, spec, chunks, signal: run.signal,
|
|
84
|
+
onIssue: (issue) => {
|
|
85
|
+
const key = renderIssueKey(issue);
|
|
86
|
+
if (seenIssues.has(key))
|
|
87
|
+
return;
|
|
88
|
+
seenIssues.add(key);
|
|
89
|
+
params.onIssue?.(issue);
|
|
90
|
+
},
|
|
91
|
+
onFrames: (frames) => report(DRAWN * Math.min(1, frames / totalFrames)),
|
|
92
|
+
onActive: params.onLanes,
|
|
93
|
+
onChunk: (chunk, bytes) => {
|
|
94
|
+
ready.set(chunk.start, { end: chunk.end, bytes });
|
|
95
|
+
pump().catch((error) => run.abort(error));
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
await pump();
|
|
99
|
+
if (joinedTo !== totalFrames)
|
|
100
|
+
throw new Error(`exportVideoParallel: joined ${joinedTo} of ${totalFrames} frames.`);
|
|
101
|
+
const bytes = await joiner.finish();
|
|
102
|
+
report(1);
|
|
103
|
+
return bytes;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
await joining.catch(() => { });
|
|
107
|
+
await joiner?.cancel().catch(() => { });
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
signal?.removeEventListener("abort", abortRun);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=export-parallel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-parallel.js","sourceRoot":"","sources":["../../src/parallel/export-parallel.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEFAULT_OUTPUT, placeTracks, Registry, renderIssueKey, resolveProjectDocument, totalFramesOf, type AssetManifest } from \"@motionscript/core\";\nimport { createSegmentJoiner, type ConcatOptions } from \"../concat\";\nimport { exportGeometry, plainVideoEncoding, resolveVideoEncoding, type ExportParams } from \"../exporter\";\nimport { formatSpec } from \"../formats\";\nimport { mixTimelineAudio } from \"../mix-timeline-audio\";\nimport type { ExportLane, SegmentSpec } from \"./lane\";\nimport { planChunks } from \"./plan\";\nimport { runLanes } from \"./run-lanes\";\n\nexport interface ParallelExportParams extends Omit<ExportParams, \"frames\" | \"returnBytes\" | \"filename\"> {\n /**\n * Where the frames are drawn, most preferred first. Every run starts on the\n * first alone and admits the next only while that raises throughput (see\n * `LaneGovernor`), so the first should be the warmest: usually `localLane()`.\n */\n lanes: readonly ExportLane[];\n /** Write the file here as it is joined; the call then resolves to an empty array. See `ConcatOptions.stream`. */\n stream?: ConcatOptions[\"stream\"];\n /** Seconds per chunk; by default from the timeline's length and the lane count. */\n chunkSeconds?: number;\n /** Handed to every lane in its spec, for what only the host knows (see `serveExportLane`). */\n host?: unknown;\n /** How many lanes are rendering, each time that changes. */\n onLanes?: (active: number) => void;\n}\n\nconst EMPTY_MANIFEST: AssetManifest = { image: {}, video: {}, audio: {}, font: {} };\nconst DEFAULT_SUPERSAMPLE = 2;\n/** The share of progress drawing takes; finishing the file is the rest. */\nconst DRAWN = 0.97;\n\n/**\n * {@link exportVideo}, rendered in chunks across `lanes` and joined into one\n * file without re-encoding.\n *\n * Each chunk is a frame range of the **whole** project, so layered and gapped\n * timelines split like any other. Every chunk is encoded with one pinned\n * configuration, the audio is mixed once over the whole timeline while the frames\n * render (joined AAC would click at every seam), and each chunk is muxed the\n * moment it and the ones before it are done. Progress is frames drawn across\n * every lane, so it climbs smoothly and never goes back.\n */\nexport async function exportVideoParallel(params: ParallelExportParams): Promise<Uint8Array> {\n const {\n registry = new Registry(), scale = 1, format = \"mp4\", manifest = EMPTY_MANIFEST,\n includeAudio = true, audio = {}, signal, onProgress, supersample = DEFAULT_SUPERSAMPLE,\n } = params;\n if (formatSpec(format).kind !== \"video\") {\n throw new RangeError(`exportVideoParallel writes video; \"${format}\" is audio, which exportAudio writes.`);\n }\n const project = resolveProjectDocument(params.document, params, DEFAULT_OUTPUT);\n if (project.tracks.length === 0) throw new Error(\"exportVideoParallel: the project has no tracks.\");\n signal?.throwIfAborted();\n const fps = project.fps!;\n const totalFrames = totalFramesOf(placeTracks(project.tracks, fps));\n\n const { output } = exportGeometry(project.viewport!, scale, supersample);\n const encoding = plainVideoEncoding(await resolveVideoEncoding(params.video, format, output, fps), output);\n const spec: SegmentSpec = {\n project, manifest, theme: params.theme, scale, supersample, format, encoding, host: params.host,\n };\n const chunks = planChunks(totalFrames, {\n fps, keyFrameInterval: encoding.keyFrameInterval, lanes: params.lanes.length, chunkSeconds: params.chunkSeconds,\n });\n\n // Started now so it is ready by the first join; failures surface there.\n const mixed = includeAudio\n ? mixTimelineAudio({\n document: project, registry, manifest, sampleRate: audio.sampleRate,\n rendererWasmUrl: params.rendererWasmUrl,\n })\n : Promise.resolve(null);\n mixed.catch(() => {});\n\n let reported = 0;\n const seenIssues = new Set<string>();\n const report = (p: number) => {\n if (p <= reported) return;\n reported = p;\n onProgress?.(p);\n };\n\n // One signal for both ways a run ends early: the caller cancelling, and the join failing.\n const run = new AbortController();\n const abortRun = () => run.abort(signal?.reason);\n if (signal?.aborted) abortRun();\n signal?.addEventListener(\"abort\", abortRun, { once: true });\n\n // Keyed by first frame: a merged range spans several planned chunks.\n const ready = new Map<number, { end: number; bytes: Uint8Array }>();\n let joinedTo = 0;\n let joining: Promise<void> = Promise.resolve();\n let joiner: ReturnType<typeof createSegmentJoiner> | null = null;\n const pump = () => {\n joining = joining.then(async () => {\n while (ready.has(joinedTo)) {\n joiner ??= createSegmentJoiner({ stream: params.stream, audio: await mixed, audioBitrate: audio.bitrate });\n const { end, bytes } = ready.get(joinedTo)!;\n ready.delete(joinedTo);\n joinedTo = end;\n await joiner.append(bytes);\n }\n });\n return joining;\n };\n\n try {\n await runLanes({\n lanes: params.lanes, spec, chunks, signal: run.signal,\n onIssue: (issue) => {\n const key = renderIssueKey(issue);\n if (seenIssues.has(key)) return;\n seenIssues.add(key);\n params.onIssue?.(issue);\n },\n onFrames: (frames) => report(DRAWN * Math.min(1, frames / totalFrames)),\n onActive: params.onLanes,\n onChunk: (chunk, bytes) => {\n ready.set(chunk.start, { end: chunk.end, bytes });\n pump().catch((error: unknown) => run.abort(error));\n },\n });\n await pump();\n if (joinedTo !== totalFrames) throw new Error(`exportVideoParallel: joined ${joinedTo} of ${totalFrames} frames.`);\n const bytes = await joiner!.finish();\n report(1);\n return bytes;\n } catch (error) {\n await joining.catch(() => {});\n await (joiner as ReturnType<typeof createSegmentJoiner> | null)?.cancel().catch(() => {});\n throw error;\n } finally {\n signal?.removeEventListener(\"abort\", abortRun);\n }\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides how many lanes render at once, from what the run itself measures.
|
|
3
|
+
*
|
|
4
|
+
* Lanes do not add throughput for free. Every lane's encoder, and on a WebGPU
|
|
5
|
+
* canvas its frame capture, run in the browser's one GPU process, so once a lane
|
|
6
|
+
* spends its time waiting on the encoder rather than drawing, a second lane only
|
|
7
|
+
* splits the same encoder between two sessions and the export gets slower. The
|
|
8
|
+
* governor starts with one lane, adds the next only while lanes are mostly
|
|
9
|
+
* drawing, keeps it only if total throughput rose, and then settles.
|
|
10
|
+
*/
|
|
11
|
+
export interface GovernorOptions {
|
|
12
|
+
/** Lanes available, the first being the one every run starts with. */
|
|
13
|
+
lanes: number;
|
|
14
|
+
/** How long a configuration is measured before it is judged, in ms. */
|
|
15
|
+
windowMs?: number;
|
|
16
|
+
/** Frames a window needs before it is judged. */
|
|
17
|
+
minFrames?: number;
|
|
18
|
+
/** Above this share of time waiting on the encoder, another lane cannot help. */
|
|
19
|
+
maxWaitShare?: number;
|
|
20
|
+
/** A lane is kept only if it raises throughput by at least this factor. */
|
|
21
|
+
minGain?: number;
|
|
22
|
+
/** How long the newest lane draws before it is measured: its first frames wait on loads and a cold encoder. */
|
|
23
|
+
warmupMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface FrameSample {
|
|
26
|
+
lane: number;
|
|
27
|
+
frames: number;
|
|
28
|
+
busyMs: number;
|
|
29
|
+
waitMs: number;
|
|
30
|
+
at: number;
|
|
31
|
+
}
|
|
32
|
+
export declare class LaneGovernor {
|
|
33
|
+
private readonly _options;
|
|
34
|
+
private _active;
|
|
35
|
+
private _settled;
|
|
36
|
+
private _window;
|
|
37
|
+
private _previousFps;
|
|
38
|
+
private _newestFirstAt;
|
|
39
|
+
private readonly _windowMs;
|
|
40
|
+
private readonly _minFrames;
|
|
41
|
+
private readonly _maxWaitShare;
|
|
42
|
+
private readonly _minGain;
|
|
43
|
+
private readonly _warmupMs;
|
|
44
|
+
constructor(_options: GovernorOptions);
|
|
45
|
+
/** Lanes `0..active-1` may take chunks. */
|
|
46
|
+
get active(): number;
|
|
47
|
+
get settled(): boolean;
|
|
48
|
+
/** Stop measuring once a lane dies: the run continues on at most `alive` lanes. */
|
|
49
|
+
laneLost(alive: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* Record frames a lane drew. Returns the new active count when it changed.
|
|
52
|
+
* A window opens once the newest lane has drawn for `warmupMs`, so its
|
|
53
|
+
* start-up is not counted against it.
|
|
54
|
+
*/
|
|
55
|
+
record(sample: FrameSample): number | null;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=governor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor.d.ts","sourceRoot":"","sources":["../../src/parallel/governor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC5B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+GAA+G;IAC/G,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACd;AASD,qBAAa,YAAY;IAYT,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAXrC,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEN,QAAQ,EAAE,eAAe;IAStD,2CAA2C;IAC3C,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK7B;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI;CA+B7C"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export class LaneGovernor {
|
|
2
|
+
_options;
|
|
3
|
+
_active = 1;
|
|
4
|
+
_settled;
|
|
5
|
+
_window = emptyWindow();
|
|
6
|
+
_previousFps = 0;
|
|
7
|
+
_newestFirstAt = null;
|
|
8
|
+
_windowMs;
|
|
9
|
+
_minFrames;
|
|
10
|
+
_maxWaitShare;
|
|
11
|
+
_minGain;
|
|
12
|
+
_warmupMs;
|
|
13
|
+
constructor(_options) {
|
|
14
|
+
this._options = _options;
|
|
15
|
+
this._windowMs = _options.windowMs ?? 1500;
|
|
16
|
+
this._minFrames = _options.minFrames ?? 24;
|
|
17
|
+
this._maxWaitShare = _options.maxWaitShare ?? 0.45;
|
|
18
|
+
this._minGain = _options.minGain ?? 1.1;
|
|
19
|
+
this._warmupMs = _options.warmupMs ?? 300;
|
|
20
|
+
this._settled = _options.lanes <= 1;
|
|
21
|
+
}
|
|
22
|
+
/** Lanes `0..active-1` may take chunks. */
|
|
23
|
+
get active() {
|
|
24
|
+
return this._active;
|
|
25
|
+
}
|
|
26
|
+
get settled() {
|
|
27
|
+
return this._settled;
|
|
28
|
+
}
|
|
29
|
+
/** Stop measuring once a lane dies: the run continues on at most `alive` lanes. */
|
|
30
|
+
laneLost(alive) {
|
|
31
|
+
this._active = Math.max(1, Math.min(this._active, alive));
|
|
32
|
+
this._settled = true;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Record frames a lane drew. Returns the new active count when it changed.
|
|
36
|
+
* A window opens once the newest lane has drawn for `warmupMs`, so its
|
|
37
|
+
* start-up is not counted against it.
|
|
38
|
+
*/
|
|
39
|
+
record(sample) {
|
|
40
|
+
if (this._settled || sample.lane >= this._active)
|
|
41
|
+
return null;
|
|
42
|
+
const w = this._window;
|
|
43
|
+
if (w.start === null) {
|
|
44
|
+
if (sample.lane !== this._active - 1)
|
|
45
|
+
return null;
|
|
46
|
+
this._newestFirstAt ??= sample.at;
|
|
47
|
+
if (sample.at - this._newestFirstAt >= this._warmupMs)
|
|
48
|
+
w.start = sample.at;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
w.frames += sample.frames;
|
|
52
|
+
w.busyMs += sample.busyMs;
|
|
53
|
+
w.waitMs += sample.waitMs;
|
|
54
|
+
const elapsed = sample.at - w.start;
|
|
55
|
+
if (elapsed < this._windowMs || w.frames < this._minFrames)
|
|
56
|
+
return null;
|
|
57
|
+
const fps = (w.frames * 1000) / elapsed;
|
|
58
|
+
const waitShare = w.waitMs / Math.max(1, w.busyMs + w.waitMs);
|
|
59
|
+
const before = this._active;
|
|
60
|
+
if (this._active > 1 && fps < this._previousFps * this._minGain) {
|
|
61
|
+
this._active -= 1;
|
|
62
|
+
this._settled = true;
|
|
63
|
+
}
|
|
64
|
+
else if (waitShare <= this._maxWaitShare && this._active < this._options.lanes) {
|
|
65
|
+
this._previousFps = fps;
|
|
66
|
+
this._active += 1;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this._settled = true;
|
|
70
|
+
}
|
|
71
|
+
this._window = emptyWindow();
|
|
72
|
+
this._newestFirstAt = null;
|
|
73
|
+
return this._active !== before ? this._active : null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function emptyWindow() {
|
|
77
|
+
return { start: null, frames: 0, busyMs: 0, waitMs: 0 };
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=governor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor.js","sourceRoot":"","sources":["../../src/parallel/governor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Decides how many lanes render at once, from what the run itself measures.\n *\n * Lanes do not add throughput for free. Every lane's encoder, and on a WebGPU\n * canvas its frame capture, run in the browser's one GPU process, so once a lane\n * spends its time waiting on the encoder rather than drawing, a second lane only\n * splits the same encoder between two sessions and the export gets slower. The\n * governor starts with one lane, adds the next only while lanes are mostly\n * drawing, keeps it only if total throughput rose, and then settles.\n */\nexport interface GovernorOptions {\n /** Lanes available, the first being the one every run starts with. */\n lanes: number;\n /** How long a configuration is measured before it is judged, in ms. */\n windowMs?: number;\n /** Frames a window needs before it is judged. */\n minFrames?: number;\n /** Above this share of time waiting on the encoder, another lane cannot help. */\n maxWaitShare?: number;\n /** A lane is kept only if it raises throughput by at least this factor. */\n minGain?: number;\n /** How long the newest lane draws before it is measured: its first frames wait on loads and a cold encoder. */\n warmupMs?: number;\n}\n\nexport interface FrameSample {\n lane: number;\n frames: number;\n busyMs: number;\n waitMs: number;\n at: number;\n}\n\ninterface Window {\n start: number | null;\n frames: number;\n busyMs: number;\n waitMs: number;\n}\n\nexport class LaneGovernor {\n private _active = 1;\n private _settled: boolean;\n private _window: Window = emptyWindow();\n private _previousFps = 0;\n private _newestFirstAt: number | null = null;\n private readonly _windowMs: number;\n private readonly _minFrames: number;\n private readonly _maxWaitShare: number;\n private readonly _minGain: number;\n private readonly _warmupMs: number;\n\n constructor(private readonly _options: GovernorOptions) {\n this._windowMs = _options.windowMs ?? 1500;\n this._minFrames = _options.minFrames ?? 24;\n this._maxWaitShare = _options.maxWaitShare ?? 0.45;\n this._minGain = _options.minGain ?? 1.1;\n this._warmupMs = _options.warmupMs ?? 300;\n this._settled = _options.lanes <= 1;\n }\n\n /** Lanes `0..active-1` may take chunks. */\n get active(): number {\n return this._active;\n }\n\n get settled(): boolean {\n return this._settled;\n }\n\n /** Stop measuring once a lane dies: the run continues on at most `alive` lanes. */\n laneLost(alive: number): void {\n this._active = Math.max(1, Math.min(this._active, alive));\n this._settled = true;\n }\n\n /**\n * Record frames a lane drew. Returns the new active count when it changed.\n * A window opens once the newest lane has drawn for `warmupMs`, so its\n * start-up is not counted against it.\n */\n record(sample: FrameSample): number | null {\n if (this._settled || sample.lane >= this._active) return null;\n const w = this._window;\n if (w.start === null) {\n if (sample.lane !== this._active - 1) return null;\n this._newestFirstAt ??= sample.at;\n if (sample.at - this._newestFirstAt >= this._warmupMs) w.start = sample.at;\n return null;\n }\n w.frames += sample.frames;\n w.busyMs += sample.busyMs;\n w.waitMs += sample.waitMs;\n const elapsed = sample.at - w.start;\n if (elapsed < this._windowMs || w.frames < this._minFrames) return null;\n\n const fps = (w.frames * 1000) / elapsed;\n const waitShare = w.waitMs / Math.max(1, w.busyMs + w.waitMs);\n const before = this._active;\n if (this._active > 1 && fps < this._previousFps * this._minGain) {\n this._active -= 1;\n this._settled = true;\n } else if (waitShare <= this._maxWaitShare && this._active < this._options.lanes) {\n this._previousFps = fps;\n this._active += 1;\n } else {\n this._settled = true;\n }\n this._window = emptyWindow();\n this._newestFirstAt = null;\n return this._active !== before ? this._active : null;\n }\n}\n\nfunction emptyWindow(): Window {\n return { start: null, frames: 0, busyMs: 0, waitMs: 0 };\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AssetManifest, ProjectDocument, Registry, RenderIssue, Theme } from "@motionscript/core";
|
|
2
|
+
import type { RendererWasmOption } from "../renderer";
|
|
3
|
+
import type { ResolvedVideoEncoding } from "../exporter";
|
|
4
|
+
import type { VideoFormat } from "../formats";
|
|
5
|
+
import type { ExportChunk } from "./plan";
|
|
6
|
+
/**
|
|
7
|
+
* Everything a lane needs to render chunks of one export. Plain data, so it
|
|
8
|
+
* crosses a `MessagePort` intact; the encoding is pinned here because segments
|
|
9
|
+
* encoded differently cannot be joined.
|
|
10
|
+
*/
|
|
11
|
+
export interface SegmentSpec {
|
|
12
|
+
/** Its fps and viewport resolved. */
|
|
13
|
+
project: ProjectDocument;
|
|
14
|
+
manifest: AssetManifest;
|
|
15
|
+
theme?: Theme;
|
|
16
|
+
scale: number;
|
|
17
|
+
supersample: number;
|
|
18
|
+
format: VideoFormat;
|
|
19
|
+
encoding: ResolvedVideoEncoding;
|
|
20
|
+
/** Passed through untouched to a remote lane's `resolve`, for what only its host knows. */
|
|
21
|
+
host?: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** A chunk's frames so far, and how its time split between drawing and waiting on the encoder. */
|
|
24
|
+
export interface LaneReport {
|
|
25
|
+
frames: number;
|
|
26
|
+
busyMs: number;
|
|
27
|
+
waitMs: number;
|
|
28
|
+
/** Render issues since the lane's last report. */
|
|
29
|
+
issues?: RenderIssue[];
|
|
30
|
+
}
|
|
31
|
+
/** One lane, open on one export. */
|
|
32
|
+
export interface LaneSession {
|
|
33
|
+
/** Render the chunk's frames to a complete, silent segment file. */
|
|
34
|
+
render(chunk: ExportChunk, onReport: (report: LaneReport) => void, signal: AbortSignal): Promise<Uint8Array>;
|
|
35
|
+
close(): void;
|
|
36
|
+
}
|
|
37
|
+
/** Something that renders chunks: this window, or another realm across a port. */
|
|
38
|
+
export interface ExportLane {
|
|
39
|
+
open(spec: SegmentSpec, signal: AbortSignal): Promise<LaneSession>;
|
|
40
|
+
}
|
|
41
|
+
/** What a realm renders with: its own registry, and where its renderer's WASM is served. */
|
|
42
|
+
export interface LaneEnvironment extends RendererWasmOption {
|
|
43
|
+
registry?: Registry;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=lane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lane.d.ts","sourceRoot":"","sources":["../../src/parallel/lane.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACvG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C;;;;GAIG;AACH,MAAM,WAAW,WAAW;IACxB,qCAAqC;IACrC,OAAO,EAAE,eAAe,CAAC;IACzB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,qBAAqB,CAAC;IAChC,2FAA2F;IAC3F,IAAI,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,kGAAkG;AAClG,MAAM,WAAW,UAAU;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;CAC1B;AAED,oCAAoC;AACpC,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7G,KAAK,IAAI,IAAI,CAAC;CACjB;AAED,kFAAkF;AAClF,MAAM,WAAW,UAAU;IACvB,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACtE;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACvD,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACvB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lane.js","sourceRoot":"","sources":["../../src/parallel/lane.ts"],"names":[],"mappings":"","sourcesContent":["import type { AssetManifest, ProjectDocument, Registry, RenderIssue, Theme } from \"@motionscript/core\";\nimport type { RendererWasmOption } from \"../renderer\";\nimport type { ResolvedVideoEncoding } from \"../exporter\";\nimport type { VideoFormat } from \"../formats\";\nimport type { ExportChunk } from \"./plan\";\n\n/**\n * Everything a lane needs to render chunks of one export. Plain data, so it\n * crosses a `MessagePort` intact; the encoding is pinned here because segments\n * encoded differently cannot be joined.\n */\nexport interface SegmentSpec {\n /** Its fps and viewport resolved. */\n project: ProjectDocument;\n manifest: AssetManifest;\n theme?: Theme;\n scale: number;\n supersample: number;\n format: VideoFormat;\n encoding: ResolvedVideoEncoding;\n /** Passed through untouched to a remote lane's `resolve`, for what only its host knows. */\n host?: unknown;\n}\n\n/** A chunk's frames so far, and how its time split between drawing and waiting on the encoder. */\nexport interface LaneReport {\n frames: number;\n busyMs: number;\n waitMs: number;\n /** Render issues since the lane's last report. */\n issues?: RenderIssue[];\n}\n\n/** One lane, open on one export. */\nexport interface LaneSession {\n /** Render the chunk's frames to a complete, silent segment file. */\n render(chunk: ExportChunk, onReport: (report: LaneReport) => void, signal: AbortSignal): Promise<Uint8Array>;\n close(): void;\n}\n\n/** Something that renders chunks: this window, or another realm across a port. */\nexport interface ExportLane {\n open(spec: SegmentSpec, signal: AbortSignal): Promise<LaneSession>;\n}\n\n/** What a realm renders with: its own registry, and where its renderer's WASM is served. */\nexport interface LaneEnvironment extends RendererWasmOption {\n registry?: Registry;\n}\n"]}
|