@meframe/core 0.0.1 → 0.0.2

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 (133) hide show
  1. package/README.md +17 -4
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +0 -3
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/assets/audio-compose.worker-nGVvHD5Q.js +1537 -0
  6. package/dist/assets/audio-compose.worker-nGVvHD5Q.js.map +1 -0
  7. package/dist/assets/audio-demux.worker-xwWBtbAe.js +8299 -0
  8. package/dist/assets/audio-demux.worker-xwWBtbAe.js.map +1 -0
  9. package/dist/assets/decode.worker-DpWHsc7R.js +1291 -0
  10. package/dist/assets/decode.worker-DpWHsc7R.js.map +1 -0
  11. package/dist/assets/encode.worker-nfOb3kw6.js +1026 -0
  12. package/dist/assets/encode.worker-nfOb3kw6.js.map +1 -0
  13. package/dist/assets/mux.worker-uEMQY066.js +8019 -0
  14. package/dist/assets/mux.worker-uEMQY066.js.map +1 -0
  15. package/dist/assets/video-compose.worker-DPzsC21d.js +1683 -0
  16. package/dist/assets/video-compose.worker-DPzsC21d.js.map +1 -0
  17. package/dist/assets/video-demux.worker-D019I7GQ.js +7957 -0
  18. package/dist/assets/video-demux.worker-D019I7GQ.js.map +1 -0
  19. package/dist/cache/CacheManager.d.ts.map +1 -1
  20. package/dist/cache/CacheManager.js +8 -1
  21. package/dist/cache/CacheManager.js.map +1 -1
  22. package/dist/config/defaults.d.ts.map +1 -1
  23. package/dist/config/defaults.js +0 -8
  24. package/dist/config/defaults.js.map +1 -1
  25. package/dist/config/types.d.ts +0 -4
  26. package/dist/config/types.d.ts.map +1 -1
  27. package/dist/controllers/PlaybackController.d.ts +4 -2
  28. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  29. package/dist/controllers/PlaybackController.js +7 -13
  30. package/dist/controllers/PlaybackController.js.map +1 -1
  31. package/dist/controllers/PreRenderService.d.ts +3 -2
  32. package/dist/controllers/PreRenderService.d.ts.map +1 -1
  33. package/dist/controllers/PreRenderService.js.map +1 -1
  34. package/dist/controllers/PreviewHandle.d.ts +2 -0
  35. package/dist/controllers/PreviewHandle.d.ts.map +1 -1
  36. package/dist/controllers/PreviewHandle.js +6 -0
  37. package/dist/controllers/PreviewHandle.js.map +1 -1
  38. package/dist/controllers/index.d.ts +1 -1
  39. package/dist/controllers/index.d.ts.map +1 -1
  40. package/dist/controllers/types.d.ts +2 -12
  41. package/dist/controllers/types.d.ts.map +1 -1
  42. package/dist/event/events.d.ts +5 -59
  43. package/dist/event/events.d.ts.map +1 -1
  44. package/dist/event/events.js +1 -6
  45. package/dist/event/events.js.map +1 -1
  46. package/dist/model/CompositionModel.js +1 -2
  47. package/dist/model/CompositionModel.js.map +1 -1
  48. package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
  49. package/dist/orchestrator/CompositionPlanner.js +1 -0
  50. package/dist/orchestrator/CompositionPlanner.js.map +1 -1
  51. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  52. package/dist/orchestrator/Orchestrator.js +1 -12
  53. package/dist/orchestrator/Orchestrator.js.map +1 -1
  54. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  55. package/dist/orchestrator/VideoClipSession.js +4 -5
  56. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  57. package/dist/orchestrator/types.d.ts +0 -1
  58. package/dist/orchestrator/types.d.ts.map +1 -1
  59. package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
  60. package/dist/stages/compose/GlobalAudioSession.js +3 -2
  61. package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
  62. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  63. package/dist/stages/compose/VideoComposer.js +2 -2
  64. package/dist/stages/compose/VideoComposer.js.map +1 -1
  65. package/dist/stages/compose/audio-compose.worker.d.ts.map +1 -1
  66. package/dist/stages/compose/audio-compose.worker.js +0 -1
  67. package/dist/stages/compose/audio-compose.worker.js.map +1 -1
  68. package/dist/stages/compose/audio-compose.worker2.js +5 -0
  69. package/dist/stages/compose/audio-compose.worker2.js.map +1 -0
  70. package/dist/stages/compose/types.d.ts +1 -0
  71. package/dist/stages/compose/types.d.ts.map +1 -1
  72. package/dist/stages/compose/video-compose.worker.d.ts.map +1 -1
  73. package/dist/stages/compose/video-compose.worker.js +18 -8
  74. package/dist/stages/compose/video-compose.worker.js.map +1 -1
  75. package/dist/stages/compose/video-compose.worker2.js +5 -0
  76. package/dist/stages/compose/video-compose.worker2.js.map +1 -0
  77. package/dist/stages/decode/AudioChunkDecoder.d.ts.map +1 -1
  78. package/dist/stages/decode/AudioChunkDecoder.js +0 -1
  79. package/dist/stages/decode/AudioChunkDecoder.js.map +1 -1
  80. package/dist/stages/decode/VideoChunkDecoder.d.ts +0 -1
  81. package/dist/stages/decode/VideoChunkDecoder.d.ts.map +1 -1
  82. package/dist/stages/decode/VideoChunkDecoder.js +1 -11
  83. package/dist/stages/decode/VideoChunkDecoder.js.map +1 -1
  84. package/dist/stages/decode/decode.worker.d.ts.map +1 -1
  85. package/dist/stages/decode/decode.worker.js +3 -16
  86. package/dist/stages/decode/decode.worker.js.map +1 -1
  87. package/dist/stages/decode/decode.worker2.js +5 -0
  88. package/dist/stages/decode/decode.worker2.js.map +1 -0
  89. package/dist/stages/demux/MP4Demuxer.d.ts +2 -0
  90. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  91. package/dist/stages/demux/MP4Demuxer.js +13 -2
  92. package/dist/stages/demux/MP4Demuxer.js.map +1 -1
  93. package/dist/stages/demux/audio-demux.worker2.js +5 -0
  94. package/dist/stages/demux/audio-demux.worker2.js.map +1 -0
  95. package/dist/stages/demux/video-demux.worker.d.ts +6 -3
  96. package/dist/stages/demux/video-demux.worker.d.ts.map +1 -1
  97. package/dist/stages/demux/video-demux.worker.js +5 -27
  98. package/dist/stages/demux/video-demux.worker.js.map +1 -1
  99. package/dist/stages/demux/video-demux.worker2.js +5 -0
  100. package/dist/stages/demux/video-demux.worker2.js.map +1 -0
  101. package/dist/stages/encode/encode.worker.d.ts.map +1 -1
  102. package/dist/stages/encode/encode.worker.js +0 -1
  103. package/dist/stages/encode/encode.worker.js.map +1 -1
  104. package/dist/stages/encode/encode.worker2.js +5 -0
  105. package/dist/stages/encode/encode.worker2.js.map +1 -0
  106. package/dist/stages/load/EventHandlers.d.ts +2 -11
  107. package/dist/stages/load/EventHandlers.d.ts.map +1 -1
  108. package/dist/stages/load/EventHandlers.js +1 -24
  109. package/dist/stages/load/EventHandlers.js.map +1 -1
  110. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  111. package/dist/stages/load/ResourceLoader.js +11 -13
  112. package/dist/stages/load/ResourceLoader.js.map +1 -1
  113. package/dist/stages/load/TaskManager.d.ts +1 -1
  114. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  115. package/dist/stages/load/TaskManager.js +3 -2
  116. package/dist/stages/load/TaskManager.js.map +1 -1
  117. package/dist/stages/load/types.d.ts +2 -0
  118. package/dist/stages/load/types.d.ts.map +1 -1
  119. package/dist/stages/mux/mux.worker2.js +5 -0
  120. package/dist/stages/mux/mux.worker2.js.map +1 -0
  121. package/dist/vite-plugin.d.ts +17 -0
  122. package/dist/vite-plugin.d.ts.map +1 -0
  123. package/dist/vite-plugin.js +88 -0
  124. package/dist/vite-plugin.js.map +1 -0
  125. package/dist/worker/WorkerPool.d.ts +0 -4
  126. package/dist/worker/WorkerPool.d.ts.map +1 -1
  127. package/dist/worker/WorkerPool.js +4 -17
  128. package/dist/worker/WorkerPool.js.map +1 -1
  129. package/dist/worker/worker-registry.d.ts +12 -0
  130. package/dist/worker/worker-registry.d.ts.map +1 -0
  131. package/dist/worker/worker-registry.js +20 -0
  132. package/dist/worker/worker-registry.js.map +1 -0
  133. package/package.json +7 -1
