@motionscript/browser 0.1.0-alpha.0 → 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/NOTICE +49 -0
  3. package/README.md +96 -24
  4. package/dist/asset-bytes.d.ts +14 -5
  5. package/dist/asset-bytes.d.ts.map +1 -1
  6. package/dist/asset-bytes.js +23 -5
  7. package/dist/asset-bytes.js.map +1 -1
  8. package/dist/audio/bus-graph.d.ts +12 -3
  9. package/dist/audio/bus-graph.d.ts.map +1 -1
  10. package/dist/audio/bus-graph.js +35 -31
  11. package/dist/audio/bus-graph.js.map +1 -1
  12. package/dist/audio/effect-graph.d.ts +62 -0
  13. package/dist/audio/effect-graph.d.ts.map +1 -0
  14. package/dist/audio/effect-graph.js +234 -0
  15. package/dist/audio/effect-graph.js.map +1 -0
  16. package/dist/audio/index.d.ts.map +1 -1
  17. package/dist/audio/index.js +2 -3
  18. package/dist/audio/index.js.map +1 -1
  19. package/dist/audio/loop-region.d.ts +31 -0
  20. package/dist/audio/loop-region.d.ts.map +1 -0
  21. package/dist/audio/loop-region.js +43 -0
  22. package/dist/audio/loop-region.js.map +1 -0
  23. package/dist/audio/mixer.d.ts +4 -4
  24. package/dist/audio/mixer.d.ts.map +1 -1
  25. package/dist/audio/mixer.js +43 -44
  26. package/dist/audio/mixer.js.map +1 -1
  27. package/dist/audio/player.d.ts +15 -9
  28. package/dist/audio/player.d.ts.map +1 -1
  29. package/dist/audio/player.js +60 -69
  30. package/dist/audio/player.js.map +1 -1
  31. package/dist/browser/THIRD_PARTY_LICENSES.txt +784 -0
  32. package/dist/browser/audio.js +1 -1
  33. package/dist/browser/chunks/chunk-F3AZKPHU.js +2 -0
  34. package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
  35. package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
  36. package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
  37. package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
  38. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
  39. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
  40. package/dist/browser/index.js +52 -1490
  41. package/dist/browser/index.js.map +4 -4
  42. package/dist/browser/manifest.json +21 -4
  43. package/dist/browser/ms_renderer_bg.wasm +0 -0
  44. package/dist/concat.d.ts +33 -7
  45. package/dist/concat.d.ts.map +1 -1
  46. package/dist/concat.js +117 -72
  47. package/dist/concat.js.map +1 -1
  48. package/dist/deliver.d.ts +10 -0
  49. package/dist/deliver.d.ts.map +1 -0
  50. package/dist/deliver.js +22 -0
  51. package/dist/deliver.js.map +1 -0
  52. package/dist/dynamic-nodes.d.ts.map +1 -1
  53. package/dist/dynamic-nodes.js +9 -12
  54. package/dist/dynamic-nodes.js.map +1 -1
  55. package/dist/engine.d.ts +43 -15
  56. package/dist/engine.d.ts.map +1 -1
  57. package/dist/engine.js +58 -36
  58. package/dist/engine.js.map +1 -1
  59. package/dist/export-audio.d.ts +36 -0
  60. package/dist/export-audio.d.ts.map +1 -0
  61. package/dist/export-audio.js +56 -0
  62. package/dist/export-audio.js.map +1 -0
  63. package/dist/exporter.d.ts +134 -15
  64. package/dist/exporter.d.ts.map +1 -1
  65. package/dist/exporter.js +135 -90
  66. package/dist/exporter.js.map +1 -1
  67. package/dist/formats.d.ts +36 -0
  68. package/dist/formats.d.ts.map +1 -0
  69. package/dist/formats.js +83 -0
  70. package/dist/formats.js.map +1 -0
  71. package/dist/frame-loop.d.ts +190 -0
  72. package/dist/frame-loop.d.ts.map +1 -0
  73. package/dist/frame-loop.js +304 -0
  74. package/dist/frame-loop.js.map +1 -0
  75. package/dist/index.d.ts +12 -10
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +18 -47
  78. package/dist/index.js.map +1 -1
  79. package/dist/mix-timeline-audio.d.ts +7 -7
  80. package/dist/mix-timeline-audio.d.ts.map +1 -1
  81. package/dist/mix-timeline-audio.js +11 -14
  82. package/dist/mix-timeline-audio.js.map +1 -1
  83. package/dist/parallel/export-parallel.d.ts +32 -0
  84. package/dist/parallel/export-parallel.d.ts.map +1 -0
  85. package/dist/parallel/export-parallel.js +114 -0
  86. package/dist/parallel/export-parallel.js.map +1 -0
  87. package/dist/parallel/governor.d.ts +57 -0
  88. package/dist/parallel/governor.d.ts.map +1 -0
  89. package/dist/parallel/governor.js +79 -0
  90. package/dist/parallel/governor.js.map +1 -0
  91. package/dist/parallel/lane.d.ts +45 -0
  92. package/dist/parallel/lane.d.ts.map +1 -0
  93. package/dist/parallel/lane.js +2 -0
  94. package/dist/parallel/lane.js.map +1 -0
  95. package/dist/parallel/lanes.d.ts +20 -0
  96. package/dist/parallel/lanes.d.ts.map +1 -0
  97. package/dist/parallel/lanes.js +152 -0
  98. package/dist/parallel/lanes.js.map +1 -0
  99. package/dist/parallel/plan.d.ts +23 -0
  100. package/dist/parallel/plan.d.ts.map +1 -0
  101. package/dist/parallel/plan.js +30 -0
  102. package/dist/parallel/plan.js.map +1 -0
  103. package/dist/parallel/run-lanes.d.ts +38 -0
  104. package/dist/parallel/run-lanes.d.ts.map +1 -0
  105. package/dist/parallel/run-lanes.js +164 -0
  106. package/dist/parallel/run-lanes.js.map +1 -0
  107. package/dist/parallel/session.d.ts +9 -0
  108. package/dist/parallel/session.d.ts.map +1 -0
  109. package/dist/parallel/session.js +79 -0
  110. package/dist/parallel/session.js.map +1 -0
  111. package/dist/renderer.d.ts +22 -0
  112. package/dist/renderer.d.ts.map +1 -0
  113. package/dist/renderer.js +23 -0
  114. package/dist/renderer.js.map +1 -0
  115. package/dist/screenshot.d.ts +6 -6
  116. package/dist/screenshot.d.ts.map +1 -1
  117. package/dist/screenshot.js +4 -5
  118. package/dist/screenshot.js.map +1 -1
  119. package/dist/still.d.ts +32 -24
  120. package/dist/still.d.ts.map +1 -1
  121. package/dist/still.js +54 -54
  122. package/dist/still.js.map +1 -1
  123. package/dist/ticker.d.ts +41 -9
  124. package/dist/ticker.d.ts.map +1 -1
  125. package/dist/ticker.js +113 -41
  126. package/dist/ticker.js.map +1 -1
  127. package/package.json +11 -10
  128. package/dist/audio/filter-graph.d.ts +0 -58
  129. package/dist/audio/filter-graph.d.ts.map +0 -1
  130. package/dist/audio/filter-graph.js +0 -235
  131. package/dist/audio/filter-graph.js.map +0 -1
  132. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +0 -2
  133. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +0 -7
  134. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +0 -2
  135. package/dist/browser/chunks/chunk-CTLBXZSU.js +0 -4119
  136. package/dist/browser/chunks/chunk-CTLBXZSU.js.map +0 -7
  137. package/dist/browser/chunks/chunk-FQ32PDIU.js +0 -2
  138. package/dist/browser/chunks/chunk-FQ32PDIU.js.map +0 -7
  139. package/dist/browser/chunks/chunk-FY3BTKY4.js +0 -2
  140. package/dist/browser/chunks/chunk-FY3BTKY4.js.map +0 -7
  141. package/dist/browser/chunks/chunk-NSFLRI4P.js +0 -2
  142. package/dist/browser/chunks/chunk-NSFLRI4P.js.map +0 -7
  143. package/dist/browser/chunks/three.module-Z4EOVUWF.js +0 -2
  144. package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +0 -7
  145. package/dist/decode/image-levels.d.ts +0 -137
  146. package/dist/decode/image-levels.d.ts.map +0 -1
  147. package/dist/decode/image-levels.js +0 -477
  148. package/dist/decode/image-levels.js.map +0 -1
  149. package/dist/decode/levels.d.ts +0 -37
  150. package/dist/decode/levels.d.ts.map +0 -1
  151. package/dist/decode/levels.js +0 -68
  152. package/dist/decode/levels.js.map +0 -1
  153. package/dist/decode/video-levels.d.ts +0 -37
  154. package/dist/decode/video-levels.d.ts.map +0 -1
  155. package/dist/decode/video-levels.js +0 -95
  156. package/dist/decode/video-levels.js.map +0 -1
  157. package/dist/getter.d.ts +0 -10
  158. package/dist/getter.d.ts.map +0 -1
  159. package/dist/getter.js +0 -38
  160. package/dist/getter.js.map +0 -1
  161. package/dist/measure-context.d.ts +0 -12
  162. package/dist/measure-context.d.ts.map +0 -1
  163. package/dist/measure-context.js +0 -12
  164. package/dist/measure-context.js.map +0 -1
  165. package/dist/output.d.ts +0 -7
  166. package/dist/output.d.ts.map +0 -1
  167. package/dist/output.js +0 -6
  168. package/dist/output.js.map +0 -1
  169. package/dist/render-context.d.ts +0 -30
  170. package/dist/render-context.d.ts.map +0 -1
  171. package/dist/render-context.js +0 -55
  172. package/dist/render-context.js.map +0 -1
  173. package/dist/storage-adapter.d.ts +0 -367
  174. package/dist/storage-adapter.d.ts.map +0 -1
  175. package/dist/storage-adapter.js +0 -1278
  176. package/dist/storage-adapter.js.map +0 -1
  177. package/dist/three/renderer.d.ts +0 -48
  178. package/dist/three/renderer.d.ts.map +0 -1
  179. package/dist/three/renderer.js +0 -177
  180. package/dist/three/renderer.js.map +0 -1
  181. /package/dist/browser/chunks/{SkeletonUtils-XYDMTGTM.js.map → chunk-KLXMRKTL.js.map} +0 -0
