@meframe/core 0.3.6 → 0.3.8

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 (57) hide show
  1. package/dist/medeo-fe/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +1 -0
  2. package/dist/{node_modules → medeo-fe/node_modules}/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js +2 -2
  3. package/dist/medeo-fe/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +1 -0
  4. package/dist/orchestrator/ExportScheduler.d.ts +9 -7
  5. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
  6. package/dist/orchestrator/ExportScheduler.js +182 -80
  7. package/dist/orchestrator/ExportScheduler.js.map +1 -1
  8. package/dist/orchestrator/Orchestrator.js +22 -22
  9. package/dist/orchestrator/Orchestrator.js.map +1 -1
  10. package/dist/orchestrator/VideoWindowDecodeSession.d.ts.map +1 -1
  11. package/dist/orchestrator/VideoWindowDecodeSession.js +15 -3
  12. package/dist/orchestrator/VideoWindowDecodeSession.js.map +1 -1
  13. package/dist/stages/compose/VideoComposer.d.ts +2 -0
  14. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  15. package/dist/stages/compose/VideoComposer.js +41 -2
  16. package/dist/stages/compose/VideoComposer.js.map +1 -1
  17. package/dist/stages/decode/video-decoder.d.ts.map +1 -1
  18. package/dist/stages/decode/video-decoder.js +5 -3
  19. package/dist/stages/decode/video-decoder.js.map +1 -1
  20. package/dist/stages/encode/BaseEncoder.d.ts.map +1 -1
  21. package/dist/stages/encode/BaseEncoder.js +34 -3
  22. package/dist/stages/encode/BaseEncoder.js.map +1 -1
  23. package/dist/stages/encode/VideoChunkEncoder.d.ts.map +1 -1
  24. package/dist/stages/mux/MP4Muxer.js +1 -1
  25. package/dist/utils/mp4box.js +1 -1
  26. package/dist/utils/time-utils.d.ts +15 -0
  27. package/dist/utils/time-utils.d.ts.map +1 -1
  28. package/dist/utils/time-utils.js +33 -0
  29. package/dist/utils/time-utils.js.map +1 -0
  30. package/dist/worker/WorkerChannel.d.ts.map +1 -1
  31. package/dist/worker/WorkerChannel.js +3 -15
  32. package/dist/worker/WorkerChannel.js.map +1 -1
  33. package/dist/worker/WorkerPool.d.ts.map +1 -1
  34. package/dist/worker/WorkerPool.js +4 -12
  35. package/dist/worker/WorkerPool.js.map +1 -1
  36. package/dist/worker/types.d.ts +1 -1
  37. package/dist/worker/types.d.ts.map +1 -1
  38. package/dist/worker/types.js.map +1 -1
  39. package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
  40. package/dist/workers/stages/{compose/video-compose.worker.KMZjuJuY.js → export/export.worker.SahP9aVK.js} +915 -217
  41. package/dist/workers/stages/export/export.worker.SahP9aVK.js.map +1 -0
  42. package/dist/workers/worker-manifest.json +1 -3
  43. package/package.json +1 -1
  44. package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +0 -1
  45. package/dist/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +0 -1
  46. package/dist/orchestrator/VideoClipSession.d.ts +0 -80
  47. package/dist/orchestrator/VideoClipSession.d.ts.map +0 -1
  48. package/dist/orchestrator/VideoClipSession.js +0 -361
  49. package/dist/orchestrator/VideoClipSession.js.map +0 -1
  50. package/dist/workers/WorkerChannel.DQK8rAab.js +0 -528
  51. package/dist/workers/WorkerChannel.DQK8rAab.js.map +0 -1
  52. package/dist/workers/stages/compose/audio-compose.worker.B4Io5w9i.js +0 -1063
  53. package/dist/workers/stages/compose/audio-compose.worker.B4Io5w9i.js.map +0 -1
  54. package/dist/workers/stages/compose/video-compose.worker.KMZjuJuY.js.map +0 -1
  55. package/dist/workers/stages/encode/video-encode.worker.D6aB_rF9.js +0 -334
  56. package/dist/workers/stages/encode/video-encode.worker.D6aB_rF9.js.map +0 -1
  57. /package/dist/{node_modules → medeo-fe/node_modules}/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js +0 -0
