@meframe/core 0.3.6 → 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.
Files changed (45) hide show
  1. package/dist/orchestrator/ExportScheduler.d.ts +9 -7
  2. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
  3. package/dist/orchestrator/ExportScheduler.js +182 -80
  4. package/dist/orchestrator/ExportScheduler.js.map +1 -1
  5. package/dist/orchestrator/Orchestrator.js +22 -22
  6. package/dist/orchestrator/Orchestrator.js.map +1 -1
  7. package/dist/orchestrator/VideoWindowDecodeSession.d.ts.map +1 -1
  8. package/dist/orchestrator/VideoWindowDecodeSession.js +15 -3
  9. package/dist/orchestrator/VideoWindowDecodeSession.js.map +1 -1
  10. package/dist/stages/compose/VideoComposer.d.ts +2 -0
  11. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  12. package/dist/stages/compose/VideoComposer.js +41 -2
  13. package/dist/stages/compose/VideoComposer.js.map +1 -1
  14. package/dist/stages/decode/video-decoder.d.ts.map +1 -1
  15. package/dist/stages/decode/video-decoder.js +5 -3
  16. package/dist/stages/decode/video-decoder.js.map +1 -1
  17. package/dist/utils/time-utils.d.ts +15 -0
  18. package/dist/utils/time-utils.d.ts.map +1 -1
  19. package/dist/utils/time-utils.js +33 -0
  20. package/dist/utils/time-utils.js.map +1 -0
  21. package/dist/worker/WorkerChannel.d.ts.map +1 -1
  22. package/dist/worker/WorkerChannel.js +3 -15
  23. package/dist/worker/WorkerChannel.js.map +1 -1
  24. package/dist/worker/WorkerPool.d.ts.map +1 -1
  25. package/dist/worker/WorkerPool.js +4 -12
  26. package/dist/worker/WorkerPool.js.map +1 -1
  27. package/dist/worker/types.d.ts +1 -1
  28. package/dist/worker/types.d.ts.map +1 -1
  29. package/dist/worker/types.js.map +1 -1
  30. package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
  31. package/dist/workers/stages/{compose/video-compose.worker.KMZjuJuY.js → export/export.worker.BYttrqTQ.js} +872 -217
  32. package/dist/workers/stages/export/export.worker.BYttrqTQ.js.map +1 -0
  33. package/dist/workers/worker-manifest.json +1 -3
  34. package/package.json +1 -1
  35. package/dist/orchestrator/VideoClipSession.d.ts +0 -80
  36. package/dist/orchestrator/VideoClipSession.d.ts.map +0 -1
  37. package/dist/orchestrator/VideoClipSession.js +0 -361
  38. package/dist/orchestrator/VideoClipSession.js.map +0 -1
  39. package/dist/workers/WorkerChannel.DQK8rAab.js +0 -528
  40. package/dist/workers/WorkerChannel.DQK8rAab.js.map +0 -1
  41. package/dist/workers/stages/compose/audio-compose.worker.B4Io5w9i.js +0 -1063
  42. package/dist/workers/stages/compose/audio-compose.worker.B4Io5w9i.js.map +0 -1
  43. package/dist/workers/stages/compose/video-compose.worker.KMZjuJuY.js.map +0 -1
  44. package/dist/workers/stages/encode/video-encode.worker.D6aB_rF9.js +0 -334
  45. package/dist/workers/stages/encode/video-encode.worker.D6aB_rF9.js.map +0 -1
