@aelionsdk/export 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/audio-export.d.ts +23 -0
- package/dist/audio-export.d.ts.map +1 -0
- package/dist/audio-export.js +120 -0
- package/dist/checkpoint.d.ts +58 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +119 -0
- package/dist/image-export.d.ts +40 -0
- package/dist/image-export.d.ts.map +1 -0
- package/dist/image-export.js +235 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/memory-sink.d.ts +17 -0
- package/dist/memory-sink.d.ts.map +1 -0
- package/dist/memory-sink.js +60 -0
- package/dist/mux-export-worker.d.ts +2 -0
- package/dist/mux-export-worker.d.ts.map +1 -0
- package/dist/mux-export-worker.js +120 -0
- package/dist/opfs-sink.d.ts +20 -0
- package/dist/opfs-sink.d.ts.map +1 -0
- package/dist/opfs-sink.js +113 -0
- package/dist/profiles.d.ts +66 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +284 -0
- package/dist/remote-export.d.ts +56 -0
- package/dist/remote-export.d.ts.map +1 -0
- package/dist/remote-export.js +83 -0
- package/dist/resumable-muxed-export.d.ts +84 -0
- package/dist/resumable-muxed-export.d.ts.map +1 -0
- package/dist/resumable-muxed-export.js +533 -0
- package/dist/session.d.ts +47 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +483 -0
- package/dist/sink-completion.d.ts +8 -0
- package/dist/sink-completion.d.ts.map +1 -0
- package/dist/sink-completion.js +13 -0
- package/dist/webm-export.d.ts +97 -0
- package/dist/webm-export.d.ts.map +1 -0
- package/dist/webm-export.js +408 -0
- package/dist/worker-export.d.ts +25 -0
- package/dist/worker-export.d.ts.map +1 -0
- package/dist/worker-export.js +202 -0
- package/dist/worker-protocol.d.ts +66 -0
- package/dist/worker-protocol.d.ts.map +1 -0
- package/dist/worker-protocol.js +1 -0
- package/package.json +46 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { AelionError } from '@aelionsdk/core';
|
|
2
|
+
import { LOCAL_RGBA8_COLOR_CAPABILITY, preflightColorPipeline, } from '@aelionsdk/render-ir';
|
|
3
|
+
import { exportAv1Mp4, exportHevcMp4, exportMp4, exportWebM, } from './webm-export.js';
|
|
4
|
+
import { exportMuxedInWorker } from './worker-export.js';
|
|
5
|
+
import { av1CodecString, hevcCodecString, negotiateAvcCodecString, } from './profiles.js';
|
|
6
|
+
const audioRuntimeSupport = new Map();
|
|
7
|
+
function verifyAudioEncoderRuntime(config) {
|
|
8
|
+
const key = JSON.stringify(config);
|
|
9
|
+
const existing = audioRuntimeSupport.get(key);
|
|
10
|
+
if (existing !== undefined)
|
|
11
|
+
return existing;
|
|
12
|
+
const probe = new Promise(resolve => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
let encoder;
|
|
15
|
+
const finish = (supported) => {
|
|
16
|
+
if (settled)
|
|
17
|
+
return;
|
|
18
|
+
settled = true;
|
|
19
|
+
try {
|
|
20
|
+
encoder?.close();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// A codec error can close the encoder before the error callback runs.
|
|
24
|
+
}
|
|
25
|
+
resolve(supported);
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
encoder = new AudioEncoder({
|
|
29
|
+
output: () => undefined,
|
|
30
|
+
error: () => finish(false),
|
|
31
|
+
});
|
|
32
|
+
encoder.configure(config);
|
|
33
|
+
const frameCount = 1_024;
|
|
34
|
+
// Some AAC implementations need several access units before flush can
|
|
35
|
+
// drain encoder priming. Probe the same 1,024-frame cadence used by the
|
|
36
|
+
// real muxed export instead of treating a single priming block as proof
|
|
37
|
+
// that the runtime rejected AAC.
|
|
38
|
+
for (let block = 0; block < 4; block += 1) {
|
|
39
|
+
const audio = new AudioData({
|
|
40
|
+
format: 'f32',
|
|
41
|
+
sampleRate: config.sampleRate,
|
|
42
|
+
numberOfFrames: frameCount,
|
|
43
|
+
numberOfChannels: config.numberOfChannels,
|
|
44
|
+
timestamp: Math.round((block * frameCount * 1_000_000) / config.sampleRate),
|
|
45
|
+
data: new Float32Array(frameCount * config.numberOfChannels),
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
encoder.encode(audio);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
audio.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
void encoder.flush().then(() => finish(true), () => finish(false));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
finish(false);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
audioRuntimeSupport.set(key, probe);
|
|
61
|
+
return probe;
|
|
62
|
+
}
|
|
63
|
+
function channelCount(layout) {
|
|
64
|
+
if (layout === 'mono')
|
|
65
|
+
return 1;
|
|
66
|
+
if (layout === 'stereo')
|
|
67
|
+
return 2;
|
|
68
|
+
if (layout === '5.1')
|
|
69
|
+
return 6;
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
async function preflightMuxedExport(options, profile) {
|
|
73
|
+
const issues = [];
|
|
74
|
+
try {
|
|
75
|
+
issues.push(...preflightColorPipeline(options.ir, LOCAL_RGBA8_COLOR_CAPABILITY).issues);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
issues.push({
|
|
79
|
+
code: 'COLOR_PIPELINE_CONTRACT_INVALID',
|
|
80
|
+
severity: 'error',
|
|
81
|
+
message: error instanceof Error ? error.message : 'Invalid color pipeline contract',
|
|
82
|
+
recoverable: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (options.projectRevision !== options.ir.revision) {
|
|
86
|
+
issues.push({
|
|
87
|
+
code: 'EXPORT_REVISION_MISMATCH',
|
|
88
|
+
severity: 'error',
|
|
89
|
+
message: `Project revision ${options.projectRevision.toString()} does not match frozen Render IR revision ${options.ir.revision.toString()}`,
|
|
90
|
+
recoverable: false,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (channelCount(options.ir.channelLayout) === undefined) {
|
|
94
|
+
issues.push({
|
|
95
|
+
code: 'EXPORT_CHANNEL_LAYOUT_UNSUPPORTED',
|
|
96
|
+
severity: 'error',
|
|
97
|
+
message: options.ir.channelLayout,
|
|
98
|
+
recoverable: false,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (options.sink.locked) {
|
|
102
|
+
issues.push({
|
|
103
|
+
code: 'EXPORT_SINK_LOCKED',
|
|
104
|
+
severity: 'error',
|
|
105
|
+
message: 'Export sink is already locked by another writer',
|
|
106
|
+
recoverable: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const videoConfig = {
|
|
110
|
+
codec: profile.videoCodec,
|
|
111
|
+
width: options.ir.width,
|
|
112
|
+
height: options.ir.height,
|
|
113
|
+
bitrate: options.videoBitrate,
|
|
114
|
+
bitrateMode: 'variable',
|
|
115
|
+
framerate: options.ir.frameRate.numerator / options.ir.frameRate.denominator,
|
|
116
|
+
latencyMode: 'quality',
|
|
117
|
+
alpha: 'discard',
|
|
118
|
+
...(profile.hevc === true ? { hevc: { format: 'hevc' } } : {}),
|
|
119
|
+
};
|
|
120
|
+
const audioConfig = {
|
|
121
|
+
codec: profile.audioCodec,
|
|
122
|
+
sampleRate: options.ir.sampleRate,
|
|
123
|
+
numberOfChannels: channelCount(options.ir.channelLayout) ?? 0,
|
|
124
|
+
bitrate: options.audioBitrate,
|
|
125
|
+
bitrateMode: 'variable',
|
|
126
|
+
...(profile.audioCodec === 'mp4a.40.2'
|
|
127
|
+
? { aac: { format: 'aac' } }
|
|
128
|
+
: profile.audioCodec === 'opus'
|
|
129
|
+
? { opus: { format: 'opus' } }
|
|
130
|
+
: {}),
|
|
131
|
+
};
|
|
132
|
+
let selectedVideoCodec;
|
|
133
|
+
let selectedAudioCodec;
|
|
134
|
+
if (typeof VideoEncoder !== 'function') {
|
|
135
|
+
issues.push({
|
|
136
|
+
code: 'EXPORT_VIDEO_ENCODER_UNAVAILABLE',
|
|
137
|
+
severity: 'error',
|
|
138
|
+
message: 'VideoEncoder is unavailable',
|
|
139
|
+
recoverable: false,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
try {
|
|
144
|
+
if (profile.negotiateAvc === true) {
|
|
145
|
+
const negotiation = await negotiateAvcCodecString({
|
|
146
|
+
width: options.ir.width,
|
|
147
|
+
height: options.ir.height,
|
|
148
|
+
framerate: options.ir.frameRate.numerator / options.ir.frameRate.denominator,
|
|
149
|
+
bitrate: options.videoBitrate,
|
|
150
|
+
});
|
|
151
|
+
selectedVideoCodec = negotiation.selected;
|
|
152
|
+
if (selectedVideoCodec === undefined) {
|
|
153
|
+
issues.push({
|
|
154
|
+
code: 'EXPORT_VIDEO_CONFIG_UNSUPPORTED',
|
|
155
|
+
severity: 'error',
|
|
156
|
+
message: `${profile.videoName} export config is unsupported`,
|
|
157
|
+
recoverable: false,
|
|
158
|
+
details: {
|
|
159
|
+
attemptedCodecStrings: negotiation.attempts.map(attempt => attempt.codec),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if ((await VideoEncoder.isConfigSupported(videoConfig)).supported) {
|
|
165
|
+
selectedVideoCodec = profile.videoCodec;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
issues.push({
|
|
169
|
+
code: 'EXPORT_VIDEO_CONFIG_UNSUPPORTED',
|
|
170
|
+
severity: 'error',
|
|
171
|
+
message: `${profile.videoName} export config is unsupported`,
|
|
172
|
+
recoverable: false,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (cause) {
|
|
177
|
+
issues.push({
|
|
178
|
+
code: 'EXPORT_VIDEO_CONFIG_PROBE_FAILED',
|
|
179
|
+
severity: 'error',
|
|
180
|
+
message: `${profile.videoName} export config probe failed`,
|
|
181
|
+
recoverable: true,
|
|
182
|
+
cause,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (typeof AudioEncoder !== 'function') {
|
|
187
|
+
issues.push({
|
|
188
|
+
code: 'EXPORT_AUDIO_ENCODER_UNAVAILABLE',
|
|
189
|
+
severity: 'error',
|
|
190
|
+
message: 'AudioEncoder is unavailable',
|
|
191
|
+
recoverable: false,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
let runtimeSupported = false;
|
|
196
|
+
try {
|
|
197
|
+
const declaredSupported = (await AudioEncoder.isConfigSupported(audioConfig)).supported === true;
|
|
198
|
+
runtimeSupported =
|
|
199
|
+
declaredSupported && profile.verifyAudioRuntime === true
|
|
200
|
+
? await verifyAudioEncoderRuntime(audioConfig)
|
|
201
|
+
: declaredSupported;
|
|
202
|
+
}
|
|
203
|
+
catch (cause) {
|
|
204
|
+
issues.push({
|
|
205
|
+
code: 'EXPORT_AUDIO_CONFIG_PROBE_FAILED',
|
|
206
|
+
severity: 'error',
|
|
207
|
+
message: `${profile.audioName} export config probe failed`,
|
|
208
|
+
recoverable: true,
|
|
209
|
+
cause,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (runtimeSupported)
|
|
213
|
+
selectedAudioCodec = profile.audioCodec;
|
|
214
|
+
else if (!issues.some(issue => issue.code === 'EXPORT_AUDIO_CONFIG_PROBE_FAILED')) {
|
|
215
|
+
issues.push({
|
|
216
|
+
code: 'EXPORT_AUDIO_CONFIG_UNSUPPORTED',
|
|
217
|
+
severity: 'error',
|
|
218
|
+
message: `${profile.audioName} export config is unsupported at runtime`,
|
|
219
|
+
recoverable: false,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const material of Object.values(options.ir.materials)) {
|
|
224
|
+
if (!material.enabled)
|
|
225
|
+
continue;
|
|
226
|
+
const available = material.program !== undefined &&
|
|
227
|
+
(options.materialBackendAvailable?.(material.id, material.parameters) ?? true);
|
|
228
|
+
if (!available) {
|
|
229
|
+
issues.push({
|
|
230
|
+
code: 'EXPORT_MATERIAL_BACKEND_UNAVAILABLE',
|
|
231
|
+
severity: 'error',
|
|
232
|
+
message: `Material ${material.id} has no offline backend`,
|
|
233
|
+
recoverable: false,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
ok: issues.length === 0,
|
|
239
|
+
revision: options.ir.revision,
|
|
240
|
+
issues,
|
|
241
|
+
...(selectedVideoCodec === undefined && selectedAudioCodec === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: {
|
|
244
|
+
encoderConfiguration: {
|
|
245
|
+
...(selectedVideoCodec === undefined ? {} : { videoCodecString: selectedVideoCodec }),
|
|
246
|
+
...(selectedAudioCodec === undefined ? {} : { audioCodecString: selectedAudioCodec }),
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
export function preflightWebMExport(options) {
|
|
252
|
+
return preflightMuxedExport(options, {
|
|
253
|
+
videoCodec: 'vp09.00.10.08',
|
|
254
|
+
audioCodec: 'opus',
|
|
255
|
+
videoName: 'VP9',
|
|
256
|
+
audioName: 'Opus',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
export function preflightMp4Export(options) {
|
|
260
|
+
return preflightMuxedExport(options, {
|
|
261
|
+
videoCodec: 'avc1.640028',
|
|
262
|
+
audioCodec: 'mp4a.40.2',
|
|
263
|
+
videoName: 'H.264',
|
|
264
|
+
audioName: 'AAC',
|
|
265
|
+
verifyAudioRuntime: true,
|
|
266
|
+
negotiateAvc: true,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
export function preflightAv1Mp4Export(options) {
|
|
270
|
+
return preflightMuxedExport(options, {
|
|
271
|
+
videoCodec: av1CodecString(options.ir.width, options.ir.height, options.ir.frameRate.numerator / options.ir.frameRate.denominator),
|
|
272
|
+
audioCodec: 'mp4a.40.2',
|
|
273
|
+
videoName: 'AV1',
|
|
274
|
+
audioName: 'AAC',
|
|
275
|
+
verifyAudioRuntime: true,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
export function preflightHevcMp4Export(options) {
|
|
279
|
+
return preflightMuxedExport(options, {
|
|
280
|
+
videoCodec: hevcCodecString(options.ir.width, options.ir.height, options.ir.frameRate.numerator / options.ir.frameRate.denominator),
|
|
281
|
+
audioCodec: 'mp4a.40.2',
|
|
282
|
+
videoName: 'HEVC',
|
|
283
|
+
audioName: 'AAC',
|
|
284
|
+
verifyAudioRuntime: true,
|
|
285
|
+
hevc: true,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/** Profile-wide preflight used by the SDK before any sink writer is acquired. */
|
|
289
|
+
export async function preflightProfileExport(options) {
|
|
290
|
+
if (options.profile === 'webm-vp9-opus' ||
|
|
291
|
+
options.profile === 'mp4-h264-aac' ||
|
|
292
|
+
options.profile === 'mp4-av1-aac' ||
|
|
293
|
+
options.profile === 'mp4-hevc-aac') {
|
|
294
|
+
const muxed = {
|
|
295
|
+
ir: options.ir,
|
|
296
|
+
projectRevision: options.projectRevision,
|
|
297
|
+
videoBitrate: options.videoBitrate ?? 8_000_000,
|
|
298
|
+
audioBitrate: options.audioBitrate ?? 192_000,
|
|
299
|
+
sink: options.sink,
|
|
300
|
+
renderFrame: () => Promise.reject(new Error('Preflight does not render frames')),
|
|
301
|
+
renderAudio: () => Promise.reject(new Error('Preflight does not render audio')),
|
|
302
|
+
...(options.materialBackendAvailable === undefined
|
|
303
|
+
? {}
|
|
304
|
+
: { materialBackendAvailable: options.materialBackendAvailable }),
|
|
305
|
+
};
|
|
306
|
+
if (options.profile === 'mp4-h264-aac')
|
|
307
|
+
return preflightMp4Export(muxed);
|
|
308
|
+
if (options.profile === 'mp4-av1-aac')
|
|
309
|
+
return preflightAv1Mp4Export(muxed);
|
|
310
|
+
if (options.profile === 'mp4-hevc-aac')
|
|
311
|
+
return preflightHevcMp4Export(muxed);
|
|
312
|
+
return preflightWebMExport(muxed);
|
|
313
|
+
}
|
|
314
|
+
const issues = [];
|
|
315
|
+
try {
|
|
316
|
+
issues.push(...preflightColorPipeline(options.ir, LOCAL_RGBA8_COLOR_CAPABILITY).issues);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
issues.push({
|
|
320
|
+
code: 'COLOR_PIPELINE_CONTRACT_INVALID',
|
|
321
|
+
severity: 'error',
|
|
322
|
+
message: error instanceof Error ? error.message : 'Invalid color pipeline contract',
|
|
323
|
+
recoverable: false,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (options.projectRevision !== options.ir.revision) {
|
|
327
|
+
issues.push({
|
|
328
|
+
code: 'EXPORT_REVISION_MISMATCH',
|
|
329
|
+
severity: 'error',
|
|
330
|
+
message: `Project revision ${options.projectRevision.toString()} does not match frozen Render IR revision ${options.ir.revision.toString()}`,
|
|
331
|
+
recoverable: false,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (options.sink.locked) {
|
|
335
|
+
issues.push({
|
|
336
|
+
code: 'EXPORT_SINK_LOCKED',
|
|
337
|
+
severity: 'error',
|
|
338
|
+
message: 'Export sink is already locked by another writer',
|
|
339
|
+
recoverable: true,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if ((options.profile === 'still-png' ||
|
|
343
|
+
options.profile === 'still-jpeg' ||
|
|
344
|
+
options.profile === 'still-webp' ||
|
|
345
|
+
options.profile === 'animated-gif') &&
|
|
346
|
+
typeof OffscreenCanvas !== 'function') {
|
|
347
|
+
issues.push({
|
|
348
|
+
code: 'EXPORT_IMAGE_CANVAS_UNAVAILABLE',
|
|
349
|
+
severity: 'error',
|
|
350
|
+
message: 'OffscreenCanvas is unavailable for image export',
|
|
351
|
+
recoverable: false,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (options.profile !== 'audio-wav') {
|
|
355
|
+
for (const material of Object.values(options.ir.materials)) {
|
|
356
|
+
if (!material.enabled)
|
|
357
|
+
continue;
|
|
358
|
+
const available = material.program !== undefined &&
|
|
359
|
+
(options.materialBackendAvailable?.(material.id, material.parameters) ?? true);
|
|
360
|
+
if (!available) {
|
|
361
|
+
issues.push({
|
|
362
|
+
code: 'EXPORT_MATERIAL_BACKEND_UNAVAILABLE',
|
|
363
|
+
severity: 'error',
|
|
364
|
+
message: `Material ${material.id} has no offline backend`,
|
|
365
|
+
recoverable: false,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { ok: issues.length === 0, revision: options.ir.revision, issues };
|
|
371
|
+
}
|
|
372
|
+
export async function exportFrozenRenderIrWebM(options) {
|
|
373
|
+
const report = await preflightWebMExport(options);
|
|
374
|
+
if (!report.ok) {
|
|
375
|
+
throw new AelionError(report.issues);
|
|
376
|
+
}
|
|
377
|
+
const exportOptions = {
|
|
378
|
+
durationUs: options.ir.durationUs,
|
|
379
|
+
width: options.ir.width,
|
|
380
|
+
height: options.ir.height,
|
|
381
|
+
frameRate: options.ir.frameRate,
|
|
382
|
+
sampleRate: options.ir.sampleRate,
|
|
383
|
+
channelCount: channelCount(options.ir.channelLayout) ?? 0,
|
|
384
|
+
videoBitrate: options.videoBitrate,
|
|
385
|
+
audioBitrate: options.audioBitrate,
|
|
386
|
+
...(report.encoderConfiguration?.videoCodecString === undefined
|
|
387
|
+
? {}
|
|
388
|
+
: { videoCodecString: report.encoderConfiguration.videoCodecString }),
|
|
389
|
+
...(report.encoderConfiguration?.audioCodecString === undefined
|
|
390
|
+
? {}
|
|
391
|
+
: { audioCodecString: report.encoderConfiguration.audioCodecString }),
|
|
392
|
+
sink: options.sink,
|
|
393
|
+
...(options.cleanupSink === undefined ? {} : { cleanupSink: options.cleanupSink }),
|
|
394
|
+
renderFrame: options.renderFrame,
|
|
395
|
+
renderAudio: options.renderAudio,
|
|
396
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
397
|
+
...(options.onProgress === undefined ? {} : { onProgress: options.onProgress }),
|
|
398
|
+
};
|
|
399
|
+
return options.execution === 'inline'
|
|
400
|
+
? exportWebM(exportOptions)
|
|
401
|
+
: exportMuxedInWorker({
|
|
402
|
+
...exportOptions,
|
|
403
|
+
profile: 'webm',
|
|
404
|
+
...(options.workerUrl === undefined ? {} : { workerUrl: options.workerUrl }),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
export async function exportFrozenRenderIrMp4(options) {
|
|
408
|
+
const report = await preflightMp4Export(options);
|
|
409
|
+
if (!report.ok)
|
|
410
|
+
throw new AelionError(report.issues);
|
|
411
|
+
const exportOptions = {
|
|
412
|
+
durationUs: options.ir.durationUs,
|
|
413
|
+
width: options.ir.width,
|
|
414
|
+
height: options.ir.height,
|
|
415
|
+
frameRate: options.ir.frameRate,
|
|
416
|
+
sampleRate: options.ir.sampleRate,
|
|
417
|
+
channelCount: channelCount(options.ir.channelLayout) ?? 0,
|
|
418
|
+
videoBitrate: options.videoBitrate,
|
|
419
|
+
audioBitrate: options.audioBitrate,
|
|
420
|
+
...(report.encoderConfiguration?.videoCodecString === undefined
|
|
421
|
+
? {}
|
|
422
|
+
: { videoCodecString: report.encoderConfiguration.videoCodecString }),
|
|
423
|
+
...(report.encoderConfiguration?.audioCodecString === undefined
|
|
424
|
+
? {}
|
|
425
|
+
: { audioCodecString: report.encoderConfiguration.audioCodecString }),
|
|
426
|
+
sink: options.sink,
|
|
427
|
+
...(options.cleanupSink === undefined ? {} : { cleanupSink: options.cleanupSink }),
|
|
428
|
+
renderFrame: options.renderFrame,
|
|
429
|
+
renderAudio: options.renderAudio,
|
|
430
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
431
|
+
...(options.onProgress === undefined ? {} : { onProgress: options.onProgress }),
|
|
432
|
+
};
|
|
433
|
+
return options.execution === 'worker'
|
|
434
|
+
? exportMuxedInWorker({
|
|
435
|
+
...exportOptions,
|
|
436
|
+
profile: 'mp4',
|
|
437
|
+
...(options.workerUrl === undefined ? {} : { workerUrl: options.workerUrl }),
|
|
438
|
+
})
|
|
439
|
+
: exportMp4(exportOptions);
|
|
440
|
+
}
|
|
441
|
+
async function exportFrozenRenderIrAlternativeMp4(options, profile) {
|
|
442
|
+
const report = profile === 'mp4-av1'
|
|
443
|
+
? await preflightAv1Mp4Export(options)
|
|
444
|
+
: await preflightHevcMp4Export(options);
|
|
445
|
+
if (!report.ok)
|
|
446
|
+
throw new AelionError(report.issues);
|
|
447
|
+
const exportOptions = {
|
|
448
|
+
durationUs: options.ir.durationUs,
|
|
449
|
+
width: options.ir.width,
|
|
450
|
+
height: options.ir.height,
|
|
451
|
+
frameRate: options.ir.frameRate,
|
|
452
|
+
sampleRate: options.ir.sampleRate,
|
|
453
|
+
channelCount: channelCount(options.ir.channelLayout) ?? 0,
|
|
454
|
+
videoBitrate: options.videoBitrate,
|
|
455
|
+
audioBitrate: options.audioBitrate,
|
|
456
|
+
...(report.encoderConfiguration?.videoCodecString === undefined
|
|
457
|
+
? {}
|
|
458
|
+
: { videoCodecString: report.encoderConfiguration.videoCodecString }),
|
|
459
|
+
...(report.encoderConfiguration?.audioCodecString === undefined
|
|
460
|
+
? {}
|
|
461
|
+
: { audioCodecString: report.encoderConfiguration.audioCodecString }),
|
|
462
|
+
sink: options.sink,
|
|
463
|
+
...(options.cleanupSink === undefined ? {} : { cleanupSink: options.cleanupSink }),
|
|
464
|
+
renderFrame: options.renderFrame,
|
|
465
|
+
renderAudio: options.renderAudio,
|
|
466
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
467
|
+
...(options.onProgress === undefined ? {} : { onProgress: options.onProgress }),
|
|
468
|
+
};
|
|
469
|
+
if (options.execution === 'worker') {
|
|
470
|
+
return exportMuxedInWorker({
|
|
471
|
+
...exportOptions,
|
|
472
|
+
profile,
|
|
473
|
+
...(options.workerUrl === undefined ? {} : { workerUrl: options.workerUrl }),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return profile === 'mp4-av1' ? exportAv1Mp4(exportOptions) : exportHevcMp4(exportOptions);
|
|
477
|
+
}
|
|
478
|
+
export function exportFrozenRenderIrAv1Mp4(options) {
|
|
479
|
+
return exportFrozenRenderIrAlternativeMp4(options, 'mp4-av1');
|
|
480
|
+
}
|
|
481
|
+
export function exportFrozenRenderIrHevcMp4(options) {
|
|
482
|
+
return exportFrozenRenderIrAlternativeMp4(options, 'mp4-hevc');
|
|
483
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StreamTargetChunk } from 'mediabunny';
|
|
2
|
+
export interface SinkCompletionBarrier {
|
|
3
|
+
readonly writable: WritableStream<StreamTargetChunk>;
|
|
4
|
+
readonly completion: Promise<void>;
|
|
5
|
+
abort(reason: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createSinkCompletionBarrier(sink: WritableStream<StreamTargetChunk>): SinkCompletionBarrier;
|
|
8
|
+
//# sourceMappingURL=sink-completion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sink-completion.d.ts","sourceRoot":"","sources":["../src/sink-completion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAC;IACrD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AAED,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,cAAc,CAAC,iBAAiB,CAAC,GACtC,qBAAqB,CAYvB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function createSinkCompletionBarrier(sink) {
|
|
2
|
+
const stream = new TransformStream();
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const completion = stream.readable.pipeTo(sink, { signal: controller.signal });
|
|
5
|
+
// A muxer or Worker may surface the same sink failure before its host-side
|
|
6
|
+
// pipe is awaited. Keep the rejection observed until the caller handles it.
|
|
7
|
+
void completion.catch(() => undefined);
|
|
8
|
+
return {
|
|
9
|
+
writable: stream.writable,
|
|
10
|
+
completion,
|
|
11
|
+
abort: reason => controller.abort(reason),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type Rational } from '@aelionsdk/core';
|
|
2
|
+
import { Mp4OutputFormat, WebMOutputFormat } from 'mediabunny';
|
|
3
|
+
export interface OfflineFrameRequest {
|
|
4
|
+
readonly frameIndex: number;
|
|
5
|
+
readonly timestampUs: number;
|
|
6
|
+
readonly durationUs: number;
|
|
7
|
+
readonly width: number;
|
|
8
|
+
readonly height: number;
|
|
9
|
+
}
|
|
10
|
+
export interface OfflineAudioRequest {
|
|
11
|
+
readonly startFrame: number;
|
|
12
|
+
readonly frameCount: number;
|
|
13
|
+
readonly sampleRate: number;
|
|
14
|
+
readonly channelCount: number;
|
|
15
|
+
}
|
|
16
|
+
export interface WebMExportOptions {
|
|
17
|
+
readonly durationUs: number;
|
|
18
|
+
readonly width: number;
|
|
19
|
+
readonly height: number;
|
|
20
|
+
readonly frameRate: Rational;
|
|
21
|
+
readonly sampleRate: number;
|
|
22
|
+
readonly channelCount: number;
|
|
23
|
+
readonly videoBitrate: number;
|
|
24
|
+
readonly audioBitrate: number;
|
|
25
|
+
/** Exact codec string selected by preflight. Defaults to the profile baseline. */
|
|
26
|
+
readonly videoCodecString?: string;
|
|
27
|
+
/** Exact audio codec string selected by preflight. */
|
|
28
|
+
readonly audioCodecString?: string;
|
|
29
|
+
readonly sink: WritableStream<{
|
|
30
|
+
readonly type: 'write';
|
|
31
|
+
readonly data: Uint8Array<ArrayBuffer>;
|
|
32
|
+
readonly position: number;
|
|
33
|
+
}>;
|
|
34
|
+
/** Idempotent sink-specific cleanup (for example deleting a partial OPFS file). */
|
|
35
|
+
readonly cleanupSink?: (reason: unknown) => void | Promise<void>;
|
|
36
|
+
readonly renderFrame: (request: OfflineFrameRequest, signal?: AbortSignal) => Promise<VideoFrame>;
|
|
37
|
+
readonly renderAudio: (request: OfflineAudioRequest, signal?: AbortSignal) => Promise<Float32Array>;
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
readonly onProgress?: (progress: number) => void;
|
|
40
|
+
}
|
|
41
|
+
export interface WebMExportResult {
|
|
42
|
+
readonly mimeType: string;
|
|
43
|
+
readonly videoFrames: number;
|
|
44
|
+
readonly audioFrames: number;
|
|
45
|
+
readonly durationUs: number;
|
|
46
|
+
/**
|
|
47
|
+
* Configuration submitted to the encoders. Variable bitrate targets are not
|
|
48
|
+
* promises about the measured bitrate of the resulting media.
|
|
49
|
+
*/
|
|
50
|
+
readonly encoderConfiguration: MuxedEncoderConfiguration;
|
|
51
|
+
}
|
|
52
|
+
export interface MuxedEncoderConfiguration {
|
|
53
|
+
readonly profile: 'webm-vp9-opus' | 'mp4-h264-aac' | 'mp4-av1-aac' | 'mp4-hevc-aac';
|
|
54
|
+
readonly video: {
|
|
55
|
+
readonly codec: string;
|
|
56
|
+
readonly codecString: string;
|
|
57
|
+
readonly width: number;
|
|
58
|
+
readonly height: number;
|
|
59
|
+
readonly frameRate: number;
|
|
60
|
+
readonly bitrateMode: 'variable';
|
|
61
|
+
readonly targetBitrate: number;
|
|
62
|
+
};
|
|
63
|
+
readonly audio: {
|
|
64
|
+
readonly codec: string;
|
|
65
|
+
readonly sampleRate: number;
|
|
66
|
+
readonly channelCount: number;
|
|
67
|
+
readonly bitrateMode: 'variable';
|
|
68
|
+
readonly targetBitrate: number;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export type Mp4ExportOptions = WebMExportOptions;
|
|
72
|
+
export type Mp4ExportResult = WebMExportResult;
|
|
73
|
+
export interface MuxedExportProfile {
|
|
74
|
+
readonly id: MuxedEncoderConfiguration['profile'];
|
|
75
|
+
readonly operationName: string;
|
|
76
|
+
readonly format: WebMOutputFormat | Mp4OutputFormat;
|
|
77
|
+
readonly videoCodec: 'vp9' | 'avc' | 'av1' | 'hevc';
|
|
78
|
+
readonly fullVideoCodecString: string;
|
|
79
|
+
readonly audioCodec: 'opus' | 'aac';
|
|
80
|
+
}
|
|
81
|
+
export interface MuxedExportRange {
|
|
82
|
+
readonly videoStartFrame: number;
|
|
83
|
+
readonly videoEndFrameExclusive: number;
|
|
84
|
+
readonly audioStartFrame: number;
|
|
85
|
+
readonly audioEndFrameExclusive: number;
|
|
86
|
+
/**
|
|
87
|
+
* `range` restarts encoder timestamps at zero. It is intended for independently
|
|
88
|
+
* encoded container fragments whose absolute decode times are patched at commit.
|
|
89
|
+
*/
|
|
90
|
+
readonly timestampBase?: 'timeline' | 'range';
|
|
91
|
+
}
|
|
92
|
+
export declare function exportMuxed(options: WebMExportOptions, profile: MuxedExportProfile, range?: MuxedExportRange): Promise<WebMExportResult>;
|
|
93
|
+
export declare function exportWebM(options: WebMExportOptions): Promise<WebMExportResult>;
|
|
94
|
+
export declare function exportMp4(options: Mp4ExportOptions): Promise<Mp4ExportResult>;
|
|
95
|
+
export declare function exportAv1Mp4(options: Mp4ExportOptions): Promise<Mp4ExportResult>;
|
|
96
|
+
export declare function exportHevcMp4(options: Mp4ExportOptions): Promise<Mp4ExportResult>;
|
|
97
|
+
//# sourceMappingURL=webm-export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webm-export.d.ts","sourceRoot":"","sources":["../src/webm-export.ts"],"names":[],"mappings":"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"}
|