package/dist/engine.d.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { Engine, EngineOptions, type DynamicNodeConfig, type NodeConstructor, type NodeTypeMap } from "@motionscript/core";
2
- import { WebStorageAdapter } from "./storage-adapter";
1
+ import { Engine, EngineOptions, EngineRenderContext, MeasureContext2D, PlatformDeps, StorageAdapter, type DynamicNodeConfig, type NodeConstructor, type NodeTypeMap } from "@motionscript/core";
2
+ import type { WgpuStorageAdapter } from "@motionscript/renderer";
3
+ import { type RendererWasmOption } from "./renderer";
3
4
  import { type AudioEncoderOptions, type VideoEncoderOptions } from "./exporter";
4
- export interface BrowserEngineOptions extends Omit<EngineOptions, "nodes" | "dynamicNodes"> {
5
+ import { type ExportFormat } from "./formats";
6
+ export interface BrowserEngineOptions extends Omit<EngineOptions, "nodes" | "dynamicNodes">, RendererWasmOption {
5
7
  /**
6
- * Node types any document opened here may name — decorated classes as an
7
- * array or a record naming each key outright, same as `EngineOptions`,
8
- * plus a TS/JSX source string in the **array** form only. A string
9
- * self-declares its key via `@node(...)` in its own source — the record
10
- * form stays class-only, so there is exactly one place a dynamic node's
11
- * key comes from, not two that could disagree.
8
+ * Node types any document opened here may name — components as an array
9
+ * or a record naming each key outright, same as `EngineOptions`, plus a
10
+ * TS source string in the **array** form only. A string default-exports a
11
+ * component, whose `key` is its key — the record form takes no string, so
12
+ * there is exactly one place a dynamic node's key comes from, not two that
13
+ * could disagree.
12
14
  */
13
15
  nodes?: readonly (NodeConstructor | string)[] | NodeTypeMap;
14
16
  /**
@@ -22,18 +24,43 @@ export interface BrowserEngineOptions extends Omit<EngineOptions, "nodes" | "dyn
22
24
  dynamicNodes?: Partial<DynamicNodeConfig>;
23
25
  /**
24
26
  * Where `esbuild-wasm`'s own `.wasm` binary is served — only read if the
25
- * default compiler is actually used. Mirrors `wasmUrl` for CanvasKit.
27
+ * default compiler is actually used.
26
28
  */
27
29
  compilerWasmUrl?: string;
28
- /** URL of the CanvasKit `.wasm` binary, forwarded to `getCanvasKit`. */
29
- wasmUrl?: string;
30
30
  /** Default video encoder settings for `session.export()`. Overridable per call. */
31
31
  video?: VideoEncoderOptions;
32
32
  /** Default audio encoder settings for `session.export()`. Overridable per call. */
33
33
  audio?: AudioEncoderOptions;
34
34
  }
35
+ /** What a session's frames are drawn with, and the storage and measurement it reads from. */
36
+ export interface BrowserRenderer<TStorage extends StorageAdapter = StorageAdapter> {
37
+ createStorageAdapter(deps: PlatformDeps): TStorage;
38
+ createRenderContext(storage: TStorage): BrowserRenderContext;
39
+ createMeasureContext(storage: TStorage): MeasureContext2D;
40
+ dispose?(): void;
41
+ }
42
+ /** A render context as this package drives it: an engine one, plus what exports and stills ask of it. */
43
+ export type BrowserRenderContext = EngineRenderContext & {
44
+ /**
45
+ * Settles once the surface `mount` began can draw. A backend whose surface
46
+ * comes up asynchronously (a GPU device) must have it, or the first frame
47
+ * draws onto nothing.
48
+ */
49
+ whenMounted?(): Promise<void>;
50
+ /**
51
+ * The frame last drawn, copied into a fresh 2D canvas, or `undefined` before
52
+ * one was. What a still encodes; a WebGPU canvas is readable only in the task
53
+ * that drew it, so a backend may draw it again here.
54
+ */
55
+ snapshotCanvas?(): HTMLCanvasElement | undefined;
56
+ };
35
57
  /** What `session.export({ encode })` accepts in this environment. */
36
58
  export interface BrowserEncodeOptions {
59
+ /**
60
+ * Defaults to `'mp4'`. An audio format (`'wav'`, `'mp3'`, `'m4a'`) renders no
61
+ * frames and ignores `scale`, `frames`, `includeAudio`, `video` and `supersample`.
62
+ */
63
+ format?: ExportFormat;
37
64
  video?: VideoEncoderOptions;
38
65
  audio?: AudioEncoderOptions;
39
66
  supersample?: number;
@@ -42,9 +69,10 @@ export interface BrowserEncodeOptions {
42
69
  filename?: string;
43
70
  }
44
71
  /**
45
- * An engine backed by the browser: WebGL/Skia surfaces, Web Audio, WebCodecs.
72
+ * An engine backed by the browser: WebGPU surfaces, Web Audio, WebCodecs.
73
+ * Rejects with `WebGpuUnavailableError` where this browser has no WebGPU adapter.
46
74
  *
47
- * Async because CanvasKit is a wasm module and every seam below needs it. Await
75
+ * Async because the renderer is a wasm module and every seam below needs it. Await
48
76
  * once, at app start, and share the result — the caches underneath (registered
49
77
  * fonts, decoded images, shaped paragraphs) are the point of keeping one engine
50
78
  * rather than one per canvas.
@@ -55,5 +83,5 @@ export interface BrowserEncodeOptions {
55
83
  * session.playback!.play();
56
84
  * ```
57
85
  */
58
- export declare function createBrowserEngine(options?: BrowserEngineOptions): Promise<Engine<WebStorageAdapter>>;
86
+ export declare function createBrowserEngine(options?: BrowserEngineOptions): Promise<Engine<WgpuStorageAdapter>>;
59
87
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAEH,MAAM,EACN,aAAa,EAWb,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,WAAW,EACnB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAkC,KAAK,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGhH,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,cAAc,CAAC;IACvF;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,eAAe,GAAG,MAAM,CAAC,EAAE,GAAG,WAAW,CAAC;IAC5D;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1C;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,mFAAmF;IACnF,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACjC,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AA6FD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACrC,OAAO,GAAE,oBAAyB,GACnC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAgEpC"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,MAAM,EACN,aAAa,EAEb,mBAAmB,EAOnB,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,WAAW,EACnB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAInE,OAAO,EAAkC,KAAK,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEhH,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAG7D,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,cAAc,CAAC,EAAE,kBAAkB;IAC3G;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,eAAe,GAAG,MAAM,CAAC,EAAE,GAAG,WAAW,CAAC;IAC5D;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1C;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,mFAAmF;IACnF,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED,6FAA6F;AAC7F,MAAM,WAAW,eAAe,CAAC,QAAQ,SAAS,cAAc,GAAG,cAAc;IAC7E,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAAC;IACnD,mBAAmB,CAAC,OAAO,EAAE,QAAQ,GAAG,oBAAoB,CAAC;IAC7D,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,gBAAgB,CAAC;IAC1D,OAAO,CAAC,IAAI,IAAI,CAAC;CACpB;AAED,yGAAyG;AACzG,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG;IACrD;;;;OAIG;IACH,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B;;;;OAIG;IACH,cAAc,CAAC,IAAI,iBAAiB,GAAG,SAAS,CAAC;CACpD,CAAC;AAEF,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACjC;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AA2HD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACrC,OAAO,GAAE,oBAAyB,GACnC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAsDrC"}
package/dist/engine.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { Engine, } from "@motionscript/core";
2
- import { getCanvasKit } from "./getter";
2
+ import { loadRenderer } from "./renderer";
3
3
  import { resolveDynamicNodes } from "./dynamic-nodes";
4
4
  import { WebAudioSink } from "./audio/player";
5
5
  import { WebTicker } from "./ticker";
6
- import { WebMeasureContext } from "./measure-context";
7
- import { WebRenderContext } from "./render-context";
8
- import { WebStorageAdapter } from "./storage-adapter";
9
6
  import { exportVideo as exportVideoFile } from "./exporter";
7
+ import { exportAudio } from "./export-audio";
8
+ import { isAudioFormat } from "./formats";
10
9
  import { StillRenderer } from "./still";
11
10
  /**
12
11
  * The browser's {@link Exporter}: WebCodecs + mediabunny for video, the
@@ -16,28 +15,44 @@ import { StillRenderer } from "./still";
16
15
  * adapter's viewport clamps the resolution images decode at — `StorageAdapter`
17
16
  * documents that it has no setter for exactly this reason — so handing an export
18
17
  * the preview's adapter would paint a 2× render from decodes sized for 1×, and
19
- * the result would be soft in a way nothing reports. What is safely shared is
20
- * CanvasKit itself, which `getCanvasKit` already memoizes process-wide.
18
+ * the result would be soft in a way nothing reports. What is shared is the
19
+ * engine's renderer backend, the same one its sessions draw with.
21
20
  */
22
21
  class WebExporter {
23
- canvasKit;
22
+ backend;
24
23
  defaults;
25
- wasmUrl;
26
24
  still = null;
27
25
  stillRegistry = null;
28
- constructor(canvasKit, defaults, wasmUrl) {
29
- this.canvasKit = canvasKit;
26
+ /** Settles once every still asked for so far has been encoded. */
27
+ stillQueue = Promise.resolve();
28
+ constructor(backend, defaults) {
29
+ this.backend = backend;
30
30
  this.defaults = defaults;
31
- this.wasmUrl = wasmUrl;
32
31
  }
33
32
  async exportVideo(request) {
34
33
  const encode = (request.encode ?? {});
34
+ if (encode.format && isAudioFormat(encode.format)) {
35
+ return exportAudio({
36
+ document: request.project,
37
+ registry: request.registry,
38
+ manifest: request.manifest,
39
+ format: encode.format,
40
+ audio: encode.audio ?? this.defaults.audio,
41
+ filename: encode.filename,
42
+ returnBytes: encode.download !== true,
43
+ onProgress: request.onProgress,
44
+ signal: request.signal,
45
+ rendererWasmUrl: this.defaults.rendererWasmUrl,
46
+ });
47
+ }
35
48
  return exportVideoFile({
36
49
  document: request.project,
37
50
  registry: request.registry,
38
51
  scale: request.scale,
39
52
  manifest: request.manifest,
40
53
  includeAudio: request.includeAudio,
54
+ frames: request.frames,
55
+ format: encode.format,
41
56
  video: encode.video ?? this.defaults.video,
42
57
  audio: encode.audio ?? this.defaults.audio,
43
58
  supersample: encode.supersample,
@@ -46,14 +61,30 @@ class WebExporter {
46
61
  // the DOM anchor path this function offers a page.
47
62
  returnBytes: encode.download !== true,
48
63
  onProgress: request.onProgress,
64
+ onIssue: request.onIssue,
49
65
  signal: request.signal,
50
- wasmUrl: this.wasmUrl,
66
+ rendererWasmUrl: this.defaults.rendererWasmUrl,
51
67
  });
52
68
  }
53
- async exportStill(request) {
69
+ /**
70
+ * Queued, rather than a renderer per call: the one renderer is what keeps a
71
+ * GPU surface and the decode cache warm between stills, and its draw and the
72
+ * encode of it are two steps another call's draw would land between.
73
+ */
74
+ exportStill(request) {
75
+ const still = this.stillQueue.then(() => this.drawStill(request));
76
+ this.stillQueue = still.catch(() => undefined);
77
+ return still;
78
+ }
79
+ dispose() {
80
+ this.still?.dispose();
81
+ this.still = null;
82
+ }
83
+ async drawStill(request) {
54
84
  const renderer = this.renderer(request);
55
85
  const drawn = await renderer.render(request.project, {
56
86
  scale: request.scale,
87
+ onIssue: request.onIssue,
57
88
  ...frameOption(request.frame),
58
89
  });
59
90
  const bytes = await renderer.toBytes({
@@ -62,10 +93,6 @@ class WebExporter {
62
93
  });
63
94
  return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };
64
95
  }
65
- dispose() {
66
- this.still?.dispose();
67
- this.still = null;
68
- }
69
96
  /**
70
97
  * One `StillRenderer`, reused across calls and re-viewported when the ask
71
98
  * changes — `setViewport` is a no-op when the size already matches, and a
@@ -79,7 +106,7 @@ class WebExporter {
79
106
  // A renderer compiles against the registry it was built with.
80
107
  if (this.still && this.stillRegistry !== registry)
81
108
  this.dispose();
82
- this.still ??= new StillRenderer(this.canvasKit, {
109
+ this.still ??= new StillRenderer(this.backend, {
83
110
  viewport: project.viewport,
84
111
  fps: project.fps,
85
112
  manifest: request.manifest,
@@ -99,9 +126,10 @@ function frameOption(frame) {
99
126
  return { frame: frame.kind };
100
127
  }
101
128
  /**
102
- * An engine backed by the browser: WebGL/Skia surfaces, Web Audio, WebCodecs.
129
+ * An engine backed by the browser: WebGPU surfaces, Web Audio, WebCodecs.
130
+ * Rejects with `WebGpuUnavailableError` where this browser has no WebGPU adapter.
103
131
  *
104
- * Async because CanvasKit is a wasm module and every seam below needs it. Await
132
+ * Async because the renderer is a wasm module and every seam below needs it. Await
105
133
  * once, at app start, and share the result — the caches underneath (registered
106
134
  * fonts, decoded images, shaped paragraphs) are the point of keeping one engine
107
135
  * rather than one per canvas.
@@ -129,35 +157,29 @@ export async function createBrowserEngine(options = {}) {
129
157
  // `engine.registerNode(source)` works even when `nodes` carried no
130
158
  // string at boot.
131
159
  const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);
132
- const canvasKit = await getCanvasKit(options.wasmUrl);
133
- const exporter = new WebExporter(canvasKit, { video: options.video, audio: options.audio }, options.wasmUrl);
160
+ const renderer = await loadRenderer(options);
161
+ const exporter = new WebExporter(renderer, { video: options.video, audio: options.audio, rendererWasmUrl: options.rendererWasmUrl });
134
162
  const platform = {
135
- createStorageAdapter({ catalog, viewport, fps }) {
136
- return new WebStorageAdapter(canvasKit, catalog, viewport, fps);
137
- },
138
- createRenderContext(storage) {
139
- return new WebRenderContext(canvasKit, storage);
140
- },
141
- createMeasureContext(storage) {
142
- return new WebMeasureContext(storage);
143
- },
163
+ createStorageAdapter: (deps) => renderer.createStorageAdapter(deps),
164
+ createRenderContext: (storage) => renderer.createRenderContext(storage),
165
+ createMeasureContext: (storage) => renderer.createMeasureContext(storage),
144
166
  createAudioSink() {
145
167
  return new WebAudioSink();
146
168
  },
147
169
  createTicker(fps, audioDevice) {
148
170
  // Shares the device's own `AudioContext`: two independent timebases
149
171
  // in one session drift against each other, and sound is the one that
150
- // cannot be nudged back.
151
- return new WebTicker({
152
- context: audioDevice.getContext(),
153
- fps,
154
- });
172
+ // cannot be nudged back. A getter, so neither creates it before it
173
+ // is needed (the first play, or the first sound to decode).
174
+ const sink = audioDevice;
175
+ return new WebTicker({ context: () => sink.getContext(), fps });
155
176
  },
156
177
  createExporter() {
157
178
  return exporter;
158
179
  },
159
180
  dispose() {
160
181
  exporter.dispose();
182
+ renderer.dispose?.();
161
183
  },
162
184
  };
163
185
  const engine = new Engine(platform, { ...options, nodes: classNodes, dynamicNodes });
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CanvasKit } from \"@motionscript/canvaskit\";\nimport {\n AudioSink,\n Engine,\n EngineOptions,\n EnginePlatform,\n EngineRenderContext,\n Exporter,\n ExportStillRequest,\n ExportVideoRequest,\n ExportedStill,\n Registry,\n Ticker,\n MeasureContext2D,\n PlatformDeps,\n type DynamicNodeConfig,\n type NodeConstructor,\n type NodeTypeMap,\n} from \"@motionscript/core\";\n\nimport { getCanvasKit } from \"./getter\";\nimport { resolveDynamicNodes } from \"./dynamic-nodes\";\nimport { WebAudioSink } from \"./audio/player\";\nimport { WebTicker } from \"./ticker\";\nimport { WebMeasureContext } from \"./measure-context\";\nimport { WebRenderContext } from \"./render-context\";\nimport { WebStorageAdapter } from \"./storage-adapter\";\nimport { exportVideo as exportVideoFile, type AudioEncoderOptions, type VideoEncoderOptions } from \"./exporter\";\nimport { StillRenderer } from \"./still\";\n\nexport interface BrowserEngineOptions extends Omit<EngineOptions, \"nodes\" | \"dynamicNodes\"> {\n /**\n * Node types any document opened here may name — decorated classes as an\n * array or a record naming each key outright, same as `EngineOptions`,\n * plus a TS/JSX source string in the **array** form only. A string\n * self-declares its key via `@node(...)` in its own source — the record\n * form stays class-only, so there is exactly one place a dynamic node's\n * key comes from, not two that could disagree.\n */\n nodes?: readonly (NodeConstructor | string)[] | NodeTypeMap;\n /**\n * Enables a source string in `nodes`, and `engine.registerNode(source)`\n * at any point afterward. Any field left unset falls back to `browser`'s\n * own default — `@motionscript/core/sdk` for `sdk`, a lazily-loaded\n * `esbuild-wasm` for `compile` — so this never needs to be passed at all\n * for the common case. `modules` has no default: only the host knows\n * which loaded packages a script it compiles may reach.\n */\n dynamicNodes?: Partial<DynamicNodeConfig>;\n /**\n * Where `esbuild-wasm`'s own `.wasm` binary is served — only read if the\n * default compiler is actually used. Mirrors `wasmUrl` for CanvasKit.\n */\n compilerWasmUrl?: string;\n /** URL of the CanvasKit `.wasm` binary, forwarded to `getCanvasKit`. */\n wasmUrl?: string;\n /** Default video encoder settings for `session.export()`. Overridable per call. */\n video?: VideoEncoderOptions;\n /** Default audio encoder settings for `session.export()`. Overridable per call. */\n audio?: AudioEncoderOptions;\n}\n\n/** What `session.export({ encode })` accepts in this environment. */\nexport interface BrowserEncodeOptions {\n video?: VideoEncoderOptions;\n audio?: AudioEncoderOptions;\n supersample?: number;\n /** Set false to get the bytes back instead of triggering a browser download. */\n download?: boolean;\n filename?: string;\n}\n\n/**\n * The browser's {@link Exporter}: WebCodecs + mediabunny for video, the\n * browser's own image codecs for a still.\n *\n * **It does not reuse a session's storage adapter, and that is deliberate.** An\n * adapter's viewport clamps the resolution images decode at — `StorageAdapter`\n * documents that it has no setter for exactly this reason — so handing an export\n * the preview's adapter would paint a 2× render from decodes sized for 1×, and\n * the result would be soft in a way nothing reports. What is safely shared is\n * CanvasKit itself, which `getCanvasKit` already memoizes process-wide.\n */\nclass WebExporter implements Exporter {\n private still: StillRenderer | null = null;\n private stillRegistry: Registry | null = null;\n\n constructor(\n private readonly canvasKit: CanvasKit,\n private readonly defaults: { video?: VideoEncoderOptions; audio?: AudioEncoderOptions },\n private readonly wasmUrl?: string,\n ) { }\n\n async exportVideo(request: ExportVideoRequest): Promise<Uint8Array | void> {\n const encode = (request.encode ?? {}) as BrowserEncodeOptions;\n return exportVideoFile({\n document: request.project,\n registry: request.registry,\n scale: request.scale,\n manifest: request.manifest,\n includeAudio: request.includeAudio,\n video: encode.video ?? this.defaults.video,\n audio: encode.audio ?? this.defaults.audio,\n supersample: encode.supersample,\n filename: encode.filename,\n // A `Session` asks for bytes; only an explicit `download: true` takes\n // the DOM anchor path this function offers a page.\n returnBytes: encode.download !== true,\n onProgress: request.onProgress,\n signal: request.signal,\n wasmUrl: this.wasmUrl,\n });\n }\n\n async exportStill(request: ExportStillRequest): Promise<ExportedStill> {\n const renderer = this.renderer(request);\n const drawn = await renderer.render(request.project, {\n scale: request.scale,\n ...frameOption(request.frame),\n });\n const bytes = await renderer.toBytes({\n format: request.format,\n quality: request.quality,\n });\n return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };\n }\n\n dispose(): void {\n this.still?.dispose();\n this.still = null;\n }\n\n /**\n * One `StillRenderer`, reused across calls and re-viewported when the ask\n * changes — `setViewport` is a no-op when the size already matches, and a\n * full reset when it does not, which is the same trade the class documents.\n */\n private renderer(request: ExportStillRequest): StillRenderer {\n const { project, registry } = request;\n if (project.viewport === undefined || project.fps === undefined) {\n throw new Error(\"exportStill takes a project whose fps and viewport are resolved — see Session.\");\n }\n // A renderer compiles against the registry it was built with.\n if (this.still && this.stillRegistry !== registry) this.dispose();\n this.still ??= new StillRenderer(this.canvasKit, {\n viewport: project.viewport,\n fps: project.fps,\n manifest: request.manifest,\n registry,\n });\n this.stillRegistry = registry;\n this.still.setViewport(project.viewport);\n if (request.manifest) this.still.setManifest(request.manifest);\n return this.still;\n }\n}\n\n/** `ExportStillRequest.frame` in the shape `StillRenderer.render` accepts. */\nfunction frameOption(frame: ExportStillRequest[\"frame\"]): { frame?: number | \"first\" | \"last\" } {\n if (frame.kind === \"frame\") return { frame: frame.frame };\n return { frame: frame.kind };\n}\n\n/**\n * An engine backed by the browser: WebGL/Skia surfaces, Web Audio, WebCodecs.\n *\n * Async because CanvasKit is a wasm module and every seam below needs it. Await\n * once, at app start, and share the result — the caches underneath (registered\n * fonts, decoded images, shaped paragraphs) are the point of keeping one engine\n * rather than one per canvas.\n *\n * ```ts\n * const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });\n * const session = engine.open({ document, target: canvasEl });\n * session.playback!.play();\n * ```\n */\nexport async function createBrowserEngine(\n options: BrowserEngineOptions = {},\n): Promise<Engine<WebStorageAdapter>> {\n const rawNodes = options.nodes;\n // `Array.isArray` doesn't narrow a `readonly T[] | U` union cleanly against\n // its `arg is any[]` (mutable) predicate — the `.filter` predicate below\n // already guarantees no string survives, so the cast is just recovering\n // what the runtime check already proved.\n const classNodes = (\n Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is NodeConstructor => typeof n !== \"string\")\n : rawNodes\n ) as NodeTypeMap | readonly NodeConstructor[] | undefined;\n const sourceNodes = Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is string => typeof n === \"string\")\n : [];\n // Cheap — see dynamic-nodes.ts: no WASM import happens here, only on\n // `compile`'s first actual call. Attached unconditionally so a later\n // `engine.registerNode(source)` works even when `nodes` carried no\n // string at boot.\n const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);\n\n const canvasKit = await getCanvasKit(options.wasmUrl);\n const exporter = new WebExporter(\n canvasKit,\n { video: options.video, audio: options.audio },\n options.wasmUrl,\n );\n\n const platform: EnginePlatform<WebStorageAdapter> = {\n createStorageAdapter({ catalog, viewport, fps }: PlatformDeps): WebStorageAdapter {\n return new WebStorageAdapter(canvasKit, catalog, viewport, fps);\n },\n createRenderContext(storage: WebStorageAdapter): EngineRenderContext {\n return new WebRenderContext(canvasKit, storage);\n },\n createMeasureContext(storage: WebStorageAdapter): MeasureContext2D {\n return new WebMeasureContext(storage);\n },\n createAudioSink(): AudioSink {\n return new WebAudioSink();\n },\n createTicker(fps: number, audioDevice: AudioSink): Ticker {\n // Shares the device's own `AudioContext`: two independent timebases\n // in one session drift against each other, and sound is the one that\n // cannot be nudged back.\n return new WebTicker({\n context: (audioDevice as WebAudioSink).getContext(),\n fps,\n });\n },\n createExporter(): Exporter {\n return exporter;\n },\n dispose(): void {\n exporter.dispose();\n },\n };\n\n const engine = new Engine<WebStorageAdapter>(platform, { ...options, nodes: classNodes, dynamicNodes });\n for (const source of sourceNodes) {\n // Sequential — one script's error shouldn't mask another's, and\n // registration order should match array order for determinism.\n await engine.registerNode(source);\n }\n return engine;\n}\n"]}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AudioSink,\n Engine,\n EngineOptions,\n EnginePlatform,\n EngineRenderContext,\n Exporter,\n ExportStillRequest,\n ExportVideoRequest,\n ExportedStill,\n Registry,\n Ticker,\n MeasureContext2D,\n PlatformDeps,\n StorageAdapter,\n type DynamicNodeConfig,\n type NodeConstructor,\n type NodeTypeMap,\n} from \"@motionscript/core\";\n\nimport type { WgpuStorageAdapter } from \"@motionscript/renderer\";\nimport { loadRenderer, type RendererWasmOption } from \"./renderer\";\nimport { resolveDynamicNodes } from \"./dynamic-nodes\";\nimport { WebAudioSink } from \"./audio/player\";\nimport { WebTicker } from \"./ticker\";\nimport { exportVideo as exportVideoFile, type AudioEncoderOptions, type VideoEncoderOptions } from \"./exporter\";\nimport { exportAudio } from \"./export-audio\";\nimport { isAudioFormat, type ExportFormat } from \"./formats\";\nimport { StillRenderer } from \"./still\";\n\nexport interface BrowserEngineOptions extends Omit<EngineOptions, \"nodes\" | \"dynamicNodes\">, RendererWasmOption {\n /**\n * Node types any document opened here may name — components as an array\n * or a record naming each key outright, same as `EngineOptions`, plus a\n * TS source string in the **array** form only. A string default-exports a\n * component, whose `key` is its key — the record form takes no string, so\n * there is exactly one place a dynamic node's key comes from, not two that\n * could disagree.\n */\n nodes?: readonly (NodeConstructor | string)[] | NodeTypeMap;\n /**\n * Enables a source string in `nodes`, and `engine.registerNode(source)`\n * at any point afterward. Any field left unset falls back to `browser`'s\n * own default — `@motionscript/core/sdk` for `sdk`, a lazily-loaded\n * `esbuild-wasm` for `compile` — so this never needs to be passed at all\n * for the common case. `modules` has no default: only the host knows\n * which loaded packages a script it compiles may reach.\n */\n dynamicNodes?: Partial<DynamicNodeConfig>;\n /**\n * Where `esbuild-wasm`'s own `.wasm` binary is served — only read if the\n * default compiler is actually used.\n */\n compilerWasmUrl?: string;\n /** Default video encoder settings for `session.export()`. Overridable per call. */\n video?: VideoEncoderOptions;\n /** Default audio encoder settings for `session.export()`. Overridable per call. */\n audio?: AudioEncoderOptions;\n}\n\n/** What a session's frames are drawn with, and the storage and measurement it reads from. */\nexport interface BrowserRenderer<TStorage extends StorageAdapter = StorageAdapter> {\n createStorageAdapter(deps: PlatformDeps): TStorage;\n createRenderContext(storage: TStorage): BrowserRenderContext;\n createMeasureContext(storage: TStorage): MeasureContext2D;\n dispose?(): void;\n}\n\n/** A render context as this package drives it: an engine one, plus what exports and stills ask of it. */\nexport type BrowserRenderContext = EngineRenderContext & {\n /**\n * Settles once the surface `mount` began can draw. A backend whose surface\n * comes up asynchronously (a GPU device) must have it, or the first frame\n * draws onto nothing.\n */\n whenMounted?(): Promise<void>;\n /**\n * The frame last drawn, copied into a fresh 2D canvas, or `undefined` before\n * one was. What a still encodes; a WebGPU canvas is readable only in the task\n * that drew it, so a backend may draw it again here.\n */\n snapshotCanvas?(): HTMLCanvasElement | undefined;\n};\n\n/** What `session.export({ encode })` accepts in this environment. */\nexport interface BrowserEncodeOptions {\n /**\n * Defaults to `'mp4'`. An audio format (`'wav'`, `'mp3'`, `'m4a'`) renders no\n * frames and ignores `scale`, `frames`, `includeAudio`, `video` and `supersample`.\n */\n format?: ExportFormat;\n video?: VideoEncoderOptions;\n audio?: AudioEncoderOptions;\n supersample?: number;\n /** Set false to get the bytes back instead of triggering a browser download. */\n download?: boolean;\n filename?: string;\n}\n\n/**\n * The browser's {@link Exporter}: WebCodecs + mediabunny for video, the\n * browser's own image codecs for a still.\n *\n * **It does not reuse a session's storage adapter, and that is deliberate.** An\n * adapter's viewport clamps the resolution images decode at — `StorageAdapter`\n * documents that it has no setter for exactly this reason — so handing an export\n * the preview's adapter would paint a 2× render from decodes sized for 1×, and\n * the result would be soft in a way nothing reports. What is shared is the\n * engine's renderer backend, the same one its sessions draw with.\n */\nclass WebExporter implements Exporter {\n private still: StillRenderer | null = null;\n private stillRegistry: Registry | null = null;\n /** Settles once every still asked for so far has been encoded. */\n private stillQueue: Promise<unknown> = Promise.resolve();\n\n constructor(\n private readonly backend: BrowserRenderer,\n private readonly defaults: { video?: VideoEncoderOptions; audio?: AudioEncoderOptions } & RendererWasmOption,\n ) { }\n\n async exportVideo(request: ExportVideoRequest): Promise<Uint8Array | void> {\n const encode = (request.encode ?? {}) as BrowserEncodeOptions;\n if (encode.format && isAudioFormat(encode.format)) {\n return exportAudio({\n document: request.project,\n registry: request.registry,\n manifest: request.manifest,\n format: encode.format,\n audio: encode.audio ?? this.defaults.audio,\n filename: encode.filename,\n returnBytes: encode.download !== true,\n onProgress: request.onProgress,\n signal: request.signal,\n rendererWasmUrl: this.defaults.rendererWasmUrl,\n });\n }\n return exportVideoFile({\n document: request.project,\n registry: request.registry,\n scale: request.scale,\n manifest: request.manifest,\n includeAudio: request.includeAudio,\n frames: request.frames,\n format: encode.format,\n video: encode.video ?? this.defaults.video,\n audio: encode.audio ?? this.defaults.audio,\n supersample: encode.supersample,\n filename: encode.filename,\n // A `Session` asks for bytes; only an explicit `download: true` takes\n // the DOM anchor path this function offers a page.\n returnBytes: encode.download !== true,\n onProgress: request.onProgress,\n onIssue: request.onIssue,\n signal: request.signal,\n rendererWasmUrl: this.defaults.rendererWasmUrl,\n });\n }\n\n /**\n * Queued, rather than a renderer per call: the one renderer is what keeps a\n * GPU surface and the decode cache warm between stills, and its draw and the\n * encode of it are two steps another call's draw would land between.\n */\n exportStill(request: ExportStillRequest): Promise<ExportedStill> {\n const still = this.stillQueue.then(() => this.drawStill(request));\n this.stillQueue = still.catch(() => undefined);\n return still;\n }\n\n dispose(): void {\n this.still?.dispose();\n this.still = null;\n }\n\n private async drawStill(request: ExportStillRequest): Promise<ExportedStill> {\n const renderer = this.renderer(request);\n const drawn = await renderer.render(request.project, {\n scale: request.scale,\n onIssue: request.onIssue,\n ...frameOption(request.frame),\n });\n const bytes = await renderer.toBytes({\n format: request.format,\n quality: request.quality,\n });\n return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };\n }\n\n /**\n * One `StillRenderer`, reused across calls and re-viewported when the ask\n * changes — `setViewport` is a no-op when the size already matches, and a\n * full reset when it does not, which is the same trade the class documents.\n */\n private renderer(request: ExportStillRequest): StillRenderer {\n const { project, registry } = request;\n if (project.viewport === undefined || project.fps === undefined) {\n throw new Error(\"exportStill takes a project whose fps and viewport are resolved — see Session.\");\n }\n // A renderer compiles against the registry it was built with.\n if (this.still && this.stillRegistry !== registry) this.dispose();\n this.still ??= new StillRenderer(this.backend, {\n viewport: project.viewport,\n fps: project.fps,\n manifest: request.manifest,\n registry,\n });\n this.stillRegistry = registry;\n this.still.setViewport(project.viewport);\n if (request.manifest) this.still.setManifest(request.manifest);\n return this.still;\n }\n}\n\n/** `ExportStillRequest.frame` in the shape `StillRenderer.render` accepts. */\nfunction frameOption(frame: ExportStillRequest[\"frame\"]): { frame?: number | \"first\" | \"last\" } {\n if (frame.kind === \"frame\") return { frame: frame.frame };\n return { frame: frame.kind };\n}\n\n/**\n * An engine backed by the browser: WebGPU surfaces, Web Audio, WebCodecs.\n * Rejects with `WebGpuUnavailableError` where this browser has no WebGPU adapter.\n *\n * Async because the renderer is a wasm module and every seam below needs it. Await\n * once, at app start, and share the result — the caches underneath (registered\n * fonts, decoded images, shaped paragraphs) are the point of keeping one engine\n * rather than one per canvas.\n *\n * ```ts\n * const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });\n * const session = engine.open({ document, target: canvasEl });\n * session.playback!.play();\n * ```\n */\nexport async function createBrowserEngine(\n options: BrowserEngineOptions = {},\n): Promise<Engine<WgpuStorageAdapter>> {\n const rawNodes = options.nodes;\n // `Array.isArray` doesn't narrow a `readonly T[] | U` union cleanly against\n // its `arg is any[]` (mutable) predicate — the `.filter` predicate below\n // already guarantees no string survives, so the cast is just recovering\n // what the runtime check already proved.\n const classNodes = (\n Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is NodeConstructor => typeof n !== \"string\")\n : rawNodes\n ) as NodeTypeMap | readonly NodeConstructor[] | undefined;\n const sourceNodes = Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is string => typeof n === \"string\")\n : [];\n // Cheap — see dynamic-nodes.ts: no WASM import happens here, only on\n // `compile`'s first actual call. Attached unconditionally so a later\n // `engine.registerNode(source)` works even when `nodes` carried no\n // string at boot.\n const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);\n\n const renderer: BrowserRenderer<WgpuStorageAdapter> = await loadRenderer(options);\n const exporter = new WebExporter(renderer, { video: options.video, audio: options.audio, rendererWasmUrl: options.rendererWasmUrl });\n\n const platform: EnginePlatform<WgpuStorageAdapter> = {\n createStorageAdapter: (deps) => renderer.createStorageAdapter(deps),\n createRenderContext: (storage) => renderer.createRenderContext(storage),\n createMeasureContext: (storage) => renderer.createMeasureContext(storage),\n createAudioSink(): AudioSink {\n return new WebAudioSink();\n },\n createTicker(fps: number, audioDevice: AudioSink): Ticker {\n // Shares the device's own `AudioContext`: two independent timebases\n // in one session drift against each other, and sound is the one that\n // cannot be nudged back. A getter, so neither creates it before it\n // is needed (the first play, or the first sound to decode).\n const sink = audioDevice as WebAudioSink;\n return new WebTicker({ context: () => sink.getContext(), fps });\n },\n createExporter(): Exporter {\n return exporter;\n },\n dispose(): void {\n exporter.dispose();\n renderer.dispose?.();\n },\n };\n\n const engine = new Engine<WgpuStorageAdapter>(platform, { ...options, nodes: classNodes, dynamicNodes });\n for (const source of sourceNodes) {\n // Sequential — one script's error shouldn't mask another's, and\n // registration order should match array order for determinism.\n await engine.registerNode(source);\n }\n return engine;\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import type { AssetManifest, ProjectDocument, Registry, Size2D, VisualDocument } from "@motionscript/core";
2
+ import type { RendererWasmOption } from "./renderer";
3
+ import type { AudioEncoderOptions, ExportProgressCallback } from "./exporter";
4
+ import { type AudioFormat } from "./formats";
5
+ export type ExportAudioParams = RendererWasmOption & {
6
+ /** A project, or a lone still or composition as the one track of one. */
7
+ document: ProjectDocument | VisualDocument;
8
+ /** The vocabulary its clips compile against. Defaults to what the library ships. */
9
+ registry?: Registry;
10
+ /** Discovery lays clips out at this size and rate; 1920×1080 at 60 fps when neither the document nor this says. */
11
+ viewport?: Size2D;
12
+ fps?: number;
13
+ manifest?: AssetManifest;
14
+ /** Defaults to `'wav'`, which needs no encoder. */
15
+ format?: AudioFormat;
16
+ audio?: AudioEncoderOptions;
17
+ /** Defaults to `export.<extension>`. */
18
+ filename?: string;
19
+ onProgress?: ExportProgressCallback;
20
+ signal?: AbortSignal;
21
+ /** When true, the encoded bytes are returned instead of triggering a browser download. */
22
+ returnBytes?: boolean;
23
+ };
24
+ /** The timeline schedules no sound, so an audio-only export has nothing to write. */
25
+ export declare class NoAudioError extends Error {
26
+ readonly name = "NoAudioError";
27
+ constructor();
28
+ }
29
+ /**
30
+ * Mix the timeline's audio and encode it alone: WAV, MP3 or M4A (AAC). Nothing
31
+ * is drawn, so no frame is rendered and no GPU surface is made.
32
+ *
33
+ * @throws {NoAudioError} when nothing on the timeline plays, rather than writing a silent file.
34
+ */
35
+ export declare function exportAudio(params: ExportAudioParams): Promise<Uint8Array | void>;
36
+ //# sourceMappingURL=export-audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-audio.d.ts","sourceRoot":"","sources":["../src/export-audio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAI3G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAG/F,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG;IACjD,yEAAyE;IACzE,QAAQ,EAAE,eAAe,GAAG,cAAc,CAAC;IAC3C,oFAAoF;IACpF,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mHAAmH;IACnH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mDAAmD;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,qFAAqF;AACrF,qBAAa,YAAa,SAAQ,KAAK;IACnC,SAAkB,IAAI,kBAAkB;;CAK3C;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqCvF"}
@@ -0,0 +1,56 @@
1
+ import { BufferTarget, Output } from "mediabunny";
2
+ import { deliver } from "./deliver";
3
+ import { audioTrackSource, ensureAudioEncoder, formatSpec } from "./formats";
4
+ import { mixTimelineAudio } from "./mix-timeline-audio";
5
+ /** The timeline schedules no sound, so an audio-only export has nothing to write. */
6
+ export class NoAudioError extends Error {
7
+ name = "NoAudioError";
8
+ constructor() {
9
+ super("The timeline has no audio to export.");
10
+ }
11
+ }
12
+ /**
13
+ * Mix the timeline's audio and encode it alone: WAV, MP3 or M4A (AAC). Nothing
14
+ * is drawn, so no frame is rendered and no GPU surface is made.
15
+ *
16
+ * @throws {NoAudioError} when nothing on the timeline plays, rather than writing a silent file.
17
+ */
18
+ export async function exportAudio(params) {
19
+ const { format = "wav", audio = {}, onProgress, signal, returnBytes = false } = params;
20
+ const spec = formatSpec(format);
21
+ if (spec.kind !== "audio")
22
+ throw new RangeError(`exportAudio writes audio; "${format}" is video, which exportVideo writes.`);
23
+ signal?.throwIfAborted();
24
+ // Before the mix, so a browser that cannot encode the format fails fast.
25
+ await ensureAudioEncoder(format);
26
+ const mixed = await mixTimelineAudio({
27
+ document: params.document,
28
+ registry: params.registry,
29
+ viewport: params.viewport,
30
+ fps: params.fps,
31
+ manifest: params.manifest,
32
+ sampleRate: audio.sampleRate,
33
+ rendererWasmUrl: params.rendererWasmUrl,
34
+ });
35
+ if (mixed === null)
36
+ throw new NoAudioError();
37
+ signal?.throwIfAborted();
38
+ onProgress?.(0.5);
39
+ const target = new BufferTarget();
40
+ const output = new Output({ format: spec.container(), target });
41
+ const source = audioTrackSource(format, audio.bitrate);
42
+ output.addAudioTrack(source);
43
+ await output.start();
44
+ await source.add(mixed);
45
+ source.close();
46
+ await output.finalize();
47
+ if (!target.buffer)
48
+ throw new Error("Export produced no data");
49
+ onProgress?.(1);
50
+ return deliver(new Uint8Array(target.buffer), {
51
+ returnBytes,
52
+ filename: params.filename ?? `export.${spec.extension}`,
53
+ mimeType: spec.mimeType,
54
+ });
55
+ }
56
+ //# sourceMappingURL=export-audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-audio.js","sourceRoot":"","sources":["../src/export-audio.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAoB,MAAM,WAAW,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAsBxD,qFAAqF;AACrF,MAAM,OAAO,YAAa,SAAQ,KAAK;IACjB,IAAI,GAAG,cAAc,CAAC;IAExC;QACI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC;CACJ;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAyB;IACvD,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,KAAK,GAAG,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,EAAE,GAAG,MAAM,CAAC;IACvF,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,MAAM,IAAI,UAAU,CAAC,8BAA8B,MAAM,uCAAuC,CAAC,CAAC;IAC7H,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,yEAAyE;IACzE,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAEjC,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,eAAe,EAAE,MAAM,CAAC,eAAe;KAC1C,CAAC,CAAC;IACH,IAAI,KAAK,KAAK,IAAI;QAAE,MAAM,IAAI,YAAY,EAAE,CAAC;IAC7C,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;IAElB,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACvD,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC7B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACrB,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxB,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IAEhB,OAAO,OAAO,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;QAC1C,WAAW;QACX,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,UAAU,IAAI,CAAC,SAAS,EAAE;QACvD,QAAQ,EAAE,IAAI,CAAC,QAAQ;KAC1B,CAAC,CAAC;AACP,CAAC","sourcesContent":["import type { AssetManifest, ProjectDocument, Registry, Size2D, VisualDocument } from \"@motionscript/core\";\nimport { BufferTarget, Output } from \"mediabunny\";\n\nimport { deliver } from \"./deliver\";\nimport type { RendererWasmOption } from \"./renderer\";\nimport type { AudioEncoderOptions, ExportProgressCallback } from \"./exporter\";\nimport { audioTrackSource, ensureAudioEncoder, formatSpec, type AudioFormat } from \"./formats\";\nimport { mixTimelineAudio } from \"./mix-timeline-audio\";\n\nexport type ExportAudioParams = RendererWasmOption & {\n /** A project, or a lone still or composition as the one track of one. */\n document: ProjectDocument | VisualDocument;\n /** The vocabulary its clips compile against. Defaults to what the library ships. */\n registry?: Registry;\n /** Discovery lays clips out at this size and rate; 1920×1080 at 60 fps when neither the document nor this says. */\n viewport?: Size2D;\n fps?: number;\n manifest?: AssetManifest;\n /** Defaults to `'wav'`, which needs no encoder. */\n format?: AudioFormat;\n audio?: AudioEncoderOptions;\n /** Defaults to `export.<extension>`. */\n filename?: string;\n onProgress?: ExportProgressCallback;\n signal?: AbortSignal;\n /** When true, the encoded bytes are returned instead of triggering a browser download. */\n returnBytes?: boolean;\n};\n\n/** The timeline schedules no sound, so an audio-only export has nothing to write. */\nexport class NoAudioError extends Error {\n override readonly name = \"NoAudioError\";\n\n constructor() {\n super(\"The timeline has no audio to export.\");\n }\n}\n\n/**\n * Mix the timeline's audio and encode it alone: WAV, MP3 or M4A (AAC). Nothing\n * is drawn, so no frame is rendered and no GPU surface is made.\n *\n * @throws {NoAudioError} when nothing on the timeline plays, rather than writing a silent file.\n */\nexport async function exportAudio(params: ExportAudioParams): Promise<Uint8Array | void> {\n const { format = \"wav\", audio = {}, onProgress, signal, returnBytes = false } = params;\n const spec = formatSpec(format);\n if (spec.kind !== \"audio\") throw new RangeError(`exportAudio writes audio; \"${format}\" is video, which exportVideo writes.`);\n signal?.throwIfAborted();\n // Before the mix, so a browser that cannot encode the format fails fast.\n await ensureAudioEncoder(format);\n\n const mixed = await mixTimelineAudio({\n document: params.document,\n registry: params.registry,\n viewport: params.viewport,\n fps: params.fps,\n manifest: params.manifest,\n sampleRate: audio.sampleRate,\n rendererWasmUrl: params.rendererWasmUrl,\n });\n if (mixed === null) throw new NoAudioError();\n signal?.throwIfAborted();\n onProgress?.(0.5);\n\n const target = new BufferTarget();\n const output = new Output({ format: spec.container(), target });\n const source = audioTrackSource(format, audio.bitrate);\n output.addAudioTrack(source);\n await output.start();\n await source.add(mixed);\n source.close();\n await output.finalize();\n if (!target.buffer) throw new Error(\"Export produced no data\");\n onProgress?.(1);\n\n return deliver(new Uint8Array(target.buffer), {\n returnBytes,\n filename: params.filename ?? `export.${spec.extension}`,\n mimeType: spec.mimeType,\n });\n}\n"]}