@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,1291 @@
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
+ class BaseDecoder {
482
+ decoder;
483
+ config;
484
+ controller = null;
485
+ constructor(config) {
486
+ this.config = config;
487
+ }
488
+ async initialize() {
489
+ if (this.decoder?.state === "configured") {
490
+ return;
491
+ }
492
+ const isSupported = await this.isConfigSupported(this.config);
493
+ if (!isSupported.supported) {
494
+ throw new Error(
495
+ `Codec not supported: ${this.config.codecString || this.config.codec}`
496
+ );
497
+ }
498
+ this.decoder = this.createDecoder({
499
+ output: this.handleOutput.bind(this),
500
+ error: this.handleError.bind(this)
501
+ });
502
+ await this.configureDecoder(this.config);
503
+ }
504
+ async reconfigure(config) {
505
+ this.config = { ...this.config, ...config };
506
+ if (!this.decoder) {
507
+ await this.initialize();
508
+ return;
509
+ }
510
+ if (this.decoder.state === "configured") {
511
+ await this.decoder.flush();
512
+ }
513
+ const isSupported = await this.isConfigSupported(this.config);
514
+ if (!isSupported.supported) {
515
+ throw new Error(
516
+ `New configuration not supported: ${this.config.codecString || this.config.codec}`
517
+ );
518
+ }
519
+ await this.configureDecoder(this.config);
520
+ }
521
+ async flush() {
522
+ if (!this.decoder) return;
523
+ await this.decoder.flush();
524
+ }
525
+ async reset() {
526
+ if (!this.decoder) return;
527
+ this.decoder.reset();
528
+ this.onReset();
529
+ }
530
+ async close() {
531
+ if (!this.decoder) return;
532
+ if (this.decoder.state === "configured") {
533
+ await this.decoder.flush();
534
+ }
535
+ this.decoder.close();
536
+ this.decoder = void 0;
537
+ }
538
+ get isReady() {
539
+ return this.decoder?.state === "configured";
540
+ }
541
+ get queueSize() {
542
+ return this.decoder?.decodeQueueSize ?? 0;
543
+ }
544
+ handleOutput(data) {
545
+ if (!this.controller) {
546
+ this.closeIfPossible(data);
547
+ return;
548
+ }
549
+ try {
550
+ this.controller.enqueue(data);
551
+ } catch (error) {
552
+ if (error instanceof TypeError && /Cannot enqueue a chunk into a readable stream that is closed/.test(error.message)) {
553
+ this.closeIfPossible(data);
554
+ return;
555
+ }
556
+ throw error;
557
+ }
558
+ }
559
+ handleError(error) {
560
+ console.error(`${this.getDecoderType()} decoder error:`, error);
561
+ this.controller?.error(error);
562
+ }
563
+ closeIfPossible(data) {
564
+ data?.close?.();
565
+ data?.frame?.close?.();
566
+ }
567
+ onReset() {
568
+ }
569
+ createStream() {
570
+ return new TransformStream(
571
+ {
572
+ start: async (controller) => {
573
+ this.controller = controller;
574
+ if (!this.isReady) {
575
+ await this.initialize();
576
+ }
577
+ },
578
+ transform: async (input) => {
579
+ if (!this.decoder || this.decoder.state !== "configured") {
580
+ throw new Error("Decoder not configured");
581
+ }
582
+ if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
583
+ await new Promise((resolve) => {
584
+ const check = () => {
585
+ if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
586
+ resolve();
587
+ } else {
588
+ setTimeout(check, 10);
589
+ }
590
+ };
591
+ check();
592
+ });
593
+ }
594
+ this.decode(input);
595
+ },
596
+ flush: async () => {
597
+ await this.flush();
598
+ }
599
+ },
600
+ {
601
+ highWaterMark: this.highWaterMark,
602
+ size: () => 1
603
+ }
604
+ );
605
+ }
606
+ }
607
+ class VideoChunkDecoder extends BaseDecoder {
608
+ static DEFAULT_HIGH_WATER_MARK = 4;
609
+ static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
610
+ trackId;
611
+ highWaterMark;
612
+ decodeQueueThreshold;
613
+ // GOP tracking (serial is derived from keyframe timestamp for idempotency)
614
+ currentGopSerial = -1;
615
+ isCurrentFrameKeyframe = false;
616
+ // Buffering support for delayed configuration
617
+ bufferedChunks = [];
618
+ isProcessingBuffer = false;
619
+ constructor(trackId, config) {
620
+ super(config || {});
621
+ this.trackId = trackId;
622
+ this.highWaterMark = config?.backpressure?.highWaterMark ?? VideoChunkDecoder.DEFAULT_HIGH_WATER_MARK;
623
+ this.decodeQueueThreshold = config?.backpressure?.decodeQueueThreshold ?? VideoChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
624
+ }
625
+ // Computed properties
626
+ get isConfigured() {
627
+ return this.isReady;
628
+ }
629
+ get state() {
630
+ return this.decoder?.state || "unconfigured";
631
+ }
632
+ /**
633
+ * Update configuration - can be called before or after initialization
634
+ */
635
+ async updateConfig(config) {
636
+ if (!this.isReady && config.codec) {
637
+ await this.configure(config);
638
+ await this.processBufferedChunks();
639
+ }
640
+ }
641
+ // Override createStream to handle GOP tracking and buffering
642
+ // Always create new stream for each clip (ReadableStreams can only be consumed once)
643
+ createStream() {
644
+ return new TransformStream(
645
+ {
646
+ start: async (controller) => {
647
+ this.controller = controller;
648
+ if (this.config?.codec && this.config?.description && !this.isReady) {
649
+ await this.initialize();
650
+ }
651
+ },
652
+ transform: async (chunk) => {
653
+ if (!this.isReady) {
654
+ this.bufferedChunks.push(chunk);
655
+ return;
656
+ }
657
+ if (this.isProcessingBuffer) {
658
+ this.bufferedChunks.push(chunk);
659
+ return;
660
+ }
661
+ await this.processChunk(chunk);
662
+ },
663
+ flush: async () => {
664
+ if (this.isReady) {
665
+ await this.flush();
666
+ }
667
+ }
668
+ },
669
+ {
670
+ highWaterMark: this.highWaterMark,
671
+ size: () => 1
672
+ }
673
+ );
674
+ }
675
+ /**
676
+ * Process a single chunk (extracted from transform for reuse)
677
+ */
678
+ async processChunk(chunk) {
679
+ if (!this.decoder) {
680
+ throw new Error("Decoder not initialized");
681
+ }
682
+ if (this.decoder.state !== "configured") {
683
+ console.error("[VideoChunkDecoder] Decoder in unexpected state:", this.decoder.state);
684
+ throw new Error(`Decoder not configured, state: ${this.decoder.state}`);
685
+ }
686
+ if (chunk.type === "key") {
687
+ this.handleKeyFrame(chunk.timestamp);
688
+ }
689
+ if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
690
+ await new Promise((resolve) => {
691
+ const check = () => {
692
+ if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
693
+ resolve();
694
+ } else {
695
+ setTimeout(check, 20);
696
+ }
697
+ };
698
+ check();
699
+ });
700
+ }
701
+ try {
702
+ this.decode(chunk);
703
+ } catch (error) {
704
+ console.error(`[VideoChunkDecoder] decode error:`, error);
705
+ throw error;
706
+ }
707
+ }
708
+ // Override handleOutput to attach GOP metadata
709
+ handleOutput(frame) {
710
+ const wrappedFrame = {
711
+ frame,
712
+ gopSerial: this.currentGopSerial,
713
+ isKeyframe: this.isCurrentFrameKeyframe,
714
+ timestamp: frame.timestamp
715
+ };
716
+ this.isCurrentFrameKeyframe = false;
717
+ super.handleOutput(wrappedFrame);
718
+ }
719
+ // Implement abstract methods
720
+ async isConfigSupported(config) {
721
+ const result = await VideoDecoder.isConfigSupported({
722
+ codec: config.codec,
723
+ codedWidth: config.width,
724
+ codedHeight: config.height
725
+ });
726
+ return { supported: result.supported ?? false };
727
+ }
728
+ createDecoder(init) {
729
+ return new VideoDecoder(init);
730
+ }
731
+ getDecoderType() {
732
+ return "Video";
733
+ }
734
+ async configureDecoder(config) {
735
+ if (!this.decoder) return;
736
+ const decoderConfig = {
737
+ codec: config.codec,
738
+ codedWidth: config.width,
739
+ codedHeight: config.height,
740
+ hardwareAcceleration: config.hardwareAcceleration || "prefer-hardware",
741
+ optimizeForLatency: false,
742
+ ...config.description && { description: config.description },
743
+ ...config.displayAspectWidth && { displayAspectWidth: config.displayAspectWidth },
744
+ ...config.displayAspectHeight && { displayAspectHeight: config.displayAspectHeight }
745
+ };
746
+ this.decoder.configure(decoderConfig);
747
+ }
748
+ decode(chunk) {
749
+ this.decoder?.decode(chunk);
750
+ }
751
+ // Override reset to clear GOP data
752
+ onReset() {
753
+ this.currentGopSerial = -1;
754
+ this.isCurrentFrameKeyframe = false;
755
+ }
756
+ // GOP management methods
757
+ handleKeyFrame(timestamp) {
758
+ this.currentGopSerial = timestamp;
759
+ this.isCurrentFrameKeyframe = true;
760
+ }
761
+ /**
762
+ * Configure the decoder with codec info (can be called after creation)
763
+ */
764
+ async configure(config) {
765
+ if (this.isReady) {
766
+ await this.reconfigure(config);
767
+ return;
768
+ }
769
+ this.config = config;
770
+ await this.initialize();
771
+ }
772
+ /**
773
+ * Process any buffered chunks after configuration
774
+ */
775
+ async processBufferedChunks() {
776
+ if (!this.isReady || this.bufferedChunks.length === 0) {
777
+ return;
778
+ }
779
+ this.isProcessingBuffer = true;
780
+ const chunks = [...this.bufferedChunks];
781
+ this.bufferedChunks = [];
782
+ for (const chunk of chunks) {
783
+ try {
784
+ await this.processChunk(chunk);
785
+ } catch (error) {
786
+ console.error("[VideoChunkDecoder] Error processing buffered chunk:", error);
787
+ }
788
+ }
789
+ this.isProcessingBuffer = false;
790
+ if (this.bufferedChunks.length > 0) {
791
+ await this.processBufferedChunks();
792
+ }
793
+ }
794
+ // Override close to clean up GOP data
795
+ async close() {
796
+ this.bufferedChunks = [];
797
+ this.onReset();
798
+ await super.close();
799
+ }
800
+ }
801
+ class AudioChunkDecoder extends BaseDecoder {
802
+ // Default values
803
+ static DEFAULT_HIGH_WATER_MARK = 20;
804
+ static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
805
+ // Exposed properties
806
+ trackId;
807
+ // Backpressure configuration
808
+ highWaterMark;
809
+ decodeQueueThreshold;
810
+ constructor(trackId, config) {
811
+ super(config);
812
+ this.trackId = trackId;
813
+ this.highWaterMark = config?.backpressure?.highWaterMark ?? AudioChunkDecoder.DEFAULT_HIGH_WATER_MARK;
814
+ this.decodeQueueThreshold = AudioChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
815
+ }
816
+ // Computed properties
817
+ get isConfigured() {
818
+ return this.isReady;
819
+ }
820
+ get state() {
821
+ return this.decoder?.state || "unconfigured";
822
+ }
823
+ /**
824
+ * Update configuration - can be called before or after initialization
825
+ */
826
+ async updateConfig(config) {
827
+ if (!this.isReady && config.codec) {
828
+ await this.configure(config);
829
+ await this.processBufferedChunks();
830
+ return;
831
+ }
832
+ }
833
+ // Implement abstract methods
834
+ async isConfigSupported(config) {
835
+ const result = await AudioDecoder.isConfigSupported({
836
+ codec: config.codec,
837
+ sampleRate: config.sampleRate,
838
+ numberOfChannels: config.numberOfChannels
839
+ });
840
+ return { supported: result.supported ?? false };
841
+ }
842
+ createDecoder(init) {
843
+ return new AudioDecoder(init);
844
+ }
845
+ getDecoderType() {
846
+ return "Audio";
847
+ }
848
+ async configureDecoder(config) {
849
+ if (!this.decoder) return;
850
+ await this.decoder.configure({
851
+ codec: config.codec,
852
+ sampleRate: config.sampleRate,
853
+ numberOfChannels: config.numberOfChannels,
854
+ ...config.description && { description: config.description }
855
+ });
856
+ }
857
+ decode(chunk) {
858
+ this.decoder?.decode(chunk);
859
+ }
860
+ /**
861
+ * Configure the decoder with codec info (can be called after creation)
862
+ */
863
+ async configure(config) {
864
+ if (this.isReady) {
865
+ await this.reconfigure(config);
866
+ return;
867
+ }
868
+ this.config = config;
869
+ await this.initialize();
870
+ }
871
+ /**
872
+ * Process any buffered chunks after configuration
873
+ * Note: Audio doesn't buffer in current implementation, but keeping for interface consistency
874
+ */
875
+ async processBufferedChunks() {
876
+ }
877
+ }
878
+ const normalizeDescription = (desc) => {
879
+ if (!desc) return void 0;
880
+ if (desc instanceof ArrayBuffer) return desc;
881
+ const view = desc;
882
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
883
+ };
884
+ class DecodeWorker {
885
+ channel;
886
+ // Map of clipId -> decoder instance
887
+ videoDecoders = /* @__PURE__ */ new Map();
888
+ audioDecoders = /* @__PURE__ */ new Map();
889
+ registeredAudioTracks = /* @__PURE__ */ new Set();
890
+ deliveredAudioTracks = /* @__PURE__ */ new Set();
891
+ audioTrackMetadata = /* @__PURE__ */ new Map();
892
+ /** Maximum number of active decoder pairs allowed at the same time */
893
+ static MAX_ACTIVE_DECODERS = 8;
894
+ // Cached default configs merged from orchestrator
895
+ defaultVideoConfig = {};
896
+ defaultAudioConfig = {};
897
+ // Connections to other workers
898
+ composePorts = /* @__PURE__ */ new Map();
899
+ audioDownstreamPort = null;
900
+ demuxPorts = /* @__PURE__ */ new Map();
901
+ // Connections from demux workers
902
+ constructor() {
903
+ this.channel = new WorkerChannel(self, {
904
+ name: "DecodeWorker",
905
+ timeout: 3e4
906
+ });
907
+ this.setupHandlers();
908
+ }
909
+ setupHandlers() {
910
+ this.channel.registerHandler("configure", this.handleConfigure.bind(this));
911
+ this.channel.registerHandler("connect", this.handleConnect.bind(this));
912
+ this.channel.registerHandler("flush", this.handleFlush.bind(this));
913
+ this.channel.registerHandler("reset", this.handleReset.bind(this));
914
+ this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
915
+ this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
916
+ }
917
+ /**
918
+ * Connect handler used by stream pipeline
919
+ */
920
+ async handleConnect(payload) {
921
+ const { port, direction, clipId } = payload;
922
+ if (direction === "upstream") {
923
+ this.demuxPorts.set(clipId || "default", port);
924
+ const channel = new WorkerChannel(port, {
925
+ name: "Demux-Decode",
926
+ timeout: 3e4
927
+ });
928
+ channel.receiveStream((stream, metadata) => {
929
+ this.handleReceiveStream(stream, {
930
+ ...metadata,
931
+ clipStartUs: payload.clipStartUs,
932
+ clipDurationUs: payload.clipDurationUs
933
+ });
934
+ });
935
+ channel.registerHandler("configure", this.handleConfigure.bind(this));
936
+ }
937
+ if (direction === "downstream") {
938
+ if (payload.streamType === "audio") {
939
+ this.audioDownstreamPort?.close();
940
+ this.audioDownstreamPort = port;
941
+ } else {
942
+ this.composePorts.set(clipId || "default", port);
943
+ }
944
+ }
945
+ return { success: true };
946
+ }
947
+ /**
948
+ * Handle configuration message from orchestrator
949
+ * @param payload.initial - If true, initialize worker and recreate decoder instances; otherwise just update config
950
+ */
951
+ async handleConfigure(payload) {
952
+ const { clipId, streamType, codec, width, height, sampleRate, numberOfChannels, description } = payload;
953
+ if (clipId && streamType) {
954
+ try {
955
+ if (streamType === "video") {
956
+ const decoder = this.videoDecoders.get(clipId);
957
+ if (decoder) {
958
+ await decoder.updateConfig({
959
+ codec,
960
+ width,
961
+ height,
962
+ description: normalizeDescription(description)
963
+ });
964
+ }
965
+ } else if (streamType === "audio") {
966
+ const decoder = this.audioDecoders.get(clipId);
967
+ if (decoder) {
968
+ await decoder.updateConfig({
969
+ codec,
970
+ sampleRate,
971
+ numberOfChannels,
972
+ description: normalizeDescription(description)
973
+ });
974
+ }
975
+ }
976
+ } catch (error) {
977
+ console.error("[DecodeWorker] Failed to configure decoder:", error);
978
+ throw {
979
+ code: "CODEC_CONFIG_ERROR",
980
+ message: error.message
981
+ };
982
+ }
983
+ return { success: true };
984
+ }
985
+ const { config } = payload;
986
+ if (!config) {
987
+ return { success: true };
988
+ }
989
+ this.channel.state = WorkerState.Ready;
990
+ if (config.video) {
991
+ Object.assign(this.defaultVideoConfig, config.video);
992
+ }
993
+ if (config.audio) {
994
+ Object.assign(this.defaultAudioConfig, config.audio);
995
+ }
996
+ if (config.video) {
997
+ for (const dec of this.videoDecoders.values()) {
998
+ await dec.updateConfig(config.video);
999
+ }
1000
+ }
1001
+ if (config.audio) {
1002
+ for (const dec of this.audioDecoders.values()) {
1003
+ await dec.updateConfig(config.audio);
1004
+ }
1005
+ }
1006
+ return { success: true };
1007
+ }
1008
+ async handleReceiveStream(stream, metadata) {
1009
+ const clipId = metadata?.clipId || "default";
1010
+ const streamType = metadata?.streamType;
1011
+ if (streamType === "video") {
1012
+ const decoder = await this.getOrCreateDecoder("video", clipId, metadata);
1013
+ const transform = decoder.createStream();
1014
+ const composePort = this.composePorts.get(clipId);
1015
+ if (composePort) {
1016
+ const channel = new WorkerChannel(composePort, {
1017
+ name: "Decode-Compose",
1018
+ timeout: 3e4
1019
+ });
1020
+ channel.sendStream(transform.readable, {
1021
+ streamType: "video",
1022
+ clipId
1023
+ });
1024
+ stream.pipeTo(transform.writable).catch(
1025
+ (error) => console.error("[DecodeWorker] Video stream pipe error:", clipId, error)
1026
+ );
1027
+ }
1028
+ } else if (streamType === "audio") {
1029
+ const decoder = await this.getOrCreateDecoder("audio", clipId, metadata);
1030
+ const transform = decoder.createStream();
1031
+ stream.pipeTo(transform.writable).catch((error) => {
1032
+ console.error("[DecodeWorker] Audio stream pipe error:", error);
1033
+ });
1034
+ const trackId = metadata?.trackId ?? clipId;
1035
+ await this.registerAudioTrack(trackId, clipId, metadata);
1036
+ this.channel.sendStream(transform.readable, {
1037
+ streamType: "audio",
1038
+ clipId,
1039
+ trackId,
1040
+ clipStartUs: metadata?.clipStartUs ?? 0,
1041
+ clipDurationUs: metadata?.clipDurationUs ?? 0
1042
+ });
1043
+ this.deliveredAudioTracks.add(trackId);
1044
+ }
1045
+ }
1046
+ /**
1047
+ * Flush decoders
1048
+ */
1049
+ async handleFlush(payload) {
1050
+ try {
1051
+ if (!payload?.type || payload.type === "video") {
1052
+ for (const dec of this.videoDecoders.values()) {
1053
+ await dec.flush();
1054
+ }
1055
+ }
1056
+ if (!payload?.type || payload.type === "audio") {
1057
+ for (const dec of this.audioDecoders.values()) {
1058
+ await dec.flush();
1059
+ }
1060
+ }
1061
+ return { success: true };
1062
+ } catch (error) {
1063
+ throw {
1064
+ code: "FLUSH_ERROR",
1065
+ message: error.message
1066
+ };
1067
+ }
1068
+ }
1069
+ /**
1070
+ * Reset decoders
1071
+ */
1072
+ async handleReset(payload) {
1073
+ try {
1074
+ if (!payload?.type || payload.type === "video") {
1075
+ for (const dec of this.videoDecoders.values()) {
1076
+ await dec.reset();
1077
+ }
1078
+ }
1079
+ if (!payload?.type || payload.type === "audio") {
1080
+ for (const dec of this.audioDecoders.values()) {
1081
+ await dec.reset();
1082
+ }
1083
+ }
1084
+ this.channel.notify("reset_complete", {
1085
+ type: payload?.type || "all"
1086
+ });
1087
+ return { success: true };
1088
+ } catch (error) {
1089
+ throw {
1090
+ code: "RESET_ERROR",
1091
+ message: error.message
1092
+ };
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Get decoder statistics
1097
+ */
1098
+ async handleGetStats() {
1099
+ const stats = {};
1100
+ if (this.videoDecoders.size) {
1101
+ stats.video = Array.from(this.videoDecoders.entries()).map(([clipId, dec]) => ({
1102
+ clipId,
1103
+ configured: dec.isConfigured,
1104
+ queueSize: dec.queueSize,
1105
+ state: dec.state
1106
+ }));
1107
+ }
1108
+ if (this.audioDecoders.size) {
1109
+ stats.audio = Array.from(this.audioDecoders.entries()).map(([clipId, dec]) => ({
1110
+ clipId,
1111
+ configured: dec.isConfigured,
1112
+ queueSize: dec.queueSize,
1113
+ state: dec.state
1114
+ }));
1115
+ }
1116
+ return stats;
1117
+ }
1118
+ /**
1119
+ * Dispose worker and cleanup resources
1120
+ */
1121
+ async handleDispose() {
1122
+ for (const dec of this.videoDecoders.values()) {
1123
+ await dec.close();
1124
+ }
1125
+ for (const dec of this.audioDecoders.values()) {
1126
+ await dec.close();
1127
+ }
1128
+ this.videoDecoders.clear();
1129
+ this.audioDecoders.clear();
1130
+ for (const port of this.composePorts.values()) {
1131
+ port.close();
1132
+ }
1133
+ this.composePorts.clear();
1134
+ if (this.audioDownstreamPort) {
1135
+ this.audioDownstreamPort.close();
1136
+ this.audioDownstreamPort = null;
1137
+ }
1138
+ this.registeredAudioTracks.clear();
1139
+ this.deliveredAudioTracks.clear();
1140
+ this.audioTrackMetadata.clear();
1141
+ for (const port of this.demuxPorts.values()) {
1142
+ port.close();
1143
+ }
1144
+ this.demuxPorts.clear();
1145
+ this.channel.state = WorkerState.Disposed;
1146
+ return { success: true };
1147
+ }
1148
+ /**
1149
+ * Get existing decoder for clip or create a new one (with LRU eviction)
1150
+ */
1151
+ async getOrCreateDecoder(kind, clipId, metadata) {
1152
+ if (kind === "video") {
1153
+ let decoder = this.videoDecoders.get(clipId);
1154
+ if (!decoder) {
1155
+ decoder = new VideoChunkDecoder(
1156
+ clipId,
1157
+ metadata ? {
1158
+ ...this.defaultVideoConfig,
1159
+ codec: metadata.codec,
1160
+ width: metadata.width,
1161
+ height: metadata.height,
1162
+ description: normalizeDescription(metadata.description)
1163
+ } : void 0
1164
+ );
1165
+ this.evictIfNeeded("video");
1166
+ this.videoDecoders.set(clipId, decoder);
1167
+ }
1168
+ this.videoDecoders.delete(clipId);
1169
+ this.videoDecoders.set(clipId, decoder);
1170
+ return decoder;
1171
+ } else {
1172
+ let decoder = this.audioDecoders.get(clipId);
1173
+ if (!decoder) {
1174
+ decoder = new AudioChunkDecoder(
1175
+ clipId,
1176
+ metadata ? {
1177
+ ...this.defaultAudioConfig,
1178
+ codec: metadata.codec,
1179
+ sampleRate: metadata.sampleRate,
1180
+ numberOfChannels: metadata.numberOfChannels,
1181
+ description: normalizeDescription(metadata.description)
1182
+ } : void 0
1183
+ );
1184
+ this.evictIfNeeded("audio");
1185
+ this.audioDecoders.set(clipId, decoder);
1186
+ }
1187
+ this.audioDecoders.delete(clipId);
1188
+ this.audioDecoders.set(clipId, decoder);
1189
+ return decoder;
1190
+ }
1191
+ }
1192
+ /**
1193
+ * Evict least-recently-used decoder if we exceed MAX_ACTIVE_DECODERS.
1194
+ */
1195
+ evictIfNeeded(kind) {
1196
+ const map = kind === "video" ? this.videoDecoders : this.audioDecoders;
1197
+ if (map.size < DecodeWorker.MAX_ACTIVE_DECODERS) return;
1198
+ const [lrucId, lruDecoder] = map.entries().next().value;
1199
+ lruDecoder.close().catch(() => void 0);
1200
+ map.delete(lrucId);
1201
+ }
1202
+ async registerAudioTrack(trackId, clipId, metadata) {
1203
+ const record = {
1204
+ clipId,
1205
+ config: this.extractTrackConfig(metadata?.runtimeConfig),
1206
+ sampleRate: metadata?.sampleRate,
1207
+ numberOfChannels: metadata?.numberOfChannels,
1208
+ type: metadata?.trackType ?? "other"
1209
+ };
1210
+ this.audioTrackMetadata.set(trackId, record);
1211
+ if (this.registeredAudioTracks.has(trackId)) {
1212
+ await this.sendAudioTrackUpdate(trackId, record);
1213
+ return;
1214
+ }
1215
+ this.registeredAudioTracks.add(trackId);
1216
+ await this.sendAudioTrackAdd(trackId, record);
1217
+ }
1218
+ // private unregisterAudioTrack(trackId: string): void {
1219
+ // if (!this.registeredAudioTracks.delete(trackId)) {
1220
+ // return;
1221
+ // }
1222
+ // const record = this.audioTrackMetadata.get(trackId);
1223
+ // this.audioTrackMetadata.delete(trackId);
1224
+ // if (!record) {
1225
+ // return;
1226
+ // }
1227
+ // const channel = this.ensureComposeChannel();
1228
+ // channel
1229
+ // ?.send(WorkerMessageType.AudioTrackRemove, {
1230
+ // clipId: record.clipId,
1231
+ // trackId,
1232
+ // })
1233
+ // .catch((error) => {
1234
+ // console.warn('[DecodeWorker] Failed to notify track removal', error);
1235
+ // });
1236
+ // }
1237
+ extractTrackConfig(config) {
1238
+ return {
1239
+ startTimeUs: config?.startTimeUs ?? 0,
1240
+ durationUs: config?.durationUs,
1241
+ volume: config?.volume ?? 1,
1242
+ fadeIn: config?.fadeIn,
1243
+ fadeOut: config?.fadeOut,
1244
+ effects: config?.effects ?? [],
1245
+ duckingTag: config?.duckingTag
1246
+ };
1247
+ }
1248
+ async sendAudioTrackAdd(trackId, record) {
1249
+ const channel = this.ensureComposeChannel();
1250
+ if (!channel) {
1251
+ return;
1252
+ }
1253
+ await channel.send(WorkerMessageType.AudioTrackAdd, {
1254
+ clipId: record.clipId,
1255
+ trackId,
1256
+ config: record.config,
1257
+ sampleRate: record.sampleRate,
1258
+ numberOfChannels: record.numberOfChannels,
1259
+ type: record.type
1260
+ });
1261
+ }
1262
+ async sendAudioTrackUpdate(trackId, record) {
1263
+ const channel = this.ensureComposeChannel();
1264
+ if (!channel) {
1265
+ return;
1266
+ }
1267
+ if (!channel) {
1268
+ return;
1269
+ }
1270
+ await channel.send(WorkerMessageType.AudioTrackUpdate, {
1271
+ clipId: record.clipId,
1272
+ trackId,
1273
+ config: record.config,
1274
+ type: record.type
1275
+ });
1276
+ }
1277
+ ensureComposeChannel() {
1278
+ if (!this.audioDownstreamPort) {
1279
+ return null;
1280
+ }
1281
+ return new WorkerChannel(this.audioDownstreamPort, {
1282
+ name: "Decode-AudioCompose",
1283
+ timeout: 3e4
1284
+ });
1285
+ }
1286
+ }
1287
+ const worker = new DecodeWorker();
1288
+ self.addEventListener("beforeunload", () => {
1289
+ worker["handleDispose"]();
1290
+ });
1291
+ //# sourceMappingURL=decode.worker-DpWHsc7R.js.map