@camstack/addon-pipeline 1.2.219 → 1.2.221
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/audio-analyzer/index.js +661 -7
- package/dist/audio-analyzer/index.mjs +658 -5
- package/dist/chunk-emK7D4bc.js +35 -0
- package/dist/{default-detection-model-d7-Q_LMo.js → default-detection-model-B1NT1A38.js} +1 -1
- package/dist/{default-detection-model-Dsk1a5pY.mjs → default-detection-model-DkVFgubU.mjs} +1 -1
- package/dist/detection-pipeline/index.js +10 -9
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-wtNhtVVo.js → dist-ATDfcxgL.js} +282 -39
- package/dist/{dist-CKK0CBUW.mjs → dist-CEFC3PkP.mjs} +265 -5
- package/dist/{lazy-sharp-zoQ07I43.js → lazy-sharp-BuwKlQLR.js} +2 -2
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-BduNi21Z.js → node-C5jnTgpv.js} +4 -3
- package/dist/{node-CrNBWFbA.mjs → node-DR3fHqUq.mjs} +1 -1
- package/dist/pipeline-runner/index.js +222 -157
- package/dist/pipeline-runner/index.mjs +218 -154
- package/dist/{process-memory-Dwl0MFPH.js → process-memory-DQbCPnMA.js} +3 -2
- package/dist/{process-memory-BRQOtDN1.mjs → process-memory-DqYj5IAb.mjs} +1 -1
- package/dist/recorder/index.js +344 -127
- package/dist/recorder/index.mjs +342 -126
- package/dist/{segment-demux-js-D2o9ShNL.mjs → segment-demux-js-CddDwD69.mjs} +1 -1
- package/dist/{segment-demux-js-CFOU3biG.js → segment-demux-js-o9KBAz9p.js} +4 -3
- package/dist/session-decode/decode-worker-child.js +155 -74
- package/dist/session-decode/decode-worker-child.mjs +154 -73
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DN_IoShh.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D5XnLZWR.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CyMYDKfM.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DICVQkNi.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CKUGpDns.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-n7c9pNlt.mjs → hostInit-C-VdcJS2.mjs} +3 -3
- package/dist/stream-broker/index.js +231 -112
- package/dist/stream-broker/index.mjs +224 -106
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-B2MfQLlu.js +184 -0
- package/dist/worker-protocol-C_W-P_g-.mjs +161 -0
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-7hqrrd88.mjs +0 -26
- package/dist/worker-protocol-Cx3Nqreq.mjs +0 -264
- package/dist/worker-protocol-iWTFnnUM.js +0 -299
|
@@ -2,12 +2,13 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
+
const require_dist = require("../dist-ATDfcxgL.js");
|
|
7
|
+
const require_process_memory = require("../process-memory-DQbCPnMA.js");
|
|
7
8
|
let node_fs = require("node:fs");
|
|
8
|
-
node_fs =
|
|
9
|
+
node_fs = require_chunk.__toESM(node_fs);
|
|
9
10
|
let node_path = require("node:path");
|
|
10
|
-
node_path =
|
|
11
|
+
node_path = require_chunk.__toESM(node_path);
|
|
11
12
|
let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
|
|
12
13
|
//#region src/audio-analyzer/audio-pipeline.ts
|
|
13
14
|
/**
|
|
@@ -369,7 +370,12 @@ var AppleSoundAnalysisPipeline = class {
|
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
};
|
|
372
|
-
/**
|
|
373
|
+
/**
|
|
374
|
+
* Resample to 16 kHz mono by linear interpolation — the ONE place a window
|
|
375
|
+
* is brought to the classifier's rate. The plane carries the source rate
|
|
376
|
+
* (8 kHz for G.711, D450); the model that needs 16 kHz gets it here, next to
|
|
377
|
+
* the model, over a whole window. Exported so the guard can pin it.
|
|
378
|
+
*/
|
|
373
379
|
function resampleMono16k(chunk) {
|
|
374
380
|
const { data, sampleRate, channels } = chunk;
|
|
375
381
|
const numSamples = data.length / channels;
|
|
@@ -391,6 +397,558 @@ function resampleMono16k(chunk) {
|
|
|
391
397
|
}
|
|
392
398
|
return out;
|
|
393
399
|
}
|
|
400
|
+
/** How many chunks to drain per poll — a small burst absorbs jitter. */
|
|
401
|
+
var PULL_MAX_COUNT = 8;
|
|
402
|
+
/**
|
|
403
|
+
* Consecutive pull failures before we attempt to re-subscribe. A single failed
|
|
404
|
+
* poll is treated as a blip (re-subscribing would leak a broker-side FIFO); a
|
|
405
|
+
* sustained failure means the broker child restarted and dropped our
|
|
406
|
+
* subscription, so we re-establish it.
|
|
407
|
+
*/
|
|
408
|
+
var RESUBSCRIBE_AFTER_FAILURES = 2;
|
|
409
|
+
/** Re-subscribe at most once every N ticks while failing (~1s at 200ms). */
|
|
410
|
+
var RESUBSCRIBE_THROTTLE_TICKS = 5;
|
|
411
|
+
/** First subscribe-retry delay, doubled on every subsequent failure. */
|
|
412
|
+
var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
|
|
413
|
+
/**
|
|
414
|
+
* Subscribe-retry backoff ceiling. 5 s matches the frame-handle poller — fast
|
|
415
|
+
* enough to recover within a single reconcile of the orchestrator and slow
|
|
416
|
+
* enough that a misconfigured camStream costs ~12 op/min, not 5/s.
|
|
417
|
+
*/
|
|
418
|
+
var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
|
|
419
|
+
/**
|
|
420
|
+
* Attempts after which a still-failing subscribe escalates from the fast 5 s
|
|
421
|
+
* ceiling to {@link LONG_SUBSCRIBE_RETRY_BACKOFF_MS}. ~15 attempts ≈ one
|
|
422
|
+
* minute of fast retries — plenty for the boot races the 5 s ceiling exists
|
|
423
|
+
* for. A broker that is STILL absent after that is a long-lived condition
|
|
424
|
+
* (e.g. a DISABLED camera, whose broker the stream-broker reconcile refuses
|
|
425
|
+
* to recreate — 2026-07-23) and retrying every 5 s forever is pure log/RPC
|
|
426
|
+
* churn. The slow loop stays alive so audio still recovers automatically
|
|
427
|
+
* (≤60 s) once the camera is re-enabled.
|
|
428
|
+
*/
|
|
429
|
+
var PERSISTENT_SUBSCRIBE_FAILURE_ATTEMPTS = 15;
|
|
430
|
+
var LONG_SUBSCRIBE_RETRY_BACKOFF_MS = 6e4;
|
|
431
|
+
/**
|
|
432
|
+
* Subscribe to a broker's decoded audio-chunk stream and start the poll loop.
|
|
433
|
+
*
|
|
434
|
+
* Always resolves to a teardown closure — when the broker is not yet
|
|
435
|
+
* registered the closure cancels the ongoing retry loop; when polling is
|
|
436
|
+
* active it stops the loop and releases the broker subscription. Mirrors
|
|
437
|
+
* `startFrameHandlePoller` so video and audio recover identically.
|
|
438
|
+
*/
|
|
439
|
+
function startAudioChunkPoller(options) {
|
|
440
|
+
const lifecycle = {
|
|
441
|
+
stopped: false,
|
|
442
|
+
retryTimer: void 0,
|
|
443
|
+
pollTimer: void 0,
|
|
444
|
+
activeSubscriptionId: null
|
|
445
|
+
};
|
|
446
|
+
const teardown = () => {
|
|
447
|
+
if (lifecycle.stopped) return;
|
|
448
|
+
lifecycle.stopped = true;
|
|
449
|
+
if (lifecycle.retryTimer) {
|
|
450
|
+
clearTimeout(lifecycle.retryTimer);
|
|
451
|
+
lifecycle.retryTimer = void 0;
|
|
452
|
+
}
|
|
453
|
+
if (lifecycle.pollTimer) {
|
|
454
|
+
clearTimeout(lifecycle.pollTimer);
|
|
455
|
+
lifecycle.pollTimer = void 0;
|
|
456
|
+
}
|
|
457
|
+
const subId = lifecycle.activeSubscriptionId;
|
|
458
|
+
if (subId) {
|
|
459
|
+
lifecycle.activeSubscriptionId = null;
|
|
460
|
+
options.api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: subId }, require_dist.nodePin(options.ownerNodeId)).catch((err) => {
|
|
461
|
+
options.logger.warn("audio-chunk poller: unsubscribeAudioChunks failed", { meta: {
|
|
462
|
+
brokerId: options.brokerId,
|
|
463
|
+
subscriptionId: subId,
|
|
464
|
+
error: require_dist.errMsg(err)
|
|
465
|
+
} });
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
subscribeWithRetry(options, lifecycle);
|
|
470
|
+
return teardown;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Run the subscribe → poll handshake with exponential backoff on subscribe
|
|
474
|
+
* failures. Resolves once the subscription is acquired (and the poll loop has
|
|
475
|
+
* been started) or once `lifecycle.stopped` flips, whichever comes first.
|
|
476
|
+
*/
|
|
477
|
+
async function subscribeWithRetry(options, lifecycle) {
|
|
478
|
+
const { api, brokerId, tag, ownerNodeId, logger } = options;
|
|
479
|
+
const pin = require_dist.nodePin(ownerNodeId);
|
|
480
|
+
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
481
|
+
let attempt = 0;
|
|
482
|
+
while (!lifecycle.stopped) {
|
|
483
|
+
attempt += 1;
|
|
484
|
+
try {
|
|
485
|
+
const result = await api.streamBroker.subscribeAudioChunks.mutate({
|
|
486
|
+
brokerId,
|
|
487
|
+
tag,
|
|
488
|
+
...options.accept ? { accept: options.accept } : {}
|
|
489
|
+
}, pin);
|
|
490
|
+
if (lifecycle.stopped) {
|
|
491
|
+
await api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: result.subscriptionId }, pin).catch((err) => {
|
|
492
|
+
logger.warn("audio-chunk poller: late unsubscribe failed", { meta: {
|
|
493
|
+
brokerId,
|
|
494
|
+
subscriptionId: result.subscriptionId,
|
|
495
|
+
error: require_dist.errMsg(err)
|
|
496
|
+
} });
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (attempt > 1) logger.info("audio-chunk poller: subscribeAudioChunks recovered", { meta: {
|
|
501
|
+
brokerId,
|
|
502
|
+
tag,
|
|
503
|
+
attempt
|
|
504
|
+
} });
|
|
505
|
+
lifecycle.activeSubscriptionId = result.subscriptionId;
|
|
506
|
+
startPolling(options, lifecycle);
|
|
507
|
+
return;
|
|
508
|
+
} catch (err) {
|
|
509
|
+
if (lifecycle.stopped) return;
|
|
510
|
+
if (attempt === 1) logger.warn("audio-chunk poller: subscribeAudioChunks failed, retrying", { meta: {
|
|
511
|
+
brokerId,
|
|
512
|
+
tag,
|
|
513
|
+
error: require_dist.errMsg(err),
|
|
514
|
+
nextRetryInMs: backoffMs
|
|
515
|
+
} });
|
|
516
|
+
else logger.debug("audio-chunk poller: subscribeAudioChunks still failing", { meta: {
|
|
517
|
+
brokerId,
|
|
518
|
+
tag,
|
|
519
|
+
attempt,
|
|
520
|
+
error: require_dist.errMsg(err),
|
|
521
|
+
nextRetryInMs: backoffMs
|
|
522
|
+
} });
|
|
523
|
+
if (attempt === PERSISTENT_SUBSCRIBE_FAILURE_ATTEMPTS) logger.warn("audio-chunk poller: broker still absent after a minute — slowing the retry", { meta: {
|
|
524
|
+
brokerId,
|
|
525
|
+
tag,
|
|
526
|
+
attempt,
|
|
527
|
+
error: require_dist.errMsg(err),
|
|
528
|
+
nextRetryInMs: LONG_SUBSCRIBE_RETRY_BACKOFF_MS,
|
|
529
|
+
hint: "a DISABLED camera, or a camStreamId the broker never registered — audio stays absent until the broker appears"
|
|
530
|
+
} });
|
|
531
|
+
await sleep(backoffMs, lifecycle);
|
|
532
|
+
backoffMs = Math.min(attempt >= PERSISTENT_SUBSCRIBE_FAILURE_ATTEMPTS ? LONG_SUBSCRIBE_RETRY_BACKOFF_MS : MAX_SUBSCRIBE_RETRY_BACKOFF_MS, backoffMs * 2);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Drive the steady-state poll loop. Mid-stream `pullAudioChunks` failures
|
|
538
|
+
* trigger a fresh `subscribeAudioChunks` via the recovery branch — covers
|
|
539
|
+
* the broker child restart case where our `subscriptionId` is silently
|
|
540
|
+
* disowned.
|
|
541
|
+
*/
|
|
542
|
+
function startPolling(options, lifecycle) {
|
|
543
|
+
const { api, brokerId, tag, ownerNodeId, onChunk, logger } = options;
|
|
544
|
+
const pin = require_dist.nodePin(ownerNodeId);
|
|
545
|
+
let consecutiveFailures = 0;
|
|
546
|
+
const resubscribe = async () => {
|
|
547
|
+
try {
|
|
548
|
+
const result = await api.streamBroker.subscribeAudioChunks.mutate({
|
|
549
|
+
brokerId,
|
|
550
|
+
tag,
|
|
551
|
+
...options.accept ? { accept: options.accept } : {}
|
|
552
|
+
}, pin);
|
|
553
|
+
lifecycle.activeSubscriptionId = result.subscriptionId;
|
|
554
|
+
logger.info("audio-chunk poller: re-subscribed after broker outage", { meta: {
|
|
555
|
+
brokerId,
|
|
556
|
+
tag,
|
|
557
|
+
subscriptionId: result.subscriptionId,
|
|
558
|
+
afterFailures: consecutiveFailures
|
|
559
|
+
} });
|
|
560
|
+
return true;
|
|
561
|
+
} catch {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const tick = async () => {
|
|
566
|
+
if (lifecycle.stopped) return;
|
|
567
|
+
const subId = lifecycle.activeSubscriptionId;
|
|
568
|
+
if (!subId) return;
|
|
569
|
+
try {
|
|
570
|
+
const chunks = await api.streamBroker.pullAudioChunks.query({
|
|
571
|
+
subscriptionId: subId,
|
|
572
|
+
maxCount: PULL_MAX_COUNT
|
|
573
|
+
}, pin);
|
|
574
|
+
if (consecutiveFailures > 0) logger.info("audio-chunk poller: stream recovered", { meta: {
|
|
575
|
+
brokerId,
|
|
576
|
+
subscriptionId: subId
|
|
577
|
+
} });
|
|
578
|
+
consecutiveFailures = 0;
|
|
579
|
+
for (const chunk of chunks) {
|
|
580
|
+
if (lifecycle.stopped) break;
|
|
581
|
+
await onChunk(chunk);
|
|
582
|
+
}
|
|
583
|
+
} catch (err) {
|
|
584
|
+
consecutiveFailures += 1;
|
|
585
|
+
if (consecutiveFailures === 1) logger.warn("audio-chunk poller: pullAudioChunks failed — attempting recovery", { meta: {
|
|
586
|
+
brokerId,
|
|
587
|
+
subscriptionId: subId,
|
|
588
|
+
error: require_dist.errMsg(err)
|
|
589
|
+
} });
|
|
590
|
+
if (!lifecycle.stopped && consecutiveFailures >= RESUBSCRIBE_AFTER_FAILURES && (consecutiveFailures - RESUBSCRIBE_AFTER_FAILURES) % RESUBSCRIBE_THROTTLE_TICKS === 0) await resubscribe();
|
|
591
|
+
}
|
|
592
|
+
if (!lifecycle.stopped) lifecycle.pollTimer = setTimeout(() => void tick(), options.pollIntervalMs ?? 200);
|
|
593
|
+
};
|
|
594
|
+
tick();
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Cancellable sleep — wakes early when `lifecycle.stopped` flips. We
|
|
598
|
+
* keep a local wrapper around the shared {@link sleep} helper because
|
|
599
|
+
* the lifecycle tracks the active retry timer for `teardown()` to
|
|
600
|
+
* clear; pure `sleep()` would leak the timer if teardown fired while
|
|
601
|
+
* we were waiting.
|
|
602
|
+
*/
|
|
603
|
+
function sleep(ms, lifecycle) {
|
|
604
|
+
return new Promise((resolve) => {
|
|
605
|
+
if (lifecycle.stopped) {
|
|
606
|
+
resolve();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
lifecycle.retryTimer = setTimeout(() => {
|
|
610
|
+
lifecycle.retryTimer = void 0;
|
|
611
|
+
resolve();
|
|
612
|
+
}, ms);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
function sameFormat(a, b) {
|
|
616
|
+
return a.sampleRate === b.sampleRate && a.channels === b.channels && a.format === b.format;
|
|
617
|
+
}
|
|
618
|
+
var AudioWindowAccumulator = class {
|
|
619
|
+
deviceId;
|
|
620
|
+
onFormat;
|
|
621
|
+
pcmParts = [];
|
|
622
|
+
accumulatedBytes = 0;
|
|
623
|
+
accumulatedMs = 0;
|
|
624
|
+
windowSampleRate = 0;
|
|
625
|
+
windowChannels = 0;
|
|
626
|
+
windowFormat = "f32le";
|
|
627
|
+
windowTimestamp = 0;
|
|
628
|
+
windowOpen = false;
|
|
629
|
+
/** Last format seen on this subscription — survives a flush, unlike the window anchor. */
|
|
630
|
+
lastFormat = null;
|
|
631
|
+
constructor(deviceId, onFormat) {
|
|
632
|
+
this.deviceId = deviceId;
|
|
633
|
+
this.onFormat = onFormat;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Append one decoded PCM chunk to the open window. Returns the flushed
|
|
637
|
+
* `AudioChunkInput` once the accumulated duration reaches
|
|
638
|
+
* `AUDIO_WINDOW_TARGET_MS` (and resets for the next window), else `null`
|
|
639
|
+
* (accumulate-only, no flush yet).
|
|
640
|
+
*/
|
|
641
|
+
push(chunk) {
|
|
642
|
+
const byteLength = chunk.data.byteLength;
|
|
643
|
+
const bytes = new Uint8Array(byteLength);
|
|
644
|
+
bytes.set(new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, byteLength));
|
|
645
|
+
const format = chunk.format ?? "f32le";
|
|
646
|
+
this.observeFormat({
|
|
647
|
+
sampleRate: chunk.sampleRate,
|
|
648
|
+
channels: chunk.channels,
|
|
649
|
+
format
|
|
650
|
+
});
|
|
651
|
+
if (!this.windowOpen) {
|
|
652
|
+
this.windowSampleRate = chunk.sampleRate;
|
|
653
|
+
this.windowChannels = chunk.channels;
|
|
654
|
+
this.windowFormat = format;
|
|
655
|
+
this.windowTimestamp = chunk.timestamp;
|
|
656
|
+
this.windowOpen = true;
|
|
657
|
+
}
|
|
658
|
+
this.pcmParts.push(bytes);
|
|
659
|
+
this.accumulatedBytes += byteLength;
|
|
660
|
+
const channels = chunk.channels > 0 ? chunk.channels : 1;
|
|
661
|
+
const framesPerChannel = byteLength / require_dist.audioChunkBytesPerSample(format) / channels;
|
|
662
|
+
this.accumulatedMs += framesPerChannel / chunk.sampleRate * 1e3;
|
|
663
|
+
if (this.accumulatedMs < 1e3) return null;
|
|
664
|
+
const windowData = new Uint8Array(this.accumulatedBytes);
|
|
665
|
+
let offset = 0;
|
|
666
|
+
for (const part of this.pcmParts) {
|
|
667
|
+
windowData.set(part, offset);
|
|
668
|
+
offset += part.byteLength;
|
|
669
|
+
}
|
|
670
|
+
const flushSampleRate = this.windowSampleRate;
|
|
671
|
+
const flushChannels = this.windowChannels;
|
|
672
|
+
const flushFormat = this.windowFormat;
|
|
673
|
+
const flushTimestamp = this.windowTimestamp;
|
|
674
|
+
this.reset();
|
|
675
|
+
return {
|
|
676
|
+
data: flushFormat === "f32le" ? windowData : require_dist.expandAudioChunkToF32le(windowData, flushFormat),
|
|
677
|
+
sampleRate: flushSampleRate,
|
|
678
|
+
channels: flushChannels,
|
|
679
|
+
timestamp: flushTimestamp,
|
|
680
|
+
deviceId: this.deviceId
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Report the first format and every change; a change under an OPEN window
|
|
685
|
+
* drops the partial window so the flushed bytes never mix two rates.
|
|
686
|
+
*/
|
|
687
|
+
observeFormat(format) {
|
|
688
|
+
const previous = this.lastFormat;
|
|
689
|
+
if (previous !== null && sameFormat(previous, format)) return;
|
|
690
|
+
const droppedMs = this.windowOpen ? Math.round(this.accumulatedMs) : 0;
|
|
691
|
+
if (this.windowOpen) this.reset();
|
|
692
|
+
this.lastFormat = format;
|
|
693
|
+
this.onFormat?.({
|
|
694
|
+
from: previous,
|
|
695
|
+
to: format,
|
|
696
|
+
droppedMs
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Drop any partially-accumulated window. Called on unsubscribe / device
|
|
701
|
+
* audio stop (no trailing flush — a sub-second partial window is not worth
|
|
702
|
+
* a classify during teardown) and internally after every flush.
|
|
703
|
+
*/
|
|
704
|
+
reset() {
|
|
705
|
+
this.pcmParts = [];
|
|
706
|
+
this.accumulatedBytes = 0;
|
|
707
|
+
this.accumulatedMs = 0;
|
|
708
|
+
this.windowOpen = false;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
/**
|
|
712
|
+
* Build the canonical `AudioResult` frame from one `analyseChunk` result —
|
|
713
|
+
* extracted verbatim from `subscribeAudioStream`'s result→event-payload
|
|
714
|
+
* mapping. The caller wraps this in the `pipeline.audio-inference-result`
|
|
715
|
+
* event; building the frame is pure and needs no controller state.
|
|
716
|
+
*/
|
|
717
|
+
function buildAudioResultFrame(deviceId, result) {
|
|
718
|
+
const windowId = `${deviceId}-${result.timestamp}`;
|
|
719
|
+
const audioDetections = [];
|
|
720
|
+
let idCounter = 0;
|
|
721
|
+
if (result.classification && result.classification.labels.length > 0) for (const label of result.classification.labels) {
|
|
722
|
+
idCounter += 1;
|
|
723
|
+
audioDetections.push({
|
|
724
|
+
id: `a${idCounter}`,
|
|
725
|
+
kind: "audio",
|
|
726
|
+
macroClass: label.className,
|
|
727
|
+
score: label.score,
|
|
728
|
+
labels: [{
|
|
729
|
+
label: label.className,
|
|
730
|
+
score: label.score
|
|
731
|
+
}],
|
|
732
|
+
startMs: 0,
|
|
733
|
+
endMs: 0
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
kind: "audio-window",
|
|
738
|
+
windowId,
|
|
739
|
+
deviceId,
|
|
740
|
+
timestamp: result.timestamp,
|
|
741
|
+
startMs: 0,
|
|
742
|
+
endMs: 0,
|
|
743
|
+
level: {
|
|
744
|
+
rms: result.level.rms,
|
|
745
|
+
dbfs: result.level.dbfs
|
|
746
|
+
},
|
|
747
|
+
detections: audioDetections,
|
|
748
|
+
debug: result.classification ? {
|
|
749
|
+
totalInferenceMs: result.classification.inferenceMs,
|
|
750
|
+
stepTimings: [{
|
|
751
|
+
source: "audio-analyzer",
|
|
752
|
+
ms: result.classification.inferenceMs,
|
|
753
|
+
detectionCount: audioDetections.length
|
|
754
|
+
}]
|
|
755
|
+
} : void 0
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/audio-analyzer/audio-device-attachments.ts
|
|
760
|
+
/**
|
|
761
|
+
* `AudioDeviceAttachments` — the analyzer's own audio subscriptions (D461).
|
|
762
|
+
*
|
|
763
|
+
* ## What this replaces
|
|
764
|
+
*
|
|
765
|
+
* Until 2026-09-11 the decoded audio-chunk plane ran through
|
|
766
|
+
* `pipeline-orchestrator`: it drained the broker's FIFO, accumulated ~1 s
|
|
767
|
+
* windows and pushed them back out to `audioAnalyzer.analyseChunk`. The
|
|
768
|
+
* orchestrator neither produced nor consumed the audio — it buffered it, and
|
|
769
|
+
* every byte crossed hub-main twice for the privilege. The poller's own
|
|
770
|
+
* docblock named the reason, and it was an accident: the code had been a
|
|
771
|
+
* closure INSIDE the broker's process, and when the `pipeline` co-location
|
|
772
|
+
* group was dissolved nobody moved it.
|
|
773
|
+
*
|
|
774
|
+
* D455 proved the relay was the PREMISE of the remaining cost rather than an
|
|
775
|
+
* alternative to it. It put the SOURCE bytes on the plane — one G.711 byte per
|
|
776
|
+
* sample instead of four f32le ones — but only for a consumer that declared it
|
|
777
|
+
* could read them, and the accumulator (the declaring consumer) sat in a third
|
|
778
|
+
* process that had to expand before forwarding. So legs one and two went coded
|
|
779
|
+
* and legs three and four stayed at ~49 MB/min each. Here the subscriber IS
|
|
780
|
+
* the decoder: there is no third interlocutor, the negotiation needs none, and
|
|
781
|
+
* legs three and four do not exist.
|
|
782
|
+
*
|
|
783
|
+
* ## What the orchestrator still owns
|
|
784
|
+
*
|
|
785
|
+
* Everything about WHETHER and WHERE, which is all policy: the `audioMode`
|
|
786
|
+
* gate, the on-motion window, the audio-track probe, the per-device node
|
|
787
|
+
* assignment and the settings read. It calls `attachDevice` / `detachDevice`
|
|
788
|
+
* and holds the teardown. What it no longer owns is the bytes.
|
|
789
|
+
*
|
|
790
|
+
* ## The failure this split ADDS, and where it is caught
|
|
791
|
+
*
|
|
792
|
+
* The broker subscription is now opened by a process the orchestrator does not
|
|
793
|
+
* supervise. A detach that never arrives — an analyzer runner that crashed, a
|
|
794
|
+
* node that went offline mid-teardown — leaves a FIFO nobody drains, and on
|
|
795
|
+
* this plane that is not merely a leak: `AudioChunkPlane.subscriberCount` is
|
|
796
|
+
* broker DEMAND, so an orphan pins the source dial and the decode session open
|
|
797
|
+
* for as long as the broker lives. It is caught in the broker
|
|
798
|
+
* (`AudioChunkPlane`'s idle lease), not here, because a process that has died
|
|
799
|
+
* cannot clean up after itself.
|
|
800
|
+
*/
|
|
801
|
+
var AudioDeviceAttachments = class {
|
|
802
|
+
deps;
|
|
803
|
+
attachments = /* @__PURE__ */ new Map();
|
|
804
|
+
shuttingDown = false;
|
|
805
|
+
constructor(deps) {
|
|
806
|
+
this.deps = deps;
|
|
807
|
+
}
|
|
808
|
+
/** Live attachment count — diagnostics only. */
|
|
809
|
+
get size() {
|
|
810
|
+
return this.attachments.size;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Open (or replace) this camera's audio pull. Returns as soon as the
|
|
814
|
+
* attachment exists: the first `subscribeAudioChunks` retries with backoff
|
|
815
|
+
* behind it, because the broker for a camStream may not be registered yet.
|
|
816
|
+
* That boot-race tolerance travelled with the poller and is unchanged.
|
|
817
|
+
*
|
|
818
|
+
* Idempotent BY REPLACEMENT. Two pollers on one camera would double the
|
|
819
|
+
* broker's fanout and each would flush its own half-windows; a re-attach
|
|
820
|
+
* (a rebalance, a settings change, a watchdog recovery) must therefore drop
|
|
821
|
+
* the prior one, and it is reported so an unexpected churn is visible.
|
|
822
|
+
*/
|
|
823
|
+
attach(request) {
|
|
824
|
+
if (this.shuttingDown) {
|
|
825
|
+
this.deps.logger.warn("audio attach REFUSED — analyzer is shutting down", {
|
|
826
|
+
tags: { deviceId: request.deviceId },
|
|
827
|
+
meta: { brokerId: request.brokerId }
|
|
828
|
+
});
|
|
829
|
+
return {
|
|
830
|
+
attached: false,
|
|
831
|
+
replaced: false
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
const api = this.deps.api();
|
|
835
|
+
if (!api) {
|
|
836
|
+
this.deps.logger.warn("audio attach REFUSED — no cross-addon api available", {
|
|
837
|
+
tags: { deviceId: request.deviceId },
|
|
838
|
+
meta: { brokerId: request.brokerId }
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
attached: false,
|
|
842
|
+
replaced: false
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
const replaced = this.releaseAttachment(request.deviceId, "replaced");
|
|
846
|
+
const { deviceId, brokerId, ingestNodeId, settings } = request;
|
|
847
|
+
const logger = this.deps.logger.withTags({ deviceId });
|
|
848
|
+
const isRemoteBroker = ingestNodeId !== this.deps.localNodeId();
|
|
849
|
+
const accumulator = new AudioWindowAccumulator(deviceId, (change) => {
|
|
850
|
+
const meta = {
|
|
851
|
+
sampleRate: change.to.sampleRate,
|
|
852
|
+
channels: change.to.channels,
|
|
853
|
+
format: change.to.format,
|
|
854
|
+
previous: change.from,
|
|
855
|
+
droppedMs: change.droppedMs
|
|
856
|
+
};
|
|
857
|
+
if (change.from === null) {
|
|
858
|
+
logger.info("audio stream delivering", {
|
|
859
|
+
tags: { deviceId },
|
|
860
|
+
meta
|
|
861
|
+
});
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
logger.warn("audio stream format changed — partial window dropped", {
|
|
865
|
+
tags: { deviceId },
|
|
866
|
+
meta
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
const teardown = startAudioChunkPoller({
|
|
870
|
+
api,
|
|
871
|
+
brokerId,
|
|
872
|
+
tag: "audio-analyzer",
|
|
873
|
+
ownerNodeId: ingestNodeId,
|
|
874
|
+
accept: ["pcmu", "pcma"],
|
|
875
|
+
pollIntervalMs: isRemoteBroker ? 500 : 200,
|
|
876
|
+
logger,
|
|
877
|
+
onChunk: async (chunk) => {
|
|
878
|
+
try {
|
|
879
|
+
const window = accumulator.push(chunk);
|
|
880
|
+
if (!window) return;
|
|
881
|
+
const result = await this.deps.analyse(window, settings);
|
|
882
|
+
if (!result) return;
|
|
883
|
+
this.deps.emitResult(deviceId, buildAudioResultFrame(deviceId, result));
|
|
884
|
+
} catch (err) {
|
|
885
|
+
logger.error("Audio analysis failed", {
|
|
886
|
+
tags: { deviceId },
|
|
887
|
+
meta: { error: require_dist.errMsg(err) }
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
this.attachments.set(deviceId, {
|
|
893
|
+
brokerId,
|
|
894
|
+
teardown,
|
|
895
|
+
accumulator
|
|
896
|
+
});
|
|
897
|
+
this.deps.logger.info("audio attached", {
|
|
898
|
+
tags: { deviceId },
|
|
899
|
+
meta: {
|
|
900
|
+
brokerId,
|
|
901
|
+
ingestNodeId,
|
|
902
|
+
remoteBroker: isRemoteBroker,
|
|
903
|
+
pollIntervalMs: isRemoteBroker ? 500 : 200,
|
|
904
|
+
replaced
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
return {
|
|
908
|
+
attached: true,
|
|
909
|
+
replaced
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/** Close this camera's audio pull and release the broker subscription. Idempotent. */
|
|
913
|
+
detach(deviceId) {
|
|
914
|
+
return { detached: this.releaseAttachment(deviceId, "detached") };
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Release every attachment — the analyzer's own shutdown. This is the only
|
|
918
|
+
* orderly path that can return the broker's FIFOs; a crash cannot, which is
|
|
919
|
+
* what the plane's idle lease exists for.
|
|
920
|
+
*/
|
|
921
|
+
detachAll() {
|
|
922
|
+
this.shuttingDown = true;
|
|
923
|
+
for (const deviceId of [...this.attachments.keys()]) this.releaseAttachment(deviceId, "shutdown");
|
|
924
|
+
}
|
|
925
|
+
releaseAttachment(deviceId, reason) {
|
|
926
|
+
const existing = this.attachments.get(deviceId);
|
|
927
|
+
if (!existing) return false;
|
|
928
|
+
this.attachments.delete(deviceId);
|
|
929
|
+
try {
|
|
930
|
+
existing.teardown();
|
|
931
|
+
} catch (err) {
|
|
932
|
+
this.deps.logger.warn("audio detach: poller teardown threw", {
|
|
933
|
+
tags: { deviceId },
|
|
934
|
+
meta: {
|
|
935
|
+
brokerId: existing.brokerId,
|
|
936
|
+
reason,
|
|
937
|
+
error: require_dist.errMsg(err)
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
existing.accumulator.reset();
|
|
942
|
+
this.deps.logger.info("audio detached", {
|
|
943
|
+
tags: { deviceId },
|
|
944
|
+
meta: {
|
|
945
|
+
brokerId: existing.brokerId,
|
|
946
|
+
reason
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
394
952
|
//#endregion
|
|
395
953
|
//#region src/audio-analyzer/addons/analyzer/index.ts
|
|
396
954
|
/**
|
|
@@ -404,6 +962,9 @@ var CLASSIFY_ERROR_SUPPRESS_MS = 3e4;
|
|
|
404
962
|
var CLASSIFY_MIN_INTERVAL_MS = 500;
|
|
405
963
|
var PIPELINE_IDLE_DISPOSE_MS = 5 * 6e4;
|
|
406
964
|
var GLOBAL_DEVICE_KEY = -1;
|
|
965
|
+
/** What the classifier consumes natively; anything else is resampled next to it. */
|
|
966
|
+
var CLASSIFIER_SAMPLE_RATE = 16e3;
|
|
967
|
+
var CLASSIFIER_CHANNELS = 1;
|
|
407
968
|
/**
|
|
408
969
|
* Reconstruct a Float32 view over f32le PCM bytes carried in a Uint8Array.
|
|
409
970
|
*
|
|
@@ -424,6 +985,7 @@ var AudioAnalyzerProvider = class {
|
|
|
424
985
|
deviceContributionResolver;
|
|
425
986
|
deviceSettingsPatcher;
|
|
426
987
|
reprobeImpl;
|
|
988
|
+
attachments;
|
|
427
989
|
log;
|
|
428
990
|
classifyCallCount = 0;
|
|
429
991
|
lastClassifyErrorMs = 0;
|
|
@@ -435,6 +997,8 @@ var AudioAnalyzerProvider = class {
|
|
|
435
997
|
debugClassifySamples = false;
|
|
436
998
|
/** Per-camera in-flight state. Key = deviceId (or GLOBAL_DEVICE_KEY for legacy callers). */
|
|
437
999
|
cameraState = /* @__PURE__ */ new Map();
|
|
1000
|
+
/** Last window format seen per camera — the first and every change is logged (D450). */
|
|
1001
|
+
windowFormats = /* @__PURE__ */ new Map();
|
|
438
1002
|
/** Global pipeline lock — Apple SA and ONNX are single-channel: only one classify() can
|
|
439
1003
|
* run at a time. Without this, concurrent calls from different cameras overwrite the
|
|
440
1004
|
* single pendingResolve slot in AppleSoundAnalysisPipeline, causing 30s timeouts. */
|
|
@@ -455,12 +1019,13 @@ var AudioAnalyzerProvider = class {
|
|
|
455
1019
|
/** Set on `dispose()` (addon stop) so a late classify/idle-timer never
|
|
456
1020
|
* re-spawns the subprocess after teardown. */
|
|
457
1021
|
disposed = false;
|
|
458
|
-
constructor(logger, pipelineFactory, initialBackend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl) {
|
|
1022
|
+
constructor(logger, pipelineFactory, initialBackend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl, attachments) {
|
|
459
1023
|
this.pipelineFactory = pipelineFactory;
|
|
460
1024
|
this.deviceSettingsResolver = deviceSettingsResolver;
|
|
461
1025
|
this.deviceContributionResolver = deviceContributionResolver;
|
|
462
1026
|
this.deviceSettingsPatcher = deviceSettingsPatcher;
|
|
463
1027
|
this.reprobeImpl = reprobeImpl;
|
|
1028
|
+
this.attachments = attachments;
|
|
464
1029
|
this.log = logger;
|
|
465
1030
|
this.backendName = initialBackend;
|
|
466
1031
|
this.desiredBackend = initialBackend;
|
|
@@ -598,8 +1163,23 @@ var AudioAnalyzerProvider = class {
|
|
|
598
1163
|
async resolveDeviceSettings({ deviceId }) {
|
|
599
1164
|
return this.deviceSettingsResolver(deviceId);
|
|
600
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Start pulling this camera's audio from the broker, in THIS process (D461).
|
|
1168
|
+
* Thin delegate — the attachment registry owns the poller, the window and
|
|
1169
|
+
* the result event; this method exists so the orchestrator can express the
|
|
1170
|
+
* policy it still owns ("this camera's audio runs, on this node") without
|
|
1171
|
+
* owning the bytes.
|
|
1172
|
+
*/
|
|
1173
|
+
async attachDevice(input) {
|
|
1174
|
+
return this.attachments.attach(input);
|
|
1175
|
+
}
|
|
1176
|
+
/** Stop pulling this camera's audio and release the broker subscription. */
|
|
1177
|
+
async detachDevice({ deviceId }) {
|
|
1178
|
+
return this.attachments.detach(deviceId);
|
|
1179
|
+
}
|
|
601
1180
|
async analyseChunk({ chunk, settings }) {
|
|
602
1181
|
const samples = float32FromBytes(chunk.data);
|
|
1182
|
+
this.reportWindowFormat(chunk, samples.length);
|
|
603
1183
|
let sumSquares = 0;
|
|
604
1184
|
for (let i = 0; i < samples.length; i++) sumSquares += samples[i] * samples[i];
|
|
605
1185
|
const rms = Math.sqrt(sumSquares / samples.length);
|
|
@@ -668,6 +1248,38 @@ var AudioAnalyzerProvider = class {
|
|
|
668
1248
|
timestamp: chunk.timestamp
|
|
669
1249
|
};
|
|
670
1250
|
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Say what this camera's windows arrive as — once, and again on every
|
|
1253
|
+
* change. The plane carries the source rate (8 kHz for G.711 since D450,
|
|
1254
|
+
* 16 kHz for AAC through the codec session), and the classifier resamples
|
|
1255
|
+
* anything that is not 16 kHz mono next to the model. A camera whose
|
|
1256
|
+
* format flips mid-subscription (a broker redeploy) is visible here, and
|
|
1257
|
+
* a consumer that stopped receiving what it expected is never silent
|
|
1258
|
+
* (D427).
|
|
1259
|
+
*/
|
|
1260
|
+
reportWindowFormat(chunk, sampleCount) {
|
|
1261
|
+
const key = chunk.deviceId ?? GLOBAL_DEVICE_KEY;
|
|
1262
|
+
const format = {
|
|
1263
|
+
sampleRate: chunk.sampleRate,
|
|
1264
|
+
channels: chunk.channels
|
|
1265
|
+
};
|
|
1266
|
+
const previous = this.windowFormats.get(key) ?? null;
|
|
1267
|
+
if (previous !== null && previous.sampleRate === format.sampleRate && previous.channels === format.channels) return;
|
|
1268
|
+
this.windowFormats.set(key, format);
|
|
1269
|
+
const channels = chunk.channels > 0 ? chunk.channels : 1;
|
|
1270
|
+
const windowMs = chunk.sampleRate > 0 ? Math.round(sampleCount / channels / chunk.sampleRate * 1e3) : null;
|
|
1271
|
+
this.log.info("audio window format", {
|
|
1272
|
+
tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
|
|
1273
|
+
meta: {
|
|
1274
|
+
sampleRate: chunk.sampleRate,
|
|
1275
|
+
channels: chunk.channels,
|
|
1276
|
+
bytes: chunk.data.byteLength,
|
|
1277
|
+
windowMs,
|
|
1278
|
+
classifierResamples: chunk.sampleRate !== CLASSIFIER_SAMPLE_RATE || chunk.channels !== CLASSIFIER_CHANNELS,
|
|
1279
|
+
previous
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
671
1283
|
async classify(chunk) {
|
|
672
1284
|
const camKey = chunk.deviceId ?? GLOBAL_DEVICE_KEY;
|
|
673
1285
|
const now = Date.now();
|
|
@@ -802,6 +1414,13 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
|
|
|
802
1414
|
* in-place rebuild in `onConfigChanged` (rebuild only when it genuinely
|
|
803
1415
|
* flips). The provider owns the (lazy) pipeline itself. */
|
|
804
1416
|
activeBackend = null;
|
|
1417
|
+
/**
|
|
1418
|
+
* This node's live audio attachments (D461). Held on the ADDON rather than
|
|
1419
|
+
* only inside the provider because shutdown must release the broker's FIFOs
|
|
1420
|
+
* even if the provider never came up — an orphaned audio subscription is
|
|
1421
|
+
* broker DEMAND, not merely a leak.
|
|
1422
|
+
*/
|
|
1423
|
+
attachments = null;
|
|
805
1424
|
constructor() {
|
|
806
1425
|
super(require_dist.DEFAULT_AUDIO_ANALYZER_CONFIG);
|
|
807
1426
|
}
|
|
@@ -946,7 +1565,40 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
|
|
|
946
1565
|
return null;
|
|
947
1566
|
};
|
|
948
1567
|
const deviceSettingsPatcher = async (_deviceId, _patch) => {};
|
|
949
|
-
this.
|
|
1568
|
+
this.attachments = new AudioDeviceAttachments({
|
|
1569
|
+
api: () => this.ctx.api,
|
|
1570
|
+
localNodeId: () => this.ctx.kernel?.localNodeId ?? "hub",
|
|
1571
|
+
logger: logger.child("audio-attach"),
|
|
1572
|
+
analyse: async (chunk, settings) => {
|
|
1573
|
+
const provider = this.provider;
|
|
1574
|
+
if (!provider) return null;
|
|
1575
|
+
return provider.analyseChunk({
|
|
1576
|
+
chunk,
|
|
1577
|
+
settings
|
|
1578
|
+
});
|
|
1579
|
+
},
|
|
1580
|
+
emitResult: (deviceId, frame) => {
|
|
1581
|
+
const nodeId = this.ctx.kernel?.localNodeId ?? "hub";
|
|
1582
|
+
this.ctx.eventBus.emit({
|
|
1583
|
+
id: `audio-inference-${deviceId}-${Date.now()}`,
|
|
1584
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1585
|
+
source: {
|
|
1586
|
+
type: "device",
|
|
1587
|
+
id: deviceId,
|
|
1588
|
+
nodeId,
|
|
1589
|
+
addonId: "audio-analyzer",
|
|
1590
|
+
deviceId
|
|
1591
|
+
},
|
|
1592
|
+
category: require_dist.EventCategory.PipelineAudioInferenceResult,
|
|
1593
|
+
data: {
|
|
1594
|
+
deviceId,
|
|
1595
|
+
frame,
|
|
1596
|
+
nodeId
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
this.provider = new AudioAnalyzerProvider(logger, (b) => this.buildPipeline(b), backend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, () => this.reprobeAudioEngine(), this.attachments);
|
|
950
1602
|
return [{
|
|
951
1603
|
capability: require_dist.audioAnalyzerCapability,
|
|
952
1604
|
provider: this.provider
|
|
@@ -956,6 +1608,8 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
|
|
|
956
1608
|
}];
|
|
957
1609
|
}
|
|
958
1610
|
async onShutdown() {
|
|
1611
|
+
this.attachments?.detachAll();
|
|
1612
|
+
this.attachments = null;
|
|
959
1613
|
if (this.provider) {
|
|
960
1614
|
await this.provider.dispose();
|
|
961
1615
|
this.provider = null;
|