@meframe/core 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/Meframe.d.ts.map +1 -1
  2. package/dist/Meframe.js +36 -0
  3. package/dist/Meframe.js.map +1 -1
  4. package/dist/cache/L2Cache.d.ts.map +1 -1
  5. package/dist/cache/L2Cache.js +0 -3
  6. package/dist/cache/L2Cache.js.map +1 -1
  7. package/dist/config/defaults.d.ts.map +1 -1
  8. package/dist/config/defaults.js +1 -0
  9. package/dist/config/defaults.js.map +1 -1
  10. package/dist/config/types.d.ts +1 -0
  11. package/dist/config/types.d.ts.map +1 -1
  12. package/dist/controllers/PreRenderService.d.ts +11 -1
  13. package/dist/controllers/PreRenderService.d.ts.map +1 -1
  14. package/dist/controllers/PreRenderService.js +65 -7
  15. package/dist/controllers/PreRenderService.js.map +1 -1
  16. package/dist/event/events.d.ts +12 -3
  17. package/dist/event/events.d.ts.map +1 -1
  18. package/dist/event/events.js +1 -0
  19. package/dist/event/events.js.map +1 -1
  20. package/dist/orchestrator/Orchestrator.d.ts +2 -2
  21. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  22. package/dist/orchestrator/Orchestrator.js +48 -46
  23. package/dist/orchestrator/Orchestrator.js.map +1 -1
  24. package/dist/orchestrator/VideoClipSession.d.ts +1 -0
  25. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  26. package/dist/orchestrator/VideoClipSession.js +104 -74
  27. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  28. package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
  29. package/dist/stages/compose/GlobalAudioSession.js +11 -3
  30. package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
  31. package/dist/stages/encode/BaseEncoder.js +1 -1
  32. package/dist/stages/encode/BaseEncoder.js.map +1 -1
  33. package/dist/stages/load/ResourceLoader.js +1 -1
  34. package/dist/stages/load/ResourceLoader.js.map +1 -1
  35. package/dist/worker/WorkerPool.d.ts.map +1 -1
  36. package/dist/worker/WorkerPool.js +6 -2
  37. package/dist/worker/WorkerPool.js.map +1 -1
  38. package/dist/worker/types.d.ts +1 -1
  39. package/dist/worker/types.d.ts.map +1 -1
  40. package/dist/worker/types.js.map +1 -1
  41. package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
  42. package/dist/workers/BaseDecoder.js +130 -0
  43. package/dist/workers/BaseDecoder.js.map +1 -0
  44. package/dist/workers/WorkerChannel.js.map +1 -1
  45. package/dist/workers/stages/compose/video-compose.worker.js +13 -9
  46. package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
  47. package/dist/workers/stages/decode/audio-decode.worker.js +243 -0
  48. package/dist/workers/stages/decode/audio-decode.worker.js.map +1 -0
  49. package/dist/workers/stages/decode/video-decode.worker.js +346 -0
  50. package/dist/workers/stages/decode/video-decode.worker.js.map +1 -0
  51. package/dist/workers/stages/encode/video-encode.worker.js +340 -0
  52. package/dist/workers/stages/encode/video-encode.worker.js.map +1 -0
  53. package/package.json +1 -1
  54. package/dist/workers/stages/decode/decode.worker.js +0 -826
  55. package/dist/workers/stages/decode/decode.worker.js.map +0 -1
  56. package/dist/workers/stages/encode/encode.worker.js +0 -547
  57. package/dist/workers/stages/encode/encode.worker.js.map +0 -1
