@meframe/core 0.0.4 → 0.0.6
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/Meframe.d.ts +16 -7
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +106 -86
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +27 -10
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +92 -30
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/L2Cache.d.ts +31 -2
- package/dist/cache/L2Cache.d.ts.map +1 -1
- package/dist/cache/L2Cache.js +242 -44
- package/dist/cache/L2Cache.js.map +1 -1
- package/dist/cache/l1/VideoL1Cache.d.ts +1 -1
- package/dist/cache/l1/VideoL1Cache.js.map +1 -1
- package/dist/controllers/PreRenderService.d.ts +31 -4
- package/dist/controllers/PreRenderService.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.js +130 -10
- package/dist/controllers/PreRenderService.js.map +1 -1
- package/dist/event/events.d.ts +12 -3
- package/dist/event/events.d.ts.map +1 -1
- package/dist/event/events.js +1 -0
- package/dist/event/events.js.map +1 -1
- package/dist/model/CompositionModel.d.ts +1 -1
- package/dist/model/CompositionModel.js +1 -1
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/patch.d.ts +1 -1
- package/dist/model/patch.js.map +1 -1
- package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js +1858 -0
- package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +1 -0
- package/dist/orchestrator/ClipSessionManager.d.ts +1 -2
- package/dist/orchestrator/ClipSessionManager.d.ts.map +1 -1
- package/dist/orchestrator/ClipSessionManager.js +1 -0
- package/dist/orchestrator/ClipSessionManager.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts +1 -1
- package/dist/orchestrator/CompositionPlanner.js +1 -1
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +10 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +83 -38
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +11 -4
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +70 -28
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts +28 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +133 -5
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts +1 -0
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/encode/AudioChunkEncoder.d.ts +2 -1
- package/dist/stages/encode/AudioChunkEncoder.d.ts.map +1 -1
- package/dist/stages/encode/AudioChunkEncoder.js +41 -0
- package/dist/stages/encode/AudioChunkEncoder.js.map +1 -0
- package/dist/stages/encode/BaseEncoder.d.ts +7 -3
- package/dist/stages/encode/BaseEncoder.d.ts.map +1 -1
- package/dist/stages/encode/BaseEncoder.js +173 -0
- package/dist/stages/encode/BaseEncoder.js.map +1 -0
- package/dist/stages/encode/ClipEncoderManager.d.ts +64 -0
- package/dist/stages/encode/ClipEncoderManager.d.ts.map +1 -0
- package/dist/stages/encode/index.d.ts +1 -1
- package/dist/stages/encode/index.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +17 -12
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +1 -1
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +2 -2
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/stages/load/types.d.ts +2 -2
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.d.ts +19 -38
- package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +60 -0
- package/dist/stages/mux/MP4Muxer.js.map +1 -0
- package/dist/stages/mux/MuxManager.d.ts +27 -0
- package/dist/stages/mux/MuxManager.d.ts.map +1 -0
- package/dist/stages/mux/MuxManager.js +148 -0
- package/dist/stages/mux/MuxManager.js.map +1 -0
- package/dist/stages/mux/index.d.ts +1 -0
- package/dist/stages/mux/index.d.ts.map +1 -1
- package/dist/stages/mux/types.d.ts +1 -0
- package/dist/stages/mux/types.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +2 -4
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/types.d.ts +1 -4
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js +0 -3
- package/dist/worker/types.js.map +1 -1
- package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
- package/dist/workers/MP4Demuxer.js +7049 -6
- package/dist/workers/MP4Demuxer.js.map +1 -1
- package/dist/workers/WorkerChannel.js +0 -3
- package/dist/workers/WorkerChannel.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js +25 -14
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/dist/workers/stages/decode/decode.worker.js +37 -28
- package/dist/workers/stages/decode/decode.worker.js.map +1 -1
- package/dist/workers/stages/demux/audio-demux.worker.js +4 -4
- package/dist/workers/stages/demux/audio-demux.worker.js.map +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js +9 -7
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -1
- package/dist/workers/stages/encode/encode.worker.js +192 -196
- package/dist/workers/stages/encode/encode.worker.js.map +1 -1
- package/package.json +2 -1
- package/dist/stages/encode/EncoderPool.d.ts +0 -28
- package/dist/stages/encode/EncoderPool.d.ts.map +0 -1
- package/dist/workers/mp4box.all.js +0 -7049
- package/dist/workers/mp4box.all.js.map +0 -1
- package/dist/workers/stages/mux/mux.worker.js +0 -501
- package/dist/workers/stages/mux/mux.worker.js.map +0 -1
|
@@ -45,7 +45,7 @@ class BaseEncoder {
|
|
|
45
45
|
}
|
|
46
46
|
const isSupported = await this.isConfigSupported(this.config);
|
|
47
47
|
if (!isSupported.supported) {
|
|
48
|
-
throw new Error(`Codec not supported: ${this.config
|
|
48
|
+
throw new Error(`Codec not supported: ${JSON.stringify(this.config)}`);
|
|
49
49
|
}
|
|
50
50
|
this.encoder = this.createEncoder({
|
|
51
51
|
output: this.handleOutput.bind(this),
|
|
@@ -105,8 +105,16 @@ class BaseEncoder {
|
|
|
105
105
|
get queueSize() {
|
|
106
106
|
return this.encoder?.encodeQueueSize ?? 0;
|
|
107
107
|
}
|
|
108
|
-
handleOutput(chunk,
|
|
109
|
-
this.controller
|
|
108
|
+
handleOutput(chunk, metadata) {
|
|
109
|
+
if (this.controller) {
|
|
110
|
+
try {
|
|
111
|
+
this.controller.enqueue({ chunk, metadata });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!(error instanceof TypeError && error.message.includes("closed"))) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
110
118
|
}
|
|
111
119
|
handleError(error) {
|
|
112
120
|
console.error(`${this.getEncoderType()} encoder error:`, error);
|
|
@@ -144,7 +152,8 @@ class BaseEncoder {
|
|
|
144
152
|
check();
|
|
145
153
|
});
|
|
146
154
|
}
|
|
147
|
-
|
|
155
|
+
const frame = input.frame || input;
|
|
156
|
+
this.encode(frame);
|
|
148
157
|
},
|
|
149
158
|
flush: async () => {
|
|
150
159
|
await this.flush();
|
|
@@ -207,12 +216,20 @@ class VideoChunkEncoder extends BaseEncoder {
|
|
|
207
216
|
class AudioChunkEncoder extends BaseEncoder {
|
|
208
217
|
static DEFAULT_HIGH_WATER_MARK = 4;
|
|
209
218
|
static DEFAULT_ENCODE_QUEUE_THRESHOLD = 16;
|
|
219
|
+
static DEFAULT_CONFIG = {
|
|
220
|
+
codec: "mp4a.40.2",
|
|
221
|
+
// AAC-LC
|
|
222
|
+
sampleRate: 48e3,
|
|
223
|
+
numberOfChannels: 2,
|
|
224
|
+
bitrate: 128e3
|
|
225
|
+
};
|
|
210
226
|
highWaterMark;
|
|
211
227
|
encodeQueueThreshold;
|
|
212
228
|
constructor(config) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.
|
|
229
|
+
const fullConfig = { ...AudioChunkEncoder.DEFAULT_CONFIG, ...config };
|
|
230
|
+
super(fullConfig);
|
|
231
|
+
this.highWaterMark = fullConfig.backpressure?.highWaterMark ?? AudioChunkEncoder.DEFAULT_HIGH_WATER_MARK;
|
|
232
|
+
this.encodeQueueThreshold = fullConfig.backpressure?.encodeQueueThreshold ?? AudioChunkEncoder.DEFAULT_ENCODE_QUEUE_THRESHOLD;
|
|
216
233
|
}
|
|
217
234
|
async isConfigSupported(config) {
|
|
218
235
|
const result = await AudioEncoder.isConfigSupported(config);
|
|
@@ -228,20 +245,131 @@ class AudioChunkEncoder extends BaseEncoder {
|
|
|
228
245
|
if (this.encoder?.state !== "configured") {
|
|
229
246
|
throw new Error("Audio encoder not configured");
|
|
230
247
|
}
|
|
231
|
-
const config = this.getConfig();
|
|
232
|
-
if (data.sampleRate !== config.sampleRate || data.numberOfChannels !== config.numberOfChannels) {
|
|
233
|
-
throw new Error("AudioData requires resampling or channel remap before encoding");
|
|
234
|
-
}
|
|
235
248
|
this.encoder.encode(data);
|
|
236
249
|
data.close();
|
|
237
250
|
}
|
|
238
251
|
}
|
|
252
|
+
class ClipEncoderManager {
|
|
253
|
+
videoEncoders = /* @__PURE__ */ new Map();
|
|
254
|
+
audioEncoders = /* @__PURE__ */ new Map();
|
|
255
|
+
videoCreationOrder = [];
|
|
256
|
+
audioCreationOrder = [];
|
|
257
|
+
maxEncoders;
|
|
258
|
+
constructor(maxEncoders = 6) {
|
|
259
|
+
this.maxEncoders = maxEncoders;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Acquire a video encoder for the given sessionId
|
|
263
|
+
* Creates new encoder if not exists; returns existing one if already created
|
|
264
|
+
*/
|
|
265
|
+
async acquireVideo(sessionId, config) {
|
|
266
|
+
let encoder = this.videoEncoders.get(sessionId);
|
|
267
|
+
if (!encoder) {
|
|
268
|
+
if (this.videoEncoders.size >= this.maxEncoders) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`[ClipEncoderManager] Video encoder limit reached (${this.maxEncoders}). Active clips: ${Array.from(this.videoEncoders.keys()).join(", ")}. Please release unused encoders before acquiring new ones.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
encoder = new VideoChunkEncoder(config);
|
|
274
|
+
await encoder.initialize();
|
|
275
|
+
this.videoEncoders.set(sessionId, encoder);
|
|
276
|
+
this.videoCreationOrder.push(sessionId);
|
|
277
|
+
}
|
|
278
|
+
return encoder;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Acquire an audio encoder for the given sessionId
|
|
282
|
+
*/
|
|
283
|
+
async acquireAudio(sessionId, config) {
|
|
284
|
+
let encoder = this.audioEncoders.get(sessionId);
|
|
285
|
+
if (!encoder) {
|
|
286
|
+
if (this.audioEncoders.size >= this.maxEncoders) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`[ClipEncoderManager] Audio encoder limit reached (${this.maxEncoders}). Active clips: ${Array.from(this.audioEncoders.keys()).join(", ")}. Please release unused encoders before acquiring new ones.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
encoder = new AudioChunkEncoder(config);
|
|
292
|
+
await encoder.initialize();
|
|
293
|
+
this.audioEncoders.set(sessionId, encoder);
|
|
294
|
+
this.audioCreationOrder.push(sessionId);
|
|
295
|
+
}
|
|
296
|
+
return encoder;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Release video encoder for the given sessionId
|
|
300
|
+
*/
|
|
301
|
+
async releaseVideo(sessionId) {
|
|
302
|
+
const encoder = this.videoEncoders.get(sessionId);
|
|
303
|
+
if (encoder) {
|
|
304
|
+
await encoder.close();
|
|
305
|
+
this.videoEncoders.delete(sessionId);
|
|
306
|
+
const index = this.videoCreationOrder.indexOf(sessionId);
|
|
307
|
+
if (index !== -1) {
|
|
308
|
+
this.videoCreationOrder.splice(index, 1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Release audio encoder for the given sessionId
|
|
314
|
+
*/
|
|
315
|
+
async releaseAudio(sessionId) {
|
|
316
|
+
const encoder = this.audioEncoders.get(sessionId);
|
|
317
|
+
if (encoder) {
|
|
318
|
+
await encoder.close();
|
|
319
|
+
this.audioEncoders.delete(sessionId);
|
|
320
|
+
const index = this.audioCreationOrder.indexOf(sessionId);
|
|
321
|
+
if (index !== -1) {
|
|
322
|
+
this.audioCreationOrder.splice(index, 1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Release both video and audio encoders for the given sessionId
|
|
328
|
+
*/
|
|
329
|
+
async releaseClip(sessionId) {
|
|
330
|
+
await Promise.all([this.releaseVideo(sessionId), this.releaseAudio(sessionId)]);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Close all encoders and clear state
|
|
334
|
+
*/
|
|
335
|
+
async closeAll() {
|
|
336
|
+
const promises = [
|
|
337
|
+
...Array.from(this.videoEncoders.values()).map((e) => e.close()),
|
|
338
|
+
...Array.from(this.audioEncoders.values()).map((e) => e.close())
|
|
339
|
+
];
|
|
340
|
+
await Promise.all(promises);
|
|
341
|
+
this.videoEncoders.clear();
|
|
342
|
+
this.audioEncoders.clear();
|
|
343
|
+
this.videoCreationOrder = [];
|
|
344
|
+
this.audioCreationOrder = [];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Check if encoders exist for the given sessionId
|
|
348
|
+
*/
|
|
349
|
+
has(sessionId) {
|
|
350
|
+
return this.videoEncoders.has(sessionId) || this.audioEncoders.has(sessionId);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get statistics about current encoder state
|
|
354
|
+
*/
|
|
355
|
+
getStats() {
|
|
356
|
+
return {
|
|
357
|
+
videoEncoders: this.videoEncoders.size,
|
|
358
|
+
audioEncoders: this.audioEncoders.size,
|
|
359
|
+
maxEncoders: this.maxEncoders,
|
|
360
|
+
videoCreationOrder: [...this.videoCreationOrder],
|
|
361
|
+
audioCreationOrder: [...this.audioCreationOrder]
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
239
365
|
class EncodeWorker {
|
|
240
366
|
channel;
|
|
241
|
-
|
|
242
|
-
|
|
367
|
+
encoderManager = new ClipEncoderManager(12);
|
|
368
|
+
// Increased to prevent eviction during active streams
|
|
369
|
+
// Default encoder configs
|
|
370
|
+
defaultVideoConfig = null;
|
|
371
|
+
defaultAudioConfig = null;
|
|
243
372
|
// Connections to other workers
|
|
244
|
-
cachePort = null;
|
|
245
373
|
muxPort = null;
|
|
246
374
|
composePorts = /* @__PURE__ */ new Map();
|
|
247
375
|
// Connections from compose workers
|
|
@@ -256,9 +384,9 @@ class EncodeWorker {
|
|
|
256
384
|
this.channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
257
385
|
this.channel.registerHandler("connect", this.handleConnect.bind(this));
|
|
258
386
|
this.channel.registerHandler("configure_video", this.handleConfigureVideo.bind(this));
|
|
259
|
-
this.channel.registerHandler("configure_audio", this.handleConfigureAudio.bind(this));
|
|
260
387
|
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
261
388
|
this.channel.registerHandler("reset", this.handleReset.bind(this));
|
|
389
|
+
this.channel.registerHandler("release_clip_encoder", this.handleReleaseClipEncoder.bind(this));
|
|
262
390
|
this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
|
|
263
391
|
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
264
392
|
}
|
|
@@ -266,15 +394,19 @@ class EncodeWorker {
|
|
|
266
394
|
* Connect handler used by stream pipeline
|
|
267
395
|
*/
|
|
268
396
|
async handleConnect(payload) {
|
|
269
|
-
const { port, streamType } = payload;
|
|
270
|
-
if (streamType === "video")
|
|
271
|
-
|
|
397
|
+
const { port, streamType, sessionId } = payload;
|
|
398
|
+
if (streamType === "video") {
|
|
399
|
+
return this.handleConnectComposer({ composeType: "video", port, sessionId });
|
|
400
|
+
}
|
|
401
|
+
if (streamType === "audio") {
|
|
402
|
+
return this.handleConnectComposer({ composeType: "audio", port, sessionId });
|
|
403
|
+
}
|
|
272
404
|
if (streamType === "chunk") return this.handleConnectMux({ port });
|
|
273
405
|
return { success: true };
|
|
274
406
|
}
|
|
275
407
|
/**
|
|
276
408
|
* Handle configuration message from orchestrator
|
|
277
|
-
* @param payload.initial - If true, initialize worker
|
|
409
|
+
* @param payload.initial - If true, initialize worker; saves default encoder configs
|
|
278
410
|
*/
|
|
279
411
|
async handleConfigure(payload) {
|
|
280
412
|
const { config, initial = false } = payload;
|
|
@@ -282,51 +414,10 @@ class EncodeWorker {
|
|
|
282
414
|
this.channel.state = WorkerState.Ready;
|
|
283
415
|
}
|
|
284
416
|
if (config.video) {
|
|
285
|
-
|
|
286
|
-
if (this.videoEncoder) {
|
|
287
|
-
await this.videoEncoder.close();
|
|
288
|
-
}
|
|
289
|
-
this.videoEncoder = new VideoChunkEncoder(config.video);
|
|
290
|
-
await this.videoEncoder.initialize();
|
|
291
|
-
const videoStream = config.video.stream?.pipeThrough(this.videoEncoder.createStream());
|
|
292
|
-
if (videoStream && this.cachePort) {
|
|
293
|
-
const cacheChannel = new WorkerChannel(this.cachePort, {
|
|
294
|
-
name: "Encode-Cache-Video",
|
|
295
|
-
timeout: 3e4
|
|
296
|
-
});
|
|
297
|
-
await cacheChannel.sendStream(videoStream, {
|
|
298
|
-
type: "video",
|
|
299
|
-
width: config.video.width,
|
|
300
|
-
height: config.video.height,
|
|
301
|
-
framerate: config.video.framerate
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
} else {
|
|
305
|
-
await this.videoEncoder.reconfigure(config.video);
|
|
306
|
-
}
|
|
417
|
+
this.defaultVideoConfig = config.video;
|
|
307
418
|
}
|
|
308
419
|
if (config.audio) {
|
|
309
|
-
|
|
310
|
-
if (this.audioEncoder) {
|
|
311
|
-
await this.audioEncoder.close();
|
|
312
|
-
}
|
|
313
|
-
this.audioEncoder = new AudioChunkEncoder(config.audio);
|
|
314
|
-
await this.audioEncoder.initialize();
|
|
315
|
-
const audioStream = config.audio.stream?.pipeThrough(this.audioEncoder.createStream());
|
|
316
|
-
if (audioStream && this.cachePort) {
|
|
317
|
-
const cacheChannel = new WorkerChannel(this.cachePort, {
|
|
318
|
-
name: "Encode-Cache-Audio",
|
|
319
|
-
timeout: 3e4
|
|
320
|
-
});
|
|
321
|
-
await cacheChannel.sendStream(audioStream, {
|
|
322
|
-
type: "audio",
|
|
323
|
-
sampleRate: config.audio.sampleRate,
|
|
324
|
-
numberOfChannels: config.audio.numberOfChannels
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
await this.audioEncoder.reconfigure(config.audio);
|
|
329
|
-
}
|
|
420
|
+
this.defaultAudioConfig = config.audio;
|
|
330
421
|
}
|
|
331
422
|
return { success: true };
|
|
332
423
|
}
|
|
@@ -334,55 +425,28 @@ class EncodeWorker {
|
|
|
334
425
|
* Connect to a compose worker to receive frames/audio data
|
|
335
426
|
*/
|
|
336
427
|
async handleConnectComposer(payload) {
|
|
337
|
-
const { composeType, port } = payload;
|
|
428
|
+
const { composeType, port, sessionId } = payload;
|
|
338
429
|
this.composePorts.set(composeType, port);
|
|
339
430
|
const composeChannel = new WorkerChannel(port, {
|
|
340
431
|
name: `Encode-${composeType}Compose`,
|
|
341
432
|
timeout: 3e4
|
|
342
433
|
});
|
|
343
434
|
composeChannel.receiveStream(async (stream, metadata) => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
} finally {
|
|
360
|
-
reader.releaseLock();
|
|
361
|
-
}
|
|
362
|
-
} else if (metadata?.streamType === "audio" && this.audioEncoder) {
|
|
363
|
-
const composedConfig = {
|
|
364
|
-
sampleRate: metadata.sampleRate,
|
|
365
|
-
numberOfChannels: metadata.numberOfChannels
|
|
366
|
-
};
|
|
367
|
-
const currentConfig = this.audioEncoder.getConfig();
|
|
368
|
-
if (typeof composedConfig.sampleRate === "number" && composedConfig.sampleRate > 0 && composedConfig.sampleRate !== currentConfig.sampleRate) {
|
|
369
|
-
await this.audioEncoder.reconfigure({ sampleRate: composedConfig.sampleRate });
|
|
370
|
-
}
|
|
371
|
-
if (typeof composedConfig.numberOfChannels === "number" && composedConfig.numberOfChannels > 0 && composedConfig.numberOfChannels !== currentConfig.numberOfChannels) {
|
|
372
|
-
await this.audioEncoder.reconfigure({
|
|
373
|
-
numberOfChannels: composedConfig.numberOfChannels
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
const reader = stream.getReader();
|
|
377
|
-
try {
|
|
378
|
-
while (true) {
|
|
379
|
-
const { done, value } = await reader.read();
|
|
380
|
-
if (done) break;
|
|
381
|
-
this.audioEncoder.encode(value);
|
|
382
|
-
}
|
|
383
|
-
} finally {
|
|
384
|
-
reader.releaseLock();
|
|
385
|
-
}
|
|
435
|
+
const streamSessionId = metadata?.sessionId || sessionId || "unknown";
|
|
436
|
+
if (metadata?.streamType === "video" && this.defaultVideoConfig) {
|
|
437
|
+
const encoder = await this.encoderManager.acquireVideo(
|
|
438
|
+
streamSessionId,
|
|
439
|
+
this.defaultVideoConfig
|
|
440
|
+
);
|
|
441
|
+
const encodingTransform = encoder.createStream();
|
|
442
|
+
const encodedStream = stream.pipeThrough(
|
|
443
|
+
encodingTransform
|
|
444
|
+
);
|
|
445
|
+
this.channel.sendStream(encodedStream, {
|
|
446
|
+
streamType: "video",
|
|
447
|
+
track: "video",
|
|
448
|
+
sessionId: streamSessionId
|
|
449
|
+
});
|
|
386
450
|
}
|
|
387
451
|
});
|
|
388
452
|
return { success: true };
|
|
@@ -406,12 +470,7 @@ class EncodeWorker {
|
|
|
406
470
|
*/
|
|
407
471
|
async handleConfigureVideo(config) {
|
|
408
472
|
try {
|
|
409
|
-
|
|
410
|
-
this.videoEncoder = new VideoChunkEncoder(config);
|
|
411
|
-
await this.videoEncoder.initialize();
|
|
412
|
-
} else {
|
|
413
|
-
await this.videoEncoder.reconfigure(config);
|
|
414
|
-
}
|
|
473
|
+
this.defaultVideoConfig = config;
|
|
415
474
|
this.channel.notify("video_configured", {
|
|
416
475
|
codec: config.codec,
|
|
417
476
|
width: config.width,
|
|
@@ -427,109 +486,46 @@ class EncodeWorker {
|
|
|
427
486
|
}
|
|
428
487
|
}
|
|
429
488
|
/**
|
|
430
|
-
*
|
|
489
|
+
* Flush encoders (deprecated with per-clip encoder architecture)
|
|
431
490
|
*/
|
|
432
|
-
async
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
this.audioEncoder = new AudioChunkEncoder(config);
|
|
436
|
-
await this.audioEncoder.initialize();
|
|
437
|
-
} else {
|
|
438
|
-
await this.audioEncoder.reconfigure(config);
|
|
439
|
-
}
|
|
440
|
-
this.channel.notify("audio_configured", {
|
|
441
|
-
codec: config.codec,
|
|
442
|
-
sampleRate: config.sampleRate,
|
|
443
|
-
numberOfChannels: config.numberOfChannels,
|
|
444
|
-
bitrate: config.bitrate
|
|
445
|
-
});
|
|
446
|
-
return { success: true };
|
|
447
|
-
} catch (error) {
|
|
448
|
-
throw {
|
|
449
|
-
code: "AUDIO_CONFIG_ERROR",
|
|
450
|
-
message: error.message
|
|
451
|
-
};
|
|
452
|
-
}
|
|
491
|
+
async handleFlush(_payload) {
|
|
492
|
+
console.warn("[EncodeWorker] handleFlush is deprecated with per-clip encoder architecture");
|
|
493
|
+
return {};
|
|
453
494
|
}
|
|
454
495
|
/**
|
|
455
|
-
*
|
|
496
|
+
* Reset encoders (deprecated with per-clip encoder architecture)
|
|
456
497
|
*/
|
|
457
|
-
async
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
result.videoChunks = chunks;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (!payload?.type || payload.type === "audio") {
|
|
467
|
-
const chunks = await this.audioEncoder?.flush();
|
|
468
|
-
if (chunks) {
|
|
469
|
-
result.audioChunks = chunks;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return result;
|
|
473
|
-
} catch (error) {
|
|
474
|
-
throw {
|
|
475
|
-
code: "FLUSH_ERROR",
|
|
476
|
-
message: error.message
|
|
477
|
-
};
|
|
478
|
-
}
|
|
498
|
+
async handleReset(payload) {
|
|
499
|
+
console.warn("[EncodeWorker] handleReset is deprecated with per-clip encoder architecture");
|
|
500
|
+
this.channel.notify("reset_complete", {
|
|
501
|
+
type: payload?.type || "all"
|
|
502
|
+
});
|
|
503
|
+
return { success: true };
|
|
479
504
|
}
|
|
480
505
|
/**
|
|
481
|
-
*
|
|
506
|
+
* Release encoder for a specific clip
|
|
482
507
|
*/
|
|
483
|
-
async
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
if (!payload?.type || payload.type === "audio") {
|
|
489
|
-
await this.audioEncoder?.reset();
|
|
490
|
-
}
|
|
491
|
-
this.channel.notify("reset_complete", {
|
|
492
|
-
type: payload?.type || "all"
|
|
493
|
-
});
|
|
494
|
-
return { success: true };
|
|
495
|
-
} catch (error) {
|
|
496
|
-
throw {
|
|
497
|
-
code: "RESET_ERROR",
|
|
498
|
-
message: error.message
|
|
499
|
-
};
|
|
500
|
-
}
|
|
508
|
+
async handleReleaseClipEncoder(payload) {
|
|
509
|
+
const { sessionId } = payload;
|
|
510
|
+
await this.encoderManager.releaseClip(sessionId);
|
|
511
|
+
return { success: true };
|
|
501
512
|
}
|
|
502
513
|
/**
|
|
503
514
|
* Get encoder statistics
|
|
504
515
|
*/
|
|
505
516
|
async handleGetStats() {
|
|
506
|
-
const stats = {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
configured: this.videoEncoder.isReady,
|
|
510
|
-
queueSize: this.videoEncoder.queueSize
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
if (this.audioEncoder) {
|
|
514
|
-
stats.audio = {
|
|
515
|
-
configured: this.audioEncoder.isReady,
|
|
516
|
-
queueSize: this.audioEncoder.queueSize
|
|
517
|
-
};
|
|
518
|
-
}
|
|
517
|
+
const stats = {
|
|
518
|
+
encoders: this.encoderManager.getStats()
|
|
519
|
+
};
|
|
519
520
|
return stats;
|
|
520
521
|
}
|
|
521
|
-
// Output and error handling is done via streams in the encoder itself
|
|
522
|
-
// These placeholder methods can be implemented when needed for direct callback handling
|
|
523
522
|
/**
|
|
524
523
|
* Dispose worker and cleanup resources
|
|
525
524
|
*/
|
|
526
525
|
async handleDispose() {
|
|
527
|
-
await this.
|
|
528
|
-
|
|
529
|
-
this.
|
|
530
|
-
this.audioEncoder = null;
|
|
531
|
-
this.cachePort?.close();
|
|
532
|
-
this.cachePort = null;
|
|
526
|
+
await this.encoderManager.closeAll();
|
|
527
|
+
this.defaultVideoConfig = null;
|
|
528
|
+
this.defaultAudioConfig = null;
|
|
533
529
|
this.muxPort?.close();
|
|
534
530
|
this.muxPort = null;
|
|
535
531
|
for (const port of this.composePorts.values()) {
|