@@ -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,272 @@ 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
+ console.info("Encoder is initializing...");
2326
+ if (this.encoder?.state === "configured") {
2327
+ console.warn("Encoder is already initialized, skipping initialization");
2328
+ return;
2329
+ }
2330
+ const isSupported = await this.isConfigSupported(this.config);
2331
+ if (!isSupported.supported) {
2332
+ console.warn("Codec not supported:", JSON.stringify(this.config));
2333
+ throw new Error(`Codec not supported: ${JSON.stringify(this.config)}`);
2334
+ }
2335
+ this.encoder = this.createEncoder({
2336
+ output: this.handleOutput.bind(this),
2337
+ error: this.handleError.bind(this)
2338
+ });
2339
+ this.encoder.configure(this.config);
2340
+ console.info("Encoder is initialized");
2341
+ }
2342
+ async reconfigure(config) {
2343
+ console.info("Encoder is reconfiguring...");
2344
+ if (!config || Object.keys(config).length === 0) {
2345
+ return;
2346
+ }
2347
+ const nextConfig = { ...this.config, ...config };
2348
+ if (this.configsEqual(this.config, nextConfig)) {
2349
+ return;
2350
+ }
2351
+ if (!this.encoder) {
2352
+ this.config = nextConfig;
2353
+ await this.initialize();
2354
+ return;
2355
+ }
2356
+ if (this.encoder.state === "configured") {
2357
+ await this.encoder.flush();
2358
+ }
2359
+ const isSupported = await this.isConfigSupported(nextConfig);
2360
+ if (!isSupported.supported) {
2361
+ throw new Error(`New configuration not supported: ${nextConfig.codec}`);
2362
+ }
2363
+ this.config = nextConfig;
2364
+ this.encoder.configure(this.config);
2365
+ console.info("Encoder is reconfigured");
2366
+ }
2367
+ async flush() {
2368
+ if (!this.encoder) {
2369
+ return;
2370
+ }
2371
+ await this.encoder.flush();
2372
+ }
2373
+ async reset() {
2374
+ console.info("Encoder is resetting...");
2375
+ if (!this.encoder) {
2376
+ console.warn("Encoder is not initialized, skipping reset");
2377
+ return;
2378
+ }
2379
+ this.encoder.reset();
2380
+ this.onReset();
2381
+ console.info("Encoder is resetted");
2382
+ }
2383
+ async close() {
2384
+ console.info("Encoder is closing...");
2385
+ if (!this.encoder) {
2386
+ return;
2387
+ }
2388
+ if (this.encoder.state === "configured") {
2389
+ await this.encoder.flush();
2390
+ }
2391
+ this.encoder.close();
2392
+ this.encoder = void 0;
2393
+ console.info("Encoder is closed");
2394
+ }
2395
+ get isReady() {
2396
+ return this.encoder?.state === "configured";
2397
+ }
2398
+ get queueSize() {
2399
+ return this.encoder?.encodeQueueSize ?? 0;
2400
+ }
2401
+ handleOutput(chunk, metadata) {
2402
+ if (this.controller) {
2403
+ try {
2404
+ this.controller.enqueue({ chunk, metadata });
2405
+ } catch (error) {
2406
+ console.error("Encoder output error:", error);
2407
+ if (!(error instanceof TypeError && error.message.includes("closed"))) {
2408
+ throw error;
2409
+ }
2410
+ }
2411
+ }
2412
+ }
2413
+ handleError(error) {
2414
+ if (error.message.includes("reclaimed")) {
2415
+ console.warn("Encoder reclaimed by browser due to inactivity, skipping error handling");
2416
+ return;
2417
+ }
2418
+ console.error(`[${this.getEncoderType()}Encoder] Encode error:`, {
2419
+ name: error.name,
2420
+ message: error.message,
2421
+ encoderState: this.encoder?.state,
2422
+ queueSize: this.queueSize,
2423
+ platform: typeof navigator !== "undefined" ? navigator.platform : "unknown"
2424
+ });
2425
+ this.controller?.error(error);
2426
+ }
2427
+ // Hook for subclasses to handle reset
2428
+ onReset() {
2429
+ }
2430
+ /**
2431
+ * Create transform stream for encoding
2432
+ * Implements common stream logic with backpressure handling
2433
+ */
2434
+ createStream() {
2435
+ return new TransformStream(
2436
+ {
2437
+ start: async (controller) => {
2438
+ this.controller = controller;
2439
+ },
2440
+ transform: async (input) => {
2441
+ if (!this.isReady) {
2442
+ console.warn("Encoder is not ready, initializing...");
2443
+ await this.initialize();
2444
+ }
2445
+ if (!this.encoder || this.encoder.state !== "configured") {
2446
+ console.error("Encoder not configured, throwing error");
2447
+ throw new Error("Encoder not configured");
2448
+ }
2449
+ if (this.encoder.encodeQueueSize >= this.encodeQueueThreshold) {
2450
+ await new Promise((resolve) => {
2451
+ const check = () => {
2452
+ if (!this.encoder || this.encoder.encodeQueueSize < this.encodeQueueThreshold - 1) {
2453
+ resolve();
2454
+ } else {
2455
+ setTimeout(check, 10);
2456
+ }
2457
+ };
2458
+ check();
2459
+ });
2460
+ }
2461
+ const frame = input.frame || input;
2462
+ try {
2463
+ this.encode(frame);
2464
+ } catch (err) {
2465
+ const msg = err instanceof Error ? err.message : String(err);
2466
+ console.error("Encoder error:", msg);
2467
+ if (err instanceof DOMException && msg.includes("reclaimed")) {
2468
+ console.warn("Codec reclaimed due to inactivity, reset encoder...");
2469
+ this.encoder = void 0;
2470
+ await this.initialize();
2471
+ this.encode(frame);
2472
+ } else {
2473
+ throw err;
2474
+ }
2475
+ }
2476
+ },
2477
+ flush: async () => {
2478
+ await this.flush();
2479
+ }
2480
+ },
2481
+ // Queuing strategy with backpressure configuration
2482
+ {
2483
+ highWaterMark: this.highWaterMark,
2484
+ size: () => 1
2485
+ // Count-based
2486
+ }
2487
+ );
2488
+ }
2489
+ }
2490
+ class VideoChunkEncoder extends BaseEncoder {
2491
+ static DEFAULT_HIGH_WATER_MARK = 2;
2492
+ static DEFAULT_ENCODE_QUEUE_THRESHOLD = 8;
2493
+ highWaterMark;
2494
+ encodeQueueThreshold;
2495
+ frameCount = 0;
2496
+ // Default 1 second at 30fps for better social media compatibility
2497
+ keyFrameInterval = 30;
2498
+ constructor(config) {
2499
+ super(config);
2500
+ this.highWaterMark = config.backpressure?.highWaterMark ?? VideoChunkEncoder.DEFAULT_HIGH_WATER_MARK;
2501
+ this.encodeQueueThreshold = config.backpressure?.encodeQueueThreshold ?? VideoChunkEncoder.DEFAULT_ENCODE_QUEUE_THRESHOLD;
2502
+ if (config.keyFrameInterval !== void 0) {
2503
+ this.keyFrameInterval = Math.max(1, config.keyFrameInterval);
2504
+ }
2505
+ console.info(
2506
+ "VideoChunkEncoder initialized with encodeQueueThreshold:",
2507
+ this.encodeQueueThreshold,
2508
+ " highWaterMark:",
2509
+ this.highWaterMark,
2510
+ " keyFrameInterval:",
2511
+ this.keyFrameInterval
2512
+ );
2513
+ }
2514
+ async isConfigSupported(config) {
2515
+ const result = await VideoEncoder.isConfigSupported(config);
2516
+ return { supported: result.supported ?? false };
2517
+ }
2518
+ createEncoder(init) {
2519
+ return new VideoEncoder(init);
2520
+ }
2521
+ getEncoderType() {
2522
+ return "Video";
2523
+ }
2524
+ onReset() {
2525
+ this.frameCount = 0;
2526
+ }
2527
+ encode(frame) {
2528
+ try {
2529
+ const keyFrame = this.shouldGenerateKeyFrame();
2530
+ const encodeOptions = {
2531
+ keyFrame
2532
+ };
2533
+ this.encoder.encode(frame, encodeOptions);
2534
+ this.frameCount++;
2535
+ } finally {
2536
+ frame.close();
2537
+ }
2538
+ }
2539
+ setKeyFrameInterval(interval) {
2540
+ this.keyFrameInterval = Math.max(1, interval);
2541
+ console.info("Key frame interval set to:", this.keyFrameInterval);
2542
+ }
2543
+ shouldGenerateKeyFrame() {
2544
+ if (this.frameCount === 0) {
2545
+ return true;
2546
+ }
2547
+ return this.frameCount % this.keyFrameInterval === 0;
2548
+ }
2549
+ }
1736
2550
  const MICROSECONDS_PER_SECOND = 1e6;