@@ -1,826 +0,0 @@
1
- import { W as WorkerChannel, a as WorkerMessageType, b as WorkerState } from "../../WorkerChannel.js";
2
- class BaseDecoder {
3
- decoder;
4
- config;
5
- controller = null;
6
- constructor(config) {
7
- this.config = config;
8
- }
9
- async initialize() {
10
- if (this.decoder?.state === "configured") {
11
- return;
12
- }
13
- const isSupported = await this.isConfigSupported(this.config);
14
- if (!isSupported.supported) {
15
- throw new Error(
16
- `Codec not supported: ${this.config.codecString || this.config.codec}`
17
- );
18
- }
19
- this.decoder = this.createDecoder({
20
- output: this.handleOutput.bind(this),
21
- error: this.handleError.bind(this)
22
- });
23
- await this.configureDecoder(this.config);
24
- }
25
- async reconfigure(config) {
26
- this.config = { ...this.config, ...config };
27
- if (!this.decoder) {
28
- await this.initialize();
29
- return;
30
- }
31
- if (this.decoder.state === "configured") {
32
- await this.decoder.flush();
33
- }
34
- const isSupported = await this.isConfigSupported(this.config);
35
- if (!isSupported.supported) {
36
- throw new Error(
37
- `New configuration not supported: ${this.config.codecString || this.config.codec}`
38
- );
39
- }
40
- await this.configureDecoder(this.config);
41
- }
42
- async flush() {
43
- if (!this.decoder) return;
44
- await this.decoder.flush();
45
- }
46
- async reset() {
47
- if (!this.decoder) return;
48
- this.decoder.reset();
49
- this.onReset();
50
- }
51
- async close() {
52
- if (!this.decoder) return;
53
- if (this.decoder.state === "configured") {
54
- await this.decoder.flush();
55
- }
56
- this.decoder.close();
57
- this.decoder = void 0;
58
- }
59
- get isReady() {
60
- return this.decoder?.state === "configured";
61
- }
62
- get queueSize() {
63
- return this.decoder?.decodeQueueSize ?? 0;
64
- }
65
- handleOutput(data) {
66
- if (!this.controller) {
67
- this.closeIfPossible(data);
68
- return;
69
- }
70
- try {
71
- this.controller.enqueue(data);
72
- } catch (error) {
73
- if (error instanceof TypeError && /Cannot enqueue a chunk into a readable stream that is closed/.test(error.message)) {
74
- this.closeIfPossible(data);
75
- return;
76
- }
77
- throw error;
78
- }
79
- }
80
- handleError(error) {
81
- console.error(`${this.getDecoderType()} decoder error:`, error);
82
- this.controller?.error(error);
83
- }
84
- closeIfPossible(data) {
85
- data?.close?.();
86
- data?.frame?.close?.();
87
- }
88
- onReset() {
89
- }
90
- createStream() {
91
- return new TransformStream(
92
- {
93
- start: async (controller) => {
94
- this.controller = controller;
95
- if (!this.isReady) {
96
- await this.initialize();
97
- }
98
- },
99
- transform: async (input) => {
100
- if (!this.decoder || this.decoder.state !== "configured") {
101
- throw new Error("Decoder not configured");
102
- }
103
- if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
104
- await new Promise((resolve) => {
105
- const check = () => {
106
- if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
107
- resolve();
108
- } else {
109
- setTimeout(check, 10);
110
- }
111
- };
112
- check();
113
- });
114
- }
115
- this.decode(input);
116
- },
117
- flush: async () => {
118
- await this.flush();
119
- }
120
- },
121
- {
122
- highWaterMark: this.highWaterMark,
123
- size: () => 1
124
- }
125
- );
126
- }
127
- }
128
- class VideoChunkDecoder extends BaseDecoder {
129
- static DEFAULT_HIGH_WATER_MARK = 4;
130
- static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
131
- trackId;
132
- highWaterMark;
133
- decodeQueueThreshold;
134
- // GOP tracking (serial is derived from keyframe timestamp for idempotency)
135
- currentGopSerial = -1;
136
- isCurrentFrameKeyframe = false;
137
- // Buffering support for delayed configuration
138
- bufferedChunks = [];
139
- isProcessingBuffer = false;
140
- constructor(trackId, config) {
141
- super(config || {});
142
- this.trackId = trackId;
143
- this.highWaterMark = config?.backpressure?.highWaterMark ?? VideoChunkDecoder.DEFAULT_HIGH_WATER_MARK;
144
- this.decodeQueueThreshold = config?.backpressure?.decodeQueueThreshold ?? VideoChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
145
- }
146
- // Computed properties
147
- get isConfigured() {
148
- return this.isReady;
149
- }
150
- get state() {
151
- return this.decoder?.state || "unconfigured";
152
- }
153
- /**
154
- * Update configuration - can be called before or after initialization
155
- */
156
- async updateConfig(config) {
157
- if (!this.isReady && config.codec) {
158
- await this.configure(config);
159
- await this.processBufferedChunks();
160
- }
161
- }
162
- // Override createStream to handle GOP tracking and buffering
163
- // Always create new stream for each clip (ReadableStreams can only be consumed once)
164
- createStream() {
165
- return new TransformStream(
166
- {
167
- start: async (controller) => {
168
- this.controller = controller;
169
- if (this.config?.codec && this.config?.description && !this.isReady) {
170
- await this.initialize();
171
- }
172
- },
173
- transform: async (chunk) => {
174
- if (!this.isReady) {
175
- this.bufferedChunks.push(chunk);
176
- return;
177
- }
178
- if (this.isProcessingBuffer) {
179
- this.bufferedChunks.push(chunk);
180
- return;
181
- }
182
- await this.processChunk(chunk);
183
- },
184
- flush: async () => {
185
- if (this.isReady) {
186
- await this.flush();
187
- }
188
- }
189
- },
190
- {
191
- highWaterMark: this.highWaterMark,
192
- size: () => 1
193
- }
194
- );
195
- }
196
- /**
197
- * Process a single chunk (extracted from transform for reuse)
198
- */
199
- async processChunk(chunk) {
200
- if (!this.decoder) {
201
- throw new Error("Decoder not initialized");
202
- }
203
- if (this.decoder.state !== "configured") {
204
- console.error("[VideoChunkDecoder] Decoder in unexpected state:", this.decoder.state);
205
- throw new Error(`Decoder not configured, state: ${this.decoder.state}`);
206
- }
207
- if (chunk.type === "key") {
208
- this.handleKeyFrame(chunk.timestamp);
209
- }
210
- if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
211
- await new Promise((resolve) => {
212
- const check = () => {
213
- if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
214
- resolve();
215
- } else {
216
- setTimeout(check, 20);
217
- }
218
- };
219
- check();
220
- });
221
- }
222
- try {
223
- this.decode(chunk);
224
- } catch (error) {
225
- console.error(`[VideoChunkDecoder] decode error:`, error);
226
- throw error;
227
- }
228
- }
229
- // Override handleOutput to attach GOP metadata
230
- handleOutput(frame) {
231
- const wrappedFrame = {
232
- frame,
233
- gopSerial: this.currentGopSerial,
234
- isKeyframe: this.isCurrentFrameKeyframe,
235
- timestamp: frame.timestamp
236
- };
237
- this.isCurrentFrameKeyframe = false;
238
- super.handleOutput(wrappedFrame);
239
- }
240
- // Implement abstract methods
241
- async isConfigSupported(config) {
242
- const result = await VideoDecoder.isConfigSupported({
243
- codec: config.codec,
244
- codedWidth: config.width,
245
- codedHeight: config.height
246
- });
247
- return { supported: result.supported ?? false };
248
- }
249
- createDecoder(init) {
250
- return new VideoDecoder(init);
251
- }
252
- getDecoderType() {
253
- return "Video";
254
- }
255
- async configureDecoder(config) {
256
- if (!this.decoder) return;
257
- const decoderConfig = {
258
- codec: config.codec,
259
- codedWidth: config.width,
260
- codedHeight: config.height,
261
- hardwareAcceleration: config.hardwareAcceleration || "prefer-hardware",
262
- optimizeForLatency: false,
263
- ...config.description && { description: config.description },
264
- ...config.displayAspectWidth && { displayAspectWidth: config.displayAspectWidth },
265
- ...config.displayAspectHeight && { displayAspectHeight: config.displayAspectHeight }
266
- };
267
- this.decoder.configure(decoderConfig);
268
- }
269
- decode(chunk) {
270
- this.decoder?.decode(chunk);
271
- }
272
- // Override reset to clear GOP data
273
- onReset() {
274
- this.currentGopSerial = -1;
275
- this.isCurrentFrameKeyframe = false;
276
- }
277
- // GOP management methods
278
- handleKeyFrame(timestamp) {
279
- this.currentGopSerial = timestamp;
280
- this.isCurrentFrameKeyframe = true;
281
- }
282
- /**
283
- * Configure the decoder with codec info (can be called after creation)
284
- */
285
- async configure(config) {
286
- if (this.isReady) {
287
- await this.reconfigure(config);
288
- return;
289
- }
290
- this.config = config;
291
- await this.initialize();
292
- }
293
- /**
294
- * Process any buffered chunks after configuration
295
- */
296
- async processBufferedChunks() {
297
- if (!this.isReady || this.bufferedChunks.length === 0) {
298
- return;
299
- }
300
- this.isProcessingBuffer = true;
301
- const chunks = [...this.bufferedChunks];
302
- this.bufferedChunks = [];
303
- for (const chunk of chunks) {
304
- try {
305
- await this.processChunk(chunk);
306
- } catch (error) {
307
- console.error("[VideoChunkDecoder] Error processing buffered chunk:", error);
308
- }
309
- }
310
- this.isProcessingBuffer = false;
311
- if (this.bufferedChunks.length > 0) {
312
- await this.processBufferedChunks();
313
- }
314
- }
315
- // Override close to clean up GOP data
316
- async close() {
317
- this.bufferedChunks = [];
318
- this.onReset();
319
- await super.close();
320
- }
321
- }
322
- class AudioChunkDecoder extends BaseDecoder {
323
- // Default values
324
- static DEFAULT_HIGH_WATER_MARK = 20;
325
- static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
326
- // Exposed properties
327
- trackId;
328
- // Backpressure configuration
329
- highWaterMark;
330
- decodeQueueThreshold;
331
- constructor(trackId, config) {
332
- super(config);
333
- this.trackId = trackId;
334
- this.highWaterMark = config?.backpressure?.highWaterMark ?? AudioChunkDecoder.DEFAULT_HIGH_WATER_MARK;
335
- this.decodeQueueThreshold = AudioChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
336
- }
337
- // Computed properties
338
- get isConfigured() {
339
- return this.isReady;
340
- }
341
- get state() {
342
- return this.decoder?.state || "unconfigured";
343
- }
344
- /**
345
- * Update configuration - can be called before or after initialization
346
- */
347
- async updateConfig(config) {
348
- if (!this.isReady && config.codec) {
349
- await this.configure(config);
350
- await this.processBufferedChunks();
351
- return;
352
- }
353
- }
354
- // Implement abstract methods
355
- async isConfigSupported(config) {
356
- const result = await AudioDecoder.isConfigSupported({
357
- codec: config.codec,
358
- sampleRate: config.sampleRate,
359
- numberOfChannels: config.numberOfChannels
360
- });
361
- return { supported: result.supported ?? false };
362
- }
363
- createDecoder(init) {
364
- return new AudioDecoder(init);
365
- }
366
- getDecoderType() {
367
- return "Audio";
368
- }
369
- async configureDecoder(config) {
370
- if (!this.decoder) return;
371
- await this.decoder.configure({
372
- codec: config.codec,
373
- sampleRate: config.sampleRate,
374
- numberOfChannels: config.numberOfChannels,
375
- ...config.description && { description: config.description }
376
- });
377
- }
378
- decode(chunk) {
379
- this.decoder?.decode(chunk);
380
- }
381
- /**
382
- * Configure the decoder with codec info (can be called after creation)
383
- */
384
- async configure(config) {
385
- if (this.isReady) {
386
- await this.reconfigure(config);
387
- return;
388
- }
389
- this.config = config;
390
- await this.initialize();
391
- }
392
- /**
393
- * Process any buffered chunks after configuration
394
- * Note: Audio doesn't buffer in current implementation, but keeping for interface consistency
395
- */
396
- async processBufferedChunks() {
397
- }
398
- }
399
- const normalizeDescription = (desc) => {
400
- if (!desc) return void 0;
401
- if (desc instanceof ArrayBuffer) return desc;
402
- const view = desc;
403
- return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
404
- };
405
- class DecodeWorker {
406
- channel;
407
- // Map of clipId -> decoder instance
408
- videoDecoders = /* @__PURE__ */ new Map();
409
- audioDecoders = /* @__PURE__ */ new Map();
410
- registeredAudioTracks = /* @__PURE__ */ new Set();
411
- deliveredAudioTracks = /* @__PURE__ */ new Set();
412
- audioTrackMetadata = /* @__PURE__ */ new Map();
413
- /** Maximum number of active decoder pairs allowed at the same time */
414
- static MAX_ACTIVE_DECODERS = 8;
415
- // Cached default configs merged from orchestrator
416
- defaultVideoConfig = {};
417
- defaultAudioConfig = {};
418
- // Connections to other workers
419
- composePorts = /* @__PURE__ */ new Map();
420
- audioDownstreamPort = null;
421
- demuxPorts = /* @__PURE__ */ new Map();
422
- // Connections from demux workers
423
- constructor() {
424
- this.channel = new WorkerChannel(self, {
425
- name: "DecodeWorker",
426
- timeout: 3e4
427
- });
428
- this.setupHandlers();
429
- }
430
- setupHandlers() {
431
- this.channel.registerHandler("configure", this.handleConfigure.bind(this));
432
- this.channel.registerHandler("connect", this.handleConnect.bind(this));
433
- this.channel.registerHandler("flush", this.handleFlush.bind(this));
434
- this.channel.registerHandler("reset", this.handleReset.bind(this));
435
- this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
436
- this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
437
- }
438
- /**
439
- * Connect handler used by stream pipeline
440
- */
441
- async handleConnect(payload) {
442
- const { port, direction, sessionId } = payload;
443
- if (direction === "upstream") {
444
- this.demuxPorts.set(sessionId || "default", port);
445
- const channel = new WorkerChannel(port, {
446
- name: "Demux-Decode",
447
- timeout: 3e4
448
- });
449
- channel.receiveStream((stream, metadata) => {
450
- this.handleReceiveStream(stream, {
451
- ...metadata,
452
- clipStartUs: payload.clipStartUs,
453
- clipDurationUs: payload.clipDurationUs
454
- });
455
- });
456
- channel.registerHandler("configure", this.handleConfigure.bind(this));
457
- }
458
- if (direction === "downstream") {
459
- if (payload.streamType === "audio") {
460
- this.audioDownstreamPort?.close();
461
- this.audioDownstreamPort = port;
462
- } else {
463
- this.composePorts.set(sessionId || "default", port);
464
- }
465
- }
466
- return { success: true };
467
- }
468
- /**
469
- * Handle configuration message from orchestrator
470
- * @param payload.initial - If true, initialize worker and recreate decoder instances; otherwise just update config
471
- */
472
- async handleConfigure(payload) {
473
- const {
474
- sessionId,
475
- streamType,
476
- codec,
477
- width,
478
- height,
479
- sampleRate,
480
- numberOfChannels,
481
- description
482
- } = payload;
483
- if (sessionId && streamType) {
484
- try {
485
- if (streamType === "video") {
486
- const decoder = this.videoDecoders.get(sessionId);
487
- if (decoder) {
488
- await decoder.updateConfig({
489
- codec,
490
- width,
491
- height,
492
- description: normalizeDescription(description)
493
- });
494
- }
495
- } else if (streamType === "audio") {
496
- const decoder = this.audioDecoders.get(sessionId);
497
- if (decoder) {
498
- await decoder.updateConfig({
499
- codec,
500
- sampleRate,
501
- numberOfChannels,
502
- description: normalizeDescription(description)
503
- });
504
- }
505
- }
506
- } catch (error) {
507
- console.error("[DecodeWorker] Failed to configure decoder:", error);
508
- throw {
509
- code: "CODEC_CONFIG_ERROR",
510
- message: error.message
511
- };
512
- }
513
- return { success: true };
514
- }
515
- const { config } = payload;
516
- if (!config) {
517
- return { success: true };
518
- }
519
- this.channel.state = WorkerState.Ready;
520
- if (config.video) {
521
- Object.assign(this.defaultVideoConfig, config.video);
522
- }
523
- if (config.audio) {
524
- Object.assign(this.defaultAudioConfig, config.audio);
525
- }
526
- if (config.video) {
527
- for (const dec of this.videoDecoders.values()) {
528
- await dec.updateConfig(config.video);
529
- }
530
- }
531
- if (config.audio) {
532
- for (const dec of this.audioDecoders.values()) {
533
- await dec.updateConfig(config.audio);
534
- }
535
- }
536
- return { success: true };
537
- }
538
- async handleReceiveStream(stream, metadata) {
539
- const sessionId = metadata?.sessionId || "default";
540
- const streamType = metadata?.streamType;
541
- if (streamType === "video") {
542
- const decoder = await this.getOrCreateDecoder("video", sessionId, metadata);
543
- const transform = decoder.createStream();
544
- const composePort = this.composePorts.get(sessionId);
545
- if (composePort) {
546
- const channel = new WorkerChannel(composePort, {
547
- name: "Decode-Compose",
548
- timeout: 3e4
549
- });
550
- channel.sendStream(transform.readable, {
551
- streamType: "video",
552
- sessionId
553
- });
554
- stream.pipeTo(transform.writable).catch(
555
- (error) => console.error("[DecodeWorker] Video stream pipe error:", sessionId, error)
556
- );
557
- }
558
- } else if (streamType === "audio") {
559
- const decoder = await this.getOrCreateDecoder("audio", sessionId, metadata);
560
- const transform = decoder.createStream();
561
- stream.pipeTo(transform.writable).catch((error) => {
562
- console.error("[DecodeWorker] Audio stream pipe error:", error);
563
- });
564
- const trackId = metadata?.trackId ?? sessionId;
565
- await this.registerAudioTrack(trackId, sessionId, metadata);
566
- this.channel.sendStream(transform.readable, {
567
- streamType: "audio",
568
- sessionId,
569
- trackId,
570
- clipStartUs: metadata?.clipStartUs ?? 0,
571
- clipDurationUs: metadata?.clipDurationUs ?? 0
572
- });
573
- this.deliveredAudioTracks.add(trackId);
574
- }
575
- }
576
- /**
577
- * Flush decoders
578
- */
579
- async handleFlush(payload) {
580
- try {
581
- if (!payload?.type || payload.type === "video") {
582
- for (const dec of this.videoDecoders.values()) {
583
- await dec.flush();
584
- }
585
- }
586
- if (!payload?.type || payload.type === "audio") {
587
- for (const dec of this.audioDecoders.values()) {
588
- await dec.flush();
589
- }
590
- }
591
- return { success: true };
592
- } catch (error) {
593
- throw {
594
- code: "FLUSH_ERROR",
595
- message: error.message
596
- };
597
- }
598
- }
599
- /**
600
- * Reset decoders
601
- */
602
- async handleReset(payload) {
603
- try {
604
- if (!payload?.type || payload.type === "video") {
605
- for (const dec of this.videoDecoders.values()) {
606
- await dec.reset();
607
- }
608
- }
609
- if (!payload?.type || payload.type === "audio") {
610
- for (const dec of this.audioDecoders.values()) {
611
- await dec.reset();
612
- }
613
- }
614
- this.channel.notify("reset_complete", {
615
- type: payload?.type || "all"
616
- });
617
- return { success: true };
618
- } catch (error) {
619
- throw {
620
- code: "RESET_ERROR",
621
- message: error.message
622
- };
623
- }
624
- }
625
- /**
626
- * Get decoder statistics
627
- */
628
- async handleGetStats() {
629
- const stats = {};
630
- if (this.videoDecoders.size) {
631
- stats.video = Array.from(this.videoDecoders.entries()).map(([clipId, dec]) => ({
632
- clipId,
633
- configured: dec.isConfigured,
634
- queueSize: dec.queueSize,
635
- state: dec.state
636
- }));
637
- }
638
- if (this.audioDecoders.size) {
639
- stats.audio = Array.from(this.audioDecoders.entries()).map(([clipId, dec]) => ({
640
- clipId,
641
- configured: dec.isConfigured,
642
- queueSize: dec.queueSize,
643
- state: dec.state
644
- }));
645
- }
646
- return stats;
647
- }
648
- /**
649
- * Dispose worker and cleanup resources
650
- */
651
- async handleDispose() {
652
- for (const dec of this.videoDecoders.values()) {
653
- await dec.close();
654
- }
655
- for (const dec of this.audioDecoders.values()) {
656
- await dec.close();
657
- }
658
- this.videoDecoders.clear();
659
- this.audioDecoders.clear();
660
- for (const port of this.composePorts.values()) {
661
- port.close();
662
- }
663
- this.composePorts.clear();
664
- if (this.audioDownstreamPort) {
665
- this.audioDownstreamPort.close();
666
- this.audioDownstreamPort = null;
667
- }
668
- this.registeredAudioTracks.clear();
669
- this.deliveredAudioTracks.clear();
670
- this.audioTrackMetadata.clear();
671
- for (const port of this.demuxPorts.values()) {
672
- port.close();
673
- }
674
- this.demuxPorts.clear();
675
- this.channel.state = WorkerState.Disposed;
676
- return { success: true };
677
- }
678
- /**
679
- * Get existing decoder for clip or create a new one (with LRU eviction)
680
- */
681
- async getOrCreateDecoder(kind, clipId, metadata) {
682
- if (kind === "video") {
683
- let decoder = this.videoDecoders.get(clipId);
684
- if (!decoder) {
685
- decoder = new VideoChunkDecoder(
686
- clipId,
687
- metadata ? {
688
- ...this.defaultVideoConfig,
689
- codec: metadata.codec,
690
- width: metadata.width,
691
- height: metadata.height,
692
- description: normalizeDescription(metadata.description)
693
- } : void 0
694
- );
695
- this.evictIfNeeded("video");
696
- this.videoDecoders.set(clipId, decoder);
697
- }
698
- this.videoDecoders.delete(clipId);
699
- this.videoDecoders.set(clipId, decoder);
700
- return decoder;
701
- } else {
702
- let decoder = this.audioDecoders.get(clipId);
703
- if (!decoder) {
704
- decoder = new AudioChunkDecoder(
705
- clipId,
706
- metadata ? {
707
- ...this.defaultAudioConfig,
708
- codec: metadata.codec,
709
- sampleRate: metadata.sampleRate,
710
- numberOfChannels: metadata.numberOfChannels,
711
- description: normalizeDescription(metadata.description)
712
- } : void 0
713
- );
714
- this.evictIfNeeded("audio");
715
- this.audioDecoders.set(clipId, decoder);
716
- }
717
- this.audioDecoders.delete(clipId);
718
- this.audioDecoders.set(clipId, decoder);
719
- return decoder;
720
- }
721
- }
722
- /**
723
- * Evict least-recently-used decoder if we exceed MAX_ACTIVE_DECODERS.
724
- */
725
- evictIfNeeded(kind) {
726
- const map = kind === "video" ? this.videoDecoders : this.audioDecoders;
727
- if (map.size < DecodeWorker.MAX_ACTIVE_DECODERS) return;
728
- const [lrucId, lruDecoder] = map.entries().next().value;
729
- lruDecoder.close().catch(() => void 0);
730
- map.delete(lrucId);
731
- }
732
- async registerAudioTrack(trackId, clipId, metadata) {
733
- const record = {
734
- clipId,
735
- config: this.extractTrackConfig(metadata?.runtimeConfig),
736
- sampleRate: metadata?.sampleRate,
737
- numberOfChannels: metadata?.numberOfChannels,
738
- type: metadata?.trackType ?? "other"
739
- };
740
- this.audioTrackMetadata.set(trackId, record);
741
- if (this.registeredAudioTracks.has(trackId)) {
742
- await this.sendAudioTrackUpdate(trackId, record);
743
- return;
744
- }
745
- this.registeredAudioTracks.add(trackId);
746
- await this.sendAudioTrackAdd(trackId, record);
747
- }
748
- // private unregisterAudioTrack(trackId: string): void {
749
- // if (!this.registeredAudioTracks.delete(trackId)) {
750
- // return;
751
- // }
752
- // const record = this.audioTrackMetadata.get(trackId);
753
- // this.audioTrackMetadata.delete(trackId);
754
- // if (!record) {
755
- // return;
756
- // }
757
- // const channel = this.ensureComposeChannel();
758
- // channel
759
- // ?.send(WorkerMessageType.AudioTrackRemove, {
760
- // clipId: record.clipId,
761
- // trackId,
762
- // })
763
- // .catch((error) => {
764
- // console.warn('[DecodeWorker] Failed to notify track removal', error);
765
- // });
766
- // }
767
- extractTrackConfig(config) {
768
- return {
769
- startTimeUs: config?.startTimeUs ?? 0,
770
- durationUs: config?.durationUs,
771
- volume: config?.volume ?? 1,
772
- fadeIn: config?.fadeIn,
773
- fadeOut: config?.fadeOut,
774
- effects: config?.effects ?? [],
775
- duckingTag: config?.duckingTag
776
- };
777
- }
778
- async sendAudioTrackAdd(trackId, record) {
779
- const channel = this.ensureComposeChannel();
780
- if (!channel) {
781
- return;
782
- }
783
- await channel.send(WorkerMessageType.AudioTrackAdd, {
784
- clipId: record.clipId,
785
- trackId,
786
- config: record.config,
787
- sampleRate: record.sampleRate,
788
- numberOfChannels: record.numberOfChannels,
789
- type: record.type
790
- });
791
- }
792
- async sendAudioTrackUpdate(trackId, record) {
793
- const channel = this.ensureComposeChannel();
794
- if (!channel) {
795
- return;
796
- }
797
- if (!channel) {
798
- return;
799
- }
800
- await channel.send(WorkerMessageType.AudioTrackUpdate, {
801
- clipId: record.clipId,
802
- trackId,
803
- config: record.config,
804
- type: record.type
805
- });
806
- }
807
- ensureComposeChannel() {
808
- if (!this.audioDownstreamPort) {
809
- return null;
810
- }
811
- return new WorkerChannel(this.audioDownstreamPort, {
812
- name: "Decode-AudioCompose",
813
- timeout: 3e4
814
- });
815
- }
816
- }
817
- const worker = new DecodeWorker();
818
- self.addEventListener("beforeunload", () => {
819
- worker["handleDispose"]();
820
- });
821
- const decode_worker = null;
822
- export {
823
- DecodeWorker,
824
- decode_worker as default
825
- };
826
- //# sourceMappingURL=decode.worker.js.map