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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +201 -201
  3. package/NOTICE +49 -48
  4. package/README.md +141 -118
  5. package/dist/asset-bytes.d.ts +7 -4
  6. package/dist/asset-bytes.d.ts.map +1 -1
  7. package/dist/asset-bytes.js +9 -4
  8. package/dist/asset-bytes.js.map +1 -1
  9. package/dist/audio/bus-graph.d.ts.map +1 -1
  10. package/dist/audio/bus-graph.js +15 -6
  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.js.map +1 -1
  20. package/dist/audio/mixer.d.ts +4 -4
  21. package/dist/audio/mixer.d.ts.map +1 -1
  22. package/dist/audio/mixer.js +18 -38
  23. package/dist/audio/mixer.js.map +1 -1
  24. package/dist/audio/player.d.ts +12 -6
  25. package/dist/audio/player.d.ts.map +1 -1
  26. package/dist/audio/player.js +49 -62
  27. package/dist/audio/player.js.map +1 -1
  28. package/dist/browser/THIRD_PARTY_LICENSES.txt +379 -27
  29. package/dist/browser/audio.js +1 -1
  30. package/dist/browser/chunks/chunk-F3AZKPHU.js +2 -0
  31. package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
  32. package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
  33. package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
  34. package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
  35. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
  36. package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
  37. package/dist/browser/index.js +51 -1343
  38. package/dist/browser/index.js.map +4 -4
  39. package/dist/browser/manifest.json +8 -8
  40. package/dist/browser/ms_renderer_bg.wasm +0 -0
  41. package/dist/concat.d.ts +33 -7
  42. package/dist/concat.d.ts.map +1 -1
  43. package/dist/concat.js +117 -72
  44. package/dist/concat.js.map +1 -1
  45. package/dist/deliver.d.ts +10 -0
  46. package/dist/deliver.d.ts.map +1 -0
  47. package/dist/deliver.js +22 -0
  48. package/dist/deliver.js.map +1 -0
  49. package/dist/dynamic-nodes.d.ts.map +1 -1
  50. package/dist/dynamic-nodes.js +2 -3
  51. package/dist/dynamic-nodes.js.map +1 -1
  52. package/dist/engine.d.ts +37 -9
  53. package/dist/engine.d.ts.map +1 -1
  54. package/dist/engine.js +41 -32
  55. package/dist/engine.js.map +1 -1
  56. package/dist/export-audio.d.ts +36 -0
  57. package/dist/export-audio.d.ts.map +1 -0
  58. package/dist/export-audio.js +56 -0
  59. package/dist/export-audio.js.map +1 -0
  60. package/dist/exporter.d.ts +125 -14
  61. package/dist/exporter.d.ts.map +1 -1
  62. package/dist/exporter.js +133 -89
  63. package/dist/exporter.js.map +1 -1
  64. package/dist/formats.d.ts +36 -0
  65. package/dist/formats.d.ts.map +1 -0
  66. package/dist/formats.js +83 -0
  67. package/dist/formats.js.map +1 -0
  68. package/dist/frame-loop.d.ts +190 -0
  69. package/dist/frame-loop.d.ts.map +1 -0
  70. package/dist/frame-loop.js +304 -0
  71. package/dist/frame-loop.js.map +1 -0
  72. package/dist/index.d.ts +12 -11
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +17 -21
  75. package/dist/index.js.map +1 -1
  76. package/dist/mix-timeline-audio.d.ts +5 -5
  77. package/dist/mix-timeline-audio.d.ts.map +1 -1
  78. package/dist/mix-timeline-audio.js +10 -12
  79. package/dist/mix-timeline-audio.js.map +1 -1
  80. package/dist/parallel/export-parallel.d.ts +32 -0
  81. package/dist/parallel/export-parallel.d.ts.map +1 -0
  82. package/dist/parallel/export-parallel.js +114 -0
  83. package/dist/parallel/export-parallel.js.map +1 -0
  84. package/dist/parallel/governor.d.ts +57 -0
  85. package/dist/parallel/governor.d.ts.map +1 -0
  86. package/dist/parallel/governor.js +79 -0
  87. package/dist/parallel/governor.js.map +1 -0
  88. package/dist/parallel/lane.d.ts +45 -0
  89. package/dist/parallel/lane.d.ts.map +1 -0
  90. package/dist/parallel/lane.js +2 -0
  91. package/dist/parallel/lane.js.map +1 -0
  92. package/dist/parallel/lanes.d.ts +20 -0
  93. package/dist/parallel/lanes.d.ts.map +1 -0
  94. package/dist/parallel/lanes.js +152 -0
  95. package/dist/parallel/lanes.js.map +1 -0
  96. package/dist/parallel/plan.d.ts +23 -0
  97. package/dist/parallel/plan.d.ts.map +1 -0
  98. package/dist/parallel/plan.js +30 -0
  99. package/dist/parallel/plan.js.map +1 -0
  100. package/dist/parallel/run-lanes.d.ts +38 -0
  101. package/dist/parallel/run-lanes.d.ts.map +1 -0
  102. package/dist/parallel/run-lanes.js +164 -0
  103. package/dist/parallel/run-lanes.js.map +1 -0
  104. package/dist/parallel/session.d.ts +9 -0
  105. package/dist/parallel/session.d.ts.map +1 -0
  106. package/dist/parallel/session.js +79 -0
  107. package/dist/parallel/session.js.map +1 -0
  108. package/dist/renderer.d.ts +22 -0
  109. package/dist/renderer.d.ts.map +1 -0
  110. package/dist/renderer.js +23 -0
  111. package/dist/renderer.js.map +1 -0
  112. package/dist/screenshot.d.ts +4 -4
  113. package/dist/screenshot.d.ts.map +1 -1
  114. package/dist/screenshot.js +3 -3
  115. package/dist/screenshot.js.map +1 -1
  116. package/dist/still.d.ts +17 -19
  117. package/dist/still.d.ts.map +1 -1
  118. package/dist/still.js +27 -34
  119. package/dist/still.js.map +1 -1
  120. package/dist/ticker.d.ts +22 -8
  121. package/dist/ticker.d.ts.map +1 -1
  122. package/dist/ticker.js +55 -20
  123. package/dist/ticker.js.map +1 -1
  124. package/package.json +9 -10
  125. package/dist/audio/filter-graph.d.ts +0 -59
  126. package/dist/audio/filter-graph.d.ts.map +0 -1
  127. package/dist/audio/filter-graph.js +0 -241
  128. package/dist/audio/filter-graph.js.map +0 -1
  129. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +0 -2
  130. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +0 -7
  131. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +0 -2
  132. package/dist/browser/chunks/chunk-2MAFJWBZ.js +0 -2
  133. package/dist/browser/chunks/chunk-2MAFJWBZ.js.map +0 -7
  134. package/dist/browser/chunks/chunk-CTLBXZSU.js +0 -4119
  135. package/dist/browser/chunks/chunk-CTLBXZSU.js.map +0 -7
  136. package/dist/browser/chunks/chunk-FQ32PDIU.js +0 -2
  137. package/dist/browser/chunks/chunk-FQ32PDIU.js.map +0 -7
  138. package/dist/browser/chunks/chunk-FY3BTKY4.js +0 -2
  139. package/dist/browser/chunks/chunk-FY3BTKY4.js.map +0 -7
  140. package/dist/browser/chunks/three.module-Z4EOVUWF.js +0 -2
  141. package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +0 -7
  142. package/dist/decode/image-levels.d.ts +0 -137
  143. package/dist/decode/image-levels.d.ts.map +0 -1
  144. package/dist/decode/image-levels.js +0 -477
  145. package/dist/decode/image-levels.js.map +0 -1
  146. package/dist/decode/levels.d.ts +0 -37
  147. package/dist/decode/levels.d.ts.map +0 -1
  148. package/dist/decode/levels.js +0 -68
  149. package/dist/decode/levels.js.map +0 -1
  150. package/dist/decode/video-levels.d.ts +0 -37
  151. package/dist/decode/video-levels.d.ts.map +0 -1
  152. package/dist/decode/video-levels.js +0 -95
  153. package/dist/decode/video-levels.js.map +0 -1
  154. package/dist/getter.d.ts +0 -10
  155. package/dist/getter.d.ts.map +0 -1
  156. package/dist/getter.js +0 -38
  157. package/dist/getter.js.map +0 -1
  158. package/dist/measure-context.d.ts +0 -12
  159. package/dist/measure-context.d.ts.map +0 -1
  160. package/dist/measure-context.js +0 -12
  161. package/dist/measure-context.js.map +0 -1
  162. package/dist/render-context.d.ts +0 -32
  163. package/dist/render-context.d.ts.map +0 -1
  164. package/dist/render-context.js +0 -59
  165. package/dist/render-context.js.map +0 -1
  166. package/dist/storage-adapter.d.ts +0 -367
  167. package/dist/storage-adapter.d.ts.map +0 -1
  168. package/dist/storage-adapter.js +0 -1280
  169. package/dist/storage-adapter.js.map +0 -1
  170. package/dist/three/renderer.d.ts +0 -47
  171. package/dist/three/renderer.d.ts.map +0 -1
  172. package/dist/three/renderer.js +0 -182
  173. package/dist/three/renderer.js.map +0 -1
  174. /package/dist/browser/chunks/{SkeletonUtils-XYDMTGTM.js.map → chunk-KLXMRKTL.js.map} +0 -0
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
@@ -1 +1 @@
1
- {"version":3,"file":"exporter.js","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n DEFAULT_OUTPUT, ManifestAssetCatalog, Registry, resolveProjectDocument, type AssetManifest,\r\n type FrameRange, type ProjectDocument, type VisualDocument, type Size2D, type Theme,\r\n} from \"@motionscript/core\";\r\nimport {\r\n renderTimeline,\r\n type VideoFrameSink,\r\n} from \"@motionscript/skia-render/export\";\r\nimport {\r\n AudioBufferSource,\r\n BufferTarget,\r\n CanvasSource,\r\n canEncodeVideo,\r\n Mp4OutputFormat,\r\n Output,\r\n type Quality,\r\n type VideoCodec,\r\n} from 'mediabunny';\r\nimport { WebRenderContext } from \"./render-context\";\r\nimport { WebStorageAdapter } from \"./storage-adapter\";\r\nimport { getCanvasKit } from \"./getter\";\r\nimport { WebAudioMixer } from \"./audio/mixer\";\r\n\r\n/** Reports export progress in [0, 1]; video encoding occupies most of the range, audio mixing/finalize the tail. */\r\nexport type ExportProgressCallback = (progress: number) => void;\r\n\r\n/**\r\n * How the video track is encoded.\r\n *\r\n * Every field is a straight pass-through to mediabunny's `VideoEncodingConfig`,\r\n * defaulted here rather than there so an export has one obvious place to look.\r\n */\r\nexport type VideoEncoderOptions = {\r\n /**\r\n * Which codec the video track is encoded with. Defaults to `'avc'` (H.264),\r\n * which every player, editor and upload target accepts.\r\n *\r\n * `'hevc'` and `'av1'` reconstruct sharp edges far better at the same\r\n * bitrate, which is what a listing of syntax-highlighted code or a hairline\r\n * stroke is made of — worth reaching for when the file is going somewhere\r\n * that decodes them. Both are still 4:2:0, so they narrow the gap rather\r\n * than close it. Availability depends on the platform's encoders; an\r\n * unsupported choice falls back to `'avc'` rather than failing the export.\r\n */\r\n codec?: 'avc' | 'hevc' | 'av1' | 'vp9';\r\n /**\r\n * Which encoder implementation WebCodecs should prefer.\r\n *\r\n * Defaults to `'no-preference'`, which lets each platform use its own\r\n * hardware encoder (VideoToolbox on macOS, Media Foundation/NVENC on\r\n * Windows, VA-API on Linux). That is typically several times faster than the\r\n * software path *and* it is the only configuration in which this sink's\r\n * zero-readback design actually holds: a software encoder needs the pixels\r\n * on the CPU, so it forces a full GPU→CPU copy of every frame that a\r\n * hardware encoder reads directly off the canvas.\r\n *\r\n * Pass `'prefer-software'` when byte-identical output across machines\r\n * matters more than speed — different hardware encoders tune quality and\r\n * colour differently at the same nominal bitrate, so the same settings do\r\n * not produce the same file.\r\n */\r\n hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';\r\n /**\r\n * Target bitrate in bits per second, or one of mediabunny's subjective\r\n * `Quality` levels.\r\n *\r\n * Defaults to {@link motionGraphicsBitrate}, which is deliberately *not*\r\n * mediabunny's `QUALITY_VERY_HIGH`: that level is frame-rate blind, so a\r\n * 60 fps export got the same ~12 Mbps at 1080p as a 30 fps one and paid for\r\n * it with ringing along every glyph edge and banding across every gradient.\r\n * See that function for the reasoning.\r\n */\r\n bitrate?: number | Quality;\r\n /** Defaults to mediabunny's `'variable'`. `'constant'` is the reproducible-output choice. */\r\n bitrateMode?: 'constant' | 'variable';\r\n /**\r\n * Seconds between key frames. Defaults to 1.\r\n *\r\n * Shorter than mediabunny's 2, because a frame's error is carried by every\r\n * inter frame that references it: on synthetic content, where a flat\r\n * background makes a shimmer obvious, a keyframe twice as often visibly\r\n * resets the drift. It also halves the seek granularity, which matters for a\r\n * clip an editor is going to scrub.\r\n */\r\n keyFrameInterval?: number;\r\n /**\r\n * WebCodecs performance/quality trade-off. Defaults to `'quality'`, which\r\n * also guarantees no frame is dropped — the only correct setting for a file\r\n * being written rather than streamed.\r\n */\r\n latencyMode?: 'quality' | 'realtime';\r\n /**\r\n * A hint about what the frames contain, per\r\n * [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).\r\n *\r\n * Defaults to `'detail'`: encoders that honour it bias towards preserving\r\n * sharp edges and fine texture over smoothing motion, which is what motion\r\n * graphics are. Ignored where it is not implemented.\r\n */\r\n contentHint?: string;\r\n};\r\n\r\n/**\r\n * Bits per pixel per frame the default bitrate is built from.\r\n *\r\n * Rendered motion graphics are the hard case for a codec tuned on camera\r\n * footage: hard glyph edges, large flat fields, and gradients with no sensor\r\n * noise to hide the quantisation in. They compress *worse* than live action at\r\n * the same perceived quality, not better, so the figure here is well above what\r\n * a streaming preset would use — this is a master being rendered once, not a\r\n * bitstream being pushed down a pipe.\r\n */\r\nconst BITS_PER_PIXEL = 0.2;\r\n/** Floor, so a tiny viewport still gets enough bits to be legible. */\r\nconst MIN_VIDEO_BITRATE = 2_000_000;\r\n/**\r\n * Ceiling. Past this the returns are gone and the levels start excluding\r\n * hardware decoders — 4K60 lands here, and AVC level 5.1 (which every consumer\r\n * device implements) tops out at 240 Mbps, well clear of it.\r\n */\r\nconst MAX_VIDEO_BITRATE = 100_000_000;\r\n\r\n/**\r\n * The default video bitrate for an export of this size and frame rate.\r\n *\r\n * Scaling with **both** axes is the point. mediabunny's `Quality` levels scale\r\n * with resolution only, so doubling the frame rate halved the bits available per\r\n * frame while leaving the number unchanged — which is exactly the configuration\r\n * that produced clean stills and a mushy video from the very same renderer.\r\n */\r\nexport function motionGraphicsBitrate(width: number, height: number, fps: number): number {\r\n const raw = width * height * fps * BITS_PER_PIXEL;\r\n return Math.round(Math.min(MAX_VIDEO_BITRATE, Math.max(MIN_VIDEO_BITRATE, raw)));\r\n}\r\n\r\n/** How the mixed audio track is encoded. */\r\nexport type AudioEncoderOptions = {\r\n /** AAC bitrate in bits per second. Defaults to 192 kbps. */\r\n bitrate?: number;\r\n /** Mixdown sample rate in Hz. Defaults to the mixer's own (44100). */\r\n sampleRate?: number;\r\n};\r\n\r\nconst DEFAULT_AUDIO_BITRATE = 192_000;\r\n/** See {@link VideoEncoderOptions.keyFrameInterval}. */\r\nconst DEFAULT_KEY_FRAME_INTERVAL = 1;\r\n/** See {@link ExportParams.supersample}. */\r\nconst DEFAULT_SUPERSAMPLE = 2;\r\n\r\nexport type ExportParams = {\r\n /** A project, or a lone still or composition as the one track of one. */\r\n document: ProjectDocument | VisualDocument;\r\n /** The vocabulary its clips compile against. Defaults to what the library ships. */\r\n registry?: Registry;\r\n /** Win over the document's own; 1920×1080 at 60 fps when neither says. */\r\n viewport?: Size2D;\r\n fps?: number;\r\n scale?: number;\r\n filename?: string;\r\n manifest?: AssetManifest;\r\n /** Theme for a document that carries none of its own. */\r\n theme?: Theme;\r\n /**\r\n * Set false to produce a silent MP4 — no AAC track is added to the\r\n * container and the mix is skipped entirely. Defaults to true.\r\n */\r\n includeAudio?: boolean;\r\n /** Video encoder settings; see {@link VideoEncoderOptions} for the defaults. */\r\n video?: VideoEncoderOptions;\r\n /** Audio encoder settings; ignored when `includeAudio` is false. */\r\n audio?: AudioEncoderOptions;\r\n /**\r\n * Encode only these frames (`end` excluded), timestamped from 0 and silent:\r\n * one segment of a split export, for `concatVideoSegments` to join. Mix\r\n * the audio once with `mixTimelineAudio` instead.\r\n */\r\n frames?: FrameRange;\r\n onProgress?: ExportProgressCallback;\r\n signal?: AbortSignal;\r\n wasmUrl?: string;\r\n /**\r\n * Render each frame at this multiple of the output resolution and downsample\r\n * it before encoding. **Defaults to 2**; pass 1 to turn it off.\r\n *\r\n * The one lever that improves an export at a *fixed* output size. Every\r\n * codec a browser can encode is 4:2:0 — colour is stored at half resolution\r\n * on both axes, and no bitrate buys that back — so the colour fringing along\r\n * a saturated glyph or a hard edge is a property of the format, not of the\r\n * settings. Handing the encoder an already-downsampled frame does not undo\r\n * the subsampling, but it does mean every chroma sample averages four real\r\n * pixels instead of one, which is what removes most of the crawl on fine\r\n * detail and much of the banding on gradients.\r\n *\r\n * Measured against a correctly-sampled reference frame of a syntax-highlighted\r\n * listing, `2` lands 2.7 dB closer than a 1× encode — and 1.3 dB closer than\r\n * an *unencoded* 1× still, which is why it is the default despite costing\r\n * `supersample²` times the render work per frame plus a rescale. Pass 1 when\r\n * turnaround matters more than the file, and note that exporting at\r\n * `scale: 2` is cheaper than supersampling *and* better whenever the extra\r\n * resolution is wanted in the file itself.\r\n *\r\n * Clamped so the render surface stays inside {@link MAX_RENDER_AXIS}, since a\r\n * supersampled 4K export is already drawing 8K frames.\r\n */\r\n supersample?: number;\r\n /**\r\n * When true, the encoded MP4 bytes are returned instead of triggering a\r\n * browser download. Used by headless drivers (e.g. the CLI) that capture\r\n * the output themselves and write it to disk; leave unset for the\r\n * interactive player, which downloads via an `<a>` element.\r\n */\r\n returnBytes?: boolean;\r\n}\r\n\r\nconst EMPTY_MANIFEST: AssetManifest = {\r\n image: {},\r\n video: {},\r\n audio: {},\r\n font: {},\r\n};\r\n\r\n// ── Audio mixing ─────────────────────────────────────────────────────────────\r\n//\r\n// The mix itself lives in ./audio/mixer, shared with `mixTimelineAudio` so a\r\n// split export and a whole one cannot mix differently.\r\n\r\n// ── Video sink ───────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * mediabunny/WebCodecs implementation of the frame sink.\r\n *\r\n * Wraps the very `<canvas>` CanvasKit draws into, so `addFrame` costs nothing but\r\n * a timestamp — WebCodecs pulls the pixels from the canvas itself, with no\r\n * GPU→CPU readback. That is why the seam signals a frame rather than passing one.\r\n *\r\n * The readback claim holds only while the encoder is a hardware one, which is\r\n * why {@link VideoEncoderOptions.hardwareAcceleration} defaults to letting the\r\n * platform choose: a software encoder has to be handed CPU pixels, so it drags\r\n * a full-frame copy off the GPU on every single frame.\r\n */\r\nclass MediabunnyVideoSink implements VideoFrameSink<AudioBuffer> {\r\n private readonly target = new BufferTarget();\r\n private readonly output: Output;\r\n private readonly video: CanvasSource;\r\n private audio: AudioBufferSource | null = null;\r\n\r\n constructor(\r\n canvas: HTMLCanvasElement,\r\n fps: number,\r\n /** Encoded output size, which is the canvas' own unless supersampling is on. */\r\n output: { width: number; height: number },\r\n video: VideoEncoderOptions = {},\r\n private readonly audioOptions: AudioEncoderOptions = {},\r\n codec: VideoCodec = 'avc',\r\n ) {\r\n const downsampling = output.width !== canvas.width || output.height !== canvas.height;\r\n this.video = new CanvasSource(canvas, {\r\n codec,\r\n // Resolved against what is actually being encoded — the *output*\r\n // size and the rate the frame loop will feed it — rather than\r\n // against a subjective level that never sees the frame rate.\r\n bitrate: video.bitrate ?? motionGraphicsBitrate(output.width, output.height, fps),\r\n hardwareAcceleration: video.hardwareAcceleration ?? 'no-preference',\r\n keyFrameInterval: video.keyFrameInterval ?? DEFAULT_KEY_FRAME_INTERVAL,\r\n latencyMode: video.latencyMode ?? 'quality',\r\n contentHint: video.contentHint ?? 'detail',\r\n ...(video.bitrateMode !== undefined && { bitrateMode: video.bitrateMode }),\r\n // `fit` is exact here — the supersampled canvas is a whole multiple\r\n // of the output — but mediabunny requires it whenever both axes are\r\n // given, so it is stated rather than inferred.\r\n ...(downsampling && { transform: { width: output.width, height: output.height, fit: 'fill' as const } }),\r\n });\r\n this.output = new Output({ format: new Mp4OutputFormat(), target: this.target });\r\n }\r\n\r\n async start(info: { fps: number; hasAudio: boolean }): Promise<void> {\r\n this.output.addVideoTrack(this.video, { frameRate: info.fps });\r\n // Tracks must be declared before the muxer starts, which is why the\r\n // orchestrator reports this rather than leaving it to be inferred.\r\n if (info.hasAudio) {\r\n this.audio = new AudioBufferSource({\r\n codec: 'aac',\r\n bitrate: this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE,\r\n });\r\n this.output.addAudioTrack(this.audio);\r\n }\r\n await this.output.start();\r\n }\r\n\r\n async addFrame(timestamp: number, duration: number): Promise<void> {\r\n await this.video.add(timestamp, duration);\r\n }\r\n\r\n async addAudio(mixed: AudioBuffer): Promise<void> {\r\n if (!this.audio) return;\r\n await this.audio.add(mixed);\r\n }\r\n\r\n async finalize(): Promise<Uint8Array> {\r\n this.video.close();\r\n this.audio?.close();\r\n await this.output.finalize();\r\n const buffer = this.target.buffer;\r\n if (!buffer) throw new Error('Export produced no data');\r\n return new Uint8Array(buffer);\r\n }\r\n}\r\n\r\n// ── Main export ──────────────────────────────────────────────────────────────\r\n\r\n/** Round up to the nearest even integer — see the call site. */\r\nfunction evenUp(n: number): number {\r\n const rounded = Math.round(n);\r\n return rounded % 2 === 0 ? rounded : rounded + 1;\r\n}\r\n\r\n/**\r\n * Longest axis the render surface is allowed to reach.\r\n *\r\n * Supersampling multiplies the surface, not the output: a 4K export at\r\n * `supersample: 2` is drawing 8K frames, and the next step up asks for a\r\n * 15360-wide WebGL surface that most drivers will not give and none will give\r\n * cheaply. 8192 is the smallest `MAX_TEXTURE_SIZE` in wide circulation, so a\r\n * surface inside it is one every machine that can run the export at all can\r\n * also allocate.\r\n */\r\nconst MAX_RENDER_AXIS = 8192;\r\n\r\n/**\r\n * How far the frame can actually be supersampled at this output size.\r\n *\r\n * Reduced rather than refused: an export that quietly renders at 1× is a\r\n * slightly softer file, and one that throws because the default was too\r\n * ambitious for a 4K project is no file at all.\r\n */\r\nfunction fittedSupersample(requested: number, output: { width: number; height: number }): number {\r\n const longest = Math.max(output.width, output.height);\r\n let factor = Math.max(1, Math.round(requested));\r\n while (factor > 1 && longest * factor > MAX_RENDER_AXIS) factor--;\r\n return factor;\r\n}\r\n\r\n/**\r\n * The codec to actually encode with: the one asked for if this platform can\r\n * encode it at this size, `'avc'` otherwise.\r\n *\r\n * A better codec is the single largest quality win available on synthetic\r\n * content — but which ones a machine has encoders for varies by OS, browser and\r\n * GPU, and an export that dies at the muxer because the machine has no HEVC\r\n * encoder is worse than one that quietly produces H.264. AVC is not probed: it\r\n * is the fallback, and there is nothing to fall back to.\r\n */\r\nasync function resolveCodec(\r\n requested: VideoCodec | undefined,\r\n size: { width: number; height: number },\r\n fps: number,\r\n): Promise<VideoCodec> {\r\n if (!requested || requested === 'avc') return 'avc';\r\n try {\r\n const ok = await canEncodeVideo(requested, {\r\n width: size.width,\r\n height: size.height,\r\n bitrate: motionGraphicsBitrate(size.width, size.height, fps),\r\n });\r\n if (ok) return requested;\r\n } catch {\r\n // A probe that throws is a probe that answered \"no\".\r\n }\r\n console.warn(`[motionscript] No ${requested} encoder available; exporting as H.264 instead.`);\r\n return 'avc';\r\n}\r\n\r\n/**\r\n * Renders a document frame-by-frame to an offscreen canvas and muxes the result\r\n * into an MP4 via mediabunny.\r\n *\r\n * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry\r\n * that keeps every frame accurate, audio scheduling, progress and cancellation —\r\n * lives in `@motionscript/skia-render`'s `renderTimeline`. What this function\r\n * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,\r\n * a Web Audio mixer, and the `<a download>` delivery.\r\n */\r\nexport async function exportVideo(params: ExportParams): Promise<Uint8Array | void> {\r\n const {\r\n // Not `document`, which would shadow the DOM global this function uses.\r\n document: source,\r\n registry = new Registry(),\r\n scale = 1,\r\n filename = 'export.mp4',\r\n manifest = EMPTY_MANIFEST,\r\n theme,\r\n includeAudio = true,\r\n video,\r\n audio = {},\r\n frames,\r\n onProgress,\r\n signal,\r\n wasmUrl,\r\n supersample = DEFAULT_SUPERSAMPLE,\r\n returnBytes = false,\r\n } = params;\r\n\r\n const project = resolveProjectDocument(source, params, DEFAULT_OUTPUT);\r\n if (project.tracks.length === 0) return;\r\n signal?.throwIfAborted();\r\n const viewport = project.viewport!;\r\n const fps = project.fps!;\r\n\r\n // Every block-based codec works in macroblocks, so an odd dimension is\r\n // padded by the encoder and the padding is *not* what was drawn — it shows up\r\n // as a smeared or discoloured strip down one edge. Rounding up to an even\r\n // size instead costs at most half a pixel of centring and keeps the whole\r\n // frame real.\r\n const outputSize = {\r\n width: evenUp(viewport.width * scale),\r\n height: evenUp(viewport.height * scale),\r\n };\r\n const oversample = fittedSupersample(supersample, outputSize);\r\n const renderScale = scale * oversample;\r\n\r\n const offscreenCanvas = document.createElement('canvas');\r\n // A whole multiple of the output, so the downsample is exact: the picture is drawn at\r\n // `renderScale` from the corner, as it is at 1×, and any odd-size padding is a strip.\r\n offscreenCanvas.width = outputSize.width * oversample;\r\n offscreenCanvas.height = outputSize.height * oversample;\r\n offscreenCanvas.style.display = 'none';\r\n document.body.appendChild(offscreenCanvas);\r\n\r\n const canvasKit = await getCanvasKit(wasmUrl);\r\n const assetCatalog = new ManifestAssetCatalog(manifest);\r\n const storageAdapter = new WebStorageAdapter(canvasKit, assetCatalog, viewport, fps);\r\n const renderContext = new WebRenderContext(canvasKit, storageAdapter);\r\n renderContext.mount(offscreenCanvas);\r\n\r\n const codec = await resolveCodec(video?.codec, outputSize, fps);\r\n\r\n let bytes: Uint8Array | undefined;\r\n try {\r\n bytes = await renderTimeline<AudioBuffer>({\r\n project, registry, theme, scale: renderScale, includeAudio, frames,\r\n renderContext, storageAdapter, assetCatalog,\r\n sink: new MediabunnyVideoSink(offscreenCanvas, fps, outputSize, video, audio, codec),\r\n mixer: new WebAudioMixer(audio.sampleRate, assetCatalog),\r\n onProgress,\r\n signal,\r\n }) as Uint8Array | undefined;\r\n } finally {\r\n renderContext.dispose();\r\n document.body.removeChild(offscreenCanvas);\r\n }\r\n\r\n if (!bytes) return;\r\n\r\n // Headless callers capture the bytes and write them to disk themselves,\r\n // so skip the DOM-based download path entirely.\r\n if (returnBytes) return bytes;\r\n\r\n // The sink builds this Uint8Array over a whole, plain ArrayBuffer, so the cast\r\n // is sound — TS only balks because the generic parameter admits a\r\n // SharedArrayBuffer, which a BlobPart may not be.\r\n const blob = new Blob([bytes.buffer as ArrayBuffer], { type: 'video/mp4' });\r\n const url = URL.createObjectURL(blob);\r\n const link = document.createElement('a');\r\n link.href = url;\r\n link.download = filename;\r\n document.body.appendChild(link);\r\n link.click();\r\n document.body.removeChild(link);\r\n URL.revokeObjectURL(url);\r\n}\r\n"]}
1
+ {"version":3,"file":"exporter.js","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_OUTPUT, ManifestAssetCatalog, Registry, resolveProjectDocument, type AssetManifest,\n type FrameRange, type ProjectDocument, type RenderIssue, type VisualDocument, type Size2D, type StorageAdapter, type Theme,\n} from \"@motionscript/core\";\nimport {\n renderTimeline,\n type VideoFrameSink,\n} from \"./frame-loop\";\nimport {\n AudioBufferSource,\n BufferTarget,\n CanvasSource,\n canEncodeVideo,\n Output,\n type Quality,\n type VideoCodec,\n} from 'mediabunny';\nimport type { BrowserRenderContext } from \"./engine\";\nimport { loadRenderer, type RendererWasmOption } from \"./renderer\";\nimport { WebAudioMixer } from \"./audio/mixer\";\nimport { deliver } from \"./deliver\";\nimport {\n audioTrackSource, DEFAULT_AUDIO_BITRATE, ensureAudioEncoder, formatSpec, type VideoFormat,\n} from \"./formats\";\n\n/** Reports export progress in [0, 1]; video encoding occupies most of the range, audio mixing/finalize the tail. */\nexport type ExportProgressCallback = (progress: number) => void;\n\n/**\n * How the video track is encoded.\n *\n * Every field is a straight pass-through to mediabunny's `VideoEncodingConfig`,\n * defaulted here rather than there so an export has one obvious place to look.\n */\nexport type VideoEncoderOptions = {\n /**\n * Which codec the video track is encoded with. Defaults to the format's own:\n * `'avc'` (H.264) for MP4, which every player, editor and upload target\n * accepts, and `'vp9'` for WebM.\n *\n * `'hevc'` and `'av1'` reconstruct sharp edges far better at the same\n * bitrate, which is what a listing of syntax-highlighted code or a hairline\n * stroke is made of — worth reaching for when the file is going somewhere\n * that decodes them. Both are still 4:2:0, so they narrow the gap rather\n * than close it. Availability depends on the platform's encoders; an\n * unsupported choice, or one the container cannot hold (`'avc'` in WebM),\n * falls back to the format's default rather than failing the export.\n */\n codec?: 'avc' | 'hevc' | 'av1' | 'vp9' | 'vp8';\n /**\n * Which encoder implementation WebCodecs should prefer.\n *\n * Defaults to `'no-preference'`, which lets each platform use its own\n * hardware encoder (VideoToolbox on macOS, Media Foundation/NVENC on\n * Windows, VA-API on Linux). That is typically several times faster than the\n * software path *and* it is the only configuration in which this sink's\n * zero-readback design actually holds: a software encoder needs the pixels\n * on the CPU, so it forces a full GPU→CPU copy of every frame that a\n * hardware encoder reads directly off the canvas.\n *\n * Pass `'prefer-software'` when byte-identical output across machines\n * matters more than speed — different hardware encoders tune quality and\n * colour differently at the same nominal bitrate, so the same settings do\n * not produce the same file.\n */\n hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';\n /**\n * Target bitrate in bits per second, or one of mediabunny's subjective\n * `Quality` levels.\n *\n * Defaults to {@link motionGraphicsBitrate}, which is deliberately *not*\n * mediabunny's `QUALITY_VERY_HIGH`: that level is frame-rate blind, so a\n * 60 fps export got the same ~12 Mbps at 1080p as a 30 fps one and paid for\n * it with ringing along every glyph edge and banding across every gradient.\n * See that function for the reasoning.\n */\n bitrate?: number | Quality;\n /** Defaults to mediabunny's `'variable'`. `'constant'` is the reproducible-output choice. */\n bitrateMode?: 'constant' | 'variable';\n /**\n * Seconds between key frames. Defaults to 1.\n *\n * Shorter than mediabunny's 2, because a frame's error is carried by every\n * inter frame that references it: on synthetic content, where a flat\n * background makes a shimmer obvious, a keyframe twice as often visibly\n * resets the drift. It also halves the seek granularity, which matters for a\n * clip an editor is going to scrub.\n */\n keyFrameInterval?: number;\n /**\n * WebCodecs performance/quality trade-off. Defaults to `'quality'`, which\n * also guarantees no frame is dropped — the only correct setting for a file\n * being written rather than streamed.\n */\n latencyMode?: 'quality' | 'realtime';\n /**\n * A hint about what the frames contain, per\n * [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).\n *\n * Defaults to `'detail'`: encoders that honour it bias towards preserving\n * sharp edges and fine texture over smoothing motion, which is what motion\n * graphics are. Ignored where it is not implemented.\n */\n contentHint?: string;\n};\n\n/**\n * Bits per pixel per frame the default bitrate is built from.\n *\n * Rendered motion graphics are the hard case for a codec tuned on camera\n * footage: hard glyph edges, large flat fields, and gradients with no sensor\n * noise to hide the quantisation in. They compress *worse* than live action at\n * the same perceived quality, not better, so the figure here is well above what\n * a streaming preset would use — this is a master being rendered once, not a\n * bitstream being pushed down a pipe.\n */\nconst BITS_PER_PIXEL = 0.2;\n/** Floor, so a tiny viewport still gets enough bits to be legible. */\nconst MIN_VIDEO_BITRATE = 2_000_000;\n/**\n * Ceiling. Past this the returns are gone and the levels start excluding\n * hardware decoders — 4K60 lands here, and AVC level 5.1 (which every consumer\n * device implements) tops out at 240 Mbps, well clear of it.\n */\nconst MAX_VIDEO_BITRATE = 100_000_000;\n\n/**\n * The default video bitrate for an export of this size and frame rate.\n *\n * Scaling with **both** axes is the point. mediabunny's `Quality` levels scale\n * with resolution only, so doubling the frame rate halved the bits available per\n * frame while leaving the number unchanged — which is exactly the configuration\n * that produced clean stills and a mushy video from the very same renderer.\n */\nexport function motionGraphicsBitrate(width: number, height: number, fps: number): number {\n const raw = width * height * fps * BITS_PER_PIXEL;\n return Math.round(Math.min(MAX_VIDEO_BITRATE, Math.max(MIN_VIDEO_BITRATE, raw)));\n}\n\n/** How the mixed audio track is encoded. */\nexport type AudioEncoderOptions = {\n /** Bitrate in bits per second for AAC, Opus and MP3; WAV ignores it. Defaults to 192 kbps. */\n bitrate?: number;\n /** Mixdown sample rate in Hz. Defaults to the mixer's own (44100); Opus is resampled to 48 kHz. */\n sampleRate?: number;\n};\n\n/** See {@link VideoEncoderOptions.keyFrameInterval}. */\nconst DEFAULT_KEY_FRAME_INTERVAL = 1;\n/** See {@link ExportParams.supersample}. */\nconst DEFAULT_SUPERSAMPLE = 2;\n\nexport type ExportParams = RendererWasmOption & {\n /** A project, or a lone still or composition as the one track of one. */\n document: ProjectDocument | VisualDocument;\n /** The vocabulary its clips compile against. Defaults to what the library ships. */\n registry?: Registry;\n /** Win over the document's own; 1920×1080 at 60 fps when neither says. */\n viewport?: Size2D;\n fps?: number;\n scale?: number;\n /** The container, and with it the default codecs: H.264 and AAC for `'mp4'` (the default), VP9 and Opus for `'webm'`. */\n format?: VideoFormat;\n /** Defaults to `export.<extension>`. */\n filename?: string;\n manifest?: AssetManifest;\n /** Theme for a document that carries none of its own. */\n theme?: Theme;\n /**\n * Set false to produce a silent file — no audio track is added to the\n * container and the mix is skipped entirely. Defaults to true.\n */\n includeAudio?: boolean;\n /** Video encoder settings; see {@link VideoEncoderOptions} for the defaults. */\n video?: VideoEncoderOptions;\n /** Audio encoder settings; ignored when `includeAudio` is false. */\n audio?: AudioEncoderOptions;\n /**\n * Encode only these frames (`end` excluded), timestamped from 0 and silent:\n * one segment of a split export, for `concatVideoSegments` to join. Mix\n * the audio once with `mixTimelineAudio` instead.\n */\n frames?: FrameRange;\n onProgress?: ExportProgressCallback;\n signal?: AbortSignal;\n /**\n * What the renderer drew but not as the document says, once per kind,\n * feature and node. A lost GPU device fails the export with a\n * `RenderBackendError` instead.\n */\n onIssue?: (issue: RenderIssue) => void;\n /**\n * Render each frame at this multiple of the output resolution and downsample\n * it before encoding. **Defaults to 2**; pass 1 to turn it off.\n *\n * The one lever that improves an export at a *fixed* output size. Every\n * codec a browser can encode is 4:2:0 — colour is stored at half resolution\n * on both axes, and no bitrate buys that back — so the colour fringing along\n * a saturated glyph or a hard edge is a property of the format, not of the\n * settings. Handing the encoder an already-downsampled frame does not undo\n * the subsampling, but it does mean every chroma sample averages four real\n * pixels instead of one, which is what removes most of the crawl on fine\n * detail and much of the banding on gradients.\n *\n * Measured against a correctly-sampled reference frame of a syntax-highlighted\n * listing, `2` lands 2.7 dB closer than a 1× encode — and 1.3 dB closer than\n * an *unencoded* 1× still, which is why it is the default despite costing\n * `supersample²` times the render work per frame plus a rescale. Pass 1 when\n * turnaround matters more than the file, and note that exporting at\n * `scale: 2` is cheaper than supersampling *and* better whenever the extra\n * resolution is wanted in the file itself.\n *\n * Clamped so the render surface stays inside {@link MAX_RENDER_AXIS}, since a\n * supersampled 4K export is already drawing 8K frames.\n */\n supersample?: number;\n /**\n * When true, the encoded bytes are returned instead of triggering a\n * browser download. Used by headless drivers (e.g. the CLI) that capture\n * the output themselves and write it to disk; leave unset for the\n * interactive player, which downloads via an `<a>` element.\n */\n returnBytes?: boolean;\n}\n\nconst EMPTY_MANIFEST: AssetManifest = {\n image: {},\n video: {},\n audio: {},\n font: {},\n};\n\n// ── Audio mixing ─────────────────────────────────────────────────────────────\n//\n// The mix itself lives in ./audio/mixer, shared with `mixTimelineAudio` so a\n// split export and a whole one cannot mix differently.\n\n// ── Video sink ───────────────────────────────────────────────────────────────\n\n/**\n * mediabunny/WebCodecs implementation of the frame sink, writing whichever\n * container `format` names.\n *\n * Wraps the very `<canvas>` the renderer draws into, so `addFrame` costs nothing but\n * a timestamp — WebCodecs pulls the pixels from the canvas itself, with no\n * GPU→CPU readback. That is why the seam signals a frame rather than passing one.\n *\n * The readback claim holds only while the encoder is a hardware one, which is\n * why {@link VideoEncoderOptions.hardwareAcceleration} defaults to letting the\n * platform choose: a software encoder has to be handed CPU pixels, so it drags\n * a full-frame copy off the GPU on every single frame.\n */\nexport class MediabunnyVideoSink implements VideoFrameSink<AudioBuffer> {\n private readonly target = new BufferTarget();\n private readonly output: Output;\n private readonly video: CanvasSource;\n private audio: AudioBufferSource | null = null;\n\n constructor(\n canvas: HTMLCanvasElement,\n /** Encoded output size, which is the canvas' own unless supersampling is on. */\n output: { width: number; height: number },\n private readonly format: VideoFormat,\n encoding: ResolvedVideoEncoding,\n private readonly audioOptions: AudioEncoderOptions = {},\n ) {\n const downsampling = output.width !== canvas.width || output.height !== canvas.height;\n this.video = new CanvasSource(canvas, {\n codec: encoding.codec,\n bitrate: encoding.bitrate,\n hardwareAcceleration: encoding.hardwareAcceleration,\n keyFrameInterval: encoding.keyFrameInterval,\n latencyMode: encoding.latencyMode,\n contentHint: encoding.contentHint,\n ...(encoding.bitrateMode !== undefined && { bitrateMode: encoding.bitrateMode }),\n // `fit` is exact here — the supersampled canvas is a whole multiple\n // of the output — but mediabunny requires it whenever both axes are\n // given, so it is stated rather than inferred.\n ...(downsampling && { transform: { width: output.width, height: output.height, fit: 'fill' as const } }),\n });\n this.output = new Output({ format: formatSpec(format).container(), target: this.target });\n }\n\n async start(info: { fps: number; hasAudio: boolean }): Promise<void> {\n this.output.addVideoTrack(this.video, { frameRate: info.fps });\n // Tracks must be declared before the muxer starts, which is why the\n // orchestrator reports this rather than leaving it to be inferred.\n if (info.hasAudio) {\n await ensureAudioEncoder(this.format);\n this.audio = audioTrackSource(this.format, this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE);\n this.output.addAudioTrack(this.audio);\n }\n await this.output.start();\n }\n\n async addFrame(timestamp: number, duration: number): Promise<void> {\n await this.video.add(timestamp, duration);\n }\n\n async addAudio(mixed: AudioBuffer): Promise<void> {\n if (!this.audio) return;\n await this.audio.add(mixed);\n }\n\n async finalize(): Promise<Uint8Array> {\n this.video.close();\n this.audio?.close();\n await this.output.finalize();\n const buffer = this.target.buffer;\n if (!buffer) throw new Error('Export produced no data');\n return new Uint8Array(buffer);\n }\n}\n\n// ── Main export ──────────────────────────────────────────────────────────────\n\n/** Round up to the nearest even integer — see the call site. */\nfunction evenUp(n: number): number {\n const rounded = Math.round(n);\n return rounded % 2 === 0 ? rounded : rounded + 1;\n}\n\n/**\n * Longest axis the render surface is allowed to reach.\n *\n * Supersampling multiplies the surface, not the output: a 4K export at\n * `supersample: 2` is drawing 8K frames, and the next step up asks for a\n * 15360-wide WebGL surface that most drivers will not give and none will give\n * cheaply. 8192 is the smallest `MAX_TEXTURE_SIZE` in wide circulation, so a\n * surface inside it is one every machine that can run the export at all can\n * also allocate.\n */\nconst MAX_RENDER_AXIS = 8192;\n\n/**\n * How far the frame can actually be supersampled at this output size.\n *\n * Reduced rather than refused: an export that quietly renders at 1× is a\n * slightly softer file, and one that throws because the default was too\n * ambitious for a 4K project is no file at all.\n */\nfunction fittedSupersample(requested: number, output: { width: number; height: number }): number {\n const longest = Math.max(output.width, output.height);\n let factor = Math.max(1, Math.round(requested));\n while (factor > 1 && longest * factor > MAX_RENDER_AXIS) factor--;\n return factor;\n}\n\n/**\n * The codec to actually encode with: the one asked for if the container holds\n * it and this platform can encode it at this size, else the format's default,\n * else its fallback (`'avc'` for MP4, `'vp8'` for WebM).\n *\n * Which encoders a machine has varies by OS, browser and GPU, and an export that\n * dies at the muxer for want of an HEVC encoder is worse than one that quietly\n * produces H.264. The fallback is not probed: there is nothing to fall back to.\n */\nasync function resolveCodec(\n requested: VideoCodec | undefined,\n format: VideoFormat,\n size: { width: number; height: number },\n fps: number,\n): Promise<VideoCodec> {\n const { codecs, fallback } = formatSpec(format).video!;\n let wanted = requested ?? codecs[0];\n if (!codecs.includes(wanted)) {\n console.warn(`[motionscript] ${format.toUpperCase()} cannot hold ${wanted}; exporting as ${CODEC_NAMES[codecs[0]]} instead.`);\n wanted = codecs[0];\n }\n for (const codec of new Set([wanted, codecs[0], fallback])) {\n if (codec === fallback || await canEncode(codec, size, fps)) {\n if (codec !== wanted) {\n console.warn(`[motionscript] No ${wanted} encoder available; exporting as ${CODEC_NAMES[codec]} instead.`);\n }\n return codec;\n }\n }\n return fallback;\n}\n\nconst CODEC_NAMES: Record<VideoCodec, string> = { avc: 'H.264', hevc: 'HEVC', vp9: 'VP9', av1: 'AV1', vp8: 'VP8' };\n\nasync function canEncode(codec: VideoCodec, size: { width: number; height: number }, fps: number): Promise<boolean> {\n try {\n return await canEncodeVideo(codec, {\n width: size.width,\n height: size.height,\n bitrate: motionGraphicsBitrate(size.width, size.height, fps),\n });\n } catch {\n // A probe that throws is a probe that answered \"no\".\n return false;\n }\n}\n\n/**\n * Every encoder setting, decided. A split export pins one of these across its\n * segments, since `concatVideoSegments` refuses parts encoded differently. With\n * a numeric `bitrate` (see {@link plainVideoEncoding}) it is plain data and\n * crosses a process boundary.\n */\nexport interface ResolvedVideoEncoding {\n codec: VideoCodec;\n /** A `Quality` is left for the encoder to size, as an export always has. */\n bitrate: number | Quality;\n hardwareAcceleration: NonNullable<VideoEncoderOptions['hardwareAcceleration']>;\n keyFrameInterval: number;\n latencyMode: NonNullable<VideoEncoderOptions['latencyMode']>;\n contentHint: string;\n bitrateMode?: VideoEncoderOptions['bitrateMode'];\n}\n\n/** {@link VideoEncoderOptions} with its defaults applied against the encoded size and rate. */\nexport async function resolveVideoEncoding(\n video: VideoEncoderOptions | undefined,\n format: VideoFormat,\n output: { width: number; height: number },\n fps: number,\n): Promise<ResolvedVideoEncoding> {\n const codec = await resolveCodec(video?.codec, format, output, fps);\n return {\n codec,\n bitrate: video?.bitrate ?? motionGraphicsBitrate(output.width, output.height, fps),\n hardwareAcceleration: video?.hardwareAcceleration ?? 'no-preference',\n keyFrameInterval: video?.keyFrameInterval ?? DEFAULT_KEY_FRAME_INTERVAL,\n latencyMode: video?.latencyMode ?? 'quality',\n contentHint: video?.contentHint ?? 'detail',\n ...(video?.bitrateMode !== undefined && { bitrateMode: video.bitrateMode }),\n };\n}\n\n/**\n * The encoding with a `Quality` turned into the bits per second mediabunny would\n * give it at this size, since a class instance cannot cross a `MessagePort`.\n */\nexport function plainVideoEncoding(\n encoding: ResolvedVideoEncoding,\n output: { width: number; height: number },\n): ResolvedVideoEncoding & { bitrate: number } {\n const { bitrate } = encoding;\n if (typeof bitrate === 'number') return { ...encoding, bitrate };\n const quality = bitrate as unknown as { _toVideoBitrate(c: VideoCodec, w: number, h: number): number };\n return { ...encoding, bitrate: quality._toVideoBitrate(encoding.codec, output.width, output.height) };\n}\n\n/** The encoded size, rounded up to even, and how far each frame is supersampled. */\nexport function exportGeometry(viewport: Size2D, scale: number, supersample: number) {\n // Every block-based codec works in macroblocks, so an odd dimension is\n // padded by the encoder with a smeared strip that was never drawn.\n const output = { width: evenUp(viewport.width * scale), height: evenUp(viewport.height * scale) };\n const oversample = fittedSupersample(supersample, output);\n return { output, oversample, renderScale: scale * oversample };\n}\n\n/** A hidden canvas with a mounted render context, reusable across renders of one project. */\nexport interface ExportSurface {\n canvas: HTMLCanvasElement;\n renderContext: BrowserRenderContext;\n storageAdapter: StorageAdapter;\n assetCatalog: ManifestAssetCatalog;\n dispose(): void;\n}\n\n/**\n * Open the canvas, renderer, storage and render context an export draws with,\n * resolving once the surface can draw (a frame drawn before an asynchronously\n * started surface is ready encodes blank). The canvas is a whole multiple of the\n * output, so the downsample is exact.\n */\nexport async function openExportSurface(params: {\n canvasSize: { width: number; height: number };\n viewport: Size2D;\n fps: number;\n manifest: AssetManifest;\n} & RendererWasmOption): Promise<ExportSurface> {\n const canvas = document.createElement('canvas');\n canvas.width = params.canvasSize.width;\n canvas.height = params.canvasSize.height;\n canvas.style.display = 'none';\n document.body.appendChild(canvas);\n try {\n const assetCatalog = new ManifestAssetCatalog(params.manifest);\n const renderer = await loadRenderer(params);\n const storageAdapter = renderer.createStorageAdapter({ catalog: assetCatalog, viewport: params.viewport, fps: params.fps });\n const renderContext = renderer.createRenderContext(storageAdapter);\n renderContext.mount(canvas);\n await renderContext.whenMounted?.();\n return {\n canvas, renderContext, storageAdapter, assetCatalog,\n dispose: () => {\n renderContext.dispose();\n canvas.remove();\n },\n };\n } catch (error) {\n canvas.remove();\n throw error;\n }\n}\n\n/**\n * Renders a document frame-by-frame to an offscreen canvas and muxes the result\n * into an MP4 or WebM via mediabunny.\n *\n * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry\n * that keeps every frame accurate, audio scheduling, progress and cancellation —\n * lives in `renderTimeline` (frame-loop.ts). What this function\n * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,\n * a Web Audio mixer, and the `<a download>` delivery.\n */\nexport async function exportVideo(params: ExportParams): Promise<Uint8Array | void> {\n const {\n // Not `document`, which would shadow the DOM global this function uses.\n document: source,\n registry = new Registry(),\n scale = 1,\n format = 'mp4',\n manifest = EMPTY_MANIFEST,\n theme,\n includeAudio = true,\n video,\n audio = {},\n frames,\n onProgress,\n signal,\n rendererWasmUrl,\n supersample = DEFAULT_SUPERSAMPLE,\n returnBytes = false,\n } = params;\n\n const spec = formatSpec(format);\n if (spec.kind !== 'video') throw new RangeError(`exportVideo writes video; \"${format}\" is audio, which exportAudio writes.`);\n const filename = params.filename ?? `export.${spec.extension}`;\n const project = resolveProjectDocument(source, params, DEFAULT_OUTPUT);\n if (project.tracks.length === 0) return;\n signal?.throwIfAborted();\n const viewport = project.viewport!;\n const fps = project.fps!;\n\n const { output, oversample, renderScale } = exportGeometry(viewport, scale, supersample);\n const surface = await openExportSurface({\n canvasSize: { width: output.width * oversample, height: output.height * oversample },\n viewport, fps, manifest, rendererWasmUrl,\n });\n\n let bytes: Uint8Array | undefined;\n try {\n const encoding = await resolveVideoEncoding(video, format, output, fps);\n bytes = await renderTimeline<AudioBuffer>({\n project, registry, theme, scale: renderScale, includeAudio, frames,\n renderContext: surface.renderContext, storageAdapter: surface.storageAdapter, assetCatalog: surface.assetCatalog,\n sink: new MediabunnyVideoSink(surface.canvas, output, format, encoding, audio),\n mixer: new WebAudioMixer(audio.sampleRate, surface.assetCatalog),\n onProgress,\n onIssue: params.onIssue,\n signal,\n }) as Uint8Array | undefined;\n } finally {\n surface.dispose();\n }\n\n if (!bytes) return;\n return deliver(bytes, { returnBytes, filename, mimeType: spec.mimeType });\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { AudioBufferSource, type AudioCodec, type OutputFormat, type VideoCodec } from "mediabunny";
2
+ /** A file with a picture: rendered frame by frame, with the mixed audio as a second track. */
3
+ export type VideoFormat = "mp4" | "webm";
4
+ /** A file of the timeline's mixed audio alone; nothing is drawn. */
5
+ export type AudioFormat = "wav" | "mp3" | "m4a";
6
+ export type ExportFormat = VideoFormat | AudioFormat;
7
+ /** What a host needs to name, save and play an exported file. */
8
+ export interface ExportFormatInfo {
9
+ kind: "video" | "audio";
10
+ /** Without the dot. */
11
+ extension: string;
12
+ mimeType: string;
13
+ }
14
+ interface FormatSpec extends ExportFormatInfo {
15
+ container: () => OutputFormat;
16
+ audioCodec: AudioCodec;
17
+ /** Video formats only: what the container takes, the default first, and the codec used unprobed when none of those encode. */
18
+ video?: {
19
+ codecs: readonly VideoCodec[];
20
+ fallback: VideoCodec;
21
+ };
22
+ }
23
+ export declare const EXPORT_FORMATS: Readonly<Record<ExportFormat, ExportFormatInfo>>;
24
+ export declare function formatSpec(format: ExportFormat): FormatSpec;
25
+ export declare function isAudioFormat(format: ExportFormat): format is AudioFormat;
26
+ export declare const DEFAULT_AUDIO_BITRATE = 192000;
27
+ /** The mixed-audio track for `format`. Ensure its encoder first with {@link ensureAudioEncoder}. */
28
+ export declare function audioTrackSource(format: ExportFormat, bitrate?: number): AudioBufferSource;
29
+ /**
30
+ * Throws before a frame is rendered when this browser cannot encode `format`'s
31
+ * audio. No browser ships an MP3 encoder, so LAME is loaded on first use (a
32
+ * WASM worker from a `blob:` URL, which a host's CSP must allow).
33
+ */
34
+ export declare function ensureAudioEncoder(format: ExportFormat): Promise<void>;
35
+ export {};
36
+ //# sourceMappingURL=formats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../src/formats.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,iBAAiB,EAMjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,UAAU,EAClB,MAAM,YAAY,CAAC;AAEpB,8FAA8F;AAC9F,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,CAAC;AACzC,oEAAoE;AACpE,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAChD,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,WAAW,CAAC;AAErD,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,UAAW,SAAQ,gBAAgB;IACzC,SAAS,EAAE,MAAM,YAAY,CAAC;IAC9B,UAAU,EAAE,UAAU,CAAC;IACvB,8HAA8H;IAC9H,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;QAAC,QAAQ,EAAE,UAAU,CAAA;KAAE,CAAC;CACnE;AAgCD,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,CAAC,CAAW,CAAC;AAExF,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,UAAU,CAI3D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,IAAI,WAAW,CAEzE;AAED,eAAO,MAAM,qBAAqB,SAAU,CAAC;AAG7C,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,SAAwB,GAAG,iBAAiB,CAQzG;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB5E"}
@@ -0,0 +1,83 @@
1
+ import { AudioBufferSource, Mp3OutputFormat, Mp4OutputFormat, WavOutputFormat, WebMOutputFormat, canEncodeAudio, } from "mediabunny";
2
+ const FORMATS = {
3
+ mp4: {
4
+ kind: "video", extension: "mp4", mimeType: "video/mp4",
5
+ container: () => new Mp4OutputFormat(),
6
+ audioCodec: "aac",
7
+ video: { codecs: ["avc", "hevc", "av1", "vp9"], fallback: "avc" },
8
+ },
9
+ webm: {
10
+ kind: "video", extension: "webm", mimeType: "video/webm",
11
+ container: () => new WebMOutputFormat(),
12
+ audioCodec: "opus",
13
+ video: { codecs: ["vp9", "av1", "vp8"], fallback: "vp8" },
14
+ },
15
+ wav: {
16
+ kind: "audio", extension: "wav", mimeType: "audio/wav",
17
+ container: () => new WavOutputFormat(),
18
+ audioCodec: "pcm-s16",
19
+ },
20
+ mp3: {
21
+ kind: "audio", extension: "mp3", mimeType: "audio/mpeg",
22
+ container: () => new Mp3OutputFormat(),
23
+ audioCodec: "mp3",
24
+ },
25
+ m4a: {
26
+ kind: "audio", extension: "m4a", mimeType: "audio/mp4",
27
+ container: () => new Mp4OutputFormat(),
28
+ audioCodec: "aac",
29
+ },
30
+ };
31
+ export const EXPORT_FORMATS = FORMATS;
32
+ export function formatSpec(format) {
33
+ const spec = FORMATS[format];
34
+ if (!spec)
35
+ throw new RangeError(`"${String(format)}" is not an export format; expected one of ${Object.keys(FORMATS).join(", ")}.`);
36
+ return spec;
37
+ }
38
+ export function isAudioFormat(format) {
39
+ return formatSpec(format).kind === "audio";
40
+ }
41
+ export const DEFAULT_AUDIO_BITRATE = 192_000;
42
+ const OPUS_SAMPLE_RATE = 48_000;
43
+ /** The mixed-audio track for `format`. Ensure its encoder first with {@link ensureAudioEncoder}. */
44
+ export function audioTrackSource(format, bitrate = DEFAULT_AUDIO_BITRATE) {
45
+ const codec = formatSpec(format).audioCodec;
46
+ return new AudioBufferSource({
47
+ codec,
48
+ bitrate,
49
+ // Opus encodes at 48 kHz only, and the mix runs at 44.1 kHz unless asked otherwise.
50
+ ...(codec === "opus" && { transform: { sampleRate: OPUS_SAMPLE_RATE } }),
51
+ });
52
+ }
53
+ let mp3Registered = null;
54
+ /**
55
+ * Throws before a frame is rendered when this browser cannot encode `format`'s
56
+ * audio. No browser ships an MP3 encoder, so LAME is loaded on first use (a
57
+ * WASM worker from a `blob:` URL, which a host's CSP must allow).
58
+ */
59
+ export async function ensureAudioEncoder(format) {
60
+ const codec = formatSpec(format).audioCodec;
61
+ if (codec === "mp3") {
62
+ mp3Registered ??= (async () => {
63
+ if (await canEncodeAudio("mp3"))
64
+ return;
65
+ const { registerMp3Encoder } = await import("@mediabunny/mp3-encoder");
66
+ registerMp3Encoder();
67
+ })();
68
+ try {
69
+ await mp3Registered;
70
+ }
71
+ catch (error) {
72
+ mp3Registered = null;
73
+ throw error;
74
+ }
75
+ return;
76
+ }
77
+ if (codec.startsWith("pcm-"))
78
+ return;
79
+ if (!(await canEncodeAudio(codec))) {
80
+ throw new Error(`This browser has no ${codec.toUpperCase()} encoder, so it cannot export ${format.toUpperCase()} audio.`);
81
+ }
82
+ }
83
+ //# sourceMappingURL=formats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formats.js","sourceRoot":"","sources":["../src/formats.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AudioBufferSource,\n Mp3OutputFormat,\n Mp4OutputFormat,\n WavOutputFormat,\n WebMOutputFormat,\n canEncodeAudio,\n type AudioCodec,\n type OutputFormat,\n type VideoCodec,\n} from \"mediabunny\";\n\n/** A file with a picture: rendered frame by frame, with the mixed audio as a second track. */\nexport type VideoFormat = \"mp4\" | \"webm\";\n/** A file of the timeline's mixed audio alone; nothing is drawn. */\nexport type AudioFormat = \"wav\" | \"mp3\" | \"m4a\";\nexport type ExportFormat = VideoFormat | AudioFormat;\n\n/** What a host needs to name, save and play an exported file. */\nexport interface ExportFormatInfo {\n kind: \"video\" | \"audio\";\n /** Without the dot. */\n extension: string;\n mimeType: string;\n}\n\ninterface FormatSpec extends ExportFormatInfo {\n container: () => OutputFormat;\n audioCodec: AudioCodec;\n /** Video formats only: what the container takes, the default first, and the codec used unprobed when none of those encode. */\n video?: { codecs: readonly VideoCodec[]; fallback: VideoCodec };\n}\n\nconst FORMATS: Record<ExportFormat, FormatSpec> = {\n mp4: {\n kind: \"video\", extension: \"mp4\", mimeType: \"video/mp4\",\n container: () => new Mp4OutputFormat(),\n audioCodec: \"aac\",\n video: { codecs: [\"avc\", \"hevc\", \"av1\", \"vp9\"], fallback: \"avc\" },\n },\n webm: {\n kind: \"video\", extension: \"webm\", mimeType: \"video/webm\",\n container: () => new WebMOutputFormat(),\n audioCodec: \"opus\",\n video: { codecs: [\"vp9\", \"av1\", \"vp8\"], fallback: \"vp8\" },\n },\n wav: {\n kind: \"audio\", extension: \"wav\", mimeType: \"audio/wav\",\n container: () => new WavOutputFormat(),\n audioCodec: \"pcm-s16\",\n },\n mp3: {\n kind: \"audio\", extension: \"mp3\", mimeType: \"audio/mpeg\",\n container: () => new Mp3OutputFormat(),\n audioCodec: \"mp3\",\n },\n m4a: {\n kind: \"audio\", extension: \"m4a\", mimeType: \"audio/mp4\",\n container: () => new Mp4OutputFormat(),\n audioCodec: \"aac\",\n },\n};\n\nexport const EXPORT_FORMATS: Readonly<Record<ExportFormat, ExportFormatInfo>> = FORMATS;\n\nexport function formatSpec(format: ExportFormat): FormatSpec {\n const spec = FORMATS[format];\n if (!spec) throw new RangeError(`\"${String(format)}\" is not an export format; expected one of ${Object.keys(FORMATS).join(\", \")}.`);\n return spec;\n}\n\nexport function isAudioFormat(format: ExportFormat): format is AudioFormat {\n return formatSpec(format).kind === \"audio\";\n}\n\nexport const DEFAULT_AUDIO_BITRATE = 192_000;\nconst OPUS_SAMPLE_RATE = 48_000;\n\n/** The mixed-audio track for `format`. Ensure its encoder first with {@link ensureAudioEncoder}. */\nexport function audioTrackSource(format: ExportFormat, bitrate = DEFAULT_AUDIO_BITRATE): AudioBufferSource {\n const codec = formatSpec(format).audioCodec;\n return new AudioBufferSource({\n codec,\n bitrate,\n // Opus encodes at 48 kHz only, and the mix runs at 44.1 kHz unless asked otherwise.\n ...(codec === \"opus\" && { transform: { sampleRate: OPUS_SAMPLE_RATE } }),\n });\n}\n\nlet mp3Registered: Promise<void> | null = null;\n\n/**\n * Throws before a frame is rendered when this browser cannot encode `format`'s\n * audio. No browser ships an MP3 encoder, so LAME is loaded on first use (a\n * WASM worker from a `blob:` URL, which a host's CSP must allow).\n */\nexport async function ensureAudioEncoder(format: ExportFormat): Promise<void> {\n const codec = formatSpec(format).audioCodec;\n if (codec === \"mp3\") {\n mp3Registered ??= (async () => {\n if (await canEncodeAudio(\"mp3\")) return;\n const { registerMp3Encoder } = await import(\"@mediabunny/mp3-encoder\");\n registerMp3Encoder();\n })();\n try {\n await mp3Registered;\n } catch (error) {\n mp3Registered = null;\n throw error;\n }\n return;\n }\n if (codec.startsWith(\"pcm-\")) return;\n if (!(await canEncodeAudio(codec))) {\n throw new Error(`This browser has no ${codec.toUpperCase()} encoder, so it cannot export ${format.toUpperCase()} audio.`);\n }\n}\n"]}