1737
2551
  const DEFAULT_FPS = 30;
1738
2552
  function normalizeFps(value) {
@@ -2203,217 +3017,109 @@ function computeAnimationState(animation, globalTimeUs) {
2203
3017
  visible: true
2204
3018
  };
2205
3019
  }
2206
- class VideoComposeWorker {
3020
+ class ExportWorker {
2207
3021
  channel;
2208
3022
  composer = null;
2209
- composeStream = null;
2210
- sessionId = null;
2211
- downstreamPort = null;
2212
- upstreamPort = null;
3023
+ encoder = null;
2213
3024
  instructions = null;
2214
3025
  imageMap = /* @__PURE__ */ new Map();
2215
3026
  constructor() {
2216
3027
  this.channel = new WorkerChannel(self, {
2217
- name: "VideoComposeWorker",
2218
- timeout: 3e4
3028
+ name: "ExportWorker",
3029
+ timeout: 6e4
2219
3030
  });
2220
3031
  this.setupHandlers();
2221
3032
  }
2222
3033
  setupHandlers() {
2223
3034
  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
3035
  this.channel.registerHandler("install_instructions", this.handleInstallInstructions.bind(this));
2228
3036
  this.channel.registerHandler("receive_image", this.handleReceiveImage.bind(this));
2229
3037
  this.channel.registerHandler("dispose_clip", this.handleDisposeClip.bind(this));
3038
+ this.channel.registerHandler("flush", this.handleFlush.bind(this));
2230
3039
  this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
2231
3040
  this.channel.receiveStream(this.handleReceiveStream.bind(this));
2232
3041
  }
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
3042
  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) {
3043
+ const { compose, encode } = payload;
3044
+ if (compose.width <= 0 || compose.height <= 0 || compose.fps <= 0) {
2263
3045
  throw new Error(
2264
- `VideoComposeWorker: invalid canvas config width=${config.width}, height=${config.height}, fps=${config.fps}`
3046
+ `ExportWorker: invalid config width=${compose.width}, height=${compose.height}, fps=${compose.fps}`
2265
3047
  );
2266
3048
  }
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);
3049
+ if (this.composer) {
3050
+ this.composer.dispose();
2278
3051
  }
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
- };
3052
+ this.composer = new VideoComposer(compose);
3053
+ this.encoder = new VideoChunkEncoder(encode);
3054
+ await this.encoder.initialize();
3055
+ this.channel.state = WorkerState.Ready;
3056
+ return { success: true };
2287
3057
  }