@@ -1,4 +1,513 @@
1
- import { W as WorkerChannel, a as WorkerMessageType, b as WorkerState } from "../../WorkerChannel.DQK8rAab.js";
1
+ var WorkerMessageType = /* @__PURE__ */ ((WorkerMessageType2) => {
2
+ WorkerMessageType2["Ready"] = "ready";
3
+ WorkerMessageType2["Error"] = "error";
4
+ WorkerMessageType2["Dispose"] = "dispose";
5
+ WorkerMessageType2["Configure"] = "configure";
6
+ WorkerMessageType2["LoadResource"] = "load_resource";
7
+ WorkerMessageType2["ResourceLoaded"] = "resource_loaded";
8
+ WorkerMessageType2["ResourceProgress"] = "resource_progress";
9
+ WorkerMessageType2["ConfigureDemux"] = "configure_demux";
10
+ WorkerMessageType2["AppendBuffer"] = "append_buffer";
11
+ WorkerMessageType2["DemuxSamples"] = "demux_samples";
12
+ WorkerMessageType2["FlushDemux"] = "flush_demux";
13
+ WorkerMessageType2["ConfigureDecode"] = "configure_decode";
14
+ WorkerMessageType2["DecodeChunk"] = "decode_chunk";
15
+ WorkerMessageType2["DecodedFrame"] = "decoded_frame";
16
+ WorkerMessageType2["SeekGop"] = "seek_gop";
17
+ WorkerMessageType2["SetComposition"] = "set_composition";
18
+ WorkerMessageType2["ApplyPatch"] = "apply_patch";
19
+ WorkerMessageType2["RenderFrame"] = "render_frame";
20
+ WorkerMessageType2["ComposeFrameReady"] = "compose_frame_ready";
21
+ WorkerMessageType2["ConfigureEncode"] = "configure_encode";
22
+ WorkerMessageType2["EncodeFrame"] = "encode_frame";
23
+ WorkerMessageType2["EncodeAudio"] = "encode_audio";
24
+ WorkerMessageType2["EncodedChunk"] = "encoded_chunk";
25
+ WorkerMessageType2["FlushEncode"] = "flush_encode";
26
+ WorkerMessageType2["AddChunk"] = "add_chunk";
27
+ WorkerMessageType2["PerformanceStats"] = "performance_stats";
28
+ WorkerMessageType2["RenderWindow"] = "renderWindow";
29
+ WorkerMessageType2["AudioTrackAdd"] = "audio_track:add";
30
+ WorkerMessageType2["AudioTrackRemove"] = "audio_track:remove";
31
+ WorkerMessageType2["AudioTrackUpdate"] = "audio_track:update";
32
+ return WorkerMessageType2;
33
+ })(WorkerMessageType || {});
34
+ var WorkerState = /* @__PURE__ */ ((WorkerState2) => {
35
+ WorkerState2["Idle"] = "idle";
36
+ WorkerState2["Initializing"] = "initializing";
37
+ WorkerState2["Ready"] = "ready";
38
+ WorkerState2["Processing"] = "processing";
39
+ WorkerState2["Error"] = "error";
40
+ WorkerState2["Disposed"] = "disposed";
41
+ return WorkerState2;
42
+ })(WorkerState || {});
43
+ const defaultRetryConfig = {
44
+ maxRetries: 3,
45
+ initialDelay: 100,
46
+ maxDelay: 5e3,
47
+ backoffFactor: 2,
48
+ retryableErrors: ["TIMEOUT", "NETWORK_ERROR", "WORKER_BUSY"]
49
+ };
50
+ function calculateRetryDelay(attempt, config) {
51
+ const { initialDelay = 100, maxDelay = 5e3, backoffFactor = 2 } = config;
52
+ const delay = initialDelay * Math.pow(backoffFactor, attempt - 1);
53
+ return Math.min(delay, maxDelay);
54
+ }
55
+ function isRetryableError(error, config) {
56
+ const { retryableErrors = defaultRetryConfig.retryableErrors } = config;
57
+ if (!error) return false;
58
+ const errorCode = error.code || error.name;
59
+ if (errorCode && retryableErrors.includes(errorCode)) {
60
+ return true;
61
+ }
62
+ const message = error.message || "";
63
+ if (message.includes("timeout") || message.includes("Timeout")) {
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ async function withRetry(fn, config) {
69
+ const { maxRetries } = config;
70
+ let lastError;
71
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
72
+ try {
73
+ return await fn();
74
+ } catch (error) {
75
+ lastError = error;
76
+ if (!isRetryableError(error, config)) {
77
+ throw error;
78
+ }
79
+ if (attempt === maxRetries) {
80
+ throw error;
81
+ }
82
+ const delay = calculateRetryDelay(attempt, config);
83
+ await sleep(delay);
84
+ }
85
+ }
86
+ throw lastError || new Error("Retry failed");
87
+ }
88
+ function sleep(ms) {
89
+ return new Promise((resolve) => setTimeout(resolve, ms));
90
+ }
91
+ function isTransferable(obj) {
92
+ return obj instanceof ArrayBuffer || obj instanceof MessagePort || typeof ImageBitmap !== "undefined" && obj instanceof ImageBitmap || typeof OffscreenCanvas !== "undefined" && obj instanceof OffscreenCanvas || typeof ReadableStream !== "undefined" && obj instanceof ReadableStream || typeof WritableStream !== "undefined" && obj instanceof WritableStream || typeof TransformStream !== "undefined" && obj instanceof TransformStream;
93
+ }
94
+ function findTransferables(obj, transferables) {
95
+ if (!obj || typeof obj !== "object") {
96
+ return;
97
+ }
98
+ if (isTransferable(obj)) {
99
+ transferables.push(obj);
100
+ return;
101
+ }
102
+ if (obj instanceof VideoFrame) {
103
+ transferables.push(obj);
104
+ return;
105
+ }
106
+ if (typeof AudioData !== "undefined" && obj instanceof AudioData) {
107
+ transferables.push(obj);
108
+ return;
109
+ }
110
+ if (typeof EncodedVideoChunk !== "undefined" && obj instanceof EncodedVideoChunk || typeof EncodedAudioChunk !== "undefined" && obj instanceof EncodedAudioChunk) {
111
+ return;
112
+ }
113
+ if (Array.isArray(obj)) {
114
+ for (const item of obj) {
115
+ findTransferables(item, transferables);
116
+ }
117
+ } else {
118
+ for (const key in obj) {
119
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
120
+ findTransferables(obj[key], transferables);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ function extractTransferables(payload) {
126
+ const transferables = [];
127
+ findTransferables(payload, transferables);
128
+ return transferables;
129
+ }
130
+ function encodedVideoChunkToTransferable(chunk) {
131
+ const data = new ArrayBuffer(chunk.byteLength);
132
+ chunk.copyTo(new Uint8Array(data));
133
+ return {
134
+ data,
135
+ type: chunk.type,
136
+ timestamp: chunk.timestamp,
137
+ duration: chunk.duration
138
+ };
139
+ }
140
+ function encodedAudioChunkToTransferable(chunk) {
141
+ const data = new ArrayBuffer(chunk.byteLength);
142
+ chunk.copyTo(new Uint8Array(data));
143
+ return {
144
+ data,
145
+ type: chunk.type,
146
+ timestamp: chunk.timestamp,
147
+ duration: chunk.duration
148
+ };
149
+ }
150
+ class WorkerChannel {
151
+ name;
152
+ port;
153
+ pendingRequests = /* @__PURE__ */ new Map();
154
+ messageHandlers = {};
155
+ state = WorkerState.Idle;
156
+ defaultTimeout;
157
+ defaultMaxRetries;
158
+ constructor(port, config) {
159
+ this.name = config.name;
160
+ this.port = port;
161
+ this.defaultTimeout = config.timeout ?? 3e4;
162
+ this.defaultMaxRetries = config.maxRetries ?? 3;
163
+ this.setupMessageHandler();
164
+ this.state = WorkerState.Ready;
165
+ }
166
+ /**
167
+ * Send a message and wait for response with retry support
168
+ */
169
+ async send(type, payload, options) {
170
+ const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
171
+ const retryConfig = {
172
+ ...defaultRetryConfig,
173
+ maxRetries,
174
+ ...options?.retryConfig
175
+ };
176
+ return withRetry(() => this.sendOnce(type, payload, options), retryConfig);
177
+ }
178
+ /**
179
+ * Send a message once (without retry)
180
+ */
181
+ async sendOnce(type, payload, options) {
182
+ const id = this.generateMessageId();
183
+ const timeout = options?.timeout ?? this.defaultTimeout;
184
+ const message = {
185
+ type,
186
+ id,
187
+ payload,
188
+ timestamp: Date.now()
189
+ };
190
+ return new Promise((resolve, reject) => {
191
+ const request = {
192
+ id,
193
+ type,
194
+ timestamp: Date.now(),
195
+ timeout,
196
+ resolve,
197
+ reject
198
+ };
199
+ this.pendingRequests.set(id, request);
200
+ const timeoutId = setTimeout(() => {
201
+ const pending = this.pendingRequests.get(id);
202
+ if (pending) {
203
+ this.pendingRequests.delete(id);
204
+ const error = new Error(`Request timeout: ${id} ${type} (${timeout}ms)`);
205
+ error.code = "TIMEOUT";
206
+ pending.reject(error);
207
+ }
208
+ }, timeout);
209
+ request.timeoutId = timeoutId;
210
+ if (options?.transfer) {
211
+ this.port.postMessage(message, options.transfer);
212
+ } else {
213
+ this.port.postMessage(message);
214
+ }
215
+ });
216
+ }
217
+ /**
218
+ * Send a message without waiting for response
219
+ */
220
+ post(type, payload, transfer) {
221
+ const message = {
222
+ type,
223
+ id: this.generateMessageId(),
224
+ payload,
225
+ timestamp: Date.now()
226
+ };
227
+ if (transfer) {
228
+ this.port.postMessage(message, transfer);
229
+ } else {
230
+ this.port.postMessage(message);
231
+ }
232
+ }
233
+ /**
234
+ * Register a message handler
235
+ */
236
+ on(type, handler) {
237
+ this.messageHandlers[type] = handler;
238
+ }
239
+ /**
240
+ * Unregister a message handler
241
+ */
242
+ off(type) {
243
+ delete this.messageHandlers[type];
244
+ }
245
+ /**
246
+ * Dispose the channel
247
+ */
248
+ dispose() {
249
+ this.state = WorkerState.Disposed;
250
+ for (const [, request] of this.pendingRequests) {
251
+ if (request.timeoutId) {
252
+ clearTimeout(request.timeoutId);
253
+ }
254
+ request.reject(new Error("Channel disposed"));
255
+ }
256
+ this.pendingRequests.clear();
257
+ this.port.onmessage = null;
258
+ }
259
+ /**
260
+ * Setup message handler for incoming messages
261
+ */
262
+ setupMessageHandler() {
263
+ this.port.onmessage = async (event) => {
264
+ const data = event.data;
265
+ if (this.isResponse(data)) {
266
+ this.handleResponse(data);
267
+ return;
268
+ }
269
+ if (this.isRequest(data)) {
270
+ await this.handleRequest(data);
271
+ return;
272
+ }
273
+ };
274
+ }
275
+ /**
276
+ * Handle incoming request
277
+ */
278
+ async handleRequest(message) {
279
+ const handler = this.messageHandlers[message.type];
280
+ if (!handler) {
281
+ this.sendResponse(message.id, false, null, {
282
+ code: "NO_HANDLER",
283
+ message: `No handler registered for message type: ${message.type}`
284
+ });
285
+ return;
286
+ }
287
+ this.state = WorkerState.Processing;
288
+ Promise.resolve().then(() => handler(message.payload, message.transfer)).then((result) => {
289
+ this.sendResponse(message.id, true, result);
290
+ this.state = WorkerState.Ready;
291
+ }).catch((error) => {
292
+ const workerError = {
293
+ code: "HANDLER_ERROR",
294
+ message: error instanceof Error ? error.message : String(error),
295
+ stack: error instanceof Error ? error.stack : void 0
296
+ };
297
+ this.sendResponse(message.id, false, null, workerError);
298
+ this.state = WorkerState.Ready;
299
+ });
300
+ }
301
+ /**
302
+ * Handle incoming response
303
+ */
304
+ handleResponse(response) {
305
+ const request = this.pendingRequests.get(response.id);
306
+ if (!request) {
307
+ return;
308
+ }
309
+ this.pendingRequests.delete(response.id);
310
+ if (request.timeoutId) {
311
+ clearTimeout(request.timeoutId);
312
+ }
313
+ if (response.success) {
314
+ request.resolve(response.result);
315
+ } else {
316
+ const error = new Error(response.error?.message || "Unknown error");
317
+ if (response.error) {
318
+ Object.assign(error, response.error);
319
+ }
320
+ request.reject(error);
321
+ }
322
+ }
323
+ /**
324
+ * Send a response message
325
+ */
326
+ sendResponse(id, success, result, error) {
327
+ let transfer = [];
328
+ if (isTransferable(result)) {
329
+ transfer.push(result);
330
+ }
331
+ const response = {
332
+ id,
333
+ success,
334
+ result,
335
+ error,
336
+ timestamp: Date.now()
337
+ };
338
+ this.port.postMessage(response, transfer);
339
+ }
340
+ /**
341
+ * Check if message is a response
342
+ */
343
+ isResponse(data) {
344
+ return data && typeof data === "object" && "id" in data && "success" in data && !("type" in data);
345
+ }
346
+ /**
347
+ * Check if message is a request
348
+ */
349
+ isRequest(data) {
350
+ return data && typeof data === "object" && "id" in data && "type" in data;
351
+ }
352
+ /**
353
+ * Generate unique message ID
354
+ */
355
+ generateMessageId() {
356
+ return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
357
+ }
358
+ /**
359
+ * Send a notification message without waiting for response
360
+ * Alias for post() method for compatibility
361
+ */
362
+ notify(type, payload, transfer) {
363
+ this.post(type, payload, transfer);
364
+ }
365
+ /**
366
+ * Register a message handler
367
+ * Alias for on() method for compatibility
368
+ */
369
+ registerHandler(type, handler) {
370
+ this.on(type, handler);
371
+ }
372
+ /**
373
+ * Send a ReadableStream to another worker
374
+ * Automatically handles transferable streams vs chunk-by-chunk fallback
375
+ */
376
+ async sendStream(stream, metadata) {
377
+ const streamId = metadata?.streamId || this.generateMessageId();
378
+ const forceChunkTransfer = metadata?.forceChunkTransfer === true || metadata?.streamType === "video" || metadata?.streamType === "audio";
379
+ if (!forceChunkTransfer && isTransferable(stream)) {
380
+ this.port.postMessage(
381
+ {
382
+ type: "stream_transfer",
383
+ ...metadata,
384
+ stream,
385
+ streamId
386
+ },
387
+ [stream]
388
+ // Transfer ownership
389
+ );
390
+ } else {
391
+ await this.streamChunks(stream, streamId, metadata);
392
+ }
393
+ }
394
+ /**
395
+ * Stream chunks from a ReadableStream (fallback when transfer is not supported)
396
+ */
397
+ async streamChunks(stream, streamId, metadata) {
398
+ const reader = stream.getReader();
399
+ this.post("stream_start", {
400
+ streamId,
401
+ ...metadata,
402
+ mode: "chunk_transfer"
403
+ });
404
+ try {
405
+ while (true) {
406
+ const { done, value } = await reader.read();
407
+ if (done) {
408
+ this.post("stream_end", { streamId });
409
+ break;
410
+ }
411
+ const transfer = [];
412
+ let chunkValue = value;
413
+ if (value instanceof ArrayBuffer) {
414
+ transfer.push(value);
415
+ } else if (value instanceof Uint8Array) {
416
+ transfer.push(value.buffer);
417
+ } else if (typeof AudioData !== "undefined" && value instanceof AudioData) {
418
+ transfer.push(value);
419
+ } else if (typeof VideoFrame !== "undefined" && value instanceof VideoFrame) {
420
+ transfer.push(value);
421
+ } else if (typeof EncodedVideoChunk !== "undefined" && value instanceof EncodedVideoChunk) {
422
+ const serialized = encodedVideoChunkToTransferable(value);
423
+ transfer.push(serialized.data);
424
+ chunkValue = serialized;
425
+ } else if (typeof EncodedAudioChunk !== "undefined" && value instanceof EncodedAudioChunk) {
426
+ const serialized = encodedAudioChunkToTransferable(value);
427
+ transfer.push(serialized.data);
428
+ chunkValue = serialized;
429
+ } else if (typeof value === "object" && value !== null) {
430
+ const extracted = extractTransferables(value);
431
+ transfer.push(...extracted);
432
+ }
433
+ this.post("stream_chunk", { streamId, chunk: chunkValue }, transfer);
434
+ for (const t of transfer) {
435
+ if (typeof VideoFrame !== "undefined" && t instanceof VideoFrame) {
436
+ try {
437
+ t.close();
438
+ } catch {
439
+ }
440
+ continue;
441
+ }
442
+ if (typeof AudioData !== "undefined" && t instanceof AudioData) {
443
+ try {
444
+ t.close();
445
+ } catch {
446
+ }
447
+ }
448
+ }
449
+ }
450
+ } catch (error) {
451
+ this.post("stream_error", {
452
+ streamId,
453
+ error: error instanceof Error ? error.message : String(error)
454
+ });
455
+ throw error;
456
+ } finally {
457
+ reader.releaseLock();
458
+ }
459
+ }
460
+ /**
461
+ * Receive a stream from another worker
462
+ * Handles both transferable streams and chunk-by-chunk reconstruction
463
+ */
464
+ async receiveStream(onStream) {
465
+ const chunkedStreams = /* @__PURE__ */ new Map();
466
+ const prev = this.port.onmessage;
467
+ const handler = (event) => {
468
+ const raw = event.data;
469
+ const envelopeType = raw?.type;
470
+ const hasPayload = raw && typeof raw === "object" && "payload" in raw;
471
+ const payload = hasPayload ? raw.payload : raw;
472
+ if (envelopeType === "stream_transfer" && payload?.stream) {
473
+ onStream(payload.stream, payload);
474
+ return;
475
+ }
476
+ if (envelopeType === "stream_start" && payload?.streamId) {
477
+ const stream = new ReadableStream({
478
+ start(controller) {
479
+ chunkedStreams.set(payload.streamId, { controller, metadata: payload });
480
+ }
481
+ });
482
+ onStream(stream, payload);
483
+ return;
484
+ }
485
+ if (envelopeType === "stream_chunk" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
486
+ const s = chunkedStreams.get(payload.streamId);
487
+ if (s) s.controller.enqueue(payload.chunk);
488
+ return;
489
+ }
490
+ if (envelopeType === "stream_end" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
491
+ const s = chunkedStreams.get(payload.streamId);
492
+ if (s) {
493
+ s.controller.close();
494
+ chunkedStreams.delete(payload.streamId);
495
+ }
496
+ return;
497
+ }
498
+ if (envelopeType === "stream_error" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
499
+ const s = chunkedStreams.get(payload.streamId);
500
+ if (s) {
501
+ s.controller.error(new Error(String(payload.error || "stream error")));
502
+ chunkedStreams.delete(payload.streamId);
503
+ }
504
+ return;
505
+ }
506
+ if (typeof prev === "function") prev.call(this.port, event);
507
+ };
508
+ this.port.onmessage = handler;
509
+ }
510
+ }
2
511
  function measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight = 400) {
3
512
  ctx.save();
4
513
  ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
@@ -1515,6 +2024,7 @@ class VideoComposer {
1515
2024
  // Cached frame duration
1516
2025
  fontsLoadingPromise = null;
1517
2026
  loadedFonts = /* @__PURE__ */ new Set();
2027
+ static FONT_LOAD_TIMEOUT_MS = 1e4;
1518
2028
  constructor(config) {
1519
2029
  this.config = this.applyDefaults(config);
1520
2030
  this.frameDurationUs = Math.round(1e6 / this.config.fps);
@@ -1602,7 +2112,10 @@ class VideoComposer {
1602
2112
  }
1603
2113
  async composeFrame(request) {
1604
2114
  if (this.fontsLoadingPromise) {
1605
- await this.fontsLoadingPromise;
2115
+ try {
2116
+ await this.withTimeout(this.fontsLoadingPromise, VideoComposer.FONT_LOAD_TIMEOUT_MS);
2117
+ } catch {
2118
+ }
1606
2119
  this.fontsLoadingPromise = null;
1607
2120
  }
1608
2121
  if (request.layers.length > this.config.maxLayers) {
@@ -1702,7 +2215,10 @@ class VideoComposer {
1702
2215
  }
1703
2216
  try {
1704
2217
  const fontFace = new FontFace(font.family, `url(${font.url})`);
1705
- const loadedFont = await fontFace.load();
2218
+ const loadedFont = await this.withTimeout(
2219
+ fontFace.load(),
2220
+ VideoComposer.FONT_LOAD_TIMEOUT_MS
2221
+ );
1706
2222
  if ("fonts" in self) {
1707
2223
  self.fonts.add(loadedFont);
1708
2224
  }
@@ -1711,6 +2227,38 @@ class VideoComposer {
1711
2227
  }
1712
2228
  }
1713
2229
  }
2230
+ async withTimeout(promise, timeoutMs) {
2231
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
2232
+ return promise;
2233
+ }
2234
+ let timeoutId = null;
2235
+ try {
2236
+ const safe = promise.then(
2237
+ (value) => ({ ok: true, value }),
2238
+ (error) => ({ ok: false, error })
2239
+ );
2240
+ const result = await Promise.race([
2241
+ safe,
2242
+ new Promise((_, reject) => {
2243
+ timeoutId = setTimeout(
2244
+ () => reject(new Error(`Timeout after ${timeoutMs}ms`)),
2245
+ timeoutMs
2246
+ );
2247
+ })
2248
+ ]);
2249
+ if (result?.ok === true) {
2250
+ return result.value;
2251
+ }
2252
+ if (result?.ok === false) {
2253
+ throw result.error;
2254
+ }
2255
+ return result;
2256
+ } finally {
2257
+ if (timeoutId !== null) {
2258
+ clearTimeout(timeoutId);
2259
+ }
2260
+ }
2261
+ }
1714
2262
  updateConfig(config) {
1715
2263
  Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
1716
2264
  if (config.fps !== void 0) {
@@ -1733,6 +2281,229 @@ class VideoComposer {
1733
2281
  this.filterProcessor.clearCache();
1734
2282
  }
1735
2283
  }
2284
+ class BaseEncoder {
2285
+ encoder;
2286
+ config;
2287
+ controller = null;
2288
+ constructor(config) {
2289
+ this.config = config;
2290
+ }
2291
+ getConfig() {
2292
+ return { ...this.config };
2293
+ }
2294
+ get currentConfig() {
2295
+ return this.config;
2296
+ }
2297
+ shouldReconfigure(partial) {
2298
+ const next = { ...this.config, ...partial };
2299
+ const keys = Object.keys(partial ?? {});
2300
+ for (const key of keys) {
2301
+ if (partial[key] !== void 0 && next[key] !== this.config[key]) {
2302
+ return true;
2303
+ }
2304
+ }
2305
+ return false;
2306
+ }
2307
+ hasConfigChanged(next) {
2308
+ const currentEntries = Object.entries(this.config);
2309
+ for (const [key, value] of currentEntries) {
2310
+ if (next[key] !== value) {
2311
+ return true;
2312
+ }
2313
+ }
2314
+ for (const key of Object.keys(next)) {
2315
+ if (this.config[key] !== next[key]) {
2316
+ return true;
2317
+ }
2318
+ }
2319
+ return false;
2320
+ }
2321
+ configsEqual(a, b) {
2322
+ return JSON.stringify(a) === JSON.stringify(b);
2323
+ }
2324
+ async initialize() {
2325
+ if (this.encoder?.state === "configured") {
2326
+ return;
2327
+ }
2328
+ const isSupported = await this.isConfigSupported(this.config);
2329
+ if (!isSupported.supported) {
2330
+ throw new Error(`Codec not supported: ${JSON.stringify(this.config)}`);
2331
+ }
2332
+ this.encoder = this.createEncoder({
2333
+ output: this.handleOutput.bind(this),
2334
+ error: this.handleError.bind(this)
2335
+ });
2336
+ this.encoder.configure(this.config);
2337
+ }
2338
+ async reconfigure(config) {
2339
+ if (!config || Object.keys(config).length === 0) {
2340
+ return;
2341
+ }
2342
+ const nextConfig = { ...this.config, ...config };
2343
+ if (this.configsEqual(this.config, nextConfig)) {
2344
+ return;
2345
+ }
2346
+ if (!this.encoder) {
2347
+ this.config = nextConfig;
2348
+ await this.initialize();
2349
+ return;
2350
+ }
2351
+ if (this.encoder.state === "configured") {
2352
+ await this.encoder.flush();
2353
+ }
2354
+ const isSupported = await this.isConfigSupported(nextConfig);
2355
+ if (!isSupported.supported) {
2356
+ throw new Error(`New configuration not supported: ${nextConfig.codec}`);
2357
+ }
2358
+ this.config = nextConfig;
2359
+ this.encoder.configure(this.config);
2360
+ }
2361
+ async flush() {
2362
+ if (!this.encoder) {
2363
+ return;
2364
+ }
2365
+ await this.encoder.flush();
2366
+ }
2367
+ async reset() {
2368
+ if (!this.encoder) {
2369
+ return;
2370
+ }
2371
+ this.encoder.reset();
2372
+ this.onReset();
2373
+ }
2374
+ async close() {
2375
+ if (!this.encoder) {
2376
+ return;
2377
+ }
2378
+ if (this.encoder.state === "configured") {
2379
+ await this.encoder.flush();
2380
+ }
2381
+ this.encoder.close();
2382
+ this.encoder = void 0;
2383
+ }
2384
+ get isReady() {
2385
+ return this.encoder?.state === "configured";
2386
+ }
2387
+ get queueSize() {
2388
+ return this.encoder?.encodeQueueSize ?? 0;
2389
+ }
2390
+ handleOutput(chunk, metadata) {
2391
+ if (this.controller) {
2392
+ try {
2393
+ this.controller.enqueue({ chunk, metadata });
2394
+ } catch (error) {
2395
+ if (!(error instanceof TypeError && error.message.includes("closed"))) {
2396
+ throw error;
2397
+ }
2398
+ }
2399
+ }
2400
+ }
2401
+ handleError(error) {
2402
+ console.error(`[${this.getEncoderType()}Encoder] Encode error:`, {
2403
+ name: error.name,
2404
+ message: error.message,
2405
+ encoderState: this.encoder?.state,
2406
+ queueSize: this.queueSize,
2407
+ platform: typeof navigator !== "undefined" ? navigator.platform : "unknown"
2408
+ });
2409
+ this.controller?.error(error);
2410
+ }
2411
+ // Hook for subclasses to handle reset
2412
+ onReset() {
2413
+ }
2414
+ /**
2415
+ * Create transform stream for encoding
2416
+ * Implements common stream logic with backpressure handling
2417
+ */
2418
+ createStream() {
2419
+ return new TransformStream(
2420
+ {
2421
+ start: async (controller) => {
2422
+ this.controller = controller;
2423
+ if (!this.isReady) {
2424
+ await this.initialize();
2425
+ }
2426
+ },
2427
+ transform: async (input) => {
2428
+ if (!this.encoder || this.encoder.state !== "configured") {
2429
+ throw new Error("Encoder not configured");
2430
+ }
2431
+ if (this.encoder.encodeQueueSize >= this.encodeQueueThreshold) {
2432
+ await new Promise((resolve) => {
2433
+ const check = () => {
2434
+ if (!this.encoder || this.encoder.encodeQueueSize < this.encodeQueueThreshold - 1) {
2435
+ resolve();
2436
+ } else {
2437
+ setTimeout(check, 10);
2438
+ }
2439
+ };
2440
+ check();
2441
+ });
2442
+ }
2443
+ const frame = input.frame || input;
2444
+ this.encode(frame);
2445
+ },
2446
+ flush: async () => {
2447
+ await this.flush();
2448
+ }
2449
+ },
2450
+ // Queuing strategy with backpressure configuration
2451
+ {
2452
+ highWaterMark: this.highWaterMark,
2453
+ size: () => 1
2454
+ // Count-based
2455
+ }
2456
+ );
2457
+ }
2458
+ }
2459
+ class VideoChunkEncoder extends BaseEncoder {
2460
+ static DEFAULT_HIGH_WATER_MARK = 2;
2461
+ static DEFAULT_ENCODE_QUEUE_THRESHOLD = 8;
2462
+ highWaterMark;
2463
+ encodeQueueThreshold;
2464
+ frameCount = 0;
2465
+ // Default 1 second at 30fps for better social media compatibility
2466
+ keyFrameInterval = 30;
2467
+ constructor(config) {
2468
+ super(config);
2469
+ this.highWaterMark = config.backpressure?.highWaterMark ?? VideoChunkEncoder.DEFAULT_HIGH_WATER_MARK;
2470
+ this.encodeQueueThreshold = config.backpressure?.encodeQueueThreshold ?? VideoChunkEncoder.DEFAULT_ENCODE_QUEUE_THRESHOLD;
2471
+ if (config.keyFrameInterval !== void 0) {
2472
+ this.keyFrameInterval = Math.max(1, config.keyFrameInterval);
2473
+ }
2474
+ }
2475
+ async isConfigSupported(config) {
2476
+ const result = await VideoEncoder.isConfigSupported(config);
2477
+ return { supported: result.supported ?? false };
2478
+ }
2479
+ createEncoder(init) {
2480
+ return new VideoEncoder(init);
2481
+ }
2482
+ getEncoderType() {
2483
+ return "Video";
2484
+ }
2485
+ onReset() {
2486
+ this.frameCount = 0;
2487
+ }
2488
+ encode(frame) {
2489
+ const keyFrame = this.shouldGenerateKeyFrame();
2490
+ const encodeOptions = {
2491
+ keyFrame
2492
+ };
2493
+ this.encoder.encode(frame, encodeOptions);
2494
+ this.frameCount++;
2495
+ frame.close();
2496
+ }
2497
+ setKeyFrameInterval(interval) {
2498
+ this.keyFrameInterval = Math.max(1, interval);
2499
+ }
2500
+ shouldGenerateKeyFrame() {
2501
+ if (this.frameCount === 0) {
2502
+ return true;
2503
+ }
2504
+ return this.frameCount % this.keyFrameInterval === 0;
2505
+ }
2506
+ }
1736
2507
  const MICROSECONDS_PER_SECOND = 1e6;
1737
2508
  const DEFAULT_FPS = 30;
1738
2509
  function normalizeFps(value) {
@@ -2203,217 +2974,109 @@ function computeAnimationState(animation, globalTimeUs) {
2203
2974
  visible: true
2204
2975
  };
2205
2976
  }
2206
- class VideoComposeWorker {
2977
+ class ExportWorker {
2207
2978
  channel;
2208
2979
  composer = null;
2209
- composeStream = null;
2210
- sessionId = null;
2211
- downstreamPort = null;
2212
- upstreamPort = null;
2980
+ encoder = null;
2213
2981
  instructions = null;
2214
2982
  imageMap = /* @__PURE__ */ new Map();
2215
2983
  constructor() {
2216
2984
  this.channel = new WorkerChannel(self, {
2217
- name: "VideoComposeWorker",
2218
- timeout: 3e4
2985
+ name: "ExportWorker",
2986
+ timeout: 6e4
2219
2987
  });
2220
2988
  this.setupHandlers();
2221
2989
  }
2222
2990
  setupHandlers() {
2223
2991
  this.channel.registerHandler("configure", this.handleConfigure.bind(this));
2224
- this.channel.registerHandler("connect", this.handleConnect.bind(this));
2225
- this.channel.registerHandler("flush", this.handleFlush.bind(this));
2226
- this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
2227
2992
  this.channel.registerHandler("install_instructions", this.handleInstallInstructions.bind(this));
2228
2993
  this.channel.registerHandler("receive_image", this.handleReceiveImage.bind(this));
2229
2994
  this.channel.registerHandler("dispose_clip", this.handleDisposeClip.bind(this));
2995
+ this.channel.registerHandler("flush", this.handleFlush.bind(this));
2230
2996
  this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
2231
2997
  this.channel.receiveStream(this.handleReceiveStream.bind(this));
2232
2998
  }
2233
- /**
2234
- * Unified connect handler used by stream pipeline
2235
- */
2236
- async handleConnect(payload) {
2237
- const { port, direction, sessionId } = payload;
2238
- if (sessionId && !this.sessionId) {
2239
- this.sessionId = sessionId;
2240
- }
2241
- if (direction === "upstream") {
2242
- this.upstreamPort = port;
2243
- const channel = new WorkerChannel(port, {
2244
- name: "VideoCompose-Decode",
2245
- timeout: 3e4
2246
- });
2247
- channel.receiveStream(this.handleReceiveStream.bind(this));
2248
- }
2249
- if (direction === "downstream") {
2250
- this.downstreamPort = port;
2251
- }
2252
- return { success: true };
2253
- }
2254
- /**
2255
- * Configure composer
2256
- * According to docs/impl/14-config, only reinitialize when initial=true
2257
- */
2258
2999
  async handleConfigure(payload) {
2259
- const { config, initial } = payload;
2260
- const hasValidDimensions = config.width > 0 && config.height > 0;
2261
- const hasValidFps = config.fps > 0;
2262
- if (!hasValidDimensions || !hasValidFps) {
3000
+ const { compose, encode } = payload;
3001
+ if (compose.width <= 0 || compose.height <= 0 || compose.fps <= 0) {
2263
3002
  throw new Error(
2264
- `VideoComposeWorker: invalid canvas config width=${config.width}, height=${config.height}, fps=${config.fps}`
3003
+ `ExportWorker: invalid config width=${compose.width}, height=${compose.height}, fps=${compose.fps}`
2265
3004
  );
2266
3005
  }
2267
- if (initial) {
2268
- this.channel.state = WorkerState.Ready;
2269
- }
2270
- if (initial || !this.composer) {
2271
- if (this.composer) {
2272
- this.composer.dispose();
2273
- this.composeStream = null;
2274
- }
2275
- this.composer = new VideoComposer(config);
2276
- } else {
2277
- this.composer.updateConfig(config);
3006
+ if (this.composer) {
3007
+ this.composer.dispose();
2278
3008
  }
2279
- this.channel.notify("configured", {
2280
- config: this.composer.config,
2281
- initialized: initial || false
2282
- });
2283
- return {
2284
- success: true,
2285
- config: this.composer.config
2286
- };
3009
+ this.composer = new VideoComposer(compose);
3010
+ this.encoder = new VideoChunkEncoder(encode);
3011
+ await this.encoder.initialize();
3012
+ this.channel.state = WorkerState.Ready;
3013
+ return { success: true };
2287
3014
  }
2288
3015
  async handleReceiveStream(stream, metadata) {
2289
- if (!this.composer) {
2290
- console.error("[VideoComposeWorker] Composer not configured");
3016
+ if (!this.composer || !this.encoder) {
3017
+ console.error("[ExportWorker] Not configured");
2291
3018
  return;
2292
3019
  }
2293
3020
  if (metadata?.instructions) {
2294
- await this.handleInstallInstructions(metadata.instructions);
3021
+ this.handleInstallInstructions(metadata.instructions);
2295
3022
  }
2296
3023
  if (!this.instructions) {
2297
- console.error("[VideoComposeWorker] No instructions available after metadata check");
3024
+ console.error("[ExportWorker] No instructions");
2298
3025
  return;
2299
3026
  }
2300
3027
  const mainLayer = this.instructions.layers.find(
2301
3028
  (l) => l.type === "video" && !l.payload.attachmentId
2302
3029
  );
2303
- const trimStartUs = mainLayer?.type === "video" ? mainLayer.payload.trimStartUs ?? 0 : 0;
3030
+ const clipTrimStartUs = mainLayer?.type === "video" ? mainLayer.payload.trimStartUs ?? 0 : 0;
2304
3031
  const timeline = this.instructions.baseConfig.timeline;
2305
- const fpsConverter = new FrameRateConverter(
2306
- timeline?.compositionFps ?? 30,
2307
- timeline?.clipDurationUs ?? Infinity,
2308
- trimStartUs
2309
- );
3032
+ const fps = timeline?.compositionFps ?? 30;
3033
+ const windowStartUs = metadata?.windowStartUs ?? clipTrimStartUs;
3034
+ const windowEndUs = metadata?.windowEndUs ?? clipTrimStartUs + (timeline?.clipDurationUs ?? Infinity);
3035
+ const windowDurationUs = windowEndUs - windowStartUs;
3036
+ const windowToClipOffsetUs = windowStartUs - clipTrimStartUs;
3037
+ const fpsConverter = new FrameRateConverter(fps, windowDurationUs, windowStartUs);
2310
3038
  const cfrStream = stream.pipeThrough(fpsConverter.createStream());
2311
- const filteredStream = cfrStream.pipeThrough(
3039
+ const composeRequestStream = cfrStream.pipeThrough(
2312
3040
  new TransformStream({
2313
3041
  transform: (frame, controller) => {
2314
- try {
2315
- const request = this.buildComposeRequest(this.instructions, frame);
2316
- if (!request) {
2317
- frame.close();
2318
- return;
2319
- }
2320
- controller.enqueue(request);
2321
- } catch (error) {
2322
- console.error("[VideoComposeWorker] buildComposeRequest error:", error);
2323
- frame?.close?.();
2324
- throw error;
3042
+ let composeFrame = frame;
3043
+ if (windowToClipOffsetUs > 0) {
3044
+ composeFrame = new VideoFrame(frame, {
3045
+ timestamp: (frame.timestamp ?? 0) + windowToClipOffsetUs
3046
+ });
3047
+ frame.close();
3048
+ }
3049
+ const request = this.buildComposeRequest(this.instructions, composeFrame);
3050
+ if (!request) {
3051
+ composeFrame.close();
3052
+ return;
2325
3053
  }
3054
+ controller.enqueue(request);
2326
3055
  }
2327
3056
  })
2328
3057
  );
2329
3058
  const composeStream = this.composer.createStreams();
2330
- filteredStream.pipeTo(composeStream.writable).catch((error) => {
2331
- console.error("[VideoComposeWorker] compose stream error", this.sessionId, error);
3059
+ const encodingTransform = this.encoder.createStream();
3060
+ composeRequestStream.pipeTo(composeStream.writable).catch((error) => {
3061
+ console.error("[ExportWorker] compose pipe error:", error);
3062
+ });
3063
+ const encodedStream = composeStream.readable.pipeThrough(encodingTransform);
3064
+ this.channel.sendStream(encodedStream, {
3065
+ streamType: "video"
2332
3066
  });
2333
- const meta = {
2334
- ...metadata,
2335
- streamType: "video",
2336
- sessionId: this.sessionId
2337
- };
2338
- if (this.downstreamPort) {
2339
- const encodeChannel = new WorkerChannel(this.downstreamPort, {
2340
- name: "VideoCompose-Encode",
2341
- timeout: 3e4
2342
- });
2343
- encodeChannel.sendStream(composeStream.readable, meta);
2344
- } else {
2345
- this.channel.sendStream(composeStream.readable, meta);
2346
- }
2347
- }
2348
- // private handleGetStream(): ReadableStream<VideoFrame> | undefined {
2349
- // return this.composer?.createStreams()?.cacheStream;
2350
- // }
2351
- /**
2352
- * Flush the composition pipeline
2353
- */
2354
- async handleFlush() {
2355
- try {
2356
- this.channel.notify("flush_complete", {});
2357
- return { success: true };
2358
- } catch (error) {
2359
- throw {
2360
- code: "FLUSH_ERROR",
2361
- message: error.message
2362
- };
2363
- }
2364
- }
2365
- /**
2366
- * Get composer statistics
2367
- */
2368
- async handleGetStats() {
2369
- return {
2370
- configured: this.composer !== null,
2371
- config: this.composer?.config,
2372
- streaming: this.composeStream !== null
2373
- };
2374
- }
2375
- /**
2376
- * Dispose worker and cleanup resources
2377
- */
2378
- async handleDispose() {
2379
- if (this.composer) {
2380
- this.composer.dispose();
2381
- this.composer = null;
2382
- }
2383
- this.composeStream = null;
2384
- this.downstreamPort?.close();
2385
- this.upstreamPort?.close();
2386
- this.downstreamPort = null;
2387
- this.upstreamPort = null;
2388
- this.imageMap.forEach((bitmap) => bitmap.close());
2389
- this.imageMap.clear();
2390
- this.instructions = null;
2391
- this.channel.state = WorkerState.Disposed;
2392
- return { success: true };
2393
3067
  }
2394
- async handleInstallInstructions(payload) {
2395
- const { clipId, revision } = payload;
2396
- if (!this.sessionId) {
2397
- this.sessionId = clipId;
2398
- }
3068
+ handleInstallInstructions(payload) {
3069
+ const { revision } = payload;
2399
3070
  if (this.instructions && this.instructions.revision > revision) {
2400
3071
  return { success: false };
2401
3072
  }
2402
3073
  this.instructions = payload;
2403
3074
  return { success: true };
2404
3075
  }
2405
- /**
2406
- * Receive image data with instructions (aligned with video pipeline)
2407
- * Note: ImageBitmap is required because VideoFrame constructor in Worker context
2408
- * only accepts ImageBitmap/OffscreenCanvas, not HTMLImageElement or Blob
2409
- */
2410
3076
  async handleReceiveImage(payload) {
2411
- const { resourceId, sessionId, imageBitmap, instructions } = payload;
2412
- if (!this.sessionId) {
2413
- this.sessionId = sessionId;
2414
- }
3077
+ const { resourceId, imageBitmap, instructions } = payload;
2415
3078
  if (instructions) {
2416
- await this.handleInstallInstructions(instructions);
3079
+ this.handleInstallInstructions(instructions);
2417
3080
  }
2418
3081
  const existing = this.imageMap.get(resourceId);
2419
3082
  if (existing) {
@@ -2429,33 +3092,19 @@ class VideoComposeWorker {
2429
3092
  }
2430
3093
  return { success: true };
2431
3094
  }
2432
- async handleDisposeClip() {
2433
- this.instructions = null;
2434
- this.downstreamPort?.close();
2435
- this.upstreamPort?.close();
2436
- this.downstreamPort = null;
2437
- this.upstreamPort = null;
2438
- this.imageMap.forEach((bitmap) => bitmap.close());
2439
- this.imageMap.clear();
2440
- return { success: true };
2441
- }
2442
3095
  async startImageFrameStream() {
2443
- if (!this.instructions || !this.composer) {
3096
+ if (!this.instructions || !this.composer || !this.encoder) {
2444
3097
  return;
2445
3098
  }
2446
3099
  const timeline = this.instructions.baseConfig.timeline;
2447
- if (!timeline) {
2448
- return;
2449
- }
3100
+ if (!timeline) return;
2450
3101
  const mainLayer = this.instructions.layers.find((l) => !l.payload.attachmentId);
2451
3102
  if (!mainLayer) return;
2452
3103
  const mainResourceId = mainLayer.payload.resourceId;
2453
3104
  const imageBitmap = this.imageMap.get(mainResourceId);
2454
- if (!imageBitmap) {
2455
- console.warn("[VideoComposeWorker] Main track ImageBitmap not found:", mainResourceId);
2456
- return;
2457
- }
3105
+ if (!imageBitmap) return;
2458
3106
  const composeStream = this.composer.createStreams();
3107
+ const encodingTransform = this.encoder.createStream();
2459
3108
  const { clipDurationUs, compositionFps } = timeline;
2460
3109
  let currentTimeUs = 0;
2461
3110
  const readableStream = new ReadableStream({
@@ -2468,36 +3117,49 @@ class VideoComposeWorker {
2468
3117
  timestamp: currentTimeUs,
2469
3118
  duration: frameDurationFromFps(compositionFps)
2470
3119
  });
2471
- try {
2472
- const request = this.buildComposeRequest(this.instructions, videoFrame);
2473
- if (request) {
2474
- controller.enqueue(request);
2475
- } else {
2476
- videoFrame.close();
2477
- }
2478
- currentTimeUs += frameDurationFromFps(compositionFps);
2479
- } catch (error) {
3120
+ const request = this.buildComposeRequest(this.instructions, videoFrame);
3121
+ if (request) {
3122
+ controller.enqueue(request);
3123
+ } else {
2480
3124
  videoFrame.close();
2481
- throw error;
2482
3125
  }
3126
+ currentTimeUs += frameDurationFromFps(compositionFps);
2483
3127
  }
2484
3128
  });
2485
3129
  readableStream.pipeTo(composeStream.writable).catch((error) => {
2486
- console.error("[VideoComposeWorker] image frame stream error", this.sessionId, error);
3130
+ console.error("[ExportWorker] image frame stream error:", error);
2487
3131
  });
2488
- const meta = {
2489
- streamType: "video",
2490
- sessionId: this.sessionId
2491
- };
2492
- if (this.downstreamPort) {
2493
- const encodeChannel = new WorkerChannel(this.downstreamPort, {
2494
- name: "VideoCompose-Encode",
2495
- timeout: 3e4
2496
- });
2497
- encodeChannel.sendStream(composeStream.readable, meta);
2498
- } else {
2499
- this.channel.sendStream(composeStream.readable, meta);
3132
+ const encodedStream = composeStream.readable.pipeThrough(encodingTransform);
3133
+ this.channel.sendStream(encodedStream, {
3134
+ streamType: "video"
3135
+ });
3136
+ }
3137
+ async handleDisposeClip() {
3138
+ this.instructions = null;
3139
+ this.imageMap.forEach((bitmap) => bitmap.close());
3140
+ this.imageMap.clear();
3141
+ return { success: true };
3142
+ }
3143
+ async handleFlush() {
3144
+ if (this.encoder) {
3145
+ await this.encoder.flush();
3146
+ }
3147
+ return { success: true };
3148
+ }
3149
+ async handleDispose() {
3150
+ if (this.composer) {
3151
+ this.composer.dispose();
3152
+ this.composer = null;
2500
3153
  }
3154
+ if (this.encoder) {
3155
+ await this.encoder.close();
3156
+ this.encoder = null;
3157
+ }
3158
+ this.imageMap.forEach((bitmap) => bitmap.close());
3159
+ this.imageMap.clear();
3160
+ this.instructions = null;
3161
+ this.channel.state = WorkerState.Disposed;
3162
+ return { success: true };
2501
3163
  }
2502
3164
  buildComposeRequest(instruction, frame) {
2503
3165
  const clipRelativeTime = frame.timestamp ?? 0;
@@ -2506,20 +3168,16 @@ class VideoComposeWorker {
2506
3168
  return null;
2507
3169
  }
2508
3170
  const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
2509
- if (!activeLayers.length) {
2510
- return null;
2511
- }
3171
+ if (!activeLayers.length) return null;
2512
3172
  const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
2513
3173
  const globalTimeUs = clipStartUs + clipRelativeTime;
2514
3174
  const layers = activeLayers.map((layer) => materializeLayer(layer, frame, this.imageMap, globalTimeUs)).filter((layer) => layer !== null);
2515
- if (!layers.length) {
2516
- return null;
2517
- }
3175
+ if (!layers.length) return null;
2518
3176
  return {
2519
3177
  timeUs: clipRelativeTime,
2520
3178
  globalTimeUs,
2521
3179
  layers,
2522
- transition: VideoComposeWorker.buildTransition(
3180
+ transition: ExportWorker.buildTransition(
2523
3181
  instruction.transitions,
2524
3182
  clipRelativeTime,
2525
3183
  instruction.baseConfig.timeline
@@ -2531,9 +3189,7 @@ class VideoComposeWorker {
2531
3189
  const { startUs, endUs } = transition.range;
2532
3190
  return timeUs >= startUs && timeUs < endUs;
2533
3191
  });
2534
- if (!entry) {
2535
- return void 0;
2536
- }
3192
+ if (!entry) return void 0;
2537
3193
  const durationUs = entry.range.endUs - entry.range.startUs;
2538
3194
  const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;
2539
3195
  return {
@@ -2545,13 +3201,12 @@ class VideoComposeWorker {
2545
3201
  };
2546
3202
  }
2547
3203
  }
2548
- const worker = new VideoComposeWorker();
3204
+ const worker = new ExportWorker();
2549
3205
  self.addEventListener("beforeunload", () => {
2550
3206
  worker["handleDispose"]();
2551
3207
  });
2552
- const videoCompose_worker = null;
3208
+ const export_worker = null;
2553
3209
  export {
2554
- VideoComposeWorker,
2555
- videoCompose_worker as default
3210
+ export_worker as default
2556
3211
  };
2557
- //# sourceMappingURL=video-compose.worker.KMZjuJuY.js.map
3212
+ //# sourceMappingURL=export.worker.BYttrqTQ.js.map