@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
@@ -1,5 +1,9 @@
1
- import { Registry, type AssetManifest, type ProjectDocument, type SceneDocument, type Size2D } from "@motionscript/core";
2
- import { type Quality } from 'mediabunny';
1
+ import { ManifestAssetCatalog, Registry, type AssetManifest, type FrameRange, type ProjectDocument, type RenderIssue, type VisualDocument, type Size2D, type StorageAdapter, type Theme } from "@motionscript/core";
2
+ import { type VideoFrameSink } from "./frame-loop";
3
+ import { type Quality, type VideoCodec } from 'mediabunny';
4
+ import type { BrowserRenderContext } from "./engine";
5
+ import { type RendererWasmOption } from "./renderer";
6
+ import { type VideoFormat } from "./formats";
3
7
  /** Reports export progress in [0, 1]; video encoding occupies most of the range, audio mixing/finalize the tail. */
4
8
  export type ExportProgressCallback = (progress: number) => void;
5
9
  /**
@@ -10,17 +14,19 @@ export type ExportProgressCallback = (progress: number) => void;
10
14
  */
11
15
  export type VideoEncoderOptions = {
12
16
  /**
13
- * Which codec the video track is encoded with. Defaults to `'avc'` (H.264),
14
- * which every player, editor and upload target accepts.
17
+ * Which codec the video track is encoded with. Defaults to the format's own:
18
+ * `'avc'` (H.264) for MP4, which every player, editor and upload target
19
+ * accepts, and `'vp9'` for WebM.
15
20
  *
16
21
  * `'hevc'` and `'av1'` reconstruct sharp edges far better at the same
17
22
  * bitrate, which is what a listing of syntax-highlighted code or a hairline
18
23
  * stroke is made of — worth reaching for when the file is going somewhere
19
24
  * that decodes them. Both are still 4:2:0, so they narrow the gap rather
20
25
  * than close it. Availability depends on the platform's encoders; an
21
- * unsupported choice falls back to `'avc'` rather than failing the export.
26
+ * unsupported choice, or one the container cannot hold (`'avc'` in WebM),
27
+ * falls back to the format's default rather than failing the export.
22
28
  */
23
- codec?: 'avc' | 'hevc' | 'av1' | 'vp9';
29
+ codec?: 'avc' | 'hevc' | 'av1' | 'vp9' | 'vp8';
24
30
  /**
25
31
  * Which encoder implementation WebCodecs should prefer.
26
32
  *
@@ -88,24 +94,29 @@ export type VideoEncoderOptions = {
88
94
  export declare function motionGraphicsBitrate(width: number, height: number, fps: number): number;
89
95
  /** How the mixed audio track is encoded. */
90
96
  export type AudioEncoderOptions = {
91
- /** AAC bitrate in bits per second. Defaults to 192 kbps. */
97
+ /** Bitrate in bits per second for AAC, Opus and MP3; WAV ignores it. Defaults to 192 kbps. */
92
98
  bitrate?: number;
93
- /** Mixdown sample rate in Hz. Defaults to the mixer's own (44100). */
99
+ /** Mixdown sample rate in Hz. Defaults to the mixer's own (44100); Opus is resampled to 48 kHz. */
94
100
  sampleRate?: number;
95
101
  };
96
- export type ExportParams = {
102
+ export type ExportParams = RendererWasmOption & {
97
103
  /** A project, or a lone still or composition as the one track of one. */
98
- document: ProjectDocument | SceneDocument;
104
+ document: ProjectDocument | VisualDocument;
99
105
  /** The vocabulary its clips compile against. Defaults to what the library ships. */
100
106
  registry?: Registry;
101
107
  /** Win over the document's own; 1920×1080 at 60 fps when neither says. */
102
108
  viewport?: Size2D;
103
109
  fps?: number;
104
110
  scale?: number;
111
+ /** The container, and with it the default codecs: H.264 and AAC for `'mp4'` (the default), VP9 and Opus for `'webm'`. */
112
+ format?: VideoFormat;
113
+ /** Defaults to `export.<extension>`. */
105
114
  filename?: string;
106
115
  manifest?: AssetManifest;
116
+ /** Theme for a document that carries none of its own. */
117
+ theme?: Theme;
107
118
  /**
108
- * Set false to produce a silent MP4 — no AAC track is added to the
119
+ * Set false to produce a silent file — no audio track is added to the
109
120
  * container and the mix is skipped entirely. Defaults to true.
110
121
  */
111
122
  includeAudio?: boolean;
@@ -113,9 +124,20 @@ export type ExportParams = {
113
124
  video?: VideoEncoderOptions;
114
125
  /** Audio encoder settings; ignored when `includeAudio` is false. */
115
126
  audio?: AudioEncoderOptions;
127
+ /**
128
+ * Encode only these frames (`end` excluded), timestamped from 0 and silent:
129
+ * one segment of a split export, for `concatVideoSegments` to join. Mix
130
+ * the audio once with `mixTimelineAudio` instead.
131
+ */
132
+ frames?: FrameRange;
116
133
  onProgress?: ExportProgressCallback;
117
134
  signal?: AbortSignal;
118
- wasmUrl?: string;
135
+ /**
136
+ * What the renderer drew but not as the document says, once per kind,
137
+ * feature and node. A lost GPU device fails the export with a
138
+ * `RenderBackendError` instead.
139
+ */
140
+ onIssue?: (issue: RenderIssue) => void;
119
141
  /**
120
142
  * Render each frame at this multiple of the output resolution and downsample
121
143
  * it before encoding. **Defaults to 2**; pass 1 to turn it off.
@@ -142,20 +164,117 @@ export type ExportParams = {
142
164
  */
143
165
  supersample?: number;
144
166
  /**
145
- * When true, the encoded MP4 bytes are returned instead of triggering a
167
+ * When true, the encoded bytes are returned instead of triggering a
146
168
  * browser download. Used by headless drivers (e.g. the CLI) that capture
147
169
  * the output themselves and write it to disk; leave unset for the
148
170
  * interactive player, which downloads via an `<a>` element.
149
171
  */
150
172
  returnBytes?: boolean;
151
173
  };
174
+ /**
175
+ * mediabunny/WebCodecs implementation of the frame sink, writing whichever
176
+ * container `format` names.
177
+ *
178
+ * Wraps the very `<canvas>` the renderer draws into, so `addFrame` costs nothing but
179
+ * a timestamp — WebCodecs pulls the pixels from the canvas itself, with no
180
+ * GPU→CPU readback. That is why the seam signals a frame rather than passing one.
181
+ *
182
+ * The readback claim holds only while the encoder is a hardware one, which is
183
+ * why {@link VideoEncoderOptions.hardwareAcceleration} defaults to letting the
184
+ * platform choose: a software encoder has to be handed CPU pixels, so it drags
185
+ * a full-frame copy off the GPU on every single frame.
186
+ */
187
+ export declare class MediabunnyVideoSink implements VideoFrameSink<AudioBuffer> {
188
+ private readonly format;
189
+ private readonly audioOptions;
190
+ private readonly target;
191
+ private readonly output;
192
+ private readonly video;
193
+ private audio;
194
+ constructor(canvas: HTMLCanvasElement,
195
+ /** Encoded output size, which is the canvas' own unless supersampling is on. */
196
+ output: {
197
+ width: number;
198
+ height: number;
199
+ }, format: VideoFormat, encoding: ResolvedVideoEncoding, audioOptions?: AudioEncoderOptions);
200
+ start(info: {
201
+ fps: number;
202
+ hasAudio: boolean;
203
+ }): Promise<void>;
204
+ addFrame(timestamp: number, duration: number): Promise<void>;
205
+ addAudio(mixed: AudioBuffer): Promise<void>;
206
+ finalize(): Promise<Uint8Array>;
207
+ }
208
+ /**
209
+ * Every encoder setting, decided. A split export pins one of these across its
210
+ * segments, since `concatVideoSegments` refuses parts encoded differently. With
211
+ * a numeric `bitrate` (see {@link plainVideoEncoding}) it is plain data and
212
+ * crosses a process boundary.
213
+ */
214
+ export interface ResolvedVideoEncoding {
215
+ codec: VideoCodec;
216
+ /** A `Quality` is left for the encoder to size, as an export always has. */
217
+ bitrate: number | Quality;
218
+ hardwareAcceleration: NonNullable<VideoEncoderOptions['hardwareAcceleration']>;
219
+ keyFrameInterval: number;
220
+ latencyMode: NonNullable<VideoEncoderOptions['latencyMode']>;
221
+ contentHint: string;
222
+ bitrateMode?: VideoEncoderOptions['bitrateMode'];
223
+ }
224
+ /** {@link VideoEncoderOptions} with its defaults applied against the encoded size and rate. */
225
+ export declare function resolveVideoEncoding(video: VideoEncoderOptions | undefined, format: VideoFormat, output: {
226
+ width: number;
227
+ height: number;
228
+ }, fps: number): Promise<ResolvedVideoEncoding>;
229
+ /**
230
+ * The encoding with a `Quality` turned into the bits per second mediabunny would
231
+ * give it at this size, since a class instance cannot cross a `MessagePort`.
232
+ */
233
+ export declare function plainVideoEncoding(encoding: ResolvedVideoEncoding, output: {
234
+ width: number;
235
+ height: number;
236
+ }): ResolvedVideoEncoding & {
237
+ bitrate: number;
238
+ };
239
+ /** The encoded size, rounded up to even, and how far each frame is supersampled. */
240
+ export declare function exportGeometry(viewport: Size2D, scale: number, supersample: number): {
241
+ output: {
242
+ width: number;
243
+ height: number;
244
+ };
245
+ oversample: number;
246
+ renderScale: number;
247
+ };
248
+ /** A hidden canvas with a mounted render context, reusable across renders of one project. */
249
+ export interface ExportSurface {
250
+ canvas: HTMLCanvasElement;
251
+ renderContext: BrowserRenderContext;
252
+ storageAdapter: StorageAdapter;
253
+ assetCatalog: ManifestAssetCatalog;
254
+ dispose(): void;
255
+ }
256
+ /**
257
+ * Open the canvas, renderer, storage and render context an export draws with,
258
+ * resolving once the surface can draw (a frame drawn before an asynchronously
259
+ * started surface is ready encodes blank). The canvas is a whole multiple of the
260
+ * output, so the downsample is exact.
261
+ */
262
+ export declare function openExportSurface(params: {
263
+ canvasSize: {
264
+ width: number;
265
+ height: number;
266
+ };
267
+ viewport: Size2D;
268
+ fps: number;
269
+ manifest: AssetManifest;
270
+ } & RendererWasmOption): Promise<ExportSurface>;
152
271
  /**
153
272
  * Renders a document frame-by-frame to an offscreen canvas and muxes the result
154
- * into an MP4 via mediabunny.
273
+ * into an MP4 or WebM via mediabunny.
155
274
  *
156
275
  * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry
157
276
  * that keeps every frame accurate, audio scheduling, progress and cancellation —
158
- * lives in `@motionscript/skia-render`'s `renderTimeline`. What this function
277
+ * lives in `renderTimeline` (frame-loop.ts). What this function
159
278
  * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,
160
279
  * a Web Audio mixer, and the `<a download>` delivery.
161
280
  */
@@ -1 +1 @@
1
- {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"AAAA,OAAO,EACmB,QAAQ,EAA0B,KAAK,aAAa,EAC1E,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EACxD,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAOH,KAAK,OAAO,EAEf,MAAM,YAAY,CAAC;AAOpB,oHAAoH;AACpH,MAAM,MAAM,sBAAsB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAC9B;;;;;;;;;;OAUG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;IACvC;;;;;;;;;;;;;;;OAeG;IACH,oBAAoB,CAAC,EAAE,eAAe,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;IAC/E;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,6FAA6F;IAC7F,WAAW,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACtC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IACrC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAsBF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGxF;AAED,4CAA4C;AAC5C,MAAM,MAAM,mBAAmB,GAAG;IAC9B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAQF,MAAM,MAAM,YAAY,GAAG;IACvB,yEAAyE;IACzE,QAAQ,EAAE,eAAe,GAAG,aAAa,CAAC;IAC1C,oFAAoF;IACpF,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gFAAgF;IAChF,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,oEAAoE;IACpE,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAA;AAgKD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAmFlF"}
1
+ {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"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"}
package/dist/exporter.js CHANGED
@@ -1,11 +1,10 @@
1
- import { ManifestAssetCatalog, Registry, resolveProjectDocument, } from "@motionscript/core";
2
- import { renderTimeline, } from "@motionscript/skia-render/export";
3
- import { AudioBufferSource, BufferTarget, CanvasSource, canEncodeVideo, Mp4OutputFormat, Output, } from 'mediabunny';
4
- import { WebRenderContext } from "./render-context";
5
- import { WebStorageAdapter } from "./storage-adapter";
6
- import { getCanvasKit } from "./getter";
1
+ import { DEFAULT_OUTPUT, ManifestAssetCatalog, Registry, resolveProjectDocument, } from "@motionscript/core";
2
+ import { renderTimeline, } from "./frame-loop";
3
+ import { BufferTarget, CanvasSource, canEncodeVideo, Output, } from 'mediabunny';
4
+ import { loadRenderer } from "./renderer";
7
5
  import { WebAudioMixer } from "./audio/mixer";
8
- import { DEFAULT_OUTPUT } from "./output";
6
+ import { deliver } from "./deliver";
7
+ import { audioTrackSource, DEFAULT_AUDIO_BITRATE, ensureAudioEncoder, formatSpec, } from "./formats";
9
8
  /**
10
9
  * Bits per pixel per frame the default bitrate is built from.
11
10
  *
@@ -37,7 +36,6 @@ export function motionGraphicsBitrate(width, height, fps) {
37
36
  const raw = width * height * fps * BITS_PER_PIXEL;
38
37
  return Math.round(Math.min(MAX_VIDEO_BITRATE, Math.max(MIN_VIDEO_BITRATE, raw)));
39
38
  }
40
- const DEFAULT_AUDIO_BITRATE = 192_000;
41
39
  /** See {@link VideoEncoderOptions.keyFrameInterval}. */
42
40
  const DEFAULT_KEY_FRAME_INTERVAL = 1;
43
41
  /** See {@link ExportParams.supersample}. */
@@ -54,9 +52,10 @@ const EMPTY_MANIFEST = {
54
52
  // split export and a whole one cannot mix differently.
55
53
  // ── Video sink ───────────────────────────────────────────────────────────────
56
54
  /**
57
- * mediabunny/WebCodecs implementation of the frame sink.
55
+ * mediabunny/WebCodecs implementation of the frame sink, writing whichever
56
+ * container `format` names.
58
57
  *
59
- * Wraps the very `<canvas>` CanvasKit draws into, so `addFrame` costs nothing but
58
+ * Wraps the very `<canvas>` the renderer draws into, so `addFrame` costs nothing but
60
59
  * a timestamp — WebCodecs pulls the pixels from the canvas itself, with no
61
60
  * GPU→CPU readback. That is why the seam signals a frame rather than passing one.
62
61
  *
@@ -65,44 +64,41 @@ const EMPTY_MANIFEST = {
65
64
  * platform choose: a software encoder has to be handed CPU pixels, so it drags
66
65
  * a full-frame copy off the GPU on every single frame.
67
66
  */
68
- class MediabunnyVideoSink {
67
+ export class MediabunnyVideoSink {
68
+ format;
69
69
  audioOptions;
70
70
  target = new BufferTarget();
71
71
  output;
72
72
  video;
73
73
  audio = null;
74
- constructor(canvas, fps,
74
+ constructor(canvas,
75
75
  /** Encoded output size, which is the canvas' own unless supersampling is on. */
76
- output, video = {}, audioOptions = {}, codec = 'avc') {
76
+ output, format, encoding, audioOptions = {}) {
77
+ this.format = format;
77
78
  this.audioOptions = audioOptions;
78
79
  const downsampling = output.width !== canvas.width || output.height !== canvas.height;
79
80
  this.video = new CanvasSource(canvas, {
80
- codec,
81
- // Resolved against what is actually being encoded — the *output*
82
- // size and the rate the frame loop will feed it — rather than
83
- // against a subjective level that never sees the frame rate.
84
- bitrate: video.bitrate ?? motionGraphicsBitrate(output.width, output.height, fps),
85
- hardwareAcceleration: video.hardwareAcceleration ?? 'no-preference',
86
- keyFrameInterval: video.keyFrameInterval ?? DEFAULT_KEY_FRAME_INTERVAL,
87
- latencyMode: video.latencyMode ?? 'quality',
88
- contentHint: video.contentHint ?? 'detail',
89
- ...(video.bitrateMode !== undefined && { bitrateMode: video.bitrateMode }),
81
+ codec: encoding.codec,
82
+ bitrate: encoding.bitrate,
83
+ hardwareAcceleration: encoding.hardwareAcceleration,
84
+ keyFrameInterval: encoding.keyFrameInterval,
85
+ latencyMode: encoding.latencyMode,
86
+ contentHint: encoding.contentHint,
87
+ ...(encoding.bitrateMode !== undefined && { bitrateMode: encoding.bitrateMode }),
90
88
  // `fit` is exact here — the supersampled canvas is a whole multiple
91
89
  // of the output — but mediabunny requires it whenever both axes are
92
90
  // given, so it is stated rather than inferred.
93
91
  ...(downsampling && { transform: { width: output.width, height: output.height, fit: 'fill' } }),
94
92
  });
95
- this.output = new Output({ format: new Mp4OutputFormat(), target: this.target });
93
+ this.output = new Output({ format: formatSpec(format).container(), target: this.target });
96
94
  }
97
95
  async start(info) {
98
96
  this.output.addVideoTrack(this.video, { frameRate: info.fps });
99
97
  // Tracks must be declared before the muxer starts, which is why the
100
98
  // orchestrator reports this rather than leaving it to be inferred.
101
99
  if (info.hasAudio) {
102
- this.audio = new AudioBufferSource({
103
- codec: 'aac',
104
- bitrate: this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE,
105
- });
100
+ await ensureAudioEncoder(this.format);
101
+ this.audio = audioTrackSource(this.format, this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE);
106
102
  this.output.addAudioTrack(this.audio);
107
103
  }
108
104
  await this.output.start();
@@ -157,107 +153,156 @@ function fittedSupersample(requested, output) {
157
153
  return factor;
158
154
  }
159
155
  /**
160
- * The codec to actually encode with: the one asked for if this platform can
161
- * encode it at this size, `'avc'` otherwise.
156
+ * The codec to actually encode with: the one asked for if the container holds
157
+ * it and this platform can encode it at this size, else the format's default,
158
+ * else its fallback (`'avc'` for MP4, `'vp8'` for WebM).
162
159
  *
163
- * A better codec is the single largest quality win available on synthetic
164
- * content — but which ones a machine has encoders for varies by OS, browser and
165
- * GPU, and an export that dies at the muxer because the machine has no HEVC
166
- * encoder is worse than one that quietly produces H.264. AVC is not probed: it
167
- * is the fallback, and there is nothing to fall back to.
160
+ * Which encoders a machine has varies by OS, browser and GPU, and an export that
161
+ * dies at the muxer for want of an HEVC encoder is worse than one that quietly
162
+ * produces H.264. The fallback is not probed: there is nothing to fall back to.
168
163
  */
169
- async function resolveCodec(requested, size, fps) {
170
- if (!requested || requested === 'avc')
171
- return 'avc';
164
+ async function resolveCodec(requested, format, size, fps) {
165
+ const { codecs, fallback } = formatSpec(format).video;
166
+ let wanted = requested ?? codecs[0];
167
+ if (!codecs.includes(wanted)) {
168
+ console.warn(`[motionscript] ${format.toUpperCase()} cannot hold ${wanted}; exporting as ${CODEC_NAMES[codecs[0]]} instead.`);
169
+ wanted = codecs[0];
170
+ }
171
+ for (const codec of new Set([wanted, codecs[0], fallback])) {
172
+ if (codec === fallback || await canEncode(codec, size, fps)) {
173
+ if (codec !== wanted) {
174
+ console.warn(`[motionscript] No ${wanted} encoder available; exporting as ${CODEC_NAMES[codec]} instead.`);
175
+ }
176
+ return codec;
177
+ }
178
+ }
179
+ return fallback;
180
+ }
181
+ const CODEC_NAMES = { avc: 'H.264', hevc: 'HEVC', vp9: 'VP9', av1: 'AV1', vp8: 'VP8' };
182
+ async function canEncode(codec, size, fps) {
172
183
  try {
173
- const ok = await canEncodeVideo(requested, {
184
+ return await canEncodeVideo(codec, {
174
185
  width: size.width,
175
186
  height: size.height,
176
187
  bitrate: motionGraphicsBitrate(size.width, size.height, fps),
177
188
  });
178
- if (ok)
179
- return requested;
180
189
  }
181
190
  catch {
182
191
  // A probe that throws is a probe that answered "no".
192
+ return false;
193
+ }
194
+ }
195
+ /** {@link VideoEncoderOptions} with its defaults applied against the encoded size and rate. */
196
+ export async function resolveVideoEncoding(video, format, output, fps) {
197
+ const codec = await resolveCodec(video?.codec, format, output, fps);
198
+ return {
199
+ codec,
200
+ bitrate: video?.bitrate ?? motionGraphicsBitrate(output.width, output.height, fps),
201
+ hardwareAcceleration: video?.hardwareAcceleration ?? 'no-preference',
202
+ keyFrameInterval: video?.keyFrameInterval ?? DEFAULT_KEY_FRAME_INTERVAL,
203
+ latencyMode: video?.latencyMode ?? 'quality',
204
+ contentHint: video?.contentHint ?? 'detail',
205
+ ...(video?.bitrateMode !== undefined && { bitrateMode: video.bitrateMode }),
206
+ };
207
+ }
208
+ /**
209
+ * The encoding with a `Quality` turned into the bits per second mediabunny would
210
+ * give it at this size, since a class instance cannot cross a `MessagePort`.
211
+ */
212
+ export function plainVideoEncoding(encoding, output) {
213
+ const { bitrate } = encoding;
214
+ if (typeof bitrate === 'number')
215
+ return { ...encoding, bitrate };
216
+ const quality = bitrate;
217
+ return { ...encoding, bitrate: quality._toVideoBitrate(encoding.codec, output.width, output.height) };
218
+ }
219
+ /** The encoded size, rounded up to even, and how far each frame is supersampled. */
220
+ export function exportGeometry(viewport, scale, supersample) {
221
+ // Every block-based codec works in macroblocks, so an odd dimension is
222
+ // padded by the encoder with a smeared strip that was never drawn.
223
+ const output = { width: evenUp(viewport.width * scale), height: evenUp(viewport.height * scale) };
224
+ const oversample = fittedSupersample(supersample, output);
225
+ return { output, oversample, renderScale: scale * oversample };
226
+ }
227
+ /**
228
+ * Open the canvas, renderer, storage and render context an export draws with,
229
+ * resolving once the surface can draw (a frame drawn before an asynchronously
230
+ * started surface is ready encodes blank). The canvas is a whole multiple of the
231
+ * output, so the downsample is exact.
232
+ */
233
+ export async function openExportSurface(params) {
234
+ const canvas = document.createElement('canvas');
235
+ canvas.width = params.canvasSize.width;
236
+ canvas.height = params.canvasSize.height;
237
+ canvas.style.display = 'none';
238
+ document.body.appendChild(canvas);
239
+ try {
240
+ const assetCatalog = new ManifestAssetCatalog(params.manifest);
241
+ const renderer = await loadRenderer(params);
242
+ const storageAdapter = renderer.createStorageAdapter({ catalog: assetCatalog, viewport: params.viewport, fps: params.fps });
243
+ const renderContext = renderer.createRenderContext(storageAdapter);
244
+ renderContext.mount(canvas);
245
+ await renderContext.whenMounted?.();
246
+ return {
247
+ canvas, renderContext, storageAdapter, assetCatalog,
248
+ dispose: () => {
249
+ renderContext.dispose();
250
+ canvas.remove();
251
+ },
252
+ };
253
+ }
254
+ catch (error) {
255
+ canvas.remove();
256
+ throw error;
183
257
  }
184
- console.warn(`[motion-script] No ${requested} encoder available; exporting as H.264 instead.`);
185
- return 'avc';
186
258
  }
187
259
  /**
188
260
  * Renders a document frame-by-frame to an offscreen canvas and muxes the result
189
- * into an MP4 via mediabunny.
261
+ * into an MP4 or WebM via mediabunny.
190
262
  *
191
263
  * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry
192
264
  * that keeps every frame accurate, audio scheduling, progress and cancellation —
193
- * lives in `@motionscript/skia-render`'s `renderTimeline`. What this function
265
+ * lives in `renderTimeline` (frame-loop.ts). What this function
194
266
  * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,
195
267
  * a Web Audio mixer, and the `<a download>` delivery.
196
268
  */
197
269
  export async function exportVideo(params) {
198
270
  const {
199
271
  // Not `document`, which would shadow the DOM global this function uses.
200
- document: source, registry = new Registry(), scale = 1, filename = 'export.mp4', manifest = EMPTY_MANIFEST, includeAudio = true, video, audio = {}, onProgress, signal, wasmUrl, supersample = DEFAULT_SUPERSAMPLE, returnBytes = false, } = params;
272
+ document: source, registry = new Registry(), scale = 1, format = 'mp4', manifest = EMPTY_MANIFEST, theme, includeAudio = true, video, audio = {}, frames, onProgress, signal, rendererWasmUrl, supersample = DEFAULT_SUPERSAMPLE, returnBytes = false, } = params;
273
+ const spec = formatSpec(format);
274
+ if (spec.kind !== 'video')
275
+ throw new RangeError(`exportVideo writes video; "${format}" is audio, which exportAudio writes.`);
276
+ const filename = params.filename ?? `export.${spec.extension}`;
201
277
  const project = resolveProjectDocument(source, params, DEFAULT_OUTPUT);
202
278
  if (project.tracks.length === 0)
203
279
  return;
204
280
  signal?.throwIfAborted();
205
281
  const viewport = project.viewport;
206
282
  const fps = project.fps;
207
- // Every block-based codec works in macroblocks, so an odd dimension is
208
- // padded by the encoder and the padding is *not* what was drawn — it shows up
209
- // as a smeared or discoloured strip down one edge. Rounding up to an even
210
- // size instead costs at most half a pixel of centring and keeps the whole
211
- // frame real.
212
- const outputSize = {
213
- width: evenUp(viewport.width * scale),
214
- height: evenUp(viewport.height * scale),
215
- };
216
- const oversample = fittedSupersample(supersample, outputSize);
217
- const renderScale = scale * oversample;
218
- const offscreenCanvas = document.createElement('canvas');
219
- offscreenCanvas.width = evenUp(viewport.width * renderScale);
220
- offscreenCanvas.height = evenUp(viewport.height * renderScale);
221
- offscreenCanvas.style.display = 'none';
222
- document.body.appendChild(offscreenCanvas);
223
- const canvasKit = await getCanvasKit(wasmUrl);
224
- const assetCatalog = new ManifestAssetCatalog(manifest);
225
- const storageAdapter = new WebStorageAdapter(canvasKit, assetCatalog, viewport, fps);
226
- const renderContext = new WebRenderContext(canvasKit, storageAdapter);
227
- renderContext.mount(offscreenCanvas);
228
- const codec = await resolveCodec(video?.codec, outputSize, fps);
283
+ const { output, oversample, renderScale } = exportGeometry(viewport, scale, supersample);
284
+ const surface = await openExportSurface({
285
+ canvasSize: { width: output.width * oversample, height: output.height * oversample },
286
+ viewport, fps, manifest, rendererWasmUrl,
287
+ });
229
288
  let bytes;
230
289
  try {
290
+ const encoding = await resolveVideoEncoding(video, format, output, fps);
231
291
  bytes = await renderTimeline({
232
- project, registry, scale: renderScale, includeAudio,
233
- renderContext, storageAdapter, assetCatalog,
234
- sink: new MediabunnyVideoSink(offscreenCanvas, fps, outputSize, video, audio, codec),
235
- mixer: new WebAudioMixer(audio.sampleRate, assetCatalog),
292
+ project, registry, theme, scale: renderScale, includeAudio, frames,
293
+ renderContext: surface.renderContext, storageAdapter: surface.storageAdapter, assetCatalog: surface.assetCatalog,
294
+ sink: new MediabunnyVideoSink(surface.canvas, output, format, encoding, audio),
295
+ mixer: new WebAudioMixer(audio.sampleRate, surface.assetCatalog),
236
296
  onProgress,
297
+ onIssue: params.onIssue,
237
298
  signal,
238
299
  });
239
300
  }
240
301
  finally {
241
- renderContext.dispose();
242
- document.body.removeChild(offscreenCanvas);
302
+ surface.dispose();
243
303
  }
244
304
  if (!bytes)
245
305
  return;
246
- // Headless callers capture the bytes and write them to disk themselves,
247
- // so skip the DOM-based download path entirely.
248
- if (returnBytes)
249
- return bytes;
250
- // The sink builds this Uint8Array over a whole, plain ArrayBuffer, so the cast
251
- // is sound — TS only balks because the generic parameter admits a
252
- // SharedArrayBuffer, which a BlobPart may not be.
253
- const blob = new Blob([bytes.buffer], { type: 'video/mp4' });
254
- const url = URL.createObjectURL(blob);
255
- const link = document.createElement('a');
256
- link.href = url;
257
- link.download = filename;
258
- document.body.appendChild(link);
259
- link.click();
260
- document.body.removeChild(link);
261
- URL.revokeObjectURL(url);
306
+ return deliver(bytes, { returnBytes, filename, mimeType: spec.mimeType });
262
307
  }
263
308
  //# sourceMappingURL=exporter.js.map