@motionscript/browser 0.1.0-alpha.7 → 0.1.0-alpha.8

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 (148) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/NOTICE +15 -14
  3. package/README.md +66 -43
  4. package/dist/asset-bytes.d.ts +7 -4
  5. package/dist/asset-bytes.d.ts.map +1 -1
  6. package/dist/asset-bytes.js +9 -4
  7. package/dist/asset-bytes.js.map +1 -1
  8. package/dist/audio/bus-graph.d.ts.map +1 -1
  9. package/dist/audio/bus-graph.js +15 -6
  10. package/dist/audio/bus-graph.js.map +1 -1
  11. package/dist/audio/effect-graph.d.ts +62 -0
  12. package/dist/audio/effect-graph.d.ts.map +1 -0
  13. package/dist/audio/effect-graph.js +234 -0
  14. package/dist/audio/effect-graph.js.map +1 -0
  15. package/dist/audio/index.d.ts.map +1 -1
  16. package/dist/audio/index.js +2 -3
  17. package/dist/audio/index.js.map +1 -1
  18. package/dist/audio/mixer.d.ts +4 -4
  19. package/dist/audio/mixer.d.ts.map +1 -1
  20. package/dist/audio/mixer.js +18 -38
  21. package/dist/audio/mixer.js.map +1 -1
  22. package/dist/audio/player.d.ts +12 -6
  23. package/dist/audio/player.d.ts.map +1 -1
  24. package/dist/audio/player.js +49 -62
  25. package/dist/audio/player.js.map +1 -1
  26. package/dist/browser/THIRD_PARTY_LICENSES.txt +379 -27
  27. package/dist/browser/audio.js +1 -1
  28. package/dist/browser/chunks/chunk-IILVN4SK.js +2 -0
  29. package/dist/browser/chunks/chunk-IILVN4SK.js.map +7 -0
  30. package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
  31. package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
  32. package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
  33. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
  34. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
  35. package/dist/browser/index.js +51 -1343
  36. package/dist/browser/index.js.map +4 -4
  37. package/dist/browser/manifest.json +8 -8
  38. package/dist/browser/ms_renderer_bg.wasm +0 -0
  39. package/dist/canvaskit-renderer.d.ts +8 -0
  40. package/dist/canvaskit-renderer.d.ts.map +1 -0
  41. package/dist/canvaskit-renderer.js +23 -0
  42. package/dist/canvaskit-renderer.js.map +1 -0
  43. package/dist/concat.d.ts +33 -7
  44. package/dist/concat.d.ts.map +1 -1
  45. package/dist/concat.js +117 -72
  46. package/dist/concat.js.map +1 -1
  47. package/dist/deliver.d.ts +10 -0
  48. package/dist/deliver.d.ts.map +1 -0
  49. package/dist/deliver.js +22 -0
  50. package/dist/deliver.js.map +1 -0
  51. package/dist/dynamic-nodes.d.ts.map +1 -1
  52. package/dist/dynamic-nodes.js +2 -3
  53. package/dist/dynamic-nodes.js.map +1 -1
  54. package/dist/engine.d.ts +37 -9
  55. package/dist/engine.d.ts.map +1 -1
  56. package/dist/engine.js +41 -32
  57. package/dist/engine.js.map +1 -1
  58. package/dist/export-audio.d.ts +36 -0
  59. package/dist/export-audio.d.ts.map +1 -0
  60. package/dist/export-audio.js +56 -0
  61. package/dist/export-audio.js.map +1 -0
  62. package/dist/exporter.d.ts +125 -14
  63. package/dist/exporter.d.ts.map +1 -1
  64. package/dist/exporter.js +133 -89
  65. package/dist/exporter.js.map +1 -1
  66. package/dist/formats.d.ts +36 -0
  67. package/dist/formats.d.ts.map +1 -0
  68. package/dist/formats.js +83 -0
  69. package/dist/formats.js.map +1 -0
  70. package/dist/frame-loop.d.ts +190 -0
  71. package/dist/frame-loop.d.ts.map +1 -0
  72. package/dist/frame-loop.js +304 -0
  73. package/dist/frame-loop.js.map +1 -0
  74. package/dist/getter.d.ts +4 -1
  75. package/dist/getter.d.ts.map +1 -1
  76. package/dist/getter.js +7 -6
  77. package/dist/getter.js.map +1 -1
  78. package/dist/index.d.ts +12 -11
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +17 -21
  81. package/dist/index.js.map +1 -1
  82. package/dist/mix-timeline-audio.d.ts +5 -5
  83. package/dist/mix-timeline-audio.d.ts.map +1 -1
  84. package/dist/mix-timeline-audio.js +10 -12
  85. package/dist/mix-timeline-audio.js.map +1 -1
  86. package/dist/parallel/export-parallel.d.ts +32 -0
  87. package/dist/parallel/export-parallel.d.ts.map +1 -0
  88. package/dist/parallel/export-parallel.js +114 -0
  89. package/dist/parallel/export-parallel.js.map +1 -0
  90. package/dist/parallel/governor.d.ts +57 -0
  91. package/dist/parallel/governor.d.ts.map +1 -0
  92. package/dist/parallel/governor.js +79 -0
  93. package/dist/parallel/governor.js.map +1 -0
  94. package/dist/parallel/lane.d.ts +45 -0
  95. package/dist/parallel/lane.d.ts.map +1 -0
  96. package/dist/parallel/lane.js +2 -0
  97. package/dist/parallel/lane.js.map +1 -0
  98. package/dist/parallel/lanes.d.ts +20 -0
  99. package/dist/parallel/lanes.d.ts.map +1 -0
  100. package/dist/parallel/lanes.js +152 -0
  101. package/dist/parallel/lanes.js.map +1 -0
  102. package/dist/parallel/plan.d.ts +23 -0
  103. package/dist/parallel/plan.d.ts.map +1 -0
  104. package/dist/parallel/plan.js +30 -0
  105. package/dist/parallel/plan.js.map +1 -0
  106. package/dist/parallel/run-lanes.d.ts +38 -0
  107. package/dist/parallel/run-lanes.d.ts.map +1 -0
  108. package/dist/parallel/run-lanes.js +164 -0
  109. package/dist/parallel/run-lanes.js.map +1 -0
  110. package/dist/parallel/session.d.ts +9 -0
  111. package/dist/parallel/session.d.ts.map +1 -0
  112. package/dist/parallel/session.js +79 -0
  113. package/dist/parallel/session.js.map +1 -0
  114. package/dist/render-context.d.ts +2 -0
  115. package/dist/render-context.d.ts.map +1 -1
  116. package/dist/render-context.js +8 -4
  117. package/dist/render-context.js.map +1 -1
  118. package/dist/renderer.d.ts +22 -0
  119. package/dist/renderer.d.ts.map +1 -0
  120. package/dist/renderer.js +23 -0
  121. package/dist/renderer.js.map +1 -0
  122. package/dist/screenshot.d.ts +4 -4
  123. package/dist/screenshot.d.ts.map +1 -1
  124. package/dist/screenshot.js +3 -3
  125. package/dist/screenshot.js.map +1 -1
  126. package/dist/still.d.ts +17 -19
  127. package/dist/still.d.ts.map +1 -1
  128. package/dist/still.js +27 -34
  129. package/dist/still.js.map +1 -1
  130. package/dist/ticker.d.ts +22 -8
  131. package/dist/ticker.d.ts.map +1 -1
  132. package/dist/ticker.js +55 -20
  133. package/dist/ticker.js.map +1 -1
  134. package/package.json +9 -10
  135. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +0 -2
  136. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +0 -7
  137. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +0 -2
  138. package/dist/browser/chunks/chunk-2MAFJWBZ.js +0 -2
  139. package/dist/browser/chunks/chunk-2MAFJWBZ.js.map +0 -7
  140. package/dist/browser/chunks/chunk-CTLBXZSU.js +0 -4119
  141. package/dist/browser/chunks/chunk-CTLBXZSU.js.map +0 -7
  142. package/dist/browser/chunks/chunk-FQ32PDIU.js +0 -2
  143. package/dist/browser/chunks/chunk-FQ32PDIU.js.map +0 -7
  144. package/dist/browser/chunks/chunk-FY3BTKY4.js +0 -2
  145. package/dist/browser/chunks/chunk-FY3BTKY4.js.map +0 -7
  146. package/dist/browser/chunks/three.module-Z4EOVUWF.js +0 -2
  147. package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +0 -7
  148. /package/dist/browser/chunks/{SkeletonUtils-XYDMTGTM.js.map → chunk-KLXMRKTL.js.map} +0 -0
