@meframe/core 0.3.5 → 0.3.7
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/dist/orchestrator/ExportScheduler.d.ts +9 -7
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +182 -80
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/Orchestrator.js +22 -22
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoWindowDecodeSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoWindowDecodeSession.js +15 -3
- package/dist/orchestrator/VideoWindowDecodeSession.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts +2 -0
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/compose/VideoComposer.js +41 -2
- package/dist/stages/compose/VideoComposer.js.map +1 -1
- package/dist/stages/decode/video-decoder.d.ts.map +1 -1
- package/dist/stages/decode/video-decoder.js +45 -2
- package/dist/stages/decode/video-decoder.js.map +1 -1
- package/dist/utils/time-utils.d.ts +15 -0
- package/dist/utils/time-utils.d.ts.map +1 -1
- package/dist/utils/time-utils.js +33 -0
- package/dist/utils/time-utils.js.map +1 -0
- package/dist/worker/WorkerChannel.d.ts.map +1 -1
- package/dist/worker/WorkerChannel.js +3 -15
- package/dist/worker/WorkerChannel.js.map +1 -1
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +4 -12
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/types.d.ts +1 -1
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js.map +1 -1
- package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
- package/dist/workers/stages/{compose/video-compose.worker.KMZjuJuY.js → export/export.worker.BYttrqTQ.js} +872 -217
- package/dist/workers/stages/export/export.worker.BYttrqTQ.js.map +1 -0
- package/dist/workers/worker-manifest.json +1 -3
- package/package.json +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +0 -80
- package/dist/orchestrator/VideoClipSession.d.ts.map +0 -1
- package/dist/orchestrator/VideoClipSession.js +0 -361
- package/dist/orchestrator/VideoClipSession.js.map +0 -1
- package/dist/workers/WorkerChannel.DQK8rAab.js +0 -528
- package/dist/workers/WorkerChannel.DQK8rAab.js.map +0 -1
- package/dist/workers/stages/compose/audio-compose.worker.B4Io5w9i.js +0 -1063
- package/dist/workers/stages/compose/audio-compose.worker.B4Io5w9i.js.map +0 -1
- package/dist/workers/stages/compose/video-compose.worker.KMZjuJuY.js.map +0 -1
- package/dist/workers/stages/encode/video-encode.worker.D6aB_rF9.js +0 -334
- package/dist/workers/stages/encode/video-encode.worker.D6aB_rF9.js.map +0 -1
|
@@ -34,16 +34,18 @@ export declare class ExportScheduler {
|
|
|
34
34
|
constructor(deps: ExportSchedulerDeps);
|
|
35
35
|
execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob | null>;
|
|
36
36
|
private executeInternal;
|
|
37
|
-
/**
|
|
38
|
-
* Preload all resources (0-40% progress)
|
|
39
|
-
*/
|
|
40
37
|
private preloadResources;
|
|
38
|
+
private processAudioInWindows;
|
|
41
39
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* Windowed video processing: one ExportWorker for all clips and windows.
|
|
41
|
+
* Memory bounded by window size (~150 frames).
|
|
44
42
|
*/
|
|
45
|
-
private
|
|
46
|
-
private
|
|
43
|
+
private processVideoWindowed;
|
|
44
|
+
private processVideoClipWindowed;
|
|
45
|
+
private processImageClip;
|
|
46
|
+
private buildClipInstructions;
|
|
47
|
+
private loadAndTransferAttachments;
|
|
48
|
+
private extractAttachmentImageResources;
|
|
47
49
|
private decideAudioStrategy;
|
|
48
50
|
}
|
|
49
51
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExportScheduler.d.ts","sourceRoot":"","sources":["../../src/orchestrator/ExportScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"ExportScheduler.d.ts","sourceRoot":"","sources":["../../src/orchestrator/ExportScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAQ,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAgB,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKhE,UAAU,mBAAmB;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,kBAAkB,CAAC;IACjC,qBAAqB,EAAE,MAAM,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACrD,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACrC;AAED,UAAU,qBAAsB,SAAQ,aAAa;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC/B,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAKD,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAsB;gBAEtB,IAAI,EAAE,mBAAmB;IAI/B,OAAO,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;YAa9E,eAAe;YAqIf,gBAAgB;YAyDhB,qBAAqB;IAwBnC;;;OAGG;YACW,oBAAoB;YAyIpB,wBAAwB;YAuDxB,gBAAgB;IA8B9B,OAAO,CAAC,qBAAqB;YAKf,0BAA0B;IA2BxC,OAAO,CAAC,+BAA+B;YAazB,mBAAmB;CA0ClC"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { VideoWindowDecodeSession } from "./VideoWindowDecodeSession.js";
|
|
2
2
|
import { hasResourceId } from "../model/types.js";
|
|
3
3
|
import { MeframeEvent } from "../event/events.js";
|
|
4
4
|
import { AudioChunkEncoder } from "../stages/encode/AudioChunkEncoder.js";
|
|
5
|
+
import { computeGOPAlignedWindows, computeFixedWindows } from "../utils/time-utils.js";
|
|
6
|
+
const VIDEO_WINDOW_DURATION_US = 5e6;
|
|
5
7
|
class ExportScheduler {
|
|
6
8
|
deps;
|
|
7
9
|
constructor(deps) {
|
|
@@ -80,7 +82,7 @@ class ExportScheduler {
|
|
|
80
82
|
const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);
|
|
81
83
|
if (mainTrack && mainTrack.clips.length > 0) {
|
|
82
84
|
const audioPromise = enableAudio ? this.processAudioInWindows(model.durationUs, audioSession, muxManager, checkStatus) : Promise.resolve();
|
|
83
|
-
await this.
|
|
85
|
+
await this.processVideoWindowed(mainTrack.clips, muxManager, model, checkStatus);
|
|
84
86
|
try {
|
|
85
87
|
await audioPromise;
|
|
86
88
|
} catch (error) {
|
|
@@ -118,9 +120,6 @@ class ExportScheduler {
|
|
|
118
120
|
this.deps.cacheManager.endExport();
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Preload all resources (0-40% progress)
|
|
123
|
-
*/
|
|
124
123
|
async preloadResources(model, resourceLoader, eventBus, checkStatus) {
|
|
125
124
|
eventBus.emit(MeframeEvent.ExportProgress, {
|
|
126
125
|
progress: 0,
|
|
@@ -164,10 +163,6 @@ class ExportScheduler {
|
|
|
164
163
|
})
|
|
165
164
|
);
|
|
166
165
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Process audio in fixed windows.
|
|
169
|
-
* Current implementation uses 5-minute windows to balance quality and memory.
|
|
170
|
-
*/
|
|
171
166
|
async processAudioInWindows(totalDurationUs, audioSession, muxManager, checkStatus) {
|
|
172
167
|
const WINDOW_DURATION_US = 5 * 60 * 1e6;
|
|
173
168
|
let currentUs = 0;
|
|
@@ -183,84 +178,191 @@ class ExportScheduler {
|
|
|
183
178
|
currentUs = endUs;
|
|
184
179
|
}
|
|
185
180
|
}
|
|
186
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Windowed video processing: one ExportWorker for all clips and windows.
|
|
183
|
+
* Memory bounded by window size (~150 frames).
|
|
184
|
+
*/
|
|
185
|
+
async processVideoWindowed(clips, muxManager, model, checkStatus) {
|
|
186
|
+
const { workerPool, planner, cacheManager, resourceLoader } = this.deps;
|
|
187
|
+
const workerConfigs = this.deps.workerConfigsProvider();
|
|
188
|
+
const exportWorker = await workerPool.getOrCreate("videoExport", "export", { lazy: true });
|
|
189
|
+
const exportConfig = workerConfigs.videoExport ?? {};
|
|
190
|
+
const composeConfig = { ...exportConfig.compose ?? {} };
|
|
191
|
+
const encodeConfig = { ...exportConfig.encode ?? {} };
|
|
192
|
+
if (model.renderConfig?.width) {
|
|
193
|
+
composeConfig.width = model.renderConfig.width;
|
|
194
|
+
encodeConfig.width = model.renderConfig.width;
|
|
195
|
+
}
|
|
196
|
+
if (model.renderConfig?.height) {
|
|
197
|
+
composeConfig.height = model.renderConfig.height;
|
|
198
|
+
encodeConfig.height = model.renderConfig.height;
|
|
199
|
+
}
|
|
200
|
+
await exportWorker.send("configure", { compose: composeConfig, encode: encodeConfig });
|
|
201
|
+
let windowResolver;
|
|
202
|
+
let windowRejecter;
|
|
187
203
|
let nextClipStartUs = 0;
|
|
188
204
|
let lastChunkEndUs = 0;
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
205
|
+
exportWorker.receiveStream(async (stream, _metadata) => {
|
|
206
|
+
const reader = stream.getReader();
|
|
207
|
+
try {
|
|
208
|
+
while (true) {
|
|
209
|
+
await checkStatus();
|
|
210
|
+
const { done, value } = await reader.read();
|
|
211
|
+
if (done) break;
|
|
212
|
+
if (!value) continue;
|
|
213
|
+
const { chunk: originalChunk, metadata } = value;
|
|
214
|
+
const chunkDuration = originalChunk.duration ?? 33333;
|
|
215
|
+
const remappedTimestamp = originalChunk.timestamp + nextClipStartUs;
|
|
216
|
+
const buffer = new ArrayBuffer(originalChunk.byteLength);
|
|
217
|
+
originalChunk.copyTo(buffer);
|
|
218
|
+
const remappedChunk = new EncodedVideoChunk({
|
|
219
|
+
type: originalChunk.type,
|
|
220
|
+
timestamp: remappedTimestamp,
|
|
221
|
+
duration: chunkDuration,
|
|
222
|
+
data: buffer
|
|
223
|
+
});
|
|
224
|
+
muxManager.writeVideoChunk(remappedChunk, metadata);
|
|
225
|
+
lastChunkEndUs = remappedTimestamp + chunkDuration;
|
|
226
|
+
const encodingProgress = remappedTimestamp / model.durationUs;
|
|
227
|
+
const totalProgress = 0.4 + encodingProgress * 0.6;
|
|
228
|
+
this.deps.eventBus.emit(MeframeEvent.ExportProgress, {
|
|
229
|
+
progress: Math.min(1, totalProgress),
|
|
230
|
+
stage: "encoding",
|
|
231
|
+
timeUs: remappedTimestamp
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
windowResolver();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
windowRejecter(error);
|
|
237
|
+
} finally {
|
|
238
|
+
reader.releaseLock();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
for (const clip of clips) {
|
|
243
|
+
await checkStatus();
|
|
244
|
+
const resource = hasResourceId(clip) ? model.getResource(clip.resourceId) : null;
|
|
245
|
+
if (!resource) {
|
|
246
|
+
console.warn("[ExportScheduler] Resource not found for clip:", clip.id);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const instructions = this.buildClipInstructions(clip, planner);
|
|
250
|
+
const createWindowPromise = () => new Promise((resolve, reject) => {
|
|
251
|
+
windowResolver = resolve;
|
|
252
|
+
windowRejecter = reject;
|
|
253
|
+
});
|
|
254
|
+
if (resource.type === "image") {
|
|
255
|
+
await this.processImageClip(
|
|
256
|
+
exportWorker,
|
|
257
|
+
clip,
|
|
258
|
+
resource,
|
|
259
|
+
instructions,
|
|
260
|
+
model,
|
|
261
|
+
resourceLoader,
|
|
262
|
+
createWindowPromise
|
|
263
|
+
);
|
|
264
|
+
} else {
|
|
265
|
+
await this.processVideoClipWindowed(
|
|
266
|
+
exportWorker,
|
|
267
|
+
clip,
|
|
268
|
+
resource,
|
|
269
|
+
instructions,
|
|
270
|
+
model,
|
|
271
|
+
cacheManager,
|
|
272
|
+
resourceLoader,
|
|
273
|
+
checkStatus,
|
|
274
|
+
createWindowPromise
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
await exportWorker.send("dispose_clip");
|
|
278
|
+
nextClipStartUs = lastChunkEndUs;
|
|
279
|
+
}
|
|
280
|
+
await exportWorker.send("flush");
|
|
281
|
+
} finally {
|
|
282
|
+
workerPool.terminate("videoExport", "export");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async processVideoClipWindowed(exportWorker, clip, resource, instructions, model, cacheManager, resourceLoader, checkStatus, createWindowPromise) {
|
|
286
|
+
await exportWorker.send("install_instructions", instructions);
|
|
287
|
+
await this.loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader);
|
|
288
|
+
const trimStartUs = clip.trimStartUs ?? 0;
|
|
289
|
+
const trimEndUs = trimStartUs + clip.durationUs;
|
|
290
|
+
const fps = model.fps ?? 30;
|
|
291
|
+
const index = cacheManager.mp4IndexCache.get(resource.id);
|
|
292
|
+
const gopIndex = index?.tracks?.video?.gopIndex;
|
|
293
|
+
const windows = gopIndex && gopIndex.length > 0 ? computeGOPAlignedWindows(gopIndex, trimStartUs, trimEndUs, VIDEO_WINDOW_DURATION_US) : computeFixedWindows(trimStartUs, trimEndUs, VIDEO_WINDOW_DURATION_US);
|
|
294
|
+
for (const { startUs, endUs } of windows) {
|
|
192
295
|
await checkStatus();
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
let streamFinishedRejecter;
|
|
196
|
-
const streamFinishedPromise = new Promise((resolve, reject) => {
|
|
197
|
-
streamFinishedResolver = resolve;
|
|
198
|
-
streamFinishedRejecter = reject;
|
|
199
|
-
});
|
|
200
|
-
const session = await VideoClipSession.create({
|
|
296
|
+
const windowDone = createWindowPromise();
|
|
297
|
+
const decodeSession = await VideoWindowDecodeSession.create({
|
|
201
298
|
clipId: clip.id,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
299
|
+
resourceId: resource.id,
|
|
300
|
+
targetTimeUs: trimStartUs,
|
|
301
|
+
globalTimeUs: clip.startUs,
|
|
302
|
+
mp4IndexCache: cacheManager.mp4IndexCache,
|
|
303
|
+
cacheManager,
|
|
206
304
|
compositionModel: model,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await checkStatus();
|
|
216
|
-
const { done, value } = await reader.read();
|
|
217
|
-
if (done) break;
|
|
218
|
-
if (value) {
|
|
219
|
-
const originalChunk = value.chunk;
|
|
220
|
-
const metadata = value.metadata;
|
|
221
|
-
const chunkDuration = originalChunk.duration ?? 33333;
|
|
222
|
-
const remappedTimestamp = originalChunk.timestamp + currentClipOffsetUs;
|
|
223
|
-
const buffer = new ArrayBuffer(originalChunk.byteLength);
|
|
224
|
-
originalChunk.copyTo(buffer);
|
|
225
|
-
const remappedChunk = new EncodedVideoChunk({
|
|
226
|
-
type: originalChunk.type,
|
|
227
|
-
timestamp: remappedTimestamp,
|
|
228
|
-
duration: chunkDuration,
|
|
229
|
-
data: buffer
|
|
230
|
-
});
|
|
231
|
-
muxManager.writeVideoChunk(remappedChunk, metadata);
|
|
232
|
-
lastChunkEndUs = remappedTimestamp + chunkDuration;
|
|
233
|
-
const encodingProgress = remappedTimestamp / model.durationUs;
|
|
234
|
-
const totalProgress = 0.4 + encodingProgress * 0.6;
|
|
235
|
-
this.deps.eventBus.emit(MeframeEvent.ExportProgress, {
|
|
236
|
-
progress: Math.min(1, totalProgress),
|
|
237
|
-
stage: "encoding",
|
|
238
|
-
timeUs: remappedTimestamp
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
streamFinishedResolver();
|
|
243
|
-
} catch (error) {
|
|
244
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
245
|
-
streamFinishedRejecter(error);
|
|
246
|
-
} else {
|
|
247
|
-
console.error(`[ExportScheduler] Stream error for clip ${clip.id}:`, error);
|
|
248
|
-
streamFinishedRejecter(error);
|
|
249
|
-
}
|
|
250
|
-
} finally {
|
|
251
|
-
reader.releaseLock();
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// Note: Attachment resources are loaded in VideoClipSession.connectPipeline
|
|
256
|
-
// before sending video stream, ensuring watermarks appear from the first frame
|
|
257
|
-
}
|
|
305
|
+
resourceLoader,
|
|
306
|
+
fps
|
|
307
|
+
});
|
|
308
|
+
const frameStream = await decodeSession.decodeRangeToStream(startUs, endUs);
|
|
309
|
+
await exportWorker.sendStream(frameStream, {
|
|
310
|
+
streamType: "video",
|
|
311
|
+
windowStartUs: startUs,
|
|
312
|
+
windowEndUs: endUs
|
|
258
313
|
});
|
|
259
|
-
await
|
|
260
|
-
await
|
|
261
|
-
|
|
262
|
-
|
|
314
|
+
await windowDone;
|
|
315
|
+
await decodeSession.dispose();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async processImageClip(exportWorker, clip, resource, instructions, model, resourceLoader, createWindowPromise) {
|
|
319
|
+
await this.loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader);
|
|
320
|
+
const clipDone = createWindowPromise();
|
|
321
|
+
const imageBitmap = await resourceLoader.loadImage(resource);
|
|
322
|
+
await exportWorker.send(
|
|
323
|
+
"receive_image",
|
|
324
|
+
{
|
|
325
|
+
resourceId: resource.id,
|
|
326
|
+
sessionId: clip.id,
|
|
327
|
+
imageBitmap,
|
|
328
|
+
instructions
|
|
329
|
+
},
|
|
330
|
+
{ transfer: [imageBitmap] }
|
|
331
|
+
);
|
|
332
|
+
await clipDone;
|
|
333
|
+
}
|
|
334
|
+
buildClipInstructions(clip, planner) {
|
|
335
|
+
const plan = planner.buildClipPlan(clip, { cache: false });
|
|
336
|
+
return plan.instructions;
|
|
337
|
+
}
|
|
338
|
+
async loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader) {
|
|
339
|
+
const attachmentResources = this.extractAttachmentImageResources(instructions);
|
|
340
|
+
if (attachmentResources.length === 0) return;
|
|
341
|
+
await Promise.all(
|
|
342
|
+
attachmentResources.map(async (resourceId) => {
|
|
343
|
+
const resource = model.getResource(resourceId);
|
|
344
|
+
if (!resource) return;
|
|
345
|
+
const imageBitmap = await resourceLoader.loadImage(resource);
|
|
346
|
+
if (!imageBitmap) return;
|
|
347
|
+
await exportWorker.send(
|
|
348
|
+
"receive_image",
|
|
349
|
+
{ clipId: clip.id, resourceId, sessionId: clip.id, imageBitmap },
|
|
350
|
+
{ transfer: [imageBitmap] }
|
|
351
|
+
);
|
|
352
|
+
})
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
extractAttachmentImageResources(instructions) {
|
|
356
|
+
const resourceIds = /* @__PURE__ */ new Set();
|
|
357
|
+
for (const layer of instructions.layers) {
|
|
358
|
+
if (!layer.payload.attachmentId) continue;
|
|
359
|
+
if (layer.type === "image") {
|
|
360
|
+
const payload = layer.payload;
|
|
361
|
+
if (payload.oldResourceId) resourceIds.add(payload.oldResourceId);
|
|
362
|
+
if (payload.resourceId) resourceIds.add(payload.resourceId);
|
|
363
|
+
}
|
|
263
364
|
}
|
|
365
|
+
return Array.from(resourceIds);
|
|
264
366
|
}
|
|
265
367
|
async decideAudioStrategy(input) {
|
|
266
368
|
if (!input.hasAudioSamples) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExportScheduler.js","sources":["../../src/orchestrator/ExportScheduler.ts"],"sourcesContent":["import { CompositionModel } from '../model';\nimport { ExportOptions } from '../types';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { AudioExportSession } from './AudioExportSession';\nimport { VideoClipSession } from './VideoClipSession';\nimport { WorkerType } from '../worker/types';\nimport { hasResourceId, type TimeUs } from '../model/types';\nimport type { ExportController } from '../controllers/ExportController';\nimport { EventBus } from '../event/EventBus';\nimport { MeframeEvent, EventPayloadMap } from '../event/events';\nimport { AudioChunkEncoder } from '../stages/encode/AudioChunkEncoder';\n\ninterface ExportSchedulerDeps {\n workerPool: WorkerPool;\n planner: CompositionPlanner;\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n muxManager: MuxManager;\n audioSession: AudioExportSession;\n workerConfigsProvider: () => Record<WorkerType, any>;\n eventBus: EventBus<EventPayloadMap>;\n}\n\ninterface ExtendedExportOptions extends ExportOptions {\n signal?: AbortSignal;\n controller?: ExportController;\n exportMode?: 'blob' | 'stream';\n onMuxData?: (data: Uint8Array, position: number) => void;\n muxChunkSizeBytes?: number;\n muxChunked?: boolean;\n}\n\nexport class ExportScheduler {\n private deps: ExportSchedulerDeps;\n\n constructor(deps: ExportSchedulerDeps) {\n this.deps = deps;\n }\n\n async execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob | null> {\n this.deps.cacheManager.clear();\n\n const projectId = this.deps.cacheManager.resourceCache.projectId;\n\n if (!navigator.locks) {\n return this.executeInternal(model, options);\n }\n\n const lockName = `meframe-resource-${projectId}`;\n return navigator.locks.request(lockName, () => this.executeInternal(model, options));\n }\n\n private async executeInternal(\n model: CompositionModel,\n options: ExtendedExportOptions\n ): Promise<Blob | null> {\n const { muxManager, audioSession, eventBus, resourceLoader } = this.deps;\n const signal = options.signal;\n const controller = options.controller;\n const exportMode = options.exportMode ?? 'blob';\n\n if (exportMode === 'stream' && typeof options.onMuxData !== 'function') {\n throw new Error('onMuxData callback is required when exportMode is stream');\n }\n\n let streamedBytes = 0;\n\n const checkStatus = async () => {\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n // TODO: ugly\n if (controller?.isPaused()) {\n // Wait until resumed\n while (controller.isPaused()) {\n if (signal?.aborted) throw new DOMException('Export aborted', 'AbortError');\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n };\n\n const width = options.width || model.renderConfig?.width || 720;\n const height = options.height || model.renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n this.deps.cacheManager.beginExport();\n\n try {\n // 1. Preload and parse all resources (0-40%)\n await this.preloadResources(model, resourceLoader, eventBus, checkStatus);\n\n const hasAudioSamples = this.deps.cacheManager.audioSampleCache.getTotalBytes() > 0;\n const { enableAudio, disabledReason } = await this.decideAudioStrategy({\n hasAudioSamples,\n requestedFormat: options.format ?? 'mp4',\n requestedAudioCodec: options.audioCodec ?? 'aac',\n });\n if (disabledReason) {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0.4,\n stage: 'encoding',\n message: disabledReason,\n });\n }\n\n // 2. Start Muxer\n muxManager.start({\n width,\n height,\n fps,\n enableAudio,\n output:\n exportMode === 'stream'\n ? {\n kind: 'stream',\n onData: (data, position) => {\n streamedBytes = Math.max(streamedBytes, position + data.byteLength);\n options.onMuxData?.(data, position);\n },\n chunkSize: options.muxChunkSizeBytes,\n chunked: options.muxChunked,\n }\n : { kind: 'blob' },\n });\n\n // 3. Process Video and Audio\n const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);\n if (mainTrack && mainTrack.clips.length > 0) {\n const audioPromise = enableAudio\n ? this.processAudioInWindows(model.durationUs, audioSession, muxManager, checkStatus)\n : Promise.resolve();\n\n // Process video clips sequentially\n await this.processVideoClipsSequentially(mainTrack.clips, muxManager, model, checkStatus);\n\n // Wait for audio encoding to complete\n try {\n await audioPromise;\n } catch (error) {\n // If audio encode fails (e.g. unsupported AAC), keep export running as video-only MP4.\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0.95,\n stage: 'encoding',\n message: `Audio skipped (runtime failure): ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n } else {\n console.warn('[ExportScheduler] No video clips found');\n }\n\n // Finalize audio session (close encoder)\n await audioSession.finalizeExportAudio();\n\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n\n // 4. Finalize\n const blob = await muxManager.finalize();\n\n eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob?.size ?? streamedBytes,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n\n // Some clients rely on a final progress=1 event to mark completion (e.g. UI progress bars).\n // Keep it emitted after ExportComplete for backward compatibility.\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 1,\n stage: 'muxing',\n });\n\n return blob;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportError, {\n error: error instanceof Error ? error : new Error(String(error)),\n stage: 'export',\n });\n throw error;\n } finally {\n this.deps.cacheManager.endExport();\n }\n }\n\n /**\n * Preload all resources (0-40% progress)\n */\n private async preloadResources(\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n eventBus: EventBus<EventPayloadMap>,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n // Collect resources in horizontal order (clip index priority)\n const tracks = model.tracks.filter((track) => ['video', 'audio'].includes(track.kind));\n if (tracks.length === 0) return;\n\n const maxClipCount = Math.max(...tracks.map((track) => track.clips.length));\n const resourcesToLoad: string[] = [];\n const seen = new Set<string>();\n\n // Horizontal collection: clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of tracks) {\n const clip = track.clips[clipIndex];\n if (clip && hasResourceId(clip)) {\n if (!seen.has(clip.resourceId)) {\n seen.add(clip.resourceId);\n resourcesToLoad.push(clip.resourceId);\n }\n }\n }\n }\n\n // Load resources with progress updates (concurrent; ResourceLoader already enforces maxConcurrent).\n const total = resourcesToLoad.length;\n let completed = 0;\n\n await Promise.all(\n resourcesToLoad.map(async (resourceId) => {\n await checkStatus();\n await resourceLoader.load(resourceId, { isPreload: false });\n\n // Export must be strict: if a resource is in error state (including terminal mismatch),\n // fail fast instead of silently producing a partial/blank output.\n const resource = model.getResource(resourceId);\n if (resource?.state === 'error') {\n const details = resource.error?.message ?? 'Unknown resource error';\n throw new Error(`Export preload failed for ${resourceId}: ${details}`);\n }\n\n completed++;\n\n // Update progress: 0-40%\n const progress = total > 0 ? (completed / total) * 0.4 : 0.4;\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'preparing',\n message: `Loading resources... (${completed}/${total})`,\n });\n })\n );\n }\n\n /**\n * Process audio in fixed windows.\n * Current implementation uses 5-minute windows to balance quality and memory.\n */\n private async processAudioInWindows(\n totalDurationUs: TimeUs,\n audioSession: AudioExportSession,\n muxManager: MuxManager,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n const WINDOW_DURATION_US = 5 * 60 * 1_000_000; // 5 minutes\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n await checkStatus();\n\n const endUs = Math.min(currentUs + WINDOW_DURATION_US, totalDurationUs);\n\n await audioSession.mixAndEncodeSegment(currentUs, endUs, (chunk, meta) =>\n muxManager.writeAudioChunk(chunk, meta)\n );\n\n // Clear audio cache after encoding each window to prevent memory accumulation\n // This is safe because audio data is already encoded and won't be reused\n this.deps.cacheManager.clearAudioCache();\n\n currentUs = endUs;\n }\n }\n\n private async processVideoClipsSequentially(\n clips: any[],\n muxManager: MuxManager,\n model: CompositionModel,\n checkStatus: () => Promise<void>\n ) {\n // Use actual last written timestamp + duration as offset for next clip\n // This avoids duplicate PTS when mp4-muxer rounds microseconds to timescale\n let nextClipStartUs = 0;\n // Track last chunk's end time (timestamp + duration) for precise offset calculation\n let lastChunkEndUs = 0;\n\n for (let i = 0; i < clips.length; i++) {\n const clip = clips[i];\n const currentClipOffsetUs = nextClipStartUs;\n\n await checkStatus(); // Check before starting new clip\n\n const sessionId = `${clip.id}-export`;\n let streamFinishedResolver: () => void;\n let streamFinishedRejecter: (err: any) => void;\n const streamFinishedPromise = new Promise<void>((resolve, reject) => {\n streamFinishedResolver = resolve;\n streamFinishedRejecter = reject;\n });\n\n const session = await VideoClipSession.create({\n clipId: clip.id,\n sessionId,\n planner: this.deps.planner,\n workerPool: this.deps.workerPool,\n cacheManager: this.deps.cacheManager,\n compositionModel: model,\n workerConfigs: this.deps.workerConfigsProvider(),\n resourceLoader: this.deps.resourceLoader,\n callbacks: {\n onEncodedStreamReady: async (stream, track) => {\n if (track === 'video') {\n const reader = stream.getReader();\n try {\n while (true) {\n await checkStatus();\n\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const originalChunk = value.chunk;\n const metadata = value.metadata;\n const chunkDuration = originalChunk.duration ?? 33333; // Default ~30fps\n\n const remappedTimestamp = originalChunk.timestamp + currentClipOffsetUs;\n\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: remappedTimestamp,\n duration: chunkDuration,\n data: buffer,\n });\n\n muxManager.writeVideoChunk(remappedChunk, metadata);\n\n // Track end time for next clip's offset\n lastChunkEndUs = remappedTimestamp + chunkDuration;\n\n // Emit progress: 40-100%\n const encodingProgress = remappedTimestamp / model.durationUs;\n const totalProgress = 0.4 + encodingProgress * 0.6; // 40% + (0-60%)\n\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: Math.min(1.0, totalProgress),\n stage: 'encoding',\n timeUs: remappedTimestamp,\n });\n }\n }\n streamFinishedResolver();\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n streamFinishedRejecter(error);\n } else {\n console.error(`[ExportScheduler] Stream error for clip ${clip.id}:`, error);\n streamFinishedRejecter(error);\n }\n } finally {\n reader.releaseLock();\n }\n }\n },\n // Note: Attachment resources are loaded in VideoClipSession.connectPipeline\n // before sending video stream, ensuring watermarks appear from the first frame\n },\n });\n\n await session.activate();\n await streamFinishedPromise;\n\n await session.dispose();\n\n // Use actual last chunk end time as next clip's start\n // This ensures no timestamp overlap even after muxer rounding\n nextClipStartUs = lastChunkEndUs;\n }\n }\n\n private async decideAudioStrategy(input: {\n hasAudioSamples: boolean;\n requestedFormat: NonNullable<ExportOptions['format']>;\n requestedAudioCodec: NonNullable<ExportOptions['audioCodec']>;\n }): Promise<{ enableAudio: boolean; disabledReason?: string }> {\n if (!input.hasAudioSamples) {\n return { enableAudio: false };\n }\n\n // Current implementation is MP4-only muxing.\n if (input.requestedFormat !== 'mp4') {\n return {\n enableAudio: false,\n disabledReason: `Audio skipped: format ${input.requestedFormat} is not supported.`,\n };\n }\n\n // MP4 mux currently assumes AAC.\n if (input.requestedAudioCodec !== 'aac') {\n return {\n enableAudio: false,\n disabledReason: `Audio skipped: mp4 output currently supports only AAC (requested ${input.requestedAudioCodec}).`,\n };\n }\n\n // Probe AAC support in WebCodecs. On some Linux Chrome builds AAC encode is unavailable.\n if (typeof AudioEncoder === 'undefined') {\n return {\n enableAudio: false,\n disabledReason:\n 'Audio skipped: WebCodecs AudioEncoder is not available in this browser/runtime (AAC required for mp4).',\n };\n }\n\n const support = await AudioEncoder.isConfigSupported(AudioChunkEncoder.DEFAULT_CONFIG);\n if (!support.supported) {\n return {\n enableAudio: false,\n disabledReason:\n 'Audio skipped: WebCodecs AAC (mp4a.40.2) AudioEncoder is not supported in this browser/runtime.',\n };\n }\n\n return { enableAudio: true };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ExportScheduler.js","sources":["../../src/orchestrator/ExportScheduler.ts"],"sourcesContent":["import { CompositionModel, Clip } from '../model';\nimport { ExportOptions } from '../types';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { AudioExportSession } from './AudioExportSession';\nimport { VideoWindowDecodeSession } from './VideoWindowDecodeSession';\nimport { WorkerType } from '../worker/types';\nimport { hasResourceId, type TimeUs, type Resource } from '../model/types';\nimport type { ExportController } from '../controllers/ExportController';\nimport type { BaseWorker } from '../worker/BaseWorker';\nimport { EventBus } from '../event/EventBus';\nimport { MeframeEvent, EventPayloadMap } from '../event/events';\nimport { AudioChunkEncoder } from '../stages/encode/AudioChunkEncoder';\nimport type { ClipInstructionSet } from '../stages/compose/instructions';\nimport { computeGOPAlignedWindows, computeFixedWindows } from '../utils/time-utils';\n\ninterface ExportSchedulerDeps {\n workerPool: WorkerPool;\n planner: CompositionPlanner;\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n muxManager: MuxManager;\n audioSession: AudioExportSession;\n workerConfigsProvider: () => Record<WorkerType, any>;\n eventBus: EventBus<EventPayloadMap>;\n}\n\ninterface ExtendedExportOptions extends ExportOptions {\n signal?: AbortSignal;\n controller?: ExportController;\n exportMode?: 'blob' | 'stream';\n onMuxData?: (data: Uint8Array, position: number) => void;\n muxChunkSizeBytes?: number;\n muxChunked?: boolean;\n}\n\n// 5 seconds per window, ~150 frames at 30fps\nconst VIDEO_WINDOW_DURATION_US: TimeUs = 5_000_000;\n\nexport class ExportScheduler {\n private deps: ExportSchedulerDeps;\n\n constructor(deps: ExportSchedulerDeps) {\n this.deps = deps;\n }\n\n async execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob | null> {\n this.deps.cacheManager.clear();\n\n const projectId = this.deps.cacheManager.resourceCache.projectId;\n\n if (!navigator.locks) {\n return this.executeInternal(model, options);\n }\n\n const lockName = `meframe-resource-${projectId}`;\n return navigator.locks.request(lockName, () => this.executeInternal(model, options));\n }\n\n private async executeInternal(\n model: CompositionModel,\n options: ExtendedExportOptions\n ): Promise<Blob | null> {\n const { muxManager, audioSession, eventBus, resourceLoader } = this.deps;\n const signal = options.signal;\n const controller = options.controller;\n const exportMode = options.exportMode ?? 'blob';\n\n if (exportMode === 'stream' && typeof options.onMuxData !== 'function') {\n throw new Error('onMuxData callback is required when exportMode is stream');\n }\n\n let streamedBytes = 0;\n\n const checkStatus = async () => {\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n if (controller?.isPaused()) {\n while (controller.isPaused()) {\n if (signal?.aborted) throw new DOMException('Export aborted', 'AbortError');\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n };\n\n const width = options.width || model.renderConfig?.width || 720;\n const height = options.height || model.renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n this.deps.cacheManager.beginExport();\n\n try {\n // 1. Preload and parse all resources (0-40%)\n await this.preloadResources(model, resourceLoader, eventBus, checkStatus);\n\n const hasAudioSamples = this.deps.cacheManager.audioSampleCache.getTotalBytes() > 0;\n const { enableAudio, disabledReason } = await this.decideAudioStrategy({\n hasAudioSamples,\n requestedFormat: options.format ?? 'mp4',\n requestedAudioCodec: options.audioCodec ?? 'aac',\n });\n if (disabledReason) {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0.4,\n stage: 'encoding',\n message: disabledReason,\n });\n }\n\n // 2. Start Muxer\n muxManager.start({\n width,\n height,\n fps,\n enableAudio,\n output:\n exportMode === 'stream'\n ? {\n kind: 'stream',\n onData: (data, position) => {\n streamedBytes = Math.max(streamedBytes, position + data.byteLength);\n options.onMuxData?.(data, position);\n },\n chunkSize: options.muxChunkSizeBytes,\n chunked: options.muxChunked,\n }\n : { kind: 'blob' },\n });\n\n // 3. Process Video and Audio\n const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);\n if (mainTrack && mainTrack.clips.length > 0) {\n const audioPromise = enableAudio\n ? this.processAudioInWindows(model.durationUs, audioSession, muxManager, checkStatus)\n : Promise.resolve();\n\n await this.processVideoWindowed(mainTrack.clips, muxManager, model, checkStatus);\n\n try {\n await audioPromise;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0.95,\n stage: 'encoding',\n message: `Audio skipped (runtime failure): ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n } else {\n console.warn('[ExportScheduler] No video clips found');\n }\n\n await audioSession.finalizeExportAudio();\n\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n\n // 4. Finalize\n const blob = await muxManager.finalize();\n\n eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob?.size ?? streamedBytes,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 1,\n stage: 'muxing',\n });\n\n return blob;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportError, {\n error: error instanceof Error ? error : new Error(String(error)),\n stage: 'export',\n });\n throw error;\n } finally {\n this.deps.cacheManager.endExport();\n }\n }\n\n private async preloadResources(\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n eventBus: EventBus<EventPayloadMap>,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n const tracks = model.tracks.filter((track) => ['video', 'audio'].includes(track.kind));\n if (tracks.length === 0) return;\n\n const maxClipCount = Math.max(...tracks.map((track) => track.clips.length));\n const resourcesToLoad: string[] = [];\n const seen = new Set<string>();\n\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of tracks) {\n const clip = track.clips[clipIndex];\n if (clip && hasResourceId(clip)) {\n if (!seen.has(clip.resourceId)) {\n seen.add(clip.resourceId);\n resourcesToLoad.push(clip.resourceId);\n }\n }\n }\n }\n\n const total = resourcesToLoad.length;\n let completed = 0;\n\n await Promise.all(\n resourcesToLoad.map(async (resourceId) => {\n await checkStatus();\n await resourceLoader.load(resourceId, { isPreload: false });\n\n const resource = model.getResource(resourceId);\n if (resource?.state === 'error') {\n const details = resource.error?.message ?? 'Unknown resource error';\n throw new Error(`Export preload failed for ${resourceId}: ${details}`);\n }\n\n completed++;\n\n const progress = total > 0 ? (completed / total) * 0.4 : 0.4;\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'preparing',\n message: `Loading resources... (${completed}/${total})`,\n });\n })\n );\n }\n\n private async processAudioInWindows(\n totalDurationUs: TimeUs,\n audioSession: AudioExportSession,\n muxManager: MuxManager,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n const WINDOW_DURATION_US = 5 * 60 * 1_000_000;\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n await checkStatus();\n\n const endUs = Math.min(currentUs + WINDOW_DURATION_US, totalDurationUs);\n\n await audioSession.mixAndEncodeSegment(currentUs, endUs, (chunk, meta) =>\n muxManager.writeAudioChunk(chunk, meta)\n );\n\n this.deps.cacheManager.clearAudioCache();\n\n currentUs = endUs;\n }\n }\n\n /**\n * Windowed video processing: one ExportWorker for all clips and windows.\n * Memory bounded by window size (~150 frames).\n */\n private async processVideoWindowed(\n clips: Clip[],\n muxManager: MuxManager,\n model: CompositionModel,\n checkStatus: () => Promise<void>\n ) {\n const { workerPool, planner, cacheManager, resourceLoader } = this.deps;\n const workerConfigs = this.deps.workerConfigsProvider();\n\n // --- One-time setup: create and configure ExportWorker ---\n const exportWorker = await workerPool.getOrCreate('videoExport', 'export', { lazy: true });\n const exportConfig = workerConfigs.videoExport ?? {};\n\n // Apply render overrides from model\n const composeConfig = { ...(exportConfig.compose ?? {}) };\n const encodeConfig = { ...(exportConfig.encode ?? {}) };\n if (model.renderConfig?.width) {\n composeConfig.width = model.renderConfig.width;\n encodeConfig.width = model.renderConfig.width;\n }\n if (model.renderConfig?.height) {\n composeConfig.height = model.renderConfig.height;\n encodeConfig.height = model.renderConfig.height;\n }\n\n await exportWorker.send('configure', { compose: composeConfig, encode: encodeConfig });\n\n // --- Stream receiver: called once per window's encoded output ---\n let windowResolver!: () => void;\n let windowRejecter!: (err: any) => void;\n let nextClipStartUs = 0;\n let lastChunkEndUs = 0;\n\n exportWorker.receiveStream(async (stream, _metadata) => {\n const reader = stream.getReader();\n try {\n while (true) {\n await checkStatus();\n\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n\n const { chunk: originalChunk, metadata } = value as {\n chunk: EncodedVideoChunk;\n metadata: EncodedVideoChunkMetadata;\n };\n const chunkDuration = originalChunk.duration ?? 33333;\n\n // Chunks arrive with clip-relative timestamps; remap to global timeline\n const remappedTimestamp = originalChunk.timestamp + nextClipStartUs;\n\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: remappedTimestamp,\n duration: chunkDuration,\n data: buffer,\n });\n\n muxManager.writeVideoChunk(remappedChunk, metadata);\n\n lastChunkEndUs = remappedTimestamp + chunkDuration;\n\n const encodingProgress = remappedTimestamp / model.durationUs;\n const totalProgress = 0.4 + encodingProgress * 0.6;\n\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: Math.min(1.0, totalProgress),\n stage: 'encoding',\n timeUs: remappedTimestamp,\n });\n }\n windowResolver();\n } catch (error) {\n windowRejecter(error);\n } finally {\n reader.releaseLock();\n }\n });\n\n // --- Per-clip + per-window processing ---\n try {\n for (const clip of clips) {\n await checkStatus();\n\n const resource = hasResourceId(clip) ? model.getResource(clip.resourceId) : null;\n if (!resource) {\n console.warn('[ExportScheduler] Resource not found for clip:', clip.id);\n continue;\n }\n\n const instructions = this.buildClipInstructions(clip, planner);\n\n const createWindowPromise = () =>\n new Promise<void>((resolve, reject) => {\n windowResolver = resolve;\n windowRejecter = reject;\n });\n\n if (resource.type === 'image') {\n await this.processImageClip(\n exportWorker,\n clip,\n resource,\n instructions,\n model,\n resourceLoader,\n createWindowPromise\n );\n } else {\n await this.processVideoClipWindowed(\n exportWorker,\n clip,\n resource,\n instructions,\n model,\n cacheManager,\n resourceLoader,\n checkStatus,\n createWindowPromise\n );\n }\n\n await exportWorker.send('dispose_clip');\n nextClipStartUs = lastChunkEndUs;\n }\n\n // Flush encoder to drain remaining frames\n await exportWorker.send('flush');\n } finally {\n workerPool.terminate('videoExport', 'export');\n }\n }\n\n private async processVideoClipWindowed(\n exportWorker: BaseWorker,\n clip: Clip,\n resource: Resource,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n cacheManager: CacheManager,\n resourceLoader: ResourceLoader,\n checkStatus: () => Promise<void>,\n createWindowPromise: () => Promise<void>\n ) {\n await exportWorker.send('install_instructions', instructions);\n await this.loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader);\n\n const trimStartUs = clip.trimStartUs ?? 0;\n const trimEndUs = trimStartUs + clip.durationUs;\n const fps = model.fps ?? 30;\n\n const index = cacheManager.mp4IndexCache.get(resource.id);\n const gopIndex = index?.tracks?.video?.gopIndex;\n const windows =\n gopIndex && gopIndex.length > 0\n ? computeGOPAlignedWindows(gopIndex, trimStartUs, trimEndUs, VIDEO_WINDOW_DURATION_US)\n : computeFixedWindows(trimStartUs, trimEndUs, VIDEO_WINDOW_DURATION_US);\n\n for (const { startUs, endUs } of windows) {\n await checkStatus();\n\n const windowDone = createWindowPromise();\n\n const decodeSession = await VideoWindowDecodeSession.create({\n clipId: clip.id,\n resourceId: resource.id,\n targetTimeUs: trimStartUs,\n globalTimeUs: clip.startUs,\n mp4IndexCache: cacheManager.mp4IndexCache,\n cacheManager,\n compositionModel: model,\n resourceLoader,\n fps,\n });\n\n const frameStream = await decodeSession.decodeRangeToStream(startUs, endUs);\n\n await exportWorker.sendStream(frameStream, {\n streamType: 'video',\n windowStartUs: startUs,\n windowEndUs: endUs,\n });\n\n await windowDone;\n await decodeSession.dispose();\n }\n }\n\n private async processImageClip(\n exportWorker: BaseWorker,\n clip: Clip,\n resource: Resource,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n createWindowPromise: () => Promise<void>\n ) {\n await this.loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader);\n\n const clipDone = createWindowPromise();\n\n const imageBitmap = await resourceLoader.loadImage(resource);\n await exportWorker.send(\n 'receive_image',\n {\n resourceId: resource.id,\n sessionId: clip.id,\n imageBitmap,\n instructions,\n },\n { transfer: [imageBitmap] }\n );\n\n // ExportWorker's startImageFrameStream sends encoded stream back;\n // the receiveStream handler resolves clipDone when stream ends\n await clipDone;\n }\n\n private buildClipInstructions(clip: Clip, planner: CompositionPlanner): ClipInstructionSet {\n const plan = planner.buildClipPlan(clip, { cache: false });\n return plan.instructions;\n }\n\n private async loadAndTransferAttachments(\n exportWorker: BaseWorker,\n clip: Clip,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n resourceLoader: ResourceLoader\n ): Promise<void> {\n const attachmentResources = this.extractAttachmentImageResources(instructions);\n if (attachmentResources.length === 0) return;\n\n await Promise.all(\n attachmentResources.map(async (resourceId) => {\n const resource = model.getResource(resourceId);\n if (!resource) return;\n\n const imageBitmap = await resourceLoader.loadImage(resource);\n if (!imageBitmap) return;\n\n await exportWorker.send(\n 'receive_image',\n { clipId: clip.id, resourceId, sessionId: clip.id, imageBitmap },\n { transfer: [imageBitmap] }\n );\n })\n );\n }\n\n private extractAttachmentImageResources(instructions: ClipInstructionSet): string[] {\n const resourceIds = new Set<string>();\n for (const layer of instructions.layers) {\n if (!layer.payload.attachmentId) continue;\n if (layer.type === 'image') {\n const payload = layer.payload;\n if (payload.oldResourceId) resourceIds.add(payload.oldResourceId);\n if (payload.resourceId) resourceIds.add(payload.resourceId);\n }\n }\n return Array.from(resourceIds);\n }\n\n private async decideAudioStrategy(input: {\n hasAudioSamples: boolean;\n requestedFormat: NonNullable<ExportOptions['format']>;\n requestedAudioCodec: NonNullable<ExportOptions['audioCodec']>;\n }): Promise<{ enableAudio: boolean; disabledReason?: string }> {\n if (!input.hasAudioSamples) {\n return { enableAudio: false };\n }\n\n if (input.requestedFormat !== 'mp4') {\n return {\n enableAudio: false,\n disabledReason: `Audio skipped: format ${input.requestedFormat} is not supported.`,\n };\n }\n\n if (input.requestedAudioCodec !== 'aac') {\n return {\n enableAudio: false,\n disabledReason: `Audio skipped: mp4 output currently supports only AAC (requested ${input.requestedAudioCodec}).`,\n };\n }\n\n if (typeof AudioEncoder === 'undefined') {\n return {\n enableAudio: false,\n disabledReason:\n 'Audio skipped: WebCodecs AudioEncoder is not available in this browser/runtime (AAC required for mp4).',\n };\n }\n\n const support = await AudioEncoder.isConfigSupported(AudioChunkEncoder.DEFAULT_CONFIG);\n if (!support.supported) {\n return {\n enableAudio: false,\n disabledReason:\n 'Audio skipped: WebCodecs AAC (mp4a.40.2) AudioEncoder is not supported in this browser/runtime.',\n };\n }\n\n return { enableAudio: true };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -373,31 +373,31 @@ class Orchestrator {
|
|
|
373
373
|
// highWaterMark: config.demux.backpressure.highWaterMark,
|
|
374
374
|
// },
|
|
375
375
|
// videoDecode: config.decode.video, // DEPRECATED: Removed - replaced by VideoWindowDecodeSession
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
376
|
+
videoExport: {
|
|
377
|
+
compose: {
|
|
378
|
+
width: defaultCanvasWidth,
|
|
379
|
+
height: defaultCanvasHeight,
|
|
380
|
+
fps: targetFps,
|
|
381
|
+
backgroundColor: "#000000",
|
|
382
|
+
enableSmoothing: true,
|
|
383
|
+
enableHardwareAcceleration: true,
|
|
384
|
+
fonts: config.global.fonts
|
|
385
|
+
},
|
|
386
|
+
encode: {
|
|
387
|
+
codec: "avc1.4D0029",
|
|
388
|
+
width: defaultCanvasWidth,
|
|
389
|
+
height: defaultCanvasHeight,
|
|
390
|
+
bitrate: config.encode.video.bitrateKbps ? config.encode.video.bitrateKbps * 1e3 : this.calculateDefaultBitrate(defaultCanvasWidth, defaultCanvasHeight),
|
|
391
|
+
framerate: targetFps,
|
|
392
|
+
latencyMode: "quality",
|
|
393
|
+
bitrateMode: "variable",
|
|
394
|
+
hardwareAcceleration: "no-preference",
|
|
395
|
+
keyFrameInterval: config.encode.video.keyIntervalS ? Math.round(config.encode.video.keyIntervalS * targetFps) : targetFps,
|
|
396
|
+
...config.encode.video
|
|
397
|
+
}
|
|
384
398
|
},
|
|
385
399
|
audioCompose: {
|
|
386
400
|
enableDucking: config.compose.audio.enableDucking
|
|
387
|
-
},
|
|
388
|
-
videoEncode: {
|
|
389
|
-
// Main Profile Level 4.1 - better compression efficiency for social media
|
|
390
|
-
codec: "avc1.4D0029",
|
|
391
|
-
width: defaultCanvasWidth,
|
|
392
|
-
height: defaultCanvasHeight,
|
|
393
|
-
bitrate: config.encode.video.bitrateKbps ? config.encode.video.bitrateKbps * 1e3 : this.calculateDefaultBitrate(defaultCanvasWidth, defaultCanvasHeight),
|
|
394
|
-
framerate: targetFps,
|
|
395
|
-
latencyMode: "quality",
|
|
396
|
-
bitrateMode: "variable",
|
|
397
|
-
hardwareAcceleration: "no-preference",
|
|
398
|
-
// Default 1 second keyframe interval for better social media compatibility
|
|
399
|
-
keyFrameInterval: config.encode.video.keyIntervalS ? Math.round(config.encode.video.keyIntervalS * targetFps) : targetFps,
|
|
400
|
-
...config.encode.video
|
|
401
401
|
}
|
|
402
402
|
};
|
|
403
403
|
}
|