@motionscript/browser 0.0.0-stage → 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +192 -0
- package/LICENSE +201 -0
- package/NOTICE +49 -0
- package/README.md +140 -3
- package/dist/asset-bytes.d.ts +59 -0
- package/dist/asset-bytes.d.ts.map +1 -0
- package/dist/asset-bytes.js +78 -0
- package/dist/asset-bytes.js.map +1 -0
- package/dist/audio/bus-graph.d.ts +52 -0
- package/dist/audio/bus-graph.d.ts.map +1 -0
- package/dist/audio/bus-graph.js +108 -0
- package/dist/audio/bus-graph.js.map +1 -0
- package/dist/audio/effect-graph.d.ts +62 -0
- package/dist/audio/effect-graph.d.ts.map +1 -0
- package/dist/audio/effect-graph.js +234 -0
- package/dist/audio/effect-graph.js.map +1 -0
- package/dist/audio/index.d.ts +5 -0
- package/dist/audio/index.d.ts.map +1 -0
- package/dist/audio/index.js +11 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/audio/loop-region.d.ts +31 -0
- package/dist/audio/loop-region.d.ts.map +1 -0
- package/dist/audio/loop-region.js +43 -0
- package/dist/audio/loop-region.js.map +1 -0
- package/dist/audio/mixer.d.ts +59 -0
- package/dist/audio/mixer.d.ts.map +1 -0
- package/dist/audio/mixer.js +192 -0
- package/dist/audio/mixer.js.map +1 -0
- package/dist/audio/player.d.ts +55 -0
- package/dist/audio/player.d.ts.map +1 -0
- package/dist/audio/player.js +217 -0
- package/dist/audio/player.js.map +1 -0
- package/dist/browser/THIRD_PARTY_LICENSES.txt +784 -0
- package/dist/browser/audio.js +2 -0
- package/dist/browser/audio.js.map +7 -0
- package/dist/browser/chunks/chunk-F3AZKPHU.js +2 -0
- package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
- package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
- package/dist/browser/chunks/chunk-KLXMRKTL.js.map +7 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
- package/dist/browser/esbuild.wasm +0 -0
- package/dist/browser/index.js +734 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/manifest.json +32 -0
- package/dist/browser/ms_renderer_bg.wasm +0 -0
- package/dist/concat.d.ts +121 -0
- package/dist/concat.d.ts.map +1 -0
- package/dist/concat.js +219 -0
- package/dist/concat.js.map +1 -0
- package/dist/deliver.d.ts +10 -0
- package/dist/deliver.d.ts.map +1 -0
- package/dist/deliver.js +22 -0
- package/dist/deliver.js.map +1 -0
- package/dist/dynamic-nodes.d.ts +9 -0
- package/dist/dynamic-nodes.d.ts.map +1 -0
- package/dist/dynamic-nodes.js +53 -0
- package/dist/dynamic-nodes.js.map +1 -0
- package/dist/engine.d.ts +87 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +193 -0
- package/dist/engine.js.map +1 -0
- package/dist/export-audio.d.ts +36 -0
- package/dist/export-audio.d.ts.map +1 -0
- package/dist/export-audio.js +56 -0
- package/dist/export-audio.js.map +1 -0
- package/dist/exporter.d.ts +282 -0
- package/dist/exporter.d.ts.map +1 -0
- package/dist/exporter.js +308 -0
- package/dist/exporter.js.map +1 -0
- package/dist/formats.d.ts +36 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +83 -0
- package/dist/formats.js.map +1 -0
- package/dist/frame-loop.d.ts +190 -0
- package/dist/frame-loop.d.ts.map +1 -0
- package/dist/frame-loop.js +304 -0
- package/dist/frame-loop.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/mix-timeline-audio.d.ts +42 -0
- package/dist/mix-timeline-audio.d.ts.map +1 -0
- package/dist/mix-timeline-audio.js +60 -0
- package/dist/mix-timeline-audio.js.map +1 -0
- package/dist/parallel/export-parallel.d.ts +32 -0
- package/dist/parallel/export-parallel.d.ts.map +1 -0
- package/dist/parallel/export-parallel.js +114 -0
- package/dist/parallel/export-parallel.js.map +1 -0
- package/dist/parallel/governor.d.ts +57 -0
- package/dist/parallel/governor.d.ts.map +1 -0
- package/dist/parallel/governor.js +79 -0
- package/dist/parallel/governor.js.map +1 -0
- package/dist/parallel/lane.d.ts +45 -0
- package/dist/parallel/lane.d.ts.map +1 -0
- package/dist/parallel/lane.js +2 -0
- package/dist/parallel/lane.js.map +1 -0
- package/dist/parallel/lanes.d.ts +20 -0
- package/dist/parallel/lanes.d.ts.map +1 -0
- package/dist/parallel/lanes.js +152 -0
- package/dist/parallel/lanes.js.map +1 -0
- package/dist/parallel/plan.d.ts +23 -0
- package/dist/parallel/plan.d.ts.map +1 -0
- package/dist/parallel/plan.js +30 -0
- package/dist/parallel/plan.js.map +1 -0
- package/dist/parallel/run-lanes.d.ts +38 -0
- package/dist/parallel/run-lanes.d.ts.map +1 -0
- package/dist/parallel/run-lanes.js +164 -0
- package/dist/parallel/run-lanes.js.map +1 -0
- package/dist/parallel/session.d.ts +9 -0
- package/dist/parallel/session.d.ts.map +1 -0
- package/dist/parallel/session.js +79 -0
- package/dist/parallel/session.js.map +1 -0
- package/dist/renderer.d.ts +22 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +23 -0
- package/dist/renderer.js.map +1 -0
- package/dist/screenshot.d.ts +45 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +31 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/still.d.ts +159 -0
- package/dist/still.d.ts.map +1 -0
- package/dist/still.js +222 -0
- package/dist/still.js.map +1 -0
- package/dist/ticker.d.ts +74 -0
- package/dist/ticker.d.ts.map +1 -0
- package/dist/ticker.js +215 -0
- package/dist/ticker.js.map +1 -0
- package/package.json +75 -3
package/dist/exporter.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { DEFAULT_OUTPUT, ManifestAssetCatalog, Registry, resolveProjectDocument, } from "@motionscript/core";
|
|
2
|
+
import { renderTimeline, } from "./frame-loop";
|
|
3
|
+
import { BufferTarget, CanvasSource, canEncodeVideo, Output, } from 'mediabunny';
|
|
4
|
+
import { loadRenderer } from "./renderer";
|
|
5
|
+
import { WebAudioMixer } from "./audio/mixer";
|
|
6
|
+
import { deliver } from "./deliver";
|
|
7
|
+
import { audioTrackSource, DEFAULT_AUDIO_BITRATE, ensureAudioEncoder, formatSpec, } from "./formats";
|
|
8
|
+
/**
|
|
9
|
+
* Bits per pixel per frame the default bitrate is built from.
|
|
10
|
+
*
|
|
11
|
+
* Rendered motion graphics are the hard case for a codec tuned on camera
|
|
12
|
+
* footage: hard glyph edges, large flat fields, and gradients with no sensor
|
|
13
|
+
* noise to hide the quantisation in. They compress *worse* than live action at
|
|
14
|
+
* the same perceived quality, not better, so the figure here is well above what
|
|
15
|
+
* a streaming preset would use — this is a master being rendered once, not a
|
|
16
|
+
* bitstream being pushed down a pipe.
|
|
17
|
+
*/
|
|
18
|
+
const BITS_PER_PIXEL = 0.2;
|
|
19
|
+
/** Floor, so a tiny viewport still gets enough bits to be legible. */
|
|
20
|
+
const MIN_VIDEO_BITRATE = 2_000_000;
|
|
21
|
+
/**
|
|
22
|
+
* Ceiling. Past this the returns are gone and the levels start excluding
|
|
23
|
+
* hardware decoders — 4K60 lands here, and AVC level 5.1 (which every consumer
|
|
24
|
+
* device implements) tops out at 240 Mbps, well clear of it.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_VIDEO_BITRATE = 100_000_000;
|
|
27
|
+
/**
|
|
28
|
+
* The default video bitrate for an export of this size and frame rate.
|
|
29
|
+
*
|
|
30
|
+
* Scaling with **both** axes is the point. mediabunny's `Quality` levels scale
|
|
31
|
+
* with resolution only, so doubling the frame rate halved the bits available per
|
|
32
|
+
* frame while leaving the number unchanged — which is exactly the configuration
|
|
33
|
+
* that produced clean stills and a mushy video from the very same renderer.
|
|
34
|
+
*/
|
|
35
|
+
export function motionGraphicsBitrate(width, height, fps) {
|
|
36
|
+
const raw = width * height * fps * BITS_PER_PIXEL;
|
|
37
|
+
return Math.round(Math.min(MAX_VIDEO_BITRATE, Math.max(MIN_VIDEO_BITRATE, raw)));
|
|
38
|
+
}
|
|
39
|
+
/** See {@link VideoEncoderOptions.keyFrameInterval}. */
|
|
40
|
+
const DEFAULT_KEY_FRAME_INTERVAL = 1;
|
|
41
|
+
/** See {@link ExportParams.supersample}. */
|
|
42
|
+
const DEFAULT_SUPERSAMPLE = 2;
|
|
43
|
+
const EMPTY_MANIFEST = {
|
|
44
|
+
image: {},
|
|
45
|
+
video: {},
|
|
46
|
+
audio: {},
|
|
47
|
+
font: {},
|
|
48
|
+
};
|
|
49
|
+
// ── Audio mixing ─────────────────────────────────────────────────────────────
|
|
50
|
+
//
|
|
51
|
+
// The mix itself lives in ./audio/mixer, shared with `mixTimelineAudio` so a
|
|
52
|
+
// split export and a whole one cannot mix differently.
|
|
53
|
+
// ── Video sink ───────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* mediabunny/WebCodecs implementation of the frame sink, writing whichever
|
|
56
|
+
* container `format` names.
|
|
57
|
+
*
|
|
58
|
+
* Wraps the very `<canvas>` the renderer draws into, so `addFrame` costs nothing but
|
|
59
|
+
* a timestamp — WebCodecs pulls the pixels from the canvas itself, with no
|
|
60
|
+
* GPU→CPU readback. That is why the seam signals a frame rather than passing one.
|
|
61
|
+
*
|
|
62
|
+
* The readback claim holds only while the encoder is a hardware one, which is
|
|
63
|
+
* why {@link VideoEncoderOptions.hardwareAcceleration} defaults to letting the
|
|
64
|
+
* platform choose: a software encoder has to be handed CPU pixels, so it drags
|
|
65
|
+
* a full-frame copy off the GPU on every single frame.
|
|
66
|
+
*/
|
|
67
|
+
export class MediabunnyVideoSink {
|
|
68
|
+
format;
|
|
69
|
+
audioOptions;
|
|
70
|
+
target = new BufferTarget();
|
|
71
|
+
output;
|
|
72
|
+
video;
|
|
73
|
+
audio = null;
|
|
74
|
+
constructor(canvas,
|
|
75
|
+
/** Encoded output size, which is the canvas' own unless supersampling is on. */
|
|
76
|
+
output, format, encoding, audioOptions = {}) {
|
|
77
|
+
this.format = format;
|
|
78
|
+
this.audioOptions = audioOptions;
|
|
79
|
+
const downsampling = output.width !== canvas.width || output.height !== canvas.height;
|
|
80
|
+
this.video = new CanvasSource(canvas, {
|
|
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 }),
|
|
88
|
+
// `fit` is exact here — the supersampled canvas is a whole multiple
|
|
89
|
+
// of the output — but mediabunny requires it whenever both axes are
|
|
90
|
+
// given, so it is stated rather than inferred.
|
|
91
|
+
...(downsampling && { transform: { width: output.width, height: output.height, fit: 'fill' } }),
|
|
92
|
+
});
|
|
93
|
+
this.output = new Output({ format: formatSpec(format).container(), target: this.target });
|
|
94
|
+
}
|
|
95
|
+
async start(info) {
|
|
96
|
+
this.output.addVideoTrack(this.video, { frameRate: info.fps });
|
|
97
|
+
// Tracks must be declared before the muxer starts, which is why the
|
|
98
|
+
// orchestrator reports this rather than leaving it to be inferred.
|
|
99
|
+
if (info.hasAudio) {
|
|
100
|
+
await ensureAudioEncoder(this.format);
|
|
101
|
+
this.audio = audioTrackSource(this.format, this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE);
|
|
102
|
+
this.output.addAudioTrack(this.audio);
|
|
103
|
+
}
|
|
104
|
+
await this.output.start();
|
|
105
|
+
}
|
|
106
|
+
async addFrame(timestamp, duration) {
|
|
107
|
+
await this.video.add(timestamp, duration);
|
|
108
|
+
}
|
|
109
|
+
async addAudio(mixed) {
|
|
110
|
+
if (!this.audio)
|
|
111
|
+
return;
|
|
112
|
+
await this.audio.add(mixed);
|
|
113
|
+
}
|
|
114
|
+
async finalize() {
|
|
115
|
+
this.video.close();
|
|
116
|
+
this.audio?.close();
|
|
117
|
+
await this.output.finalize();
|
|
118
|
+
const buffer = this.target.buffer;
|
|
119
|
+
if (!buffer)
|
|
120
|
+
throw new Error('Export produced no data');
|
|
121
|
+
return new Uint8Array(buffer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ── Main export ──────────────────────────────────────────────────────────────
|
|
125
|
+
/** Round up to the nearest even integer — see the call site. */
|
|
126
|
+
function evenUp(n) {
|
|
127
|
+
const rounded = Math.round(n);
|
|
128
|
+
return rounded % 2 === 0 ? rounded : rounded + 1;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Longest axis the render surface is allowed to reach.
|
|
132
|
+
*
|
|
133
|
+
* Supersampling multiplies the surface, not the output: a 4K export at
|
|
134
|
+
* `supersample: 2` is drawing 8K frames, and the next step up asks for a
|
|
135
|
+
* 15360-wide WebGL surface that most drivers will not give and none will give
|
|
136
|
+
* cheaply. 8192 is the smallest `MAX_TEXTURE_SIZE` in wide circulation, so a
|
|
137
|
+
* surface inside it is one every machine that can run the export at all can
|
|
138
|
+
* also allocate.
|
|
139
|
+
*/
|
|
140
|
+
const MAX_RENDER_AXIS = 8192;
|
|
141
|
+
/**
|
|
142
|
+
* How far the frame can actually be supersampled at this output size.
|
|
143
|
+
*
|
|
144
|
+
* Reduced rather than refused: an export that quietly renders at 1× is a
|
|
145
|
+
* slightly softer file, and one that throws because the default was too
|
|
146
|
+
* ambitious for a 4K project is no file at all.
|
|
147
|
+
*/
|
|
148
|
+
function fittedSupersample(requested, output) {
|
|
149
|
+
const longest = Math.max(output.width, output.height);
|
|
150
|
+
let factor = Math.max(1, Math.round(requested));
|
|
151
|
+
while (factor > 1 && longest * factor > MAX_RENDER_AXIS)
|
|
152
|
+
factor--;
|
|
153
|
+
return factor;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
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).
|
|
159
|
+
*
|
|
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.
|
|
163
|
+
*/
|
|
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) {
|
|
183
|
+
try {
|
|
184
|
+
return await canEncodeVideo(codec, {
|
|
185
|
+
width: size.width,
|
|
186
|
+
height: size.height,
|
|
187
|
+
bitrate: motionGraphicsBitrate(size.width, size.height, fps),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
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;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Renders a document frame-by-frame to an offscreen canvas and muxes the result
|
|
261
|
+
* into an MP4 or WebM via mediabunny.
|
|
262
|
+
*
|
|
263
|
+
* The frame loop itself — discovery, state evaluation, the warm-and-re-render retry
|
|
264
|
+
* that keeps every frame accurate, audio scheduling, progress and cancellation —
|
|
265
|
+
* lives in `renderTimeline` (frame-loop.ts). What this function
|
|
266
|
+
* supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,
|
|
267
|
+
* a Web Audio mixer, and the `<a download>` delivery.
|
|
268
|
+
*/
|
|
269
|
+
export async function exportVideo(params) {
|
|
270
|
+
const {
|
|
271
|
+
// Not `document`, which would shadow the DOM global this function uses.
|
|
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}`;
|
|
277
|
+
const project = resolveProjectDocument(source, params, DEFAULT_OUTPUT);
|
|
278
|
+
if (project.tracks.length === 0)
|
|
279
|
+
return;
|
|
280
|
+
signal?.throwIfAborted();
|
|
281
|
+
const viewport = project.viewport;
|
|
282
|
+
const fps = project.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
|
+
});
|
|
288
|
+
let bytes;
|
|
289
|
+
try {
|
|
290
|
+
const encoding = await resolveVideoEncoding(video, format, output, fps);
|
|
291
|
+
bytes = await renderTimeline({
|
|
292
|
+
project, registry, theme, scale: renderScale, includeAudio, frames,
|
|
293
|
+
renderContext: surface.renderContext, storageAdapter: surface.storageAdapter, assetCatalog: surface.assetCatalog,
|
|
294
|
+
sink: new MediabunnyVideoSink(surface.canvas, output, format, encoding, audio),
|
|
295
|
+
mixer: new WebAudioMixer(audio.sampleRate, surface.assetCatalog),
|
|
296
|
+
onProgress,
|
|
297
|
+
onIssue: params.onIssue,
|
|
298
|
+
signal,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
surface.dispose();
|
|
303
|
+
}
|
|
304
|
+
if (!bytes)
|
|
305
|
+
return;
|
|
306
|
+
return deliver(bytes, { returnBytes, filename, mimeType: spec.mimeType });
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=exporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
package/dist/formats.js
ADDED
|
@@ -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"]}
|