@@ -1,5 +1,9 @@
1
- import { Registry, type AssetManifest, type FrameRange, type ProjectDocument, type VisualDocument, type Size2D, type Theme } 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,12 +94,12 @@ 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
104
  document: ProjectDocument | VisualDocument;
99
105
  /** The vocabulary its clips compile against. Defaults to what the library ships. */
@@ -102,12 +108,15 @@ export type ExportParams = {
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;
107
116
  /** Theme for a document that carries none of its own. */
108
117
  theme?: Theme;
109
118
  /**
110
- * 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
111
120
  * container and the mix is skipped entirely. Defaults to true.
112
121
  */
113
122
  includeAudio?: boolean;
@@ -123,7 +132,12 @@ export type ExportParams = {
123
132
  frames?: FrameRange;
124
133
  onProgress?: ExportProgressCallback;
125
134
  signal?: AbortSignal;
126
- 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;
127
141
  /**
128
142
  * Render each frame at this multiple of the output resolution and downsample
129
143
  * it before encoding. **Defaults to 2**; pass 1 to turn it off.
@@ -150,20 +164,117 @@ export type ExportParams = {
150
164
  */
151
165
  supersample?: number;
152
166
  /**
153
- * When true, the encoded MP4 bytes are returned instead of triggering a
167
+ * When true, the encoded bytes are returned instead of triggering a
154
168
  * browser download. Used by headless drivers (e.g. the CLI) that capture
155
169
  * the output themselves and write it to disk; leave unset for the
156
170
  * interactive player, which downloads via an `<a>` element.
157
171
  */
158
172
  returnBytes?: boolean;
159
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>;
160
271
  /**
161
272
  * Renders a document frame-by-frame to an offscreen canvas and muxes the result
162
- * into an MP4 via mediabunny.
273
+ * into an MP4 or WebM via mediabunny.
163
274
  *
164
275
  * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry
165
276
  * that keeps every frame accurate, audio scheduling, progress and cancellation —
166
- * lives in `@motionscript/skia-render`'s `renderTimeline`. What this function
277
+ * lives in `renderTimeline` (frame-loop.ts). What this function
167
278
  * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,
168
279
  * a Web Audio mixer, and the `<a download>` delivery.
169
280
  */
@@ -1 +1 @@
1
- {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"AAAA,OAAO,EACmC,QAAQ,EAA0B,KAAK,aAAa,EAC1F,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,KAAK,KAAK,EACtF,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAOH,KAAK,OAAO,EAEf,MAAM,YAAY,CAAC;AAMpB,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,cAAc,CAAC;IAC3C,oFAAoF;IACpF,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,yDAAyD;IACzD,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gFAAgF;IAChF,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,oEAAoE;IACpE,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,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,CAuFlF"}
1
+ {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"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"}
package/dist/exporter.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { DEFAULT_OUTPUT, 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";
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";
6
+ import { deliver } from "./deliver";
7
+ import { audioTrackSource, DEFAULT_AUDIO_BITRATE, ensureAudioEncoder, formatSpec, } from "./formats";
8
8
  /**
9
9
  * Bits per pixel per frame the default bitrate is built from.
10
10
  *
@@ -36,7 +36,6 @@ export function motionGraphicsBitrate(width, height, fps) {
36
36
  const raw = width * height * fps * BITS_PER_PIXEL;
37
37
  return Math.round(Math.min(MAX_VIDEO_BITRATE, Math.max(MIN_VIDEO_BITRATE, raw)));
38
38
  }
39
- const DEFAULT_AUDIO_BITRATE = 192_000;
40
39
  /** See {@link VideoEncoderOptions.keyFrameInterval}. */
41
40
  const DEFAULT_KEY_FRAME_INTERVAL = 1;
42
41
  /** See {@link ExportParams.supersample}. */
@@ -53,9 +52,10 @@ const EMPTY_MANIFEST = {
53
52
  // split export and a whole one cannot mix differently.
54
53
  // ── Video sink ───────────────────────────────────────────────────────────────
55
54
  /**
56
- * mediabunny/WebCodecs implementation of the frame sink.
55
+ * mediabunny/WebCodecs implementation of the frame sink, writing whichever
56
+ * container `format` names.
57
57
  *
58
- * 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
59
59
  * a timestamp — WebCodecs pulls the pixels from the canvas itself, with no
60
60
  * GPU→CPU readback. That is why the seam signals a frame rather than passing one.
61
61
  *
@@ -64,44 +64,41 @@ const EMPTY_MANIFEST = {
64
64
  * platform choose: a software encoder has to be handed CPU pixels, so it drags
65
65
  * a full-frame copy off the GPU on every single frame.
66
66
  */
67
- class MediabunnyVideoSink {
67
+ export class MediabunnyVideoSink {
68
+ format;
68
69
  audioOptions;
69
70
  target = new BufferTarget();
70
71
  output;
71
72
  video;
72
73
  audio = null;
73
- constructor(canvas, fps,
74
+ constructor(canvas,
74
75
  /** Encoded output size, which is the canvas' own unless supersampling is on. */
75
- output, video = {}, audioOptions = {}, codec = 'avc') {
76
+ output, format, encoding, audioOptions = {}) {
77
+ this.format = format;
76
78
  this.audioOptions = audioOptions;
77
79
  const downsampling = output.width !== canvas.width || output.height !== canvas.height;
78
80
  this.video = new CanvasSource(canvas, {
79
- codec,
80
- // Resolved against what is actually being encoded — the *output*
81
- // size and the rate the frame loop will feed it — rather than
82
- // against a subjective level that never sees the frame rate.
83
- bitrate: video.bitrate ?? motionGraphicsBitrate(output.width, output.height, fps),
84
- hardwareAcceleration: video.hardwareAcceleration ?? 'no-preference',
85
- keyFrameInterval: video.keyFrameInterval ?? DEFAULT_KEY_FRAME_INTERVAL,
86
- latencyMode: video.latencyMode ?? 'quality',
87
- contentHint: video.contentHint ?? 'detail',
88
- ...(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 }),
89
88
  // `fit` is exact here — the supersampled canvas is a whole multiple
90
89
  // of the output — but mediabunny requires it whenever both axes are
91
90
  // given, so it is stated rather than inferred.
92
91
  ...(downsampling && { transform: { width: output.width, height: output.height, fit: 'fill' } }),
93
92
  });
94
- this.output = new Output({ format: new Mp4OutputFormat(), target: this.target });
93
+ this.output = new Output({ format: formatSpec(format).container(), target: this.target });
95
94
  }
96
95
  async start(info) {
97
96
  this.output.addVideoTrack(this.video, { frameRate: info.fps });
98
97
  // Tracks must be declared before the muxer starts, which is why the
99
98
  // orchestrator reports this rather than leaving it to be inferred.
100
99
  if (info.hasAudio) {
101
- this.audio = new AudioBufferSource({
102
- codec: 'aac',
103
- bitrate: this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE,
104
- });
100
+ await ensureAudioEncoder(this.format);
101
+ this.audio = audioTrackSource(this.format, this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE);
105
102
  this.output.addAudioTrack(this.audio);
106
103
  }
107
104
  await this.output.start();
@@ -156,109 +153,156 @@ function fittedSupersample(requested, output) {
156
153
  return factor;
157
154
  }
158
155
  /**
159
- * The codec to actually encode with: the one asked for if this platform can
160
- * 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).
161
159
  *
162
- * A better codec is the single largest quality win available on synthetic
163
- * content — but which ones a machine has encoders for varies by OS, browser and
164
- * GPU, and an export that dies at the muxer because the machine has no HEVC
165
- * encoder is worse than one that quietly produces H.264. AVC is not probed: it
166
- * 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.
167
163
  */
168
- async function resolveCodec(requested, size, fps) {
169
- if (!requested || requested === 'avc')
170
- 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) {
171
183
  try {
172
- const ok = await canEncodeVideo(requested, {
184
+ return await canEncodeVideo(codec, {
173
185
  width: size.width,
174
186
  height: size.height,
175
187
  bitrate: motionGraphicsBitrate(size.width, size.height, fps),
176
188
  });
177
- if (ok)
178
- return requested;
179
189
  }
180
190
  catch {
181
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;
182
257
  }
183
- console.warn(`[motionscript] No ${requested} encoder available; exporting as H.264 instead.`);
184
- return 'avc';
185
258
  }
186
259
  /**
187
260
  * Renders a document frame-by-frame to an offscreen canvas and muxes the result
188
- * into an MP4 via mediabunny.
261
+ * into an MP4 or WebM via mediabunny.
189
262
  *
190
263
  * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry
191
264
  * that keeps every frame accurate, audio scheduling, progress and cancellation —
192
- * lives in `@motionscript/skia-render`'s `renderTimeline`. What this function
265
+ * lives in `renderTimeline` (frame-loop.ts). What this function
193
266
  * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,
194
267
  * a Web Audio mixer, and the `<a download>` delivery.
195
268
  */
196
269
  export async function exportVideo(params) {
197
270
  const {
198
271
  // Not `document`, which would shadow the DOM global this function uses.
199
- document: source, registry = new Registry(), scale = 1, filename = 'export.mp4', manifest = EMPTY_MANIFEST, theme, includeAudio = true, video, audio = {}, frames, 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}`;
200
277
  const project = resolveProjectDocument(source, params, DEFAULT_OUTPUT);
201
278
  if (project.tracks.length === 0)
202
279
  return;
203
280
  signal?.throwIfAborted();
204
281
  const viewport = project.viewport;
205
282
  const fps = project.fps;
206
- // Every block-based codec works in macroblocks, so an odd dimension is
207
- // padded by the encoder and the padding is *not* what was drawn — it shows up
208
- // as a smeared or discoloured strip down one edge. Rounding up to an even
209
- // size instead costs at most half a pixel of centring and keeps the whole
210
- // frame real.
211
- const outputSize = {
212
- width: evenUp(viewport.width * scale),
213
- height: evenUp(viewport.height * scale),
214
- };
215
- const oversample = fittedSupersample(supersample, outputSize);
216
- const renderScale = scale * oversample;
217
- const offscreenCanvas = document.createElement('canvas');
218
- // A whole multiple of the output, so the downsample is exact: the picture is drawn at
219
- // `renderScale` from the corner, as it is at 1×, and any odd-size padding is a strip.
220
- offscreenCanvas.width = outputSize.width * oversample;
221
- offscreenCanvas.height = outputSize.height * oversample;
222
- offscreenCanvas.style.display = 'none';
223
- document.body.appendChild(offscreenCanvas);
224
- const canvasKit = await getCanvasKit(wasmUrl);
225
- const assetCatalog = new ManifestAssetCatalog(manifest);
226
- const storageAdapter = new WebStorageAdapter(canvasKit, assetCatalog, viewport, fps);
227
- const renderContext = new WebRenderContext(canvasKit, storageAdapter);
228
- renderContext.mount(offscreenCanvas);
229
- 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
+ });
230
288
  let bytes;
231
289
  try {
290
+ const encoding = await resolveVideoEncoding(video, format, output, fps);
232
291
  bytes = await renderTimeline({
233
292
  project, registry, theme, scale: renderScale, includeAudio, frames,
234
- renderContext, storageAdapter, assetCatalog,
235
- sink: new MediabunnyVideoSink(offscreenCanvas, fps, outputSize, video, audio, codec),
236
- mixer: new WebAudioMixer(audio.sampleRate, assetCatalog),
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),
237
296
  onProgress,
297
+ onIssue: params.onIssue,
238
298
  signal,
239
299
  });
240
300
  }
241
301
  finally {
242
- renderContext.dispose();
243
- document.body.removeChild(offscreenCanvas);
302
+ surface.dispose();
244
303
  }
245
304
  if (!bytes)
246
305
  return;
247
- // Headless callers capture the bytes and write them to disk themselves,
248
- // so skip the DOM-based download path entirely.
249
- if (returnBytes)
250
- return bytes;
251
- // The sink builds this Uint8Array over a whole, plain ArrayBuffer, so the cast
252
- // is sound — TS only balks because the generic parameter admits a
253
- // SharedArrayBuffer, which a BlobPart may not be.
254
- const blob = new Blob([bytes.buffer], { type: 'video/mp4' });
255
- const url = URL.createObjectURL(blob);
256
- const link = document.createElement('a');
257
- link.href = url;
258
- link.download = filename;
259
- document.body.appendChild(link);
260
- link.click();
261
- document.body.removeChild(link);
262
- URL.revokeObjectURL(url);
306
+ return deliver(bytes, { returnBytes, filename, mimeType: spec.mimeType });
263
307
  }
264
308
  //# sourceMappingURL=exporter.js.map