@@ -0,0 +1,1683 @@
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["ConfigureMux"] = "configure_mux";
27
+ WorkerMessageType2["AddChunk"] = "add_chunk";
28
+ WorkerMessageType2["FinishMux"] = "finish_mux";
29
+ WorkerMessageType2["MuxComplete"] = "mux_complete";
30
+ WorkerMessageType2["PerformanceStats"] = "performance_stats";
31
+ WorkerMessageType2["RenderWindow"] = "renderWindow";
32
+ WorkerMessageType2["AudioTrackAdd"] = "audio_track:add";
33
+ WorkerMessageType2["AudioTrackRemove"] = "audio_track:remove";
34
+ WorkerMessageType2["AudioTrackUpdate"] = "audio_track:update";
35
+ return WorkerMessageType2;
36
+ })(WorkerMessageType || {});
37
+ var WorkerState = /* @__PURE__ */ ((WorkerState2) => {
38
+ WorkerState2["Idle"] = "idle";
39
+ WorkerState2["Initializing"] = "initializing";
40
+ WorkerState2["Ready"] = "ready";
41
+ WorkerState2["Processing"] = "processing";
42
+ WorkerState2["Error"] = "error";
43
+ WorkerState2["Disposed"] = "disposed";
44
+ return WorkerState2;
45
+ })(WorkerState || {});
46
+ const defaultRetryConfig = {
47
+ maxRetries: 3,
48
+ initialDelay: 100,
49
+ maxDelay: 5e3,
50
+ backoffFactor: 2,
51
+ retryableErrors: ["TIMEOUT", "NETWORK_ERROR", "WORKER_BUSY"]
52
+ };
53
+ function calculateRetryDelay(attempt, config) {
54
+ const { initialDelay = 100, maxDelay = 5e3, backoffFactor = 2 } = config;
55
+ const delay = initialDelay * Math.pow(backoffFactor, attempt - 1);
56
+ return Math.min(delay, maxDelay);
57
+ }
58
+ function isRetryableError(error, config) {
59
+ const { retryableErrors = defaultRetryConfig.retryableErrors } = config;
60
+ if (!error) return false;
61
+ const errorCode = error.code || error.name;
62
+ if (errorCode && retryableErrors.includes(errorCode)) {
63
+ return true;
64
+ }
65
+ const message = error.message || "";
66
+ if (message.includes("timeout") || message.includes("Timeout")) {
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+ async function withRetry(fn, config) {
72
+ const { maxRetries } = config;
73
+ let lastError;
74
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
75
+ try {
76
+ return await fn();
77
+ } catch (error) {
78
+ lastError = error;
79
+ if (!isRetryableError(error, config)) {
80
+ throw error;
81
+ }
82
+ if (attempt === maxRetries) {
83
+ throw error;
84
+ }
85
+ const delay = calculateRetryDelay(attempt, config);
86
+ await sleep(delay);
87
+ }
88
+ }
89
+ throw lastError || new Error("Retry failed");
90
+ }
91
+ function sleep(ms) {
92
+ return new Promise((resolve) => setTimeout(resolve, ms));
93
+ }
94
+ function isTransferable(obj) {
95
+ 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;
96
+ }
97
+ function findTransferables(obj, transferables) {
98
+ if (!obj || typeof obj !== "object") {
99
+ return;
100
+ }
101
+ if (isTransferable(obj)) {
102
+ transferables.push(obj);
103
+ return;
104
+ }
105
+ if (obj instanceof VideoFrame) {
106
+ transferables.push(obj);
107
+ return;
108
+ }
109
+ if (typeof AudioData !== "undefined" && obj instanceof AudioData) {
110
+ transferables.push(obj);
111
+ return;
112
+ }
113
+ if (typeof EncodedVideoChunk !== "undefined" && obj instanceof EncodedVideoChunk || typeof EncodedAudioChunk !== "undefined" && obj instanceof EncodedAudioChunk) {
114
+ return;
115
+ }
116
+ if (Array.isArray(obj)) {
117
+ for (const item of obj) {
118
+ findTransferables(item, transferables);
119
+ }
120
+ } else {
121
+ for (const key in obj) {
122
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
123
+ findTransferables(obj[key], transferables);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ function extractTransferables(payload) {
129
+ const transferables = [];
130
+ findTransferables(payload, transferables);
131
+ return transferables;
132
+ }
133
+ class WorkerChannel {
134
+ name;
135
+ port;
136
+ pendingRequests = /* @__PURE__ */ new Map();
137
+ messageHandlers = {};
138
+ state = WorkerState.Idle;
139
+ defaultTimeout;
140
+ defaultMaxRetries;
141
+ constructor(port, config) {
142
+ this.name = config.name;
143
+ this.port = port;
144
+ this.defaultTimeout = config.timeout ?? 3e4;
145
+ this.defaultMaxRetries = config.maxRetries ?? 3;
146
+ this.setupMessageHandler();
147
+ this.state = WorkerState.Ready;
148
+ }
149
+ /**
150
+ * Send a message and wait for response with retry support
151
+ */
152
+ async send(type, payload, options) {
153
+ const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
154
+ const retryConfig = {
155
+ ...defaultRetryConfig,
156
+ maxRetries,
157
+ ...options?.retryConfig
158
+ };
159
+ return withRetry(() => this.sendOnce(type, payload, options), retryConfig);
160
+ }
161
+ /**
162
+ * Send a message once (without retry)
163
+ */
164
+ async sendOnce(type, payload, options) {
165
+ const id = this.generateMessageId();
166
+ const timeout = options?.timeout ?? this.defaultTimeout;
167
+ const message = {
168
+ type,
169
+ id,
170
+ payload,
171
+ timestamp: Date.now()
172
+ };
173
+ return new Promise((resolve, reject) => {
174
+ const request = {
175
+ id,
176
+ type,
177
+ timestamp: Date.now(),
178
+ timeout,
179
+ resolve,
180
+ reject
181
+ };
182
+ this.pendingRequests.set(id, request);
183
+ const timeoutId = setTimeout(() => {
184
+ const pending = this.pendingRequests.get(id);
185
+ if (pending) {
186
+ this.pendingRequests.delete(id);
187
+ const error = new Error(`Request timeout: ${id} ${type} (${timeout}ms)`);
188
+ error.code = "TIMEOUT";
189
+ pending.reject(error);
190
+ }
191
+ }, timeout);
192
+ request.timeoutId = timeoutId;
193
+ if (options?.transfer) {
194
+ this.port.postMessage(message, options.transfer);
195
+ } else {
196
+ this.port.postMessage(message);
197
+ }
198
+ });
199
+ }
200
+ /**
201
+ * Send a message without waiting for response
202
+ */
203
+ post(type, payload, transfer) {
204
+ const message = {
205
+ type,
206
+ id: this.generateMessageId(),
207
+ payload,
208
+ timestamp: Date.now()
209
+ };
210
+ if (transfer) {
211
+ this.port.postMessage(message, transfer);
212
+ } else {
213
+ this.port.postMessage(message);
214
+ }
215
+ }
216
+ /**
217
+ * Register a message handler
218
+ */
219
+ on(type, handler) {
220
+ this.messageHandlers[type] = handler;
221
+ }
222
+ /**
223
+ * Unregister a message handler
224
+ */
225
+ off(type) {
226
+ delete this.messageHandlers[type];
227
+ }
228
+ /**
229
+ * Dispose the channel
230
+ */
231
+ dispose() {
232
+ this.state = WorkerState.Disposed;
233
+ for (const [, request] of this.pendingRequests) {
234
+ if (request.timeoutId) {
235
+ clearTimeout(request.timeoutId);
236
+ }
237
+ request.reject(new Error("Channel disposed"));
238
+ }
239
+ this.pendingRequests.clear();
240
+ this.port.onmessage = null;
241
+ }
242
+ /**
243
+ * Setup message handler for incoming messages
244
+ */
245
+ setupMessageHandler() {
246
+ this.port.onmessage = async (event) => {
247
+ const data = event.data;
248
+ if (this.isResponse(data)) {
249
+ this.handleResponse(data);
250
+ return;
251
+ }
252
+ if (this.isRequest(data)) {
253
+ await this.handleRequest(data);
254
+ return;
255
+ }
256
+ };
257
+ }
258
+ /**
259
+ * Handle incoming request
260
+ */
261
+ async handleRequest(message) {
262
+ const handler = this.messageHandlers[message.type];
263
+ if (!handler) {
264
+ this.sendResponse(message.id, false, null, {
265
+ code: "NO_HANDLER",
266
+ message: `No handler registered for message type: ${message.type}`
267
+ });
268
+ return;
269
+ }
270
+ this.state = WorkerState.Processing;
271
+ Promise.resolve().then(() => handler(message.payload, message.transfer)).then((result) => {
272
+ this.sendResponse(message.id, true, result);
273
+ this.state = WorkerState.Ready;
274
+ }).catch((error) => {
275
+ const workerError = {
276
+ code: "HANDLER_ERROR",
277
+ message: error instanceof Error ? error.message : String(error),
278
+ stack: error instanceof Error ? error.stack : void 0
279
+ };
280
+ this.sendResponse(message.id, false, null, workerError);
281
+ this.state = WorkerState.Ready;
282
+ });
283
+ }
284
+ /**
285
+ * Handle incoming response
286
+ */
287
+ handleResponse(response) {
288
+ const request = this.pendingRequests.get(response.id);
289
+ if (!request) {
290
+ return;
291
+ }
292
+ this.pendingRequests.delete(response.id);
293
+ if (request.timeoutId) {
294
+ clearTimeout(request.timeoutId);
295
+ }
296
+ if (response.success) {
297
+ request.resolve(response.result);
298
+ } else {
299
+ const error = new Error(response.error?.message || "Unknown error");
300
+ if (response.error) {
301
+ Object.assign(error, response.error);
302
+ }
303
+ request.reject(error);
304
+ }
305
+ }
306
+ /**
307
+ * Send a response message
308
+ */
309
+ sendResponse(id, success, result, error) {
310
+ let transfer = [];
311
+ if (isTransferable(result)) {
312
+ transfer.push(result);
313
+ }
314
+ const response = {
315
+ id,
316
+ success,
317
+ result,
318
+ error,
319
+ timestamp: Date.now()
320
+ };
321
+ this.port.postMessage(response, transfer);
322
+ }
323
+ /**
324
+ * Check if message is a response
325
+ */
326
+ isResponse(data) {
327
+ return data && typeof data === "object" && "id" in data && "success" in data && !("type" in data);
328
+ }
329
+ /**
330
+ * Check if message is a request
331
+ */
332
+ isRequest(data) {
333
+ return data && typeof data === "object" && "id" in data && "type" in data;
334
+ }
335
+ /**
336
+ * Generate unique message ID
337
+ */
338
+ generateMessageId() {
339
+ return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
340
+ }
341
+ /**
342
+ * Send a notification message without waiting for response
343
+ * Alias for post() method for compatibility
344
+ */
345
+ notify(type, payload, transfer) {
346
+ this.post(type, payload, transfer);
347
+ }
348
+ /**
349
+ * Register a message handler
350
+ * Alias for on() method for compatibility
351
+ */
352
+ registerHandler(type, handler) {
353
+ this.on(type, handler);
354
+ }
355
+ /**
356
+ * Send a ReadableStream to another worker
357
+ * Automatically handles transferable streams vs chunk-by-chunk fallback
358
+ */
359
+ async sendStream(stream, metadata) {
360
+ const streamId = metadata?.streamId || this.generateMessageId();
361
+ if (isTransferable(stream)) {
362
+ this.port.postMessage(
363
+ {
364
+ type: "stream_transfer",
365
+ ...metadata,
366
+ stream,
367
+ streamId
368
+ },
369
+ [stream]
370
+ // Transfer ownership
371
+ );
372
+ } else {
373
+ await this.streamChunks(stream, streamId, metadata);
374
+ }
375
+ }
376
+ /**
377
+ * Stream chunks from a ReadableStream (fallback when transfer is not supported)
378
+ */
379
+ async streamChunks(stream, streamId, metadata) {
380
+ const reader = stream.getReader();
381
+ this.post("stream_start", {
382
+ streamId,
383
+ ...metadata,
384
+ mode: "chunk_transfer"
385
+ });
386
+ try {
387
+ while (true) {
388
+ const { done, value } = await reader.read();
389
+ if (done) {
390
+ this.post("stream_end", {
391
+ streamId,
392
+ ...metadata
393
+ });
394
+ break;
395
+ }
396
+ const transfer = [];
397
+ if (value instanceof ArrayBuffer) {
398
+ transfer.push(value);
399
+ } else if (value instanceof Uint8Array) {
400
+ transfer.push(value.buffer);
401
+ } else if (typeof AudioData !== "undefined" && value instanceof AudioData) {
402
+ transfer.push(value);
403
+ } else if (typeof VideoFrame !== "undefined" && value instanceof VideoFrame) {
404
+ transfer.push(value);
405
+ } else if (typeof value === "object" && value !== null) {
406
+ const extracted = extractTransferables(value);
407
+ transfer.push(...extracted);
408
+ }
409
+ this.post(
410
+ "stream_chunk",
411
+ {
412
+ streamId,
413
+ chunk: value,
414
+ ...metadata
415
+ },
416
+ transfer
417
+ );
418
+ }
419
+ } catch (error) {
420
+ this.post("stream_error", {
421
+ streamId,
422
+ error: error instanceof Error ? error.message : String(error),
423
+ ...metadata
424
+ });
425
+ throw error;
426
+ } finally {
427
+ reader.releaseLock();
428
+ }
429
+ }
430
+ /**
431
+ * Receive a stream from another worker
432
+ * Handles both transferable streams and chunk-by-chunk reconstruction
433
+ */
434
+ async receiveStream(onStream) {
435
+ const chunkedStreams = /* @__PURE__ */ new Map();
436
+ const prev = this.port.onmessage;
437
+ const handler = (event) => {
438
+ const raw = event.data;
439
+ const envelopeType = raw?.type;
440
+ const hasPayload = raw && typeof raw === "object" && "payload" in raw;
441
+ const payload = hasPayload ? raw.payload : raw;
442
+ if (envelopeType === "stream_transfer" && payload?.stream) {
443
+ onStream(payload.stream, payload);
444
+ return;
445
+ }
446
+ if (envelopeType === "stream_start" && payload?.streamId) {
447
+ const stream = new ReadableStream({
448
+ start(controller) {
449
+ chunkedStreams.set(payload.streamId, { controller, metadata: payload });
450
+ }
451
+ });
452
+ onStream(stream, payload);
453
+ return;
454
+ }
455
+ if (envelopeType === "stream_chunk" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
456
+ const s = chunkedStreams.get(payload.streamId);
457
+ if (s) s.controller.enqueue(payload.chunk);
458
+ return;
459
+ }
460
+ if (envelopeType === "stream_end" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
461
+ const s = chunkedStreams.get(payload.streamId);
462
+ if (s) {
463
+ s.controller.close();
464
+ chunkedStreams.delete(payload.streamId);
465
+ }
466
+ return;
467
+ }
468
+ if (envelopeType === "stream_error" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
469
+ const s = chunkedStreams.get(payload.streamId);
470
+ if (s) {
471
+ s.controller.error(new Error(String(payload.error || "stream error")));
472
+ chunkedStreams.delete(payload.streamId);
473
+ }
474
+ return;
475
+ }
476
+ if (typeof prev === "function") prev.call(this.port, event);
477
+ };
478
+ this.port.onmessage = handler;
479
+ }
480
+ }
481
+ const MICROSECONDS_PER_SECOND = 1e6;
482
+ const DEFAULT_FPS = 30;
483
+ function normalizeFps(value) {
484
+ if (!Number.isFinite(value) || value <= 0) {
485
+ return DEFAULT_FPS;
486
+ }
487
+ return value;
488
+ }
489
+ function frameDurationFromFps(fps) {
490
+ const normalized = normalizeFps(fps);
491
+ const duration = MICROSECONDS_PER_SECOND / normalized;
492
+ return Math.max(Math.round(duration), 1);
493
+ }
494
+ function frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy = "nearest") {
495
+ const frameDurationUs = frameDurationFromFps(fps);
496
+ if (frameDurationUs <= 0) {
497
+ return 0;
498
+ }
499
+ const delta = timestampUs - baseTimestampUs;
500
+ const rawIndex = delta / frameDurationUs;
501
+ if (!Number.isFinite(rawIndex)) {
502
+ return 0;
503
+ }
504
+ switch (strategy) {
505
+ case "floor":
506
+ return Math.floor(rawIndex);
507
+ case "ceil":
508
+ return Math.ceil(rawIndex);
509
+ default:
510
+ return Math.round(rawIndex);
511
+ }
512
+ }
513
+ function quantizeTimestampToFrame(timestampUs, baseTimestampUs, fps, strategy = "nearest") {
514
+ const frameDurationUs = frameDurationFromFps(fps);
515
+ const index = frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy);
516
+ return baseTimestampUs + index * frameDurationUs;
517
+ }
518
+ class LayerRenderer {
519
+ ctx;
520
+ width;
521
+ height;
522
+ constructor(ctx, width, height) {
523
+ this.ctx = ctx;
524
+ this.width = width;
525
+ this.height = height;
526
+ this.ensureHighQualityRendering();
527
+ }
528
+ ensureHighQualityRendering() {
529
+ this.ctx.imageSmoothingEnabled = true;
530
+ this.ctx.imageSmoothingQuality = "high";
531
+ }
532
+ /**
533
+ * Render a single layer with all its properties
534
+ */
535
+ async renderLayer(layer) {
536
+ if (!layer.visible || layer.opacity <= 0) return;
537
+ this.ctx.save();
538
+ try {
539
+ this.ensureHighQualityRendering();
540
+ this.ctx.globalAlpha = layer.opacity;
541
+ if (layer.blendMode) {
542
+ this.ctx.globalCompositeOperation = layer.blendMode;
543
+ }
544
+ if (layer.transform) {
545
+ this.applyTransform(layer.transform);
546
+ }
547
+ switch (layer.type) {
548
+ case "video":
549
+ await this.renderVideoLayer(layer);
550
+ break;
551
+ case "image":
552
+ await this.renderImageLayer(layer);
553
+ break;
554
+ case "text":
555
+ await this.renderTextLayer(layer);
556
+ break;
557
+ }
558
+ if (layer.mask) {
559
+ this.applyMask(layer.mask);
560
+ }
561
+ } finally {
562
+ this.ctx.restore();
563
+ }
564
+ }
565
+ applyTransform(transform) {
566
+ const centerX = this.width * (transform.anchorX ?? 0.5);
567
+ const centerY = this.height * (transform.anchorY ?? 0.5);
568
+ this.ctx.translate(transform.x + centerX, transform.y + centerY);
569
+ if (transform.rotation) {
570
+ this.ctx.rotate(transform.rotation);
571
+ }
572
+ this.ctx.scale(transform.scaleX, transform.scaleY);
573
+ if (transform.skewX || transform.skewY) {
574
+ this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);
575
+ }
576
+ this.ctx.translate(-centerX, -centerY);
577
+ }
578
+ async renderVideoLayer(layer) {
579
+ const { videoFrame, crop } = layer;
580
+ const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;
581
+ const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;
582
+ const scaleX = this.width / videoWidth;
583
+ const scaleY = this.height / videoHeight;
584
+ const scale = Math.min(scaleX, scaleY);
585
+ const renderWidth = Math.round(videoWidth * scale);
586
+ const renderHeight = Math.round(videoHeight * scale);
587
+ const renderX = Math.round((this.width - renderWidth) / 2);
588
+ const renderY = Math.round((this.height - renderHeight) / 2);
589
+ if (crop) {
590
+ this.ctx.drawImage(
591
+ videoFrame,
592
+ crop.x,
593
+ crop.y,
594
+ crop.width,
595
+ crop.height,
596
+ renderX,
597
+ renderY,
598
+ renderWidth,
599
+ renderHeight
600
+ );
601
+ } else {
602
+ this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);
603
+ }
604
+ }
605
+ async renderImageLayer(layer) {
606
+ const { source, crop } = layer;
607
+ if (source instanceof ImageData) {
608
+ if (crop) {
609
+ const tempCanvas = new OffscreenCanvas(crop.width, crop.height);
610
+ const tempCtx = tempCanvas.getContext("2d");
611
+ tempCtx.putImageData(source, -crop.x, -crop.y);
612
+ this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);
613
+ } else {
614
+ this.ctx.putImageData(source, 0, 0);
615
+ }
616
+ } else {
617
+ if (!source) {
618
+ return;
619
+ }
620
+ if (crop) {
621
+ this.ctx.drawImage(
622
+ source,
623
+ crop.x,
624
+ crop.y,
625
+ crop.width,
626
+ crop.height,
627
+ 0,
628
+ 0,
629
+ this.width,
630
+ this.height
631
+ );
632
+ } else {
633
+ this.ctx.drawImage(source, 0, 0, this.width, this.height);
634
+ }
635
+ }
636
+ }
637
+ async renderTextLayer(layer) {
638
+ const fontSize = layer.fontSize ?? 16;
639
+ const fontFamily = layer.fontFamily ?? "sans-serif";
640
+ const fontWeight = layer.fontWeight ?? "normal";
641
+ const fontStyle = layer.fontStyle ?? "normal";
642
+ this.ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
643
+ this.ctx.fillStyle = layer.color ?? "#000000";
644
+ this.ctx.textAlign = layer.textAlign ?? "left";
645
+ this.ctx.textBaseline = layer.verticalAlign ?? "top";
646
+ if (layer.letterSpacing && typeof this.ctx.letterSpacing !== "undefined") {
647
+ this.ctx.letterSpacing = `${layer.letterSpacing}px`;
648
+ }
649
+ this.ensureHighQualityRendering();
650
+ const baseX = this.calculateTextX(layer.textAlign);
651
+ const baseY = this.calculateTextY(layer.verticalAlign, fontSize);
652
+ const x = Math.round(baseX) + 0.5;
653
+ const y = Math.round(baseY) + 0.5;
654
+ if (layer.shadow) {
655
+ this.ctx.shadowColor = layer.shadow.color;
656
+ this.ctx.shadowOffsetX = layer.shadow.offsetX;
657
+ this.ctx.shadowOffsetY = layer.shadow.offsetY;
658
+ this.ctx.shadowBlur = layer.shadow.blur;
659
+ }
660
+ if (layer.strokeColor && layer.strokeWidth && layer.strokeWidth > 0) {
661
+ this.drawEnhancedStroke(layer.text, x, y, layer.strokeColor, layer.strokeWidth);
662
+ }
663
+ this.ctx.fillText(layer.text, x, y);
664
+ if (layer.shadow) {
665
+ this.ctx.shadowColor = "transparent";
666
+ this.ctx.shadowOffsetX = 0;
667
+ this.ctx.shadowOffsetY = 0;
668
+ this.ctx.shadowBlur = 0;
669
+ }
670
+ }
671
+ /**
672
+ * Draw enhanced multi-layer stroke for better text visibility
673
+ */
674
+ drawEnhancedStroke(text, x, y, strokeColor, strokeWidth) {
675
+ this.ctx.save();
676
+ this.ctx.strokeStyle = strokeColor;
677
+ this.ctx.lineJoin = "round";
678
+ this.ctx.lineCap = "round";
679
+ this.ctx.miterLimit = 2;
680
+ const layers = [1.1, 1];
681
+ layers.forEach((multiplier) => {
682
+ this.ctx.lineWidth = strokeWidth * multiplier;
683
+ this.ctx.strokeText(text, x, y);
684
+ });
685
+ this.ctx.restore();
686
+ }
687
+ calculateTextX(align) {
688
+ switch (align) {
689
+ case "center":
690
+ return this.width / 2;
691
+ case "right":
692
+ return this.width;
693
+ default:
694
+ return 0;
695
+ }
696
+ }
697
+ calculateTextY(align, fontSize = 16) {
698
+ switch (align) {
699
+ case "middle":
700
+ return this.height / 2;
701
+ case "bottom":
702
+ return this.height * 0.85;
703
+ default:
704
+ return fontSize;
705
+ }
706
+ }
707
+ applyMask(mask) {
708
+ this.ctx.globalCompositeOperation = mask.invert ? "source-out" : "destination-in";
709
+ if (mask.source) {
710
+ this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);
711
+ } else if (mask.shape === "circle") {
712
+ this.ctx.beginPath();
713
+ this.ctx.arc(
714
+ this.width / 2,
715
+ this.height / 2,
716
+ Math.min(this.width, this.height) / 2,
717
+ 0,
718
+ Math.PI * 2
719
+ );
720
+ this.ctx.fill();
721
+ }
722
+ }
723
+ updateDimensions(width, height) {
724
+ this.width = width;
725
+ this.height = height;
726
+ this.ensureHighQualityRendering();
727
+ }
728
+ }
729
+ class TransitionProcessor {
730
+ width;
731
+ height;
732
+ constructor(width, height) {
733
+ this.width = width;
734
+ this.height = height;
735
+ }
736
+ /**
737
+ * Apply transition effect to the canvas context
738
+ * Returns true if transition was applied, false if not needed
739
+ */
740
+ applyTransition(ctx, transition) {
741
+ if (!transition || transition.progress <= 0) return false;
742
+ const progress = this.calculateEasedProgress(transition.progress, transition.easing);
743
+ switch (transition.type) {
744
+ case "fade":
745
+ return this.applyFade(ctx, progress);
746
+ case "slide":
747
+ return this.applySlide(ctx, progress, transition.direction);
748
+ case "wipe":
749
+ return this.applyWipe(ctx, progress, transition.direction);
750
+ case "zoom":
751
+ return this.applyZoom(ctx, progress, transition.direction);
752
+ case "rotate":
753
+ return this.applyRotate(ctx, progress);
754
+ case "dissolve":
755
+ return this.applyDissolve(ctx, progress);
756
+ default:
757
+ return false;
758
+ }
759
+ }
760
+ calculateEasedProgress(progress, easing) {
761
+ switch (easing) {
762
+ case "ease-in":
763
+ return progress * progress;
764
+ case "ease-out":
765
+ return 1 - (1 - progress) * (1 - progress);
766
+ case "ease-in-out":
767
+ return progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
768
+ default:
769
+ return progress;
770
+ }
771
+ }
772
+ applyFade(ctx, progress) {
773
+ ctx.globalAlpha = progress;
774
+ return true;
775
+ }
776
+ applySlide(ctx, progress, direction) {
777
+ const distance = 1 - progress;
778
+ switch (direction) {
779
+ case "left":
780
+ ctx.translate(-this.width * distance, 0);
781
+ break;
782
+ case "right":
783
+ ctx.translate(this.width * distance, 0);
784
+ break;
785
+ case "up":
786
+ ctx.translate(0, -this.height * distance);
787
+ break;
788
+ case "down":
789
+ ctx.translate(0, this.height * distance);
790
+ break;
791
+ default:
792
+ ctx.translate(-this.width * distance, 0);
793
+ }
794
+ return true;
795
+ }
796
+ applyWipe(ctx, progress, direction) {
797
+ ctx.save();
798
+ ctx.beginPath();
799
+ switch (direction) {
800
+ case "left":
801
+ ctx.rect(0, 0, this.width * progress, this.height);
802
+ break;
803
+ case "right":
804
+ ctx.rect(this.width * (1 - progress), 0, this.width * progress, this.height);
805
+ break;
806
+ case "up":
807
+ ctx.rect(0, 0, this.width, this.height * progress);
808
+ break;
809
+ case "down":
810
+ ctx.rect(0, this.height * (1 - progress), this.width, this.height * progress);
811
+ break;
812
+ default:
813
+ ctx.rect(0, 0, this.width * progress, this.height);
814
+ }
815
+ ctx.clip();
816
+ return true;
817
+ }
818
+ applyZoom(ctx, progress, direction) {
819
+ const scale = direction === "out" ? 1 + (1 - progress) : progress;
820
+ const centerX = this.width / 2;
821
+ const centerY = this.height / 2;
822
+ ctx.translate(centerX, centerY);
823
+ ctx.scale(scale, scale);
824
+ ctx.translate(-centerX, -centerY);
825
+ if (direction === "out") {
826
+ ctx.globalAlpha = progress;
827
+ }
828
+ return true;
829
+ }
830
+ applyRotate(ctx, progress) {
831
+ const rotation = (1 - progress) * Math.PI * 2;
832
+ const centerX = this.width / 2;
833
+ const centerY = this.height / 2;
834
+ ctx.translate(centerX, centerY);
835
+ ctx.rotate(rotation);
836
+ ctx.translate(-centerX, -centerY);
837
+ return true;
838
+ }
839
+ applyDissolve(ctx, progress) {
840
+ ctx.globalAlpha = progress;
841
+ ctx.globalCompositeOperation = "multiply";
842
+ return true;
843
+ }
844
+ /**
845
+ * Create a transition mask for advanced effects
846
+ */
847
+ createTransitionMask(transition, canvas) {
848
+ const ctx = canvas.getContext("2d");
849
+ if (!ctx) return null;
850
+ const imageData = ctx.createImageData(this.width, this.height);
851
+ const data = imageData.data;
852
+ const progress = this.calculateEasedProgress(transition.progress, transition.easing);
853
+ for (let y = 0; y < this.height; y++) {
854
+ for (let x = 0; x < this.width; x++) {
855
+ const index = (y * this.width + x) * 4;
856
+ let alpha = 255;
857
+ switch (transition.type) {
858
+ case "wipe":
859
+ alpha = this.calculateWipeAlpha(x, y, progress, transition.direction);
860
+ break;
861
+ case "dissolve":
862
+ alpha = Math.random() < progress ? 255 : 0;
863
+ break;
864
+ default:
865
+ alpha = Math.floor(255 * progress);
866
+ }
867
+ data[index] = 255;
868
+ data[index + 1] = 255;
869
+ data[index + 2] = 255;
870
+ data[index + 3] = alpha;
871
+ }
872
+ }
873
+ return imageData;
874
+ }
875
+ calculateWipeAlpha(x, y, progress, direction) {
876
+ let position = 0;
877
+ switch (direction) {
878
+ case "left":
879
+ position = x / this.width;
880
+ break;
881
+ case "right":
882
+ position = 1 - x / this.width;
883
+ break;
884
+ case "up":
885
+ position = y / this.height;
886
+ break;
887
+ case "down":
888
+ position = 1 - y / this.height;
889
+ break;
890
+ case "in": {
891
+ const cx = x - this.width / 2;
892
+ const cy = y - this.height / 2;
893
+ const maxDist = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
894
+ position = Math.sqrt(cx * cx + cy * cy) / maxDist;
895
+ break;
896
+ }
897
+ case "out": {
898
+ const cx2 = x - this.width / 2;
899
+ const cy2 = y - this.height / 2;
900
+ const maxDist2 = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
901
+ position = 1 - Math.sqrt(cx2 * cx2 + cy2 * cy2) / maxDist2;
902
+ break;
903
+ }
904
+ default:
905
+ position = x / this.width;
906
+ }
907
+ return position < progress ? 255 : 0;
908
+ }
909
+ updateDimensions(width, height) {
910
+ this.width = width;
911
+ this.height = height;
912
+ }
913
+ }
914
+ class FilterProcessor {
915
+ filterCache = /* @__PURE__ */ new Map();
916
+ /**
917
+ * Apply filters to canvas context
918
+ * Combines multiple filters into a single CSS filter string for performance
919
+ */
920
+ applyFilters(ctx, filters) {
921
+ if (!filters || filters.length === 0) {
922
+ ctx.filter = "none";
923
+ return;
924
+ }
925
+ const cacheKey = this.generateCacheKey(filters);
926
+ let filterString = this.filterCache.get(cacheKey);
927
+ if (!filterString) {
928
+ filterString = this.buildFilterString(filters);
929
+ this.filterCache.set(cacheKey, filterString);
930
+ }
931
+ ctx.filter = filterString;
932
+ }
933
+ /**
934
+ * Build CSS filter string from filter array
935
+ */
936
+ buildFilterString(filters) {
937
+ const filterStrings = [];
938
+ for (const filter of filters) {
939
+ const filterStr = this.buildSingleFilter(filter);
940
+ if (filterStr) {
941
+ filterStrings.push(filterStr);
942
+ }
943
+ }
944
+ return filterStrings.length > 0 ? filterStrings.join(" ") : "none";
945
+ }
946
+ buildSingleFilter(filter) {
947
+ switch (filter.type) {
948
+ case "blur":
949
+ return `blur(${filter.value ?? 0}px)`;
950
+ case "brightness":
951
+ return `brightness(${filter.value ?? 1})`;
952
+ case "contrast":
953
+ return `contrast(${filter.value ?? 1})`;
954
+ case "grayscale":
955
+ return `grayscale(${filter.value ?? 0})`;
956
+ case "hue-rotate":
957
+ return `hue-rotate(${filter.value ?? 0}deg)`;
958
+ case "saturate":
959
+ return `saturate(${filter.value ?? 1})`;
960
+ case "sepia":
961
+ return `sepia(${filter.value ?? 0})`;
962
+ case "custom":
963
+ return this.buildCustomFilter(filter);
964
+ default:
965
+ console.warn(`Unknown filter type: ${filter.type}`);
966
+ return null;
967
+ }
968
+ }
969
+ /**
970
+ * Build custom filter from params
971
+ */
972
+ buildCustomFilter(filter) {
973
+ if (!filter.params) return null;
974
+ const { type, ...params } = filter.params;
975
+ switch (type) {
976
+ case "drop-shadow":
977
+ return `drop-shadow(${params.offsetX}px ${params.offsetY}px ${params.blur}px ${params.color})`;
978
+ case "opacity":
979
+ return `opacity(${params.value})`;
980
+ case "invert":
981
+ return `invert(${params.value})`;
982
+ default:
983
+ return null;
984
+ }
985
+ }
986
+ /**
987
+ * Apply color matrix transformation for advanced effects
988
+ * This allows for more complex color manipulations than CSS filters
989
+ */
990
+ applyColorMatrix(imageData, matrix) {
991
+ if (matrix.length !== 20) {
992
+ throw new Error("Color matrix must have 20 values (4x5 matrix)");
993
+ }
994
+ const data = imageData.data;
995
+ const length = data.length;
996
+ for (let i = 0; i < length; i += 4) {
997
+ const r = data[i];
998
+ const g = data[i + 1];
999
+ const b = data[i + 2];
1000
+ const a = data[i + 3];
1001
+ const m = matrix;
1002
+ data[i] = this.clamp(r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255);
1003
+ data[i + 1] = this.clamp(r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255);
1004
+ data[i + 2] = this.clamp(r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255);
1005
+ data[i + 3] = this.clamp(r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255);
1006
+ }
1007
+ return imageData;
1008
+ }
1009
+ /**
1010
+ * Predefined color matrices for common effects
1011
+ */
1012
+ getPresetMatrix(preset) {
1013
+ switch (preset) {
1014
+ case "vintage":
1015
+ return [
1016
+ 0.393,
1017
+ 0.769,
1018
+ 0.189,
1019
+ 0,
1020
+ 0,
1021
+ 0.349,
1022
+ 0.686,
1023
+ 0.168,
1024
+ 0,
1025
+ 0,
1026
+ 0.272,
1027
+ 0.534,
1028
+ 0.131,
1029
+ 0,
1030
+ 0,
1031
+ 0,
1032
+ 0,
1033
+ 0,
1034
+ 1,
1035
+ 0
1036
+ ];
1037
+ case "noir":
1038
+ return [
1039
+ 0.25,
1040
+ 0.25,
1041
+ 0.25,
1042
+ 0,
1043
+ 0,
1044
+ 0.25,
1045
+ 0.25,
1046
+ 0.25,
1047
+ 0,
1048
+ 0,
1049
+ 0.25,
1050
+ 0.25,
1051
+ 0.25,
1052
+ 0,
1053
+ 0,
1054
+ 0,
1055
+ 0,
1056
+ 0,
1057
+ 1,
1058
+ 0
1059
+ ];
1060
+ case "cool":
1061
+ return [0.8, 0, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0];
1062
+ case "warm":
1063
+ return [1.2, 0, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 0, 1, 0];
1064
+ default:
1065
+ return null;
1066
+ }
1067
+ }
1068
+ /**
1069
+ * Apply Gaussian blur manually (for cases where CSS filter is not enough)
1070
+ */
1071
+ applyGaussianBlur(imageData, radius) {
1072
+ const output = new ImageData(
1073
+ new Uint8ClampedArray(imageData.data),
1074
+ imageData.width,
1075
+ imageData.height
1076
+ );
1077
+ const width = imageData.width;
1078
+ const height = imageData.height;
1079
+ const data = imageData.data;
1080
+ const outData = output.data;
1081
+ for (let y = 0; y < height; y++) {
1082
+ for (let x = 0; x < width; x++) {
1083
+ let r = 0, g = 0, b = 0, a = 0;
1084
+ let count = 0;
1085
+ for (let dx = -radius; dx <= radius; dx++) {
1086
+ const nx = Math.min(Math.max(x + dx, 0), width - 1);
1087
+ const idx2 = (y * width + nx) * 4;
1088
+ r += data[idx2];
1089
+ g += data[idx2 + 1];
1090
+ b += data[idx2 + 2];
1091
+ a += data[idx2 + 3];
1092
+ count++;
1093
+ }
1094
+ const idx = (y * width + x) * 4;
1095
+ outData[idx] = r / count;
1096
+ outData[idx + 1] = g / count;
1097
+ outData[idx + 2] = b / count;
1098
+ outData[idx + 3] = a / count;
1099
+ }
1100
+ }
1101
+ for (let x = 0; x < width; x++) {
1102
+ for (let y = 0; y < height; y++) {
1103
+ let r = 0, g = 0, b = 0, a = 0;
1104
+ let count = 0;
1105
+ for (let dy = -radius; dy <= radius; dy++) {
1106
+ const ny = Math.min(Math.max(y + dy, 0), height - 1);
1107
+ const idx2 = (ny * width + x) * 4;
1108
+ r += outData[idx2];
1109
+ g += outData[idx2 + 1];
1110
+ b += outData[idx2 + 2];
1111
+ a += outData[idx2 + 3];
1112
+ count++;
1113
+ }
1114
+ const idx = (y * width + x) * 4;
1115
+ data[idx] = r / count;
1116
+ data[idx + 1] = g / count;
1117
+ data[idx + 2] = b / count;
1118
+ data[idx + 3] = a / count;
1119
+ }
1120
+ }
1121
+ return imageData;
1122
+ }
1123
+ clamp(value) {
1124
+ return Math.min(255, Math.max(0, Math.round(value)));
1125
+ }
1126
+ generateCacheKey(filters) {
1127
+ return filters.map((f) => `${f.type}:${f.value ?? "default"}`).join("|");
1128
+ }
1129
+ clearCache() {
1130
+ this.filterCache.clear();
1131
+ }
1132
+ getCacheSize() {
1133
+ return this.filterCache.size;
1134
+ }
1135
+ }
1136
+ class VideoComposer {
1137
+ config;
1138
+ canvas;
1139
+ ctx;
1140
+ layerRenderer;
1141
+ transitionProcessor;
1142
+ filterProcessor;
1143
+ timelineContext;
1144
+ constructor(config) {
1145
+ this.config = this.applyDefaults(config);
1146
+ this.canvas = new OffscreenCanvas(this.config.width, this.config.height);
1147
+ const ctx = this.canvas.getContext("2d", {
1148
+ alpha: true,
1149
+ desynchronized: true,
1150
+ willReadFrequently: false,
1151
+ colorSpace: "srgb"
1152
+ });
1153
+ if (!ctx) {
1154
+ throw new Error("Failed to create 2D rendering context");
1155
+ }
1156
+ this.ctx = ctx;
1157
+ this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
1158
+ this.ctx.imageSmoothingQuality = "high";
1159
+ this.layerRenderer = new LayerRenderer(ctx, this.config.width, this.config.height);
1160
+ this.transitionProcessor = new TransitionProcessor(this.config.width, this.config.height);
1161
+ this.filterProcessor = new FilterProcessor();
1162
+ this.timelineContext = this.config.timeline;
1163
+ }
1164
+ applyDefaults(config) {
1165
+ return {
1166
+ width: config.width || 720,
1167
+ height: config.height || 1280,
1168
+ fps: config.fps || 30,
1169
+ backgroundColor: config.backgroundColor ?? "#000",
1170
+ renderer: config.renderer ?? "canvas2d",
1171
+ enableSmoothing: config.enableSmoothing ?? true,
1172
+ enableHardwareAcceleration: config.enableHardwareAcceleration ?? true,
1173
+ revision: config.revision ?? 0,
1174
+ inputHighWaterMark: config.inputHighWaterMark ?? 3,
1175
+ outputHighWaterMark: config.outputHighWaterMark ?? 1,
1176
+ maxLayers: config.maxLayers ?? 100,
1177
+ timeline: config.timeline ?? {
1178
+ clipId: "default",
1179
+ trackId: "main",
1180
+ clipStartUs: 0,
1181
+ clipDurationUs: Infinity,
1182
+ compositionFps: 30
1183
+ }
1184
+ };
1185
+ }
1186
+ createStreams(_instruction) {
1187
+ if (_instruction?.baseConfig.timeline) {
1188
+ this.timelineContext = _instruction.baseConfig.timeline;
1189
+ }
1190
+ const stream = new TransformStream(
1191
+ {
1192
+ transform: async (request, controller) => {
1193
+ const result = await this.composeFrame(request);
1194
+ controller.enqueue({
1195
+ frame: result.frame,
1196
+ metadata: result.metadata
1197
+ });
1198
+ },
1199
+ flush: async () => {
1200
+ this.filterProcessor.clearCache();
1201
+ }
1202
+ },
1203
+ {
1204
+ highWaterMark: this.config.inputHighWaterMark
1205
+ },
1206
+ {
1207
+ highWaterMark: this.config.outputHighWaterMark
1208
+ }
1209
+ );
1210
+ return {
1211
+ composeStream: stream.writable,
1212
+ cacheStream: stream.readable
1213
+ };
1214
+ }
1215
+ async composeFrame(request) {
1216
+ if (request.layers.length > this.config.maxLayers) {
1217
+ throw new Error(`Too many layers: ${request.layers.length} > ${this.config.maxLayers}`);
1218
+ }
1219
+ this.clearCanvas();
1220
+ if (request.transition) {
1221
+ this.ctx.save();
1222
+ this.transitionProcessor.applyTransition(this.ctx, request.transition);
1223
+ }
1224
+ for (const layer of request.layers) {
1225
+ if (!layer.visible || layer.opacity <= 0) {
1226
+ if (layer.type === "video") {
1227
+ const vf = layer.videoFrame;
1228
+ vf?.close?.();
1229
+ }
1230
+ continue;
1231
+ }
1232
+ try {
1233
+ if (layer.filters && layer.filters.length > 0) {
1234
+ this.ctx.save();
1235
+ this.filterProcessor.applyFilters(this.ctx, layer.filters);
1236
+ }
1237
+ await this.layerRenderer.renderLayer(layer);
1238
+ if (layer.filters && layer.filters.length > 0) {
1239
+ this.ctx.restore();
1240
+ }
1241
+ } finally {
1242
+ if (layer.type === "video") {
1243
+ const vf = layer.videoFrame;
1244
+ vf?.close?.();
1245
+ }
1246
+ }
1247
+ }
1248
+ if (request.transition) {
1249
+ this.ctx.restore();
1250
+ }
1251
+ const frame = await this.createOutputFrame(request.timeUs);
1252
+ const gopSerial = request.gopSerial;
1253
+ const isKeyframe = request.isKeyframe;
1254
+ return {
1255
+ frame,
1256
+ timeUs: request.timeUs,
1257
+ metadata: {
1258
+ layerCount: request.layers.length,
1259
+ renderTime: 0,
1260
+ gpuAccelerated: this.config.enableHardwareAcceleration && this.config.renderer !== "canvas2d",
1261
+ ...typeof gopSerial === "number" && { gopSerial },
1262
+ ...typeof isKeyframe === "boolean" && { isKeyframe }
1263
+ }
1264
+ };
1265
+ }
1266
+ async composeTransition(fromRequest, toRequest, transition) {
1267
+ await this.composeFrame(fromRequest);
1268
+ const toFrameRequest = {
1269
+ ...toRequest,
1270
+ transition
1271
+ };
1272
+ return this.composeFrame(toFrameRequest);
1273
+ }
1274
+ clearCanvas() {
1275
+ if (this.config.backgroundColor) {
1276
+ this.ctx.fillStyle = this.config.backgroundColor;
1277
+ this.ctx.fillRect(0, 0, this.config.width, this.config.height);
1278
+ } else {
1279
+ this.ctx.clearRect(0, 0, this.config.width, this.config.height);
1280
+ }
1281
+ }
1282
+ // private sortLayers(layers: Layer[]): Layer[] {
1283
+ // return [...layers].sort((a, b) => a.zIndex - b.zIndex);
1284
+ // }
1285
+ async createOutputFrame(timeUs) {
1286
+ const duration = frameDurationFromFps(this.timelineContext.compositionFps);
1287
+ const frame = new VideoFrame(this.canvas, {
1288
+ timestamp: timeUs,
1289
+ duration,
1290
+ alpha: "discard",
1291
+ visibleRect: { x: 0, y: 0, width: this.canvas.width, height: this.canvas.height }
1292
+ });
1293
+ return frame;
1294
+ }
1295
+ updateConfig(config) {
1296
+ Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
1297
+ if (config.width || config.height) {
1298
+ this.canvas.width = this.config.width;
1299
+ this.canvas.height = this.config.height;
1300
+ this.layerRenderer.updateDimensions(this.config.width, this.config.height);
1301
+ this.transitionProcessor.updateDimensions(this.config.width, this.config.height);
1302
+ }
1303
+ if (config.enableSmoothing !== void 0) {
1304
+ this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
1305
+ }
1306
+ if (config.timeline) {
1307
+ this.timelineContext = config.timeline;
1308
+ }
1309
+ }
1310
+ dispose() {
1311
+ this.filterProcessor.clearCache();
1312
+ }
1313
+ }
1314
+ function resolveActiveLayers(layers, timestamp, _frame) {
1315
+ return layers.filter((layer) => {
1316
+ if (layer.status !== "ready") {
1317
+ return false;
1318
+ }
1319
+ return layer.activeRanges.some(
1320
+ (range) => timestamp >= range.startUs && timestamp < range.endUs
1321
+ );
1322
+ });
1323
+ }
1324
+ function materializeLayer(layer, frame) {
1325
+ const baseLayer = {
1326
+ id: layer.layerId,
1327
+ type: layer.type,
1328
+ zIndex: layer.zIndex ?? 0,
1329
+ visible: true,
1330
+ opacity: layer.opacity ?? 1
1331
+ };
1332
+ if (layer.type === "video") {
1333
+ return {
1334
+ ...baseLayer,
1335
+ type: "video",
1336
+ videoFrame: frame
1337
+ };
1338
+ }
1339
+ if (layer.type === "text") {
1340
+ const payload = layer.payload;
1341
+ return {
1342
+ ...baseLayer,
1343
+ type: "text",
1344
+ text: payload.text,
1345
+ fontFamily: payload.fontFamily,
1346
+ fontSize: payload.fontSize,
1347
+ fontWeight: payload.fontWeight,
1348
+ color: payload.color,
1349
+ strokeColor: payload.strokeColor,
1350
+ strokeWidth: payload.strokeWidth,
1351
+ lineHeight: payload.lineHeight,
1352
+ textAlign: payload.align,
1353
+ verticalAlign: "bottom"
1354
+ // Subtitles positioned at bottom
1355
+ };
1356
+ }
1357
+ if (layer.type === "image") {
1358
+ const payload = layer.payload;
1359
+ const source = payload.bitmapHandle ?? null;
1360
+ if (source) {
1361
+ return {
1362
+ ...baseLayer,
1363
+ type: "image",
1364
+ source
1365
+ };
1366
+ }
1367
+ }
1368
+ return baseLayer;
1369
+ }
1370
+ class VideoComposeWorker {
1371
+ channel;
1372
+ composer = null;
1373
+ composeStream = null;
1374
+ downstreamPorts = /* @__PURE__ */ new Map();
1375
+ upstreamPorts = /* @__PURE__ */ new Map();
1376
+ instructionRegistry = /* @__PURE__ */ new Map();
1377
+ pendingReplay = /* @__PURE__ */ new Map();
1378
+ streamState = /* @__PURE__ */ new Map();
1379
+ constructor() {
1380
+ this.channel = new WorkerChannel(self, {
1381
+ name: "VideoComposeWorker",
1382
+ timeout: 3e4
1383
+ });
1384
+ this.setupHandlers();
1385
+ }
1386
+ setupHandlers() {
1387
+ this.channel.registerHandler("configure", this.handleConfigure.bind(this));
1388
+ this.channel.registerHandler("connect", this.handleConnect.bind(this));
1389
+ this.channel.registerHandler("flush", this.handleFlush.bind(this));
1390
+ this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
1391
+ this.channel.registerHandler("install_instructions", this.handleInstallInstructions.bind(this));
1392
+ this.channel.registerHandler("sync_clip", this.handleSyncClip.bind(this));
1393
+ this.channel.registerHandler("dispose_clip", this.handleDisposeClip.bind(this));
1394
+ this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
1395
+ }
1396
+ /**
1397
+ * Unified connect handler used by stream pipeline
1398
+ */
1399
+ async handleConnect(payload) {
1400
+ const { port, direction, clipId = "default" } = payload;
1401
+ if (direction === "upstream") {
1402
+ this.upstreamPorts.set(clipId, port);
1403
+ const channel = new WorkerChannel(port, {
1404
+ name: "VideoCompose-Decode",
1405
+ timeout: 3e4
1406
+ });
1407
+ channel.receiveStream(this.handleReceiveStream.bind(this));
1408
+ }
1409
+ if (direction === "downstream") {
1410
+ this.downstreamPorts.set(clipId, port);
1411
+ }
1412
+ return { success: true };
1413
+ }
1414
+ /**
1415
+ * Configure composer
1416
+ * According to docs/impl/14-config, only reinitialize when initial=true
1417
+ */
1418
+ async handleConfigure(payload) {
1419
+ const { config, initial } = payload;
1420
+ const hasValidDimensions = config.width > 0 && config.height > 0;
1421
+ const hasValidFps = config.fps > 0;
1422
+ if (!hasValidDimensions || !hasValidFps) {
1423
+ throw new Error(
1424
+ `VideoComposeWorker: invalid canvas config width=${config.width}, height=${config.height}, fps=${config.fps}`
1425
+ );
1426
+ }
1427
+ if (initial) {
1428
+ this.channel.state = WorkerState.Ready;
1429
+ }
1430
+ if (initial || !this.composer) {
1431
+ if (this.composer) {
1432
+ this.composer.dispose();
1433
+ this.composeStream = null;
1434
+ }
1435
+ this.composer = new VideoComposer(config);
1436
+ } else {
1437
+ this.composer.updateConfig(config);
1438
+ }
1439
+ this.channel.notify("configured", {
1440
+ config: this.composer.config,
1441
+ initialized: initial || false
1442
+ });
1443
+ return {
1444
+ success: true,
1445
+ config: this.composer.config
1446
+ };
1447
+ }
1448
+ async handleReceiveStream(stream, metadata) {
1449
+ const { clipId = "default" } = metadata || {};
1450
+ if (!this.composer) {
1451
+ console.error("[VideoComposeWorker] Composer not configured");
1452
+ return;
1453
+ }
1454
+ const instruction = this.instructionRegistry.get(clipId);
1455
+ if (!instruction) {
1456
+ console.warn("[VideoComposeWorker] No instructions for clip", clipId);
1457
+ return;
1458
+ }
1459
+ const filteredStream = stream.pipeThrough(
1460
+ new TransformStream({
1461
+ transform: (wrappedFrame, controller) => {
1462
+ try {
1463
+ const frame = wrappedFrame.frame || wrappedFrame;
1464
+ const gopSerial = wrappedFrame.gopSerial;
1465
+ const isKeyframe = wrappedFrame.isKeyframe;
1466
+ const timestamp = frame.timestamp ?? 0;
1467
+ if (this.shouldSkipFrame(clipId, timestamp)) {
1468
+ frame.close();
1469
+ return;
1470
+ }
1471
+ const request = this.buildComposeRequest(clipId, instruction, frame, timestamp);
1472
+ if (!request) {
1473
+ frame.close();
1474
+ return;
1475
+ }
1476
+ request.gopSerial = gopSerial;
1477
+ request.isKeyframe = isKeyframe;
1478
+ controller.enqueue(request);
1479
+ } catch (error) {
1480
+ const frame = wrappedFrame.frame || wrappedFrame;
1481
+ frame?.close?.();
1482
+ throw error;
1483
+ }
1484
+ }
1485
+ })
1486
+ );
1487
+ const { composeStream, cacheStream } = this.composer.createStreams();
1488
+ this.channel.sendStream(cacheStream, metadata);
1489
+ filteredStream.pipeTo(composeStream).catch((error) => {
1490
+ console.error("[VideoComposeWorker] compose stream error", clipId, error);
1491
+ });
1492
+ }
1493
+ // private handleGetStream(): ReadableStream<VideoFrame> | undefined {
1494
+ // return this.composer?.createStreams()?.cacheStream;
1495
+ // }
1496
+ /**
1497
+ * Flush the composition pipeline
1498
+ */
1499
+ async handleFlush() {
1500
+ try {
1501
+ this.channel.notify("flush_complete", {});
1502
+ return { success: true };
1503
+ } catch (error) {
1504
+ throw {
1505
+ code: "FLUSH_ERROR",
1506
+ message: error.message
1507
+ };
1508
+ }
1509
+ }
1510
+ /**
1511
+ * Get composer statistics
1512
+ */
1513
+ async handleGetStats() {
1514
+ return {
1515
+ configured: this.composer !== null,
1516
+ config: this.composer?.config,
1517
+ streaming: this.composeStream !== null
1518
+ };
1519
+ }
1520
+ /**
1521
+ * Dispose worker and cleanup resources
1522
+ */
1523
+ async handleDispose() {
1524
+ if (this.composer) {
1525
+ this.composer.dispose();
1526
+ this.composer = null;
1527
+ }
1528
+ this.composeStream = null;
1529
+ this.downstreamPorts.get("default")?.close();
1530
+ this.upstreamPorts.get("default")?.close();
1531
+ this.downstreamPorts.clear();
1532
+ this.upstreamPorts.clear();
1533
+ this.channel.state = WorkerState.Disposed;
1534
+ return { success: true };
1535
+ }
1536
+ async handleInstallInstructions(data) {
1537
+ const { clipId, revision } = data;
1538
+ const current = this.instructionRegistry.get(clipId);
1539
+ if (current && current.revision > revision) {
1540
+ return { success: false };
1541
+ }
1542
+ this.instructionRegistry.set(clipId, data);
1543
+ return { success: true };
1544
+ }
1545
+ async handleSyncClip(payload) {
1546
+ const { clipId, revision, range } = payload;
1547
+ const current = this.instructionRegistry.get(clipId);
1548
+ if (!current || current.revision > revision) {
1549
+ return { success: false };
1550
+ }
1551
+ this.pendingReplay.set(clipId, { ...range, revision });
1552
+ this.channel.notify("sync_ack", { clipId, revision });
1553
+ return { success: true };
1554
+ }
1555
+ async handleDisposeClip(payload) {
1556
+ const { clipId } = payload;
1557
+ this.instructionRegistry.delete(clipId);
1558
+ this.pendingReplay.delete(clipId);
1559
+ this.downstreamPorts.get(clipId)?.close();
1560
+ this.upstreamPorts.get(clipId)?.close();
1561
+ this.downstreamPorts.delete(clipId);
1562
+ this.upstreamPorts.delete(clipId);
1563
+ return { success: true };
1564
+ }
1565
+ /**
1566
+ * Check if frame should be skipped (outside dirty range)
1567
+ * Returns true if frame is NOT in the dirty range and should use cached version
1568
+ */
1569
+ shouldSkipFrame(clipId, timestamp) {
1570
+ const dirtyRange = this.pendingReplay.get(clipId);
1571
+ if (!dirtyRange) {
1572
+ return false;
1573
+ }
1574
+ if (timestamp >= dirtyRange.startUs && timestamp <= dirtyRange.endUs) {
1575
+ return false;
1576
+ }
1577
+ if (timestamp > dirtyRange.endUs) {
1578
+ this.pendingReplay.delete(clipId);
1579
+ }
1580
+ return true;
1581
+ }
1582
+ buildComposeRequest(clipId, instruction, frame, _timestamp) {
1583
+ const normalizedTime = this.computeTimelineTimestamp(clipId, frame, instruction.baseConfig);
1584
+ const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
1585
+ const clipDurationUs = instruction.baseConfig.timeline?.clipDurationUs ?? Infinity;
1586
+ const clipEndUs = clipStartUs + clipDurationUs;
1587
+ if (normalizedTime < clipStartUs || normalizedTime >= clipEndUs) {
1588
+ return null;
1589
+ }
1590
+ const clipRelativeTime = normalizedTime - clipStartUs;
1591
+ const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
1592
+ if (!activeLayers.length) {
1593
+ return null;
1594
+ }
1595
+ const layers = activeLayers.map((layer) => materializeLayer(layer, frame));
1596
+ return {
1597
+ timeUs: normalizedTime,
1598
+ layers,
1599
+ transition: VideoComposeWorker.buildTransition(
1600
+ instruction.transitions,
1601
+ clipRelativeTime,
1602
+ instruction.baseConfig.timeline
1603
+ )
1604
+ };
1605
+ }
1606
+ static buildTransition(transitions, timeUs, _timeline) {
1607
+ const entry = transitions.find((transition) => {
1608
+ const { startUs, endUs } = transition.range;
1609
+ return timeUs >= startUs && timeUs < endUs;
1610
+ });
1611
+ if (!entry) {
1612
+ return void 0;
1613
+ }
1614
+ const durationUs = entry.range.endUs - entry.range.startUs;
1615
+ const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;
1616
+ return {
1617
+ type: entry.params.type,
1618
+ progress: Math.min(Math.max(progress, 0), 1),
1619
+ easing: entry.params.easing,
1620
+ params: entry.params.payload,
1621
+ direction: entry.params.payload?.direction
1622
+ };
1623
+ }
1624
+ computeTimelineTimestamp(clipId, frame, config) {
1625
+ const key = clipId;
1626
+ let state = this.streamState.get(key);
1627
+ if (!state) {
1628
+ state = {
1629
+ baseTimestamp: null,
1630
+ lastSourceTimestamp: null,
1631
+ nextFrameIndex: 0
1632
+ };
1633
+ this.streamState.set(key, state);
1634
+ }
1635
+ const timeline = config.timeline;
1636
+ if (!timeline) {
1637
+ const ts = frame.timestamp ?? 0;
1638
+ state.lastSourceTimestamp = frame.timestamp ?? null;
1639
+ return ts;
1640
+ }
1641
+ const { clipStartUs, compositionFps } = timeline;
1642
+ const sourceTimestamp = frame.timestamp ?? null;
1643
+ if (sourceTimestamp !== null && state.lastSourceTimestamp !== null && sourceTimestamp < state.lastSourceTimestamp) {
1644
+ state.baseTimestamp = null;
1645
+ state.nextFrameIndex = 0;
1646
+ }
1647
+ if (state.baseTimestamp === null) {
1648
+ state.baseTimestamp = sourceTimestamp ?? 0;
1649
+ state.nextFrameIndex = 0;
1650
+ if (state.baseTimestamp > 1e3) {
1651
+ console.warn(
1652
+ `[VideoComposeWorker] First frame timestamp is ${state.baseTimestamp}us, expected ~0. Check MP4Demuxer normalization.`
1653
+ );
1654
+ }
1655
+ }
1656
+ const frameDuration = frameDurationFromFps(compositionFps);
1657
+ let frameIndex = state.nextFrameIndex;
1658
+ if (sourceTimestamp !== null) {
1659
+ const approxIndex = frameIndexFromTimestamp(
1660
+ state.baseTimestamp,
1661
+ sourceTimestamp,
1662
+ compositionFps,
1663
+ "nearest"
1664
+ );
1665
+ frameIndex = Math.max(frameIndex, approxIndex);
1666
+ }
1667
+ const rawTimeline = clipStartUs + frameIndex * frameDuration;
1668
+ const timelineTime = quantizeTimestampToFrame(
1669
+ rawTimeline,
1670
+ clipStartUs,
1671
+ compositionFps,
1672
+ "nearest"
1673
+ );
1674
+ state.nextFrameIndex = frameIndex + 1;
1675
+ state.lastSourceTimestamp = sourceTimestamp;
1676
+ return timelineTime;
1677
+ }
1678
+ }
1679
+ const worker = new VideoComposeWorker();
1680
+ self.addEventListener("beforeunload", () => {
1681
+ worker["handleDispose"]();
1682
+ });
1683
+ //# sourceMappingURL=video-compose.worker-DPzsC21d.js.map