@motionscript/browser 0.0.0-stage → 0.1.0-alpha.0

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 (120) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +201 -0
  3. package/README.md +68 -3
  4. package/dist/asset-bytes.d.ts +50 -0
  5. package/dist/asset-bytes.d.ts.map +1 -0
  6. package/dist/asset-bytes.js +60 -0
  7. package/dist/asset-bytes.js.map +1 -0
  8. package/dist/audio/bus-graph.d.ts +43 -0
  9. package/dist/audio/bus-graph.d.ts.map +1 -0
  10. package/dist/audio/bus-graph.js +104 -0
  11. package/dist/audio/bus-graph.js.map +1 -0
  12. package/dist/audio/filter-graph.d.ts +58 -0
  13. package/dist/audio/filter-graph.d.ts.map +1 -0
  14. package/dist/audio/filter-graph.js +235 -0
  15. package/dist/audio/filter-graph.js.map +1 -0
  16. package/dist/audio/index.d.ts +5 -0
  17. package/dist/audio/index.d.ts.map +1 -0
  18. package/dist/audio/index.js +12 -0
  19. package/dist/audio/index.js.map +1 -0
  20. package/dist/audio/mixer.d.ts +59 -0
  21. package/dist/audio/mixer.d.ts.map +1 -0
  22. package/dist/audio/mixer.js +193 -0
  23. package/dist/audio/mixer.js.map +1 -0
  24. package/dist/audio/player.d.ts +49 -0
  25. package/dist/audio/player.d.ts.map +1 -0
  26. package/dist/audio/player.js +226 -0
  27. package/dist/audio/player.js.map +1 -0
  28. package/dist/browser/audio.js +2 -0
  29. package/dist/browser/audio.js.map +7 -0
  30. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +2 -0
  31. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +7 -0
  32. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +2 -0
  33. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js.map +7 -0
  34. package/dist/browser/chunks/chunk-CTLBXZSU.js +4119 -0
  35. package/dist/browser/chunks/chunk-CTLBXZSU.js.map +7 -0
  36. package/dist/browser/chunks/chunk-FQ32PDIU.js +2 -0
  37. package/dist/browser/chunks/chunk-FQ32PDIU.js.map +7 -0
  38. package/dist/browser/chunks/chunk-FY3BTKY4.js +2 -0
  39. package/dist/browser/chunks/chunk-FY3BTKY4.js.map +7 -0
  40. package/dist/browser/chunks/chunk-NSFLRI4P.js +2 -0
  41. package/dist/browser/chunks/chunk-NSFLRI4P.js.map +7 -0
  42. package/dist/browser/chunks/three.module-Z4EOVUWF.js +2 -0
  43. package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +7 -0
  44. package/dist/browser/esbuild.wasm +0 -0
  45. package/dist/browser/index.js +2172 -0
  46. package/dist/browser/index.js.map +7 -0
  47. package/dist/browser/manifest.json +15 -0
  48. package/dist/concat.d.ts +95 -0
  49. package/dist/concat.d.ts.map +1 -0
  50. package/dist/concat.js +174 -0
  51. package/dist/concat.js.map +1 -0
  52. package/dist/decode/image-levels.d.ts +137 -0
  53. package/dist/decode/image-levels.d.ts.map +1 -0
  54. package/dist/decode/image-levels.js +477 -0
  55. package/dist/decode/image-levels.js.map +1 -0
  56. package/dist/decode/levels.d.ts +37 -0
  57. package/dist/decode/levels.d.ts.map +1 -0
  58. package/dist/decode/levels.js +68 -0
  59. package/dist/decode/levels.js.map +1 -0
  60. package/dist/decode/video-levels.d.ts +37 -0
  61. package/dist/decode/video-levels.d.ts.map +1 -0
  62. package/dist/decode/video-levels.js +95 -0
  63. package/dist/decode/video-levels.js.map +1 -0
  64. package/dist/dynamic-nodes.d.ts +9 -0
  65. package/dist/dynamic-nodes.d.ts.map +1 -0
  66. package/dist/dynamic-nodes.js +56 -0
  67. package/dist/dynamic-nodes.js.map +1 -0
  68. package/dist/engine.d.ts +59 -0
  69. package/dist/engine.d.ts.map +1 -0
  70. package/dist/engine.js +171 -0
  71. package/dist/engine.js.map +1 -0
  72. package/dist/exporter.d.ts +163 -0
  73. package/dist/exporter.d.ts.map +1 -0
  74. package/dist/exporter.js +263 -0
  75. package/dist/exporter.js.map +1 -0
  76. package/dist/getter.d.ts +10 -0
  77. package/dist/getter.d.ts.map +1 -0
  78. package/dist/getter.js +38 -0
  79. package/dist/getter.js.map +1 -0
  80. package/dist/index.d.ts +22 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +72 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/measure-context.d.ts +12 -0
  85. package/dist/measure-context.d.ts.map +1 -0
  86. package/dist/measure-context.js +12 -0
  87. package/dist/measure-context.js.map +1 -0
  88. package/dist/mix-timeline-audio.d.ts +42 -0
  89. package/dist/mix-timeline-audio.d.ts.map +1 -0
  90. package/dist/mix-timeline-audio.js +63 -0
  91. package/dist/mix-timeline-audio.js.map +1 -0
  92. package/dist/output.d.ts +7 -0
  93. package/dist/output.d.ts.map +1 -0
  94. package/dist/output.js +6 -0
  95. package/dist/output.js.map +1 -0
  96. package/dist/render-context.d.ts +30 -0
  97. package/dist/render-context.d.ts.map +1 -0
  98. package/dist/render-context.js +55 -0
  99. package/dist/render-context.js.map +1 -0
  100. package/dist/screenshot.d.ts +45 -0
  101. package/dist/screenshot.d.ts.map +1 -0
  102. package/dist/screenshot.js +32 -0
  103. package/dist/screenshot.js.map +1 -0
  104. package/dist/still.d.ts +151 -0
  105. package/dist/still.d.ts.map +1 -0
  106. package/dist/still.js +222 -0
  107. package/dist/still.js.map +1 -0
  108. package/dist/storage-adapter.d.ts +367 -0
  109. package/dist/storage-adapter.d.ts.map +1 -0
  110. package/dist/storage-adapter.js +1278 -0
  111. package/dist/storage-adapter.js.map +1 -0
  112. package/dist/three/renderer.d.ts +48 -0
  113. package/dist/three/renderer.d.ts.map +1 -0
  114. package/dist/three/renderer.js +177 -0
  115. package/dist/three/renderer.js.map +1 -0
  116. package/dist/ticker.d.ts +42 -0
  117. package/dist/ticker.d.ts.map +1 -0
  118. package/dist/ticker.js +143 -0
  119. package/dist/ticker.js.map +1 -0
  120. package/package.json +74 -3
