@meframe/core 0.0.6 → 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.
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +0 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +24 -37
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +1 -0
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +96 -82
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +11 -3
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/load/ResourceLoader.js +1 -1
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +6 -2
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/types.d.ts +1 -1
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js.map +1 -1
- package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
- package/dist/workers/BaseDecoder.js +130 -0
- package/dist/workers/BaseDecoder.js.map +1 -0
- package/dist/workers/WorkerChannel.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js +13 -9
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/dist/workers/stages/decode/audio-decode.worker.js +243 -0
- package/dist/workers/stages/decode/audio-decode.worker.js.map +1 -0
- package/dist/workers/stages/decode/video-decode.worker.js +346 -0
- package/dist/workers/stages/decode/video-decode.worker.js.map +1 -0
- package/dist/workers/stages/encode/video-encode.worker.js +340 -0
- package/dist/workers/stages/encode/video-encode.worker.js.map +1 -0
- package/package.json +1 -1
- package/dist/workers/stages/decode/decode.worker.js +0 -826
- package/dist/workers/stages/decode/decode.worker.js.map +0 -1
- package/dist/workers/stages/encode/encode.worker.js +0 -547
- 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 || "no-preference",
|
|
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, sessionId, metadata) {
|
|
682
|
-
if (kind === "video") {
|
|
683
|
-
let decoder = this.videoDecoders.get(sessionId);
|
|
684
|
-
if (!decoder) {
|
|
685
|
-
decoder = new VideoChunkDecoder(
|
|
686
|
-
sessionId,
|
|
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(sessionId, decoder);
|
|
697
|
-
}
|
|
698
|
-
this.videoDecoders.delete(sessionId);
|
|
699
|
-
this.videoDecoders.set(sessionId, decoder);
|
|
700
|
-
return decoder;
|
|
701
|
-
} else {
|
|
702
|
-
let decoder = this.audioDecoders.get(sessionId);
|
|
703
|
-
if (!decoder) {
|
|
704
|
-
decoder = new AudioChunkDecoder(
|
|
705
|
-
sessionId,
|
|
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(sessionId, decoder);
|
|
716
|
-
}
|
|
717
|
-
this.audioDecoders.delete(sessionId);
|
|
718
|
-
this.audioDecoders.set(sessionId, 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
|