2288
3058
  async handleReceiveStream(stream, metadata) {
2289
- if (!this.composer) {
2290
- console.error("[VideoComposeWorker] Composer not configured");
3059
+ if (!this.composer || !this.encoder) {
3060
+ console.error("[ExportWorker] Not configured");
2291
3061
  return;
2292
3062
  }
2293
3063
  if (metadata?.instructions) {
2294
- await this.handleInstallInstructions(metadata.instructions);
3064
+ this.handleInstallInstructions(metadata.instructions);
2295
3065
  }
2296
3066
  if (!this.instructions) {
2297
- console.error("[VideoComposeWorker] No instructions available after metadata check");
3067
+ console.error("[ExportWorker] No instructions");
2298
3068
  return;
2299
3069
  }
2300
3070
  const mainLayer = this.instructions.layers.find(
2301
3071
  (l) => l.type === "video" && !l.payload.attachmentId
2302
3072
  );
2303
- const trimStartUs = mainLayer?.type === "video" ? mainLayer.payload.trimStartUs ?? 0 : 0;
3073
+ const clipTrimStartUs = mainLayer?.type === "video" ? mainLayer.payload.trimStartUs ?? 0 : 0;
2304
3074
  const timeline = this.instructions.baseConfig.timeline;
2305
- const fpsConverter = new FrameRateConverter(
2306
- timeline?.compositionFps ?? 30,
2307
- timeline?.clipDurationUs ?? Infinity,
2308
- trimStartUs
2309
- );
3075
+ const fps = timeline?.compositionFps ?? 30;
3076
+ const windowStartUs = metadata?.windowStartUs ?? clipTrimStartUs;
3077
+ const windowEndUs = metadata?.windowEndUs ?? clipTrimStartUs + (timeline?.clipDurationUs ?? Infinity);
3078
+ const windowDurationUs = windowEndUs - windowStartUs;
3079
+ const windowToClipOffsetUs = windowStartUs - clipTrimStartUs;
3080
+ const fpsConverter = new FrameRateConverter(fps, windowDurationUs, windowStartUs);
2310
3081
  const cfrStream = stream.pipeThrough(fpsConverter.createStream());
2311
- const filteredStream = cfrStream.pipeThrough(
3082
+ const composeRequestStream = cfrStream.pipeThrough(
2312
3083
  new TransformStream({
2313
3084
  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;
3085
+ let composeFrame = frame;
3086
+ if (windowToClipOffsetUs > 0) {
3087
+ composeFrame = new VideoFrame(frame, {
3088
+ timestamp: (frame.timestamp ?? 0) + windowToClipOffsetUs
3089
+ });
3090
+ frame.close();
3091
+ }
3092
+ const request = this.buildComposeRequest(this.instructions, composeFrame);
3093
+ if (!request) {
3094
+ composeFrame.close();
3095
+ return;
2325
3096
  }
3097
+ controller.enqueue(request);
2326
3098
  }
2327
3099
  })
2328
3100
  );
2329
3101
  const composeStream = this.composer.createStreams();
2330
- filteredStream.pipeTo(composeStream.writable).catch((error) => {
2331
- console.error("[VideoComposeWorker] compose stream error", this.sessionId, error);
3102
+ const encodingTransform = this.encoder.createStream();
3103
+ composeRequestStream.pipeTo(composeStream.writable).catch((error) => {
3104
+ console.error("[ExportWorker] compose pipe error:", error);
3105
+ });
3106
+ const encodedStream = composeStream.readable.pipeThrough(encodingTransform);
3107
+ this.channel.sendStream(encodedStream, {
3108
+ streamType: "video"
2332
3109
  });
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
3110
  }
2394
- async handleInstallInstructions(payload) {
2395
- const { clipId, revision } = payload;
2396
- if (!this.sessionId) {
2397
- this.sessionId = clipId;
2398
- }
3111
+ handleInstallInstructions(payload) {
3112
+ const { revision } = payload;
2399
3113
  if (this.instructions && this.instructions.revision > revision) {
2400
3114
  return { success: false };
2401
3115
  }
2402
3116
  this.instructions = payload;
2403
3117
  return { success: true };
2404
3118
  }
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
3119
  async handleReceiveImage(payload) {
2411
- const { resourceId, sessionId, imageBitmap, instructions } = payload;
2412
- if (!this.sessionId) {
2413
- this.sessionId = sessionId;
2414
- }
3120
+ const { resourceId, imageBitmap, instructions } = payload;
2415
3121
  if (instructions) {
2416
- await this.handleInstallInstructions(instructions);
3122
+ this.handleInstallInstructions(instructions);
2417
3123
  }
2418
3124
  const existing = this.imageMap.get(resourceId);
2419
3125
  if (existing) {
@@ -2429,33 +3135,19 @@ class VideoComposeWorker {
2429
3135
  }
2430
3136
  return { success: true };
2431
3137
  }
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
3138
  async startImageFrameStream() {
2443
- if (!this.instructions || !this.composer) {
3139
+ if (!this.instructions || !this.composer || !this.encoder) {
2444
3140
  return;
2445
3141
  }
2446
3142
  const timeline = this.instructions.baseConfig.timeline;
2447
- if (!timeline) {
2448
- return;
2449
- }
3143
+ if (!timeline) return;
2450
3144
  const mainLayer = this.instructions.layers.find((l) => !l.payload.attachmentId);
2451
3145
  if (!mainLayer) return;
2452
3146
  const mainResourceId = mainLayer.payload.resourceId;
2453
3147
  const imageBitmap = this.imageMap.get(mainResourceId);
2454
- if (!imageBitmap) {
2455
- console.warn("[VideoComposeWorker] Main track ImageBitmap not found:", mainResourceId);
2456
- return;
2457
- }
3148
+ if (!imageBitmap) return;
2458
3149
  const composeStream = this.composer.createStreams();
3150
+ const encodingTransform = this.encoder.createStream();
2459
3151
  const { clipDurationUs, compositionFps } = timeline;
2460
3152
  let currentTimeUs = 0;
2461
3153
  const readableStream = new ReadableStream({
@@ -2468,36 +3160,49 @@ class VideoComposeWorker {
2468
3160
  timestamp: currentTimeUs,
2469
3161
  duration: frameDurationFromFps(compositionFps)
2470
3162
  });
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) {
3163
+ const request = this.buildComposeRequest(this.instructions, videoFrame);
3164
+ if (request) {
3165
+ controller.enqueue(request);
3166
+ } else {
2480
3167
  videoFrame.close();
2481
- throw error;
2482
3168
  }
3169
+ currentTimeUs += frameDurationFromFps(compositionFps);
2483
3170
  }
2484
3171
  });
2485
3172
  readableStream.pipeTo(composeStream.writable).catch((error) => {
2486
- console.error("[VideoComposeWorker] image frame stream error", this.sessionId, error);
3173
+ console.error("[ExportWorker] image frame stream error:", error);
2487
3174
  });
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);
3175
+ const encodedStream = composeStream.readable.pipeThrough(encodingTransform);
3176
+ this.channel.sendStream(encodedStream, {
3177
+ streamType: "video"
3178
+ });
3179
+ }
3180
+ async handleDisposeClip() {
3181
+ this.instructions = null;
3182
+ this.imageMap.forEach((bitmap) => bitmap.close());
3183
+ this.imageMap.clear();
3184
+ return { success: true };
3185
+ }
3186
+ async handleFlush() {
3187
+ if (this.encoder) {
3188
+ await this.encoder.flush();
3189
+ }
3190
+ return { success: true };
3191
+ }
3192
+ async handleDispose() {
3193
+ if (this.composer) {
3194
+ this.composer.dispose();
3195
+ this.composer = null;
2500
3196
  }
3197
+ if (this.encoder) {
3198
+ await this.encoder.close();
3199
+ this.encoder = null;
3200
+ }
3201
+ this.imageMap.forEach((bitmap) => bitmap.close());
3202
+ this.imageMap.clear();
3203
+ this.instructions = null;
3204
+ this.channel.state = WorkerState.Disposed;
3205
+ return { success: true };
2501
3206
  }
2502
3207
  buildComposeRequest(instruction, frame) {
2503
3208
  const clipRelativeTime = frame.timestamp ?? 0;
@@ -2506,20 +3211,16 @@ class VideoComposeWorker {
2506
3211
  return null;
2507
3212
  }
2508
3213
  const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
2509
- if (!activeLayers.length) {
2510
- return null;
2511
- }
3214
+ if (!activeLayers.length) return null;
2512
3215
  const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
2513
3216
  const globalTimeUs = clipStartUs + clipRelativeTime;
2514
3217
  const layers = activeLayers.map((layer) => materializeLayer(layer, frame, this.imageMap, globalTimeUs)).filter((layer) => layer !== null);
2515
- if (!layers.length) {
2516
- return null;
2517
- }
3218
+ if (!layers.length) return null;
2518
3219
  return {
2519
3220
  timeUs: clipRelativeTime,
2520
3221
  globalTimeUs,
2521
3222
  layers,
2522
- transition: VideoComposeWorker.buildTransition(
3223
+ transition: ExportWorker.buildTransition(
2523
3224
  instruction.transitions,
2524
3225
  clipRelativeTime,
2525
3226
  instruction.baseConfig.timeline
@@ -2531,9 +3232,7 @@ class VideoComposeWorker {
2531
3232
  const { startUs, endUs } = transition.range;
2532
3233
  return timeUs >= startUs && timeUs < endUs;
2533
3234
  });
2534
- if (!entry) {
2535
- return void 0;
2536
- }
3235
+ if (!entry) return void 0;
2537
3236
  const durationUs = entry.range.endUs - entry.range.startUs;
2538
3237
  const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;
2539
3238
  return {
@@ -2545,13 +3244,12 @@ class VideoComposeWorker {
2545
3244
  };
2546
3245
  }
2547
3246
  }
2548
- const worker = new VideoComposeWorker();
3247
+ const worker = new ExportWorker();
2549
3248
  self.addEventListener("beforeunload", () => {
2550
3249
  worker["handleDispose"]();
2551
3250
  });
2552
- const videoCompose_worker = null;
3251
+ const export_worker = null;
2553
3252
  export {
2554
- VideoComposeWorker,
2555
- videoCompose_worker as default
3253
+ export_worker as default
2556
3254
  };
2557
- //# sourceMappingURL=video-compose.worker.KMZjuJuY.js.map
3255
+ //# sourceMappingURL=export.worker.SahP9aVK.js.map