@@ -0,0 +1,163 @@
1
+ import { Registry, type AssetManifest, type ProjectDocument, type SceneDocument, type Size2D } from "@motionscript/core";
2
+ import { type Quality } from 'mediabunny';
3
+ /** Reports export progress in [0, 1]; video encoding occupies most of the range, audio mixing/finalize the tail. */
4
+ export type ExportProgressCallback = (progress: number) => void;
5
+ /**
6
+ * How the video track is encoded.
7
+ *
8
+ * Every field is a straight pass-through to mediabunny's `VideoEncodingConfig`,
9
+ * defaulted here rather than there so an export has one obvious place to look.
10
+ */
11
+ export type VideoEncoderOptions = {
12
+ /**
13
+ * Which codec the video track is encoded with. Defaults to `'avc'` (H.264),
14
+ * which every player, editor and upload target accepts.
15
+ *
16
+ * `'hevc'` and `'av1'` reconstruct sharp edges far better at the same
17
+ * bitrate, which is what a listing of syntax-highlighted code or a hairline
18
+ * stroke is made of — worth reaching for when the file is going somewhere
19
+ * that decodes them. Both are still 4:2:0, so they narrow the gap rather
20
+ * than close it. Availability depends on the platform's encoders; an
21
+ * unsupported choice falls back to `'avc'` rather than failing the export.
22
+ */
23
+ codec?: 'avc' | 'hevc' | 'av1' | 'vp9';
24
+ /**
25
+ * Which encoder implementation WebCodecs should prefer.
26
+ *
27
+ * Defaults to `'no-preference'`, which lets each platform use its own
28
+ * hardware encoder (VideoToolbox on macOS, Media Foundation/NVENC on
29
+ * Windows, VA-API on Linux). That is typically several times faster than the
30
+ * software path *and* it is the only configuration in which this sink's
31
+ * zero-readback design actually holds: a software encoder needs the pixels
32
+ * on the CPU, so it forces a full GPU→CPU copy of every frame that a
33
+ * hardware encoder reads directly off the canvas.
34
+ *
35
+ * Pass `'prefer-software'` when byte-identical output across machines
36
+ * matters more than speed — different hardware encoders tune quality and
37
+ * colour differently at the same nominal bitrate, so the same settings do
38
+ * not produce the same file.
39
+ */
40
+ hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
41
+ /**
42
+ * Target bitrate in bits per second, or one of mediabunny's subjective
43
+ * `Quality` levels.
44
+ *
45
+ * Defaults to {@link motionGraphicsBitrate}, which is deliberately *not*
46
+ * mediabunny's `QUALITY_VERY_HIGH`: that level is frame-rate blind, so a
47
+ * 60 fps export got the same ~12 Mbps at 1080p as a 30 fps one and paid for
48
+ * it with ringing along every glyph edge and banding across every gradient.
49
+ * See that function for the reasoning.
50
+ */
51
+ bitrate?: number | Quality;
52
+ /** Defaults to mediabunny's `'variable'`. `'constant'` is the reproducible-output choice. */
53
+ bitrateMode?: 'constant' | 'variable';
54
+ /**
55
+ * Seconds between key frames. Defaults to 1.
56
+ *
57
+ * Shorter than mediabunny's 2, because a frame's error is carried by every
58
+ * inter frame that references it: on synthetic content, where a flat
59
+ * background makes a shimmer obvious, a keyframe twice as often visibly
60
+ * resets the drift. It also halves the seek granularity, which matters for a
61
+ * clip an editor is going to scrub.
62
+ */
63
+ keyFrameInterval?: number;
64
+ /**
65
+ * WebCodecs performance/quality trade-off. Defaults to `'quality'`, which
66
+ * also guarantees no frame is dropped — the only correct setting for a file
67
+ * being written rather than streamed.
68
+ */
69
+ latencyMode?: 'quality' | 'realtime';
70
+ /**
71
+ * A hint about what the frames contain, per
72
+ * [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).
73
+ *
74
+ * Defaults to `'detail'`: encoders that honour it bias towards preserving
75
+ * sharp edges and fine texture over smoothing motion, which is what motion
76
+ * graphics are. Ignored where it is not implemented.
77
+ */
78
+ contentHint?: string;
79
+ };
80
+ /**
81
+ * The default video bitrate for an export of this size and frame rate.
82
+ *
83
+ * Scaling with **both** axes is the point. mediabunny's `Quality` levels scale
84
+ * with resolution only, so doubling the frame rate halved the bits available per
85
+ * frame while leaving the number unchanged — which is exactly the configuration
86
+ * that produced clean stills and a mushy video from the very same renderer.
87
+ */
88
+ export declare function motionGraphicsBitrate(width: number, height: number, fps: number): number;
89
+ /** How the mixed audio track is encoded. */
90
+ export type AudioEncoderOptions = {
91
+ /** AAC bitrate in bits per second. Defaults to 192 kbps. */
92
+ bitrate?: number;
93
+ /** Mixdown sample rate in Hz. Defaults to the mixer's own (44100). */
94
+ sampleRate?: number;
95
+ };
96
+ export type ExportParams = {
97
+ /** A project, or a lone still or composition as the one track of one. */
98
+ document: ProjectDocument | SceneDocument;
99
+ /** The vocabulary its clips compile against. Defaults to what the library ships. */
100
+ registry?: Registry;
101
+ /** Win over the document's own; 1920×1080 at 60 fps when neither says. */
102
+ viewport?: Size2D;
103
+ fps?: number;
104
+ scale?: number;
105
+ filename?: string;
106
+ manifest?: AssetManifest;
107
+ /**
108
+ * Set false to produce a silent MP4 — no AAC track is added to the
109
+ * container and the mix is skipped entirely. Defaults to true.
110
+ */
111
+ includeAudio?: boolean;
112
+ /** Video encoder settings; see {@link VideoEncoderOptions} for the defaults. */
113
+ video?: VideoEncoderOptions;
114
+ /** Audio encoder settings; ignored when `includeAudio` is false. */
115
+ audio?: AudioEncoderOptions;
116
+ onProgress?: ExportProgressCallback;
117
+ signal?: AbortSignal;
118
+ wasmUrl?: string;
119
+ /**
120
+ * Render each frame at this multiple of the output resolution and downsample
121
+ * it before encoding. **Defaults to 2**; pass 1 to turn it off.
122
+ *
123
+ * The one lever that improves an export at a *fixed* output size. Every
124
+ * codec a browser can encode is 4:2:0 — colour is stored at half resolution
125
+ * on both axes, and no bitrate buys that back — so the colour fringing along
126
+ * a saturated glyph or a hard edge is a property of the format, not of the
127
+ * settings. Handing the encoder an already-downsampled frame does not undo
128
+ * the subsampling, but it does mean every chroma sample averages four real
129
+ * pixels instead of one, which is what removes most of the crawl on fine
130
+ * detail and much of the banding on gradients.
131
+ *
132
+ * Measured against a correctly-sampled reference frame of a syntax-highlighted
133
+ * listing, `2` lands 2.7 dB closer than a 1× encode — and 1.3 dB closer than
134
+ * an *unencoded* 1× still, which is why it is the default despite costing
135
+ * `supersample²` times the render work per frame plus a rescale. Pass 1 when
136
+ * turnaround matters more than the file, and note that exporting at
137
+ * `scale: 2` is cheaper than supersampling *and* better whenever the extra
138
+ * resolution is wanted in the file itself.
139
+ *
140
+ * Clamped so the render surface stays inside {@link MAX_RENDER_AXIS}, since a
141
+ * supersampled 4K export is already drawing 8K frames.
142
+ */
143
+ supersample?: number;
144
+ /**
145
+ * When true, the encoded MP4 bytes are returned instead of triggering a
146
+ * browser download. Used by headless drivers (e.g. the CLI) that capture
147
+ * the output themselves and write it to disk; leave unset for the
148
+ * interactive player, which downloads via an `<a>` element.
149
+ */
150
+ returnBytes?: boolean;
151
+ };
152
+ /**
153
+ * Renders a document frame-by-frame to an offscreen canvas and muxes the result
154
+ * into an MP4 via mediabunny.
155
+ *
156
+ * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry
157
+ * that keeps every frame accurate, audio scheduling, progress and cancellation —
158
+ * lives in `@motionscript/skia-render`'s `renderTimeline`. What this function
159
+ * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,
160
+ * a Web Audio mixer, and the `<a download>` delivery.
161
+ */
162
+ export declare function exportVideo(params: ExportParams): Promise<Uint8Array | void>;
163
+ //# sourceMappingURL=exporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"AAAA,OAAO,EACmB,QAAQ,EAA0B,KAAK,aAAa,EAC1E,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EACxD,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAOH,KAAK,OAAO,EAEf,MAAM,YAAY,CAAC;AAOpB,oHAAoH;AACpH,MAAM,MAAM,sBAAsB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAC9B;;;;;;;;;;OAUG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;IACvC;;;;;;;;;;;;;;;OAeG;IACH,oBAAoB,CAAC,EAAE,eAAe,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;IAC/E;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,6FAA6F;IAC7F,WAAW,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACtC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IACrC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAsBF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGxF;AAED,4CAA4C;AAC5C,MAAM,MAAM,mBAAmB,GAAG;IAC9B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAQF,MAAM,MAAM,YAAY,GAAG;IACvB,yEAAyE;IACzE,QAAQ,EAAE,eAAe,GAAG,aAAa,CAAC;IAC1C,oFAAoF;IACpF,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gFAAgF;IAChF,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,oEAAoE;IACpE,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAA;AAgKD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAmFlF"}
@@ -0,0 +1,263 @@
1
+ import { ManifestAssetCatalog, Registry, resolveProjectDocument, } from "@motionscript/core";
2
+ import { renderTimeline, } from "@motionscript/skia-render/export";
3
+ import { AudioBufferSource, BufferTarget, CanvasSource, canEncodeVideo, Mp4OutputFormat, Output, } from 'mediabunny';
4
+ import { WebRenderContext } from "./render-context";
5
+ import { WebStorageAdapter } from "./storage-adapter";
6
+ import { getCanvasKit } from "./getter";
7
+ import { WebAudioMixer } from "./audio/mixer";
8
+ import { DEFAULT_OUTPUT } from "./output";
9
+ /**
10
+ * Bits per pixel per frame the default bitrate is built from.
11
+ *
12
+ * Rendered motion graphics are the hard case for a codec tuned on camera
13
+ * footage: hard glyph edges, large flat fields, and gradients with no sensor
14
+ * noise to hide the quantisation in. They compress *worse* than live action at
15
+ * the same perceived quality, not better, so the figure here is well above what
16
+ * a streaming preset would use — this is a master being rendered once, not a
17
+ * bitstream being pushed down a pipe.
18
+ */
19
+ const BITS_PER_PIXEL = 0.2;
20
+ /** Floor, so a tiny viewport still gets enough bits to be legible. */
21
+ const MIN_VIDEO_BITRATE = 2_000_000;
22
+ /**
23
+ * Ceiling. Past this the returns are gone and the levels start excluding
24
+ * hardware decoders — 4K60 lands here, and AVC level 5.1 (which every consumer
25
+ * device implements) tops out at 240 Mbps, well clear of it.
26
+ */
27
+ const MAX_VIDEO_BITRATE = 100_000_000;
28
+ /**
29
+ * The default video bitrate for an export of this size and frame rate.
30
+ *
31
+ * Scaling with **both** axes is the point. mediabunny's `Quality` levels scale
32
+ * with resolution only, so doubling the frame rate halved the bits available per
33
+ * frame while leaving the number unchanged — which is exactly the configuration
34
+ * that produced clean stills and a mushy video from the very same renderer.
35
+ */
36
+ export function motionGraphicsBitrate(width, height, fps) {
37
+ const raw = width * height * fps * BITS_PER_PIXEL;
38
+ return Math.round(Math.min(MAX_VIDEO_BITRATE, Math.max(MIN_VIDEO_BITRATE, raw)));
39
+ }
40
+ const DEFAULT_AUDIO_BITRATE = 192_000;
41
+ /** See {@link VideoEncoderOptions.keyFrameInterval}. */
42
+ const DEFAULT_KEY_FRAME_INTERVAL = 1;
43
+ /** See {@link ExportParams.supersample}. */
44
+ const DEFAULT_SUPERSAMPLE = 2;
45
+ const EMPTY_MANIFEST = {
46
+ image: {},
47
+ video: {},
48
+ audio: {},
49
+ font: {},
50
+ };
51
+ // ── Audio mixing ─────────────────────────────────────────────────────────────
52
+ //
53
+ // The mix itself lives in ./audio/mixer, shared with `mixTimelineAudio` so a
54
+ // split export and a whole one cannot mix differently.
55
+ // ── Video sink ───────────────────────────────────────────────────────────────
56
+ /**
57
+ * mediabunny/WebCodecs implementation of the frame sink.
58
+ *
59
+ * Wraps the very `<canvas>` CanvasKit draws into, so `addFrame` costs nothing but
60
+ * a timestamp — WebCodecs pulls the pixels from the canvas itself, with no
61
+ * GPU→CPU readback. That is why the seam signals a frame rather than passing one.
62
+ *
63
+ * The readback claim holds only while the encoder is a hardware one, which is
64
+ * why {@link VideoEncoderOptions.hardwareAcceleration} defaults to letting the
65
+ * platform choose: a software encoder has to be handed CPU pixels, so it drags
66
+ * a full-frame copy off the GPU on every single frame.
67
+ */
68
+ class MediabunnyVideoSink {
69
+ audioOptions;
70
+ target = new BufferTarget();
71
+ output;
72
+ video;
73
+ audio = null;
74
+ constructor(canvas, fps,
75
+ /** Encoded output size, which is the canvas' own unless supersampling is on. */
76
+ output, video = {}, audioOptions = {}, codec = 'avc') {
77
+ this.audioOptions = audioOptions;
78
+ const downsampling = output.width !== canvas.width || output.height !== canvas.height;
79
+ this.video = new CanvasSource(canvas, {
80
+ codec,
81
+ // Resolved against what is actually being encoded — the *output*
82
+ // size and the rate the frame loop will feed it — rather than
83
+ // against a subjective level that never sees the frame rate.
84
+ bitrate: video.bitrate ?? motionGraphicsBitrate(output.width, output.height, fps),
85
+ hardwareAcceleration: video.hardwareAcceleration ?? 'no-preference',
86
+ keyFrameInterval: video.keyFrameInterval ?? DEFAULT_KEY_FRAME_INTERVAL,
87
+ latencyMode: video.latencyMode ?? 'quality',
88
+ contentHint: video.contentHint ?? 'detail',
89
+ ...(video.bitrateMode !== undefined && { bitrateMode: video.bitrateMode }),
90
+ // `fit` is exact here — the supersampled canvas is a whole multiple
91
+ // of the output — but mediabunny requires it whenever both axes are
92
+ // given, so it is stated rather than inferred.
93
+ ...(downsampling && { transform: { width: output.width, height: output.height, fit: 'fill' } }),
94
+ });
95
+ this.output = new Output({ format: new Mp4OutputFormat(), target: this.target });
96
+ }
97
+ async start(info) {
98
+ this.output.addVideoTrack(this.video, { frameRate: info.fps });
99
+ // Tracks must be declared before the muxer starts, which is why the
100
+ // orchestrator reports this rather than leaving it to be inferred.
101
+ if (info.hasAudio) {
102
+ this.audio = new AudioBufferSource({
103
+ codec: 'aac',
104
+ bitrate: this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE,
105
+ });
106
+ this.output.addAudioTrack(this.audio);
107
+ }
108
+ await this.output.start();
109
+ }
110
+ async addFrame(timestamp, duration) {
111
+ await this.video.add(timestamp, duration);
112
+ }
113
+ async addAudio(mixed) {
114
+ if (!this.audio)
115
+ return;
116
+ await this.audio.add(mixed);
117
+ }
118
+ async finalize() {
119
+ this.video.close();
120
+ this.audio?.close();
121
+ await this.output.finalize();
122
+ const buffer = this.target.buffer;
123
+ if (!buffer)
124
+ throw new Error('Export produced no data');
125
+ return new Uint8Array(buffer);
126
+ }
127
+ }
128
+ // ── Main export ──────────────────────────────────────────────────────────────
129
+ /** Round up to the nearest even integer — see the call site. */
130
+ function evenUp(n) {
131
+ const rounded = Math.round(n);
132
+ return rounded % 2 === 0 ? rounded : rounded + 1;
133
+ }
134
+ /**
135
+ * Longest axis the render surface is allowed to reach.
136
+ *
137
+ * Supersampling multiplies the surface, not the output: a 4K export at
138
+ * `supersample: 2` is drawing 8K frames, and the next step up asks for a
139
+ * 15360-wide WebGL surface that most drivers will not give and none will give
140
+ * cheaply. 8192 is the smallest `MAX_TEXTURE_SIZE` in wide circulation, so a
141
+ * surface inside it is one every machine that can run the export at all can
142
+ * also allocate.
143
+ */
144
+ const MAX_RENDER_AXIS = 8192;
145
+ /**
146
+ * How far the frame can actually be supersampled at this output size.
147
+ *
148
+ * Reduced rather than refused: an export that quietly renders at 1× is a
149
+ * slightly softer file, and one that throws because the default was too
150
+ * ambitious for a 4K project is no file at all.
151
+ */
152
+ function fittedSupersample(requested, output) {
153
+ const longest = Math.max(output.width, output.height);
154
+ let factor = Math.max(1, Math.round(requested));
155
+ while (factor > 1 && longest * factor > MAX_RENDER_AXIS)
156
+ factor--;
157
+ return factor;
158
+ }
159
+ /**
160
+ * The codec to actually encode with: the one asked for if this platform can
161
+ * encode it at this size, `'avc'` otherwise.
162
+ *
163
+ * A better codec is the single largest quality win available on synthetic
164
+ * content — but which ones a machine has encoders for varies by OS, browser and
165
+ * GPU, and an export that dies at the muxer because the machine has no HEVC
166
+ * encoder is worse than one that quietly produces H.264. AVC is not probed: it
167
+ * is the fallback, and there is nothing to fall back to.
168
+ */
169
+ async function resolveCodec(requested, size, fps) {
170
+ if (!requested || requested === 'avc')
171
+ return 'avc';
172
+ try {
173
+ const ok = await canEncodeVideo(requested, {
174
+ width: size.width,
175
+ height: size.height,
176
+ bitrate: motionGraphicsBitrate(size.width, size.height, fps),
177
+ });
178
+ if (ok)
179
+ return requested;
180
+ }
181
+ catch {
182
+ // A probe that throws is a probe that answered "no".
183
+ }
184
+ console.warn(`[motion-script] No ${requested} encoder available; exporting as H.264 instead.`);
185
+ return 'avc';
186
+ }
187
+ /**
188
+ * Renders a document frame-by-frame to an offscreen canvas and muxes the result
189
+ * into an MP4 via mediabunny.
190
+ *
191
+ * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry
192
+ * that keeps every frame accurate, audio scheduling, progress and cancellation —
193
+ * lives in `@motionscript/skia-render`'s `renderTimeline`. What this function
194
+ * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,
195
+ * a Web Audio mixer, and the `<a download>` delivery.
196
+ */
197
+ export async function exportVideo(params) {
198
+ const {
199
+ // Not `document`, which would shadow the DOM global this function uses.
200
+ document: source, registry = new Registry(), scale = 1, filename = 'export.mp4', manifest = EMPTY_MANIFEST, includeAudio = true, video, audio = {}, onProgress, signal, wasmUrl, supersample = DEFAULT_SUPERSAMPLE, returnBytes = false, } = params;
201
+ const project = resolveProjectDocument(source, params, DEFAULT_OUTPUT);
202
+ if (project.tracks.length === 0)
203
+ return;
204
+ signal?.throwIfAborted();
205
+ const viewport = project.viewport;
206
+ const fps = project.fps;
207
+ // Every block-based codec works in macroblocks, so an odd dimension is
208
+ // padded by the encoder and the padding is *not* what was drawn — it shows up
209
+ // as a smeared or discoloured strip down one edge. Rounding up to an even
210
+ // size instead costs at most half a pixel of centring and keeps the whole
211
+ // frame real.
212
+ const outputSize = {
213
+ width: evenUp(viewport.width * scale),
214
+ height: evenUp(viewport.height * scale),
215
+ };
216
+ const oversample = fittedSupersample(supersample, outputSize);
217
+ const renderScale = scale * oversample;
218
+ const offscreenCanvas = document.createElement('canvas');
219
+ offscreenCanvas.width = evenUp(viewport.width * renderScale);
220
+ offscreenCanvas.height = evenUp(viewport.height * renderScale);
221
+ offscreenCanvas.style.display = 'none';
222
+ document.body.appendChild(offscreenCanvas);
223
+ const canvasKit = await getCanvasKit(wasmUrl);
224
+ const assetCatalog = new ManifestAssetCatalog(manifest);
225
+ const storageAdapter = new WebStorageAdapter(canvasKit, assetCatalog, viewport, fps);
226
+ const renderContext = new WebRenderContext(canvasKit, storageAdapter);
227
+ renderContext.mount(offscreenCanvas);
228
+ const codec = await resolveCodec(video?.codec, outputSize, fps);
229
+ let bytes;
230
+ try {
231
+ bytes = await renderTimeline({
232
+ project, registry, scale: renderScale, includeAudio,
233
+ renderContext, storageAdapter, assetCatalog,
234
+ sink: new MediabunnyVideoSink(offscreenCanvas, fps, outputSize, video, audio, codec),
235
+ mixer: new WebAudioMixer(audio.sampleRate, assetCatalog),
236
+ onProgress,
237
+ signal,
238
+ });
239
+ }
240
+ finally {
241
+ renderContext.dispose();
242
+ document.body.removeChild(offscreenCanvas);
243
+ }
244
+ if (!bytes)
245
+ return;
246
+ // Headless callers capture the bytes and write them to disk themselves,
247
+ // so skip the DOM-based download path entirely.
248
+ if (returnBytes)
249
+ return bytes;
250
+ // The sink builds this Uint8Array over a whole, plain ArrayBuffer, so the cast
251
+ // is sound — TS only balks because the generic parameter admits a
252
+ // SharedArrayBuffer, which a BlobPart may not be.
253
+ const blob = new Blob([bytes.buffer], { type: 'video/mp4' });
254
+ const url = URL.createObjectURL(blob);
255
+ const link = document.createElement('a');
256
+ link.href = url;
257
+ link.download = filename;
258
+ document.body.appendChild(link);
259
+ link.click();
260
+ document.body.removeChild(link);
261
+ URL.revokeObjectURL(url);
262
+ }
263
+ //# sourceMappingURL=exporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exporter.js","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,oBAAoB,EAAE,QAAQ,EAAE,sBAAsB,GAEzD,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACH,cAAc,GAEjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACH,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,eAAe,EACf,MAAM,GAGT,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAiF1C;;;;;;;;;GASG;AACH,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,sEAAsE;AACtE,MAAM,iBAAiB,GAAG,SAAS,CAAC;AACpC;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAa,EAAE,MAAc,EAAE,GAAW;IAC5E,MAAM,GAAG,GAAG,KAAK,GAAG,MAAM,GAAG,GAAG,GAAG,cAAc,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC;AAUD,MAAM,qBAAqB,GAAG,OAAO,CAAC;AACtC,wDAAwD;AACxD,MAAM,0BAA0B,GAAG,CAAC,CAAC;AACrC,4CAA4C;AAC5C,MAAM,mBAAmB,GAAG,CAAC,CAAC;AA2D9B,MAAM,cAAc,GAAkB;IAClC,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,EAAE;CACX,CAAC;AAEF,gFAAgF;AAChF,EAAE;AACF,6EAA6E;AAC7E,uDAAuD;AAEvD,gFAAgF;AAEhF;;;;;;;;;;;GAWG;AACH,MAAM,mBAAmB;IAYA;IAXJ,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAC5B,MAAM,CAAS;IACf,KAAK,CAAe;IAC7B,KAAK,GAA6B,IAAI,CAAC;IAE/C,YACI,MAAyB,EACzB,GAAW;IACX,gFAAgF;IAChF,MAAyC,EACzC,QAA6B,EAAE,EACd,eAAoC,EAAE,EACvD,QAAoB,KAAK;QADR,iBAAY,GAAZ,YAAY,CAA0B;QAGvD,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC;QACtF,IAAI,CAAC,KAAK,GAAG,IAAI,YAAY,CAAC,MAAM,EAAE;YAClC,KAAK;YACL,iEAAiE;YACjE,8DAA8D;YAC9D,6DAA6D;YAC7D,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,qBAAqB,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;YACjF,oBAAoB,EAAE,KAAK,CAAC,oBAAoB,IAAI,eAAe;YACnE,gBAAgB,EAAE,KAAK,CAAC,gBAAgB,IAAI,0BAA0B;YACtE,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,SAAS;YAC3C,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,QAAQ;YAC1C,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;YAC1E,oEAAoE;YACpE,oEAAoE;YACpE,+CAA+C;YAC/C,GAAG,CAAC,YAAY,IAAI,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,MAAe,EAAE,EAAE,CAAC;SAC3G,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,eAAe,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACrF,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAwC;QAChD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/D,oEAAoE;QACpE,mEAAmE;QACnE,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,GAAG,IAAI,iBAAiB,CAAC;gBAC/B,KAAK,EAAE,KAAK;gBACZ,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,IAAI,qBAAqB;aAC9D,CAAC,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,SAAiB,EAAE,QAAgB;QAC9C,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,KAAkB;QAC7B,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,QAAQ;QACV,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC;QACpB,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QAClC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACxD,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;CACJ;AAED,gFAAgF;AAEhF,gEAAgE;AAChE,SAAS,MAAM,CAAC,CAAS;IACrB,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC9B,OAAO,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,eAAe,GAAG,IAAI,CAAC;AAE7B;;;;;;GAMG;AACH,SAAS,iBAAiB,CAAC,SAAiB,EAAE,MAAyC;IACnF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAChD,OAAO,MAAM,GAAG,CAAC,IAAI,OAAO,GAAG,MAAM,GAAG,eAAe;QAAE,MAAM,EAAE,CAAC;IAClE,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,YAAY,CACvB,SAAiC,EACjC,IAAuC,EACvC,GAAW;IAEX,IAAI,CAAC,SAAS,IAAI,SAAS,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACpD,IAAI,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,cAAc,CAAC,SAAS,EAAE;YACvC,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,OAAO,EAAE,qBAAqB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;SAC/D,CAAC,CAAC;QACH,IAAI,EAAE;YAAE,OAAO,SAAS,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACL,qDAAqD;IACzD,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,sBAAsB,SAAS,iDAAiD,CAAC,CAAC;IAC/F,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAoB;IAClD,MAAM;IACF,wEAAwE;IACxE,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAG,IAAI,QAAQ,EAAE,EACzB,KAAK,GAAG,CAAC,EACT,QAAQ,GAAG,YAAY,EACvB,QAAQ,GAAG,cAAc,EACzB,YAAY,GAAG,IAAI,EACnB,KAAK,EACL,KAAK,GAAG,EAAE,EACV,UAAU,EACV,MAAM,EACN,OAAO,EACP,WAAW,GAAG,mBAAmB,EACjC,WAAW,GAAG,KAAK,GACtB,GAAG,MAAM,CAAC;IAEX,MAAM,OAAO,GAAG,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC;IACvE,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACxC,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAS,CAAC;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAI,CAAC;IAEzB,uEAAuE;IACvE,8EAA8E;IAC9E,0EAA0E;IAC1E,0EAA0E;IAC1E,cAAc;IACd,MAAM,UAAU,GAAG;QACf,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC;QACrC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC;KAC1C,CAAC;IACF,MAAM,UAAU,GAAG,iBAAiB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAC9D,MAAM,WAAW,GAAG,KAAK,GAAG,UAAU,CAAC;IAEvC,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzD,eAAe,CAAC,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,WAAW,CAAC,CAAC;IAC7D,eAAe,CAAC,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC;IAC/D,eAAe,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;IAE3C,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,IAAI,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACxD,MAAM,cAAc,GAAG,IAAI,iBAAiB,CAAC,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;IACrF,MAAM,aAAa,GAAG,IAAI,gBAAgB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;IACtE,aAAa,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAErC,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IAEhE,IAAI,KAA6B,CAAC;IAClC,IAAI,CAAC;QACD,KAAK,GAAG,MAAM,cAAc,CAAc;YACtC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,YAAY;YACnD,aAAa,EAAE,cAAc,EAAE,YAAY;YAC3C,IAAI,EAAE,IAAI,mBAAmB,CAAC,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC;YACpF,KAAK,EAAE,IAAI,aAAa,CAAC,KAAK,CAAC,UAAU,EAAE,YAAY,CAAC;YACxD,UAAU;YACV,MAAM;SACT,CAA2B,CAAC;IACjC,CAAC;YAAS,CAAC;QACP,aAAa,CAAC,OAAO,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,CAAC,KAAK;QAAE,OAAO;IAEnB,wEAAwE;IACxE,gDAAgD;IAChD,IAAI,WAAW;QAAE,OAAO,KAAK,CAAC;IAE9B,+EAA+E;IAC/E,kEAAkE;IAClE,kDAAkD;IAClD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,MAAqB,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;IAChB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;IACb,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC","sourcesContent":["import {\n ManifestAssetCatalog, Registry, resolveProjectDocument, type AssetManifest,\n type ProjectDocument, type SceneDocument, type Size2D,\n} from \"@motionscript/core\";\nimport {\n renderTimeline,\n type VideoFrameSink,\n} from \"@motionscript/skia-render/export\";\nimport {\n AudioBufferSource,\n BufferTarget,\n CanvasSource,\n canEncodeVideo,\n Mp4OutputFormat,\n Output,\n type Quality,\n type VideoCodec,\n} from 'mediabunny';\nimport { WebRenderContext } from \"./render-context\";\nimport { WebStorageAdapter } from \"./storage-adapter\";\nimport { getCanvasKit } from \"./getter\";\nimport { WebAudioMixer } from \"./audio/mixer\";\nimport { DEFAULT_OUTPUT } from \"./output\";\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 `'avc'` (H.264),\n * which every player, editor and upload target accepts.\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 falls back to `'avc'` rather than failing the export.\n */\n codec?: 'avc' | 'hevc' | 'av1' | 'vp9';\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 /** AAC bitrate in bits per second. Defaults to 192 kbps. */\n bitrate?: number;\n /** Mixdown sample rate in Hz. Defaults to the mixer's own (44100). */\n sampleRate?: number;\n};\n\nconst DEFAULT_AUDIO_BITRATE = 192_000;\n/** See {@link VideoEncoderOptions.keyFrameInterval}. */\nconst DEFAULT_KEY_FRAME_INTERVAL = 1;\n/** See {@link ExportParams.supersample}. */\nconst DEFAULT_SUPERSAMPLE = 2;\n\nexport type ExportParams = {\n /** A project, or a lone still or composition as the one track of one. */\n document: ProjectDocument | SceneDocument;\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 filename?: string;\n manifest?: AssetManifest;\n /**\n * Set false to produce a silent MP4 — no AAC 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 onProgress?: ExportProgressCallback;\n signal?: AbortSignal;\n wasmUrl?: string;\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 MP4 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.\n *\n * Wraps the very `<canvas>` CanvasKit 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 */\nclass 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 fps: number,\n /** Encoded output size, which is the canvas' own unless supersampling is on. */\n output: { width: number; height: number },\n video: VideoEncoderOptions = {},\n private readonly audioOptions: AudioEncoderOptions = {},\n codec: VideoCodec = 'avc',\n ) {\n const downsampling = output.width !== canvas.width || output.height !== canvas.height;\n this.video = new CanvasSource(canvas, {\n codec,\n // Resolved against what is actually being encoded — the *output*\n // size and the rate the frame loop will feed it — rather than\n // against a subjective level that never sees the frame rate.\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 // `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: new Mp4OutputFormat(), 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 this.audio = new AudioBufferSource({\n codec: 'aac',\n bitrate: this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE,\n });\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 this platform can\n * encode it at this size, `'avc'` otherwise.\n *\n * A better codec is the single largest quality win available on synthetic\n * content — but which ones a machine has encoders for varies by OS, browser and\n * GPU, and an export that dies at the muxer because the machine has no HEVC\n * encoder is worse than one that quietly produces H.264. AVC is not probed: it\n * is the fallback, and there is nothing to fall back to.\n */\nasync function resolveCodec(\n requested: VideoCodec | undefined,\n size: { width: number; height: number },\n fps: number,\n): Promise<VideoCodec> {\n if (!requested || requested === 'avc') return 'avc';\n try {\n const ok = await canEncodeVideo(requested, {\n width: size.width,\n height: size.height,\n bitrate: motionGraphicsBitrate(size.width, size.height, fps),\n });\n if (ok) return requested;\n } catch {\n // A probe that throws is a probe that answered \"no\".\n }\n console.warn(`[motion-script] No ${requested} encoder available; exporting as H.264 instead.`);\n return 'avc';\n}\n\n/**\n * Renders a document frame-by-frame to an offscreen canvas and muxes the result\n * into an MP4 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 `@motionscript/skia-render`'s `renderTimeline`. 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 filename = 'export.mp4',\n manifest = EMPTY_MANIFEST,\n includeAudio = true,\n video,\n audio = {},\n onProgress,\n signal,\n wasmUrl,\n supersample = DEFAULT_SUPERSAMPLE,\n returnBytes = false,\n } = params;\n\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 // Every block-based codec works in macroblocks, so an odd dimension is\n // padded by the encoder and the padding is *not* what was drawn — it shows up\n // as a smeared or discoloured strip down one edge. Rounding up to an even\n // size instead costs at most half a pixel of centring and keeps the whole\n // frame real.\n const outputSize = {\n width: evenUp(viewport.width * scale),\n height: evenUp(viewport.height * scale),\n };\n const oversample = fittedSupersample(supersample, outputSize);\n const renderScale = scale * oversample;\n\n const offscreenCanvas = document.createElement('canvas');\n offscreenCanvas.width = evenUp(viewport.width * renderScale);\n offscreenCanvas.height = evenUp(viewport.height * renderScale);\n offscreenCanvas.style.display = 'none';\n document.body.appendChild(offscreenCanvas);\n\n const canvasKit = await getCanvasKit(wasmUrl);\n const assetCatalog = new ManifestAssetCatalog(manifest);\n const storageAdapter = new WebStorageAdapter(canvasKit, assetCatalog, viewport, fps);\n const renderContext = new WebRenderContext(canvasKit, storageAdapter);\n renderContext.mount(offscreenCanvas);\n\n const codec = await resolveCodec(video?.codec, outputSize, fps);\n\n let bytes: Uint8Array | undefined;\n try {\n bytes = await renderTimeline<AudioBuffer>({\n project, registry, scale: renderScale, includeAudio,\n renderContext, storageAdapter, assetCatalog,\n sink: new MediabunnyVideoSink(offscreenCanvas, fps, outputSize, video, audio, codec),\n mixer: new WebAudioMixer(audio.sampleRate, assetCatalog),\n onProgress,\n signal,\n }) as Uint8Array | undefined;\n } finally {\n renderContext.dispose();\n document.body.removeChild(offscreenCanvas);\n }\n\n if (!bytes) return;\n\n // Headless callers capture the bytes and write them to disk themselves,\n // so skip the DOM-based download path entirely.\n if (returnBytes) return bytes;\n\n // The sink builds this Uint8Array over a whole, plain ArrayBuffer, so the cast\n // is sound — TS only balks because the generic parameter admits a\n // SharedArrayBuffer, which a BlobPart may not be.\n const blob = new Blob([bytes.buffer as ArrayBuffer], { type: 'video/mp4' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { type CanvasKit } from "@motionscript/canvaskit";
2
+ /**
3
+ * Loads and caches the CanvasKit wasm module (memoized — concurrent callers
4
+ * share the same init promise, and once resolved every call returns the same
5
+ * instance). `wasmUrl` defaults to the wasm shipped by `@motionscript/canvaskit`
6
+ * (resolved by the consumer's bundler), so consumers don't have to configure
7
+ * anything; pass a URL to override where the binary is loaded from.
8
+ */
9
+ export declare function getCanvasKit(wasmUrl?: string): Promise<CanvasKit>;
10
+ //# sourceMappingURL=getter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getter.d.ts","sourceRoot":"","sources":["../src/getter.ts"],"names":[],"mappings":"AACA,OAAsB,EAAE,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAkBxE;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAYvE"}
package/dist/getter.js ADDED
@@ -0,0 +1,38 @@
1
+ // @motionscript/core/src/engine.ts
2
+ import CanvasKitInit from "@motionscript/canvaskit";
3
+ let canvasKit = null;
4
+ let initPromise = null;
5
+ // Default to the wasm shipped by @motionscript/canvaskit so consumers don't
6
+ // have to configure anything. This MUST be the custom build that matches the
7
+ // glue imported above — the stock canvaskit-wasm binary lacks imports our glue
8
+ // expects (e.g. `pd`) and fails to link with a LinkError.
9
+ //
10
+ // Resolved **lazily**, not at module scope. `import.meta.url` is empty in a
11
+ // non-ESM output — an IIFE bundle, which is what the headless page loads — and
12
+ // `new URL("…", "")` throws on import, before any caller has had the chance to
13
+ // pass the explicit URL that would have made the default irrelevant.
14
+ function defaultWasmUrl() {
15
+ return new URL("@motionscript/canvaskit/canvaskit.wasm", import.meta.url).href;
16
+ }
17
+ /**
18
+ * Loads and caches the CanvasKit wasm module (memoized — concurrent callers
19
+ * share the same init promise, and once resolved every call returns the same
20
+ * instance). `wasmUrl` defaults to the wasm shipped by `@motionscript/canvaskit`
21
+ * (resolved by the consumer's bundler), so consumers don't have to configure
22
+ * anything; pass a URL to override where the binary is loaded from.
23
+ */
24
+ export async function getCanvasKit(wasmUrl) {
25
+ if (canvasKit)
26
+ return canvasKit;
27
+ if (initPromise)
28
+ return initPromise;
29
+ const url = wasmUrl ?? defaultWasmUrl();
30
+ initPromise = CanvasKitInit({
31
+ locateFile: () => url,
32
+ }).then((ck) => {
33
+ canvasKit = ck;
34
+ return ck;
35
+ });
36
+ return initPromise;
37
+ }
38
+ //# sourceMappingURL=getter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getter.js","sourceRoot":"","sources":["../src/getter.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,OAAO,aAAiC,MAAM,yBAAyB,CAAC;AAExE,IAAI,SAAS,GAAqB,IAAI,CAAC;AACvC,IAAI,WAAW,GAA8B,IAAI,CAAC;AAElD,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,0DAA0D;AAC1D,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,qEAAqE;AACrE,SAAS,cAAc;IACnB,OAAO,IAAI,GAAG,CAAC,wCAAwC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;AACnF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAgB;IAC/C,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAChC,IAAI,WAAW;QAAE,OAAO,WAAW,CAAC;IACpC,MAAM,GAAG,GAAG,OAAO,IAAI,cAAc,EAAE,CAAC;IAExC,WAAW,GAAG,aAAa,CAAC;QACxB,UAAU,EAAE,GAAG,EAAE,CAAC,GAAG;KACxB,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE;QACX,SAAS,GAAG,EAAE,CAAC;QACf,OAAO,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;IACH,OAAO,WAAW,CAAC;AACvB,CAAC","sourcesContent":["// @motionscript/core/src/engine.ts\nimport CanvasKitInit, { type CanvasKit } from \"@motionscript/canvaskit\";\n\nlet canvasKit: CanvasKit | null = null;\nlet initPromise: Promise<CanvasKit> | null = null;\n\n// Default to the wasm shipped by @motionscript/canvaskit so consumers don't\n// have to configure anything. This MUST be the custom build that matches the\n// glue imported above — the stock canvaskit-wasm binary lacks imports our glue\n// expects (e.g. `pd`) and fails to link with a LinkError.\n//\n// Resolved **lazily**, not at module scope. `import.meta.url` is empty in a\n// non-ESM output — an IIFE bundle, which is what the headless page loads — and\n// `new URL(\"…\", \"\")` throws on import, before any caller has had the chance to\n// pass the explicit URL that would have made the default irrelevant.\nfunction defaultWasmUrl(): string {\n return new URL(\"@motionscript/canvaskit/canvaskit.wasm\", import.meta.url).href;\n}\n\n/**\n * Loads and caches the CanvasKit wasm module (memoized — concurrent callers\n * share the same init promise, and once resolved every call returns the same\n * instance). `wasmUrl` defaults to the wasm shipped by `@motionscript/canvaskit`\n * (resolved by the consumer's bundler), so consumers don't have to configure\n * anything; pass a URL to override where the binary is loaded from.\n */\nexport async function getCanvasKit(wasmUrl?: string): Promise<CanvasKit> {\n if (canvasKit) return canvasKit;\n if (initPromise) return initPromise;\n const url = wasmUrl ?? defaultWasmUrl();\n\n initPromise = CanvasKitInit({\n locateFile: () => url,\n }).then((ck) => {\n canvasKit = ck;\n return ck;\n });\n return initPromise;\n}"]}
@@ -0,0 +1,22 @@
1
+ export { WebRenderContext } from "./render-context";
2
+ export { WebStorageAdapter } from "./storage-adapter";
3
+ export { setAssetByteSource, type AssetByteSource } from "./asset-bytes";
4
+ export { getCanvasKit } from "./getter";
5
+ export { EffectRegistry } from "@motionscript/skia-render";
6
+ export type { EffectHandler, EffectGeometry, EffectResources, EffectTarget, RenderEffect } from "@motionscript/skia-render";
7
+ export { exportVideo, type ExportParams, type ExportProgressCallback, type VideoEncoderOptions, type AudioEncoderOptions, } from "./exporter";
8
+ export { QUALITY_VERY_LOW, QUALITY_LOW, QUALITY_MEDIUM, QUALITY_HIGH, QUALITY_VERY_HIGH, type Quality, } from "mediabunny";
9
+ export { exportScreenshot, type ScreenshotParams, type ScreenshotResult, type ScreenshotFormat, type FrameSpec } from "./screenshot";
10
+ export { concatVideoSegments, ConcatMismatchError, type ConcatOptions } from "./concat";
11
+ export { mixTimelineAudio, type MixTimelineAudioParams } from "./mix-timeline-audio";
12
+ export { StillRenderer, createStillRenderer } from "./still";
13
+ export type { StillRendererOptions, RenderStillOptions, EncodeStillOptions, StillSource, FrameRef, } from "./still";
14
+ export { WebAudioSink as WebAudioPlayer } from "./audio/player";
15
+ export { mixAudio, encodeWav, type MixAudioOptions } from "./audio/mixer";
16
+ export { WebMeasureContext } from "./measure-context";
17
+ export { WebTicker } from "./ticker";
18
+ export { createBrowserEngine } from "./engine";
19
+ export type { BrowserEngineOptions, BrowserEncodeOptions } from "./engine";
20
+ export { Canvas3DBackend, canvas3DBackend, loadCanvas3D, disposeCanvas3DBackend } from "@motionscript/skia-render";
21
+ export type { Canvas3DAssets, RenderedCanvas3D } from "@motionscript/skia-render";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxC,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC5H,OAAO,EACH,WAAW,EACX,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GAC3B,MAAM,YAAY,CAAC;AAIpB,OAAO,EACH,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,KAAK,OAAO,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAKrI,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAIxF,OAAO,EAAE,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAIrF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC7D,YAAY,EACR,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,QAAQ,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIhE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAMrC,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC/C,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAQ3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnH,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}