@camstack/addon-pipeline-orchestrator 1.2.183 → 1.2.184
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/_stub.js +2 -2
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-C1GCjKqE.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DESgaosf.mjs} +3 -3
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCgZLytW.mjs +26 -0
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DY6aTgRF.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DZ_ciK86.mjs} +1 -1
- package/dist/{hostInit-DKGgP2I9.mjs → hostInit-D832usvf.mjs} +3 -3
- package/dist/index.js +387 -584
- package/dist/index.mjs +387 -584
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-H_CMYslN.mjs +0 -26
package/dist/index.mjs
CHANGED
|
@@ -5348,6 +5348,86 @@ var ZodIssueCode = {
|
|
|
5348
5348
|
/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
|
|
5349
5349
|
var ZodFirstPartyTypeKind;
|
|
5350
5350
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5351
|
+
//#endregion
|
|
5352
|
+
//#region ../types/dist/sleep-BnujYGPe.mjs
|
|
5353
|
+
/**
|
|
5354
|
+
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5355
|
+
* window to float samples (D455).
|
|
5356
|
+
*
|
|
5357
|
+
* ## Why a format at all
|
|
5358
|
+
*
|
|
5359
|
+
* D450 took the plane off its 8 → 16 kHz upsample: it carries the SOURCE
|
|
5360
|
+
* RATE, and the one consumer that needs 16 kHz resamples next to the model.
|
|
5361
|
+
* It left the FORMAT alone — the broker still turned each G.711 byte into a
|
|
5362
|
+
* 4-byte f32le sample before the bytes entered the transport, so every leg of
|
|
5363
|
+
* the plane carried four times the source. The plane crosses hub-main twice on
|
|
5364
|
+
* the way to the analyzer, and the fleet's G.711 cameras are ~79 % of it.
|
|
5365
|
+
*
|
|
5366
|
+
* So the plane carries the source BYTES too, and whoever needs floats expands
|
|
5367
|
+
* them where it needs them. That is the same argument D450 made for the rate,
|
|
5368
|
+
* one step further along the same wire.
|
|
5369
|
+
*
|
|
5370
|
+
* ## Why the expansion lives here
|
|
5371
|
+
*
|
|
5372
|
+
* Two packages need it and they must never disagree: `addon-pipeline`'s broker
|
|
5373
|
+
* (which still has to serve a subscriber that did NOT ask for coded bytes —
|
|
5374
|
+
* `AudioChunkPlane` expands per subscription) and
|
|
5375
|
+
* `addon-pipeline-orchestrator`'s `AudioWindowAccumulator` (which flushes an
|
|
5376
|
+
* f32le window to the analyzer cap, whose `AudioChunkInput` contract is
|
|
5377
|
+
* unchanged and stays f32le). Both bundle the bare `@camstack/types` entry
|
|
5378
|
+
* into their own dist (`self-contained` externals), so this travels with a
|
|
5379
|
+
* `camstack deploy` and needs no published server.
|
|
5380
|
+
*
|
|
5381
|
+
* A second μ-law table anywhere else is the defect this module exists to
|
|
5382
|
+
* prevent. (`stream-broker.ts`'s `mulawToPcm` / `alawToPcm` are the ENCODE
|
|
5383
|
+
* direction for the WebRTC egress — a different transform, not a copy.)
|
|
5384
|
+
*
|
|
5385
|
+
* ## Absent means f32le
|
|
5386
|
+
*
|
|
5387
|
+
* `format` is optional on the wire and its absence means `f32le` — today's
|
|
5388
|
+
* bytes, byte for byte. A peer that never heard of the field is served what it
|
|
5389
|
+
* has always been served, because the broker only emits a coded window to a
|
|
5390
|
+
* subscription that DECLARED it accepts one (`AudioSubscribeOptions.accept`).
|
|
5391
|
+
* That is the D448 `rawForward` negotiation, and it is what makes this
|
|
5392
|
+
* deployable one addon at a time across three nodes.
|
|
5393
|
+
*/
|
|
5394
|
+
/** Every byte format the audio chunk plane can carry. `f32le` is the default. */
|
|
5395
|
+
var AUDIO_CHUNK_FORMATS = [
|
|
5396
|
+
"f32le",
|
|
5397
|
+
"pcmu",
|
|
5398
|
+
"pcma"
|
|
5399
|
+
];
|
|
5400
|
+
/**
|
|
5401
|
+
* Build the μ-law decode table (ITU-T G.711). Each of the 256 byte values maps
|
|
5402
|
+
* to a 16-bit PCM sample, normalised to [-1.0, 1.0] for f32le output.
|
|
5403
|
+
*
|
|
5404
|
+
* Moved here verbatim from `audio-rtp-decoder.ts`, which no longer decodes:
|
|
5405
|
+
* it buffers the coded bytes and the plane's consumers expand.
|
|
5406
|
+
*/
|
|
5407
|
+
function buildUlawTable() {
|
|
5408
|
+
const table = new Float32Array(256);
|
|
5409
|
+
for (let i = 0; i < 256; i++) {
|
|
5410
|
+
const complemented = ~i & 255;
|
|
5411
|
+
const sign = (complemented & 128) !== 0 ? -1 : 1;
|
|
5412
|
+
const exponent = complemented >> 4 & 7;
|
|
5413
|
+
table[i] = sign * ((8 * (complemented & 15) + 132 << exponent) - 132) / 32768;
|
|
5414
|
+
}
|
|
5415
|
+
return table;
|
|
5416
|
+
}
|
|
5417
|
+
/** Build the A-law decode table (ITU-T G.711). */
|
|
5418
|
+
function buildAlawTable() {
|
|
5419
|
+
const table = new Float32Array(256);
|
|
5420
|
+
for (let i = 0; i < 256; i++) {
|
|
5421
|
+
const xored = i ^ 85;
|
|
5422
|
+
const sign = (xored & 128) !== 0 ? 1 : -1;
|
|
5423
|
+
const exponent = xored >> 4 & 7;
|
|
5424
|
+
const mantissa = xored & 15;
|
|
5425
|
+
table[i] = sign * (exponent === 0 ? 16 * mantissa + 8 : 16 * mantissa + 264 << exponent - 1) / 32768;
|
|
5426
|
+
}
|
|
5427
|
+
return table;
|
|
5428
|
+
}
|
|
5429
|
+
buildUlawTable();
|
|
5430
|
+
buildAlawTable();
|
|
5351
5431
|
Object.fromEntries([
|
|
5352
5432
|
{
|
|
5353
5433
|
id: "overview",
|
|
@@ -6667,11 +6747,20 @@ var SubscribeFramesResultSchema = object({
|
|
|
6667
6747
|
* (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
|
|
6668
6748
|
* / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
|
|
6669
6749
|
*/
|
|
6750
|
+
var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
|
|
6670
6751
|
var DecodedAudioChunkSchema = object({
|
|
6671
6752
|
data: _instanceof(Uint8Array),
|
|
6672
6753
|
sampleRate: number().int().positive(),
|
|
6673
6754
|
channels: number().int().positive(),
|
|
6674
|
-
timestamp: number()
|
|
6755
|
+
timestamp: number(),
|
|
6756
|
+
/**
|
|
6757
|
+
* Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
|
|
6758
|
+
* byte, for any peer that never heard of this field. A coded window
|
|
6759
|
+
* (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
|
|
6760
|
+
* subscription that DECLARED it accepts one, so absence can never mean
|
|
6761
|
+
* "coded bytes a consumer will read as floats" (D455).
|
|
6762
|
+
*/
|
|
6763
|
+
format: AudioChunkFormatSchema.optional()
|
|
6675
6764
|
});
|
|
6676
6765
|
/**
|
|
6677
6766
|
* Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
|
|
@@ -6683,7 +6772,18 @@ var DecodedAudioChunkSchema = object({
|
|
|
6683
6772
|
var SubscribeAudioChunksInputSchema = object({
|
|
6684
6773
|
brokerId: string(),
|
|
6685
6774
|
/** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
|
|
6686
|
-
tag: string().optional()
|
|
6775
|
+
tag: string().optional(),
|
|
6776
|
+
/**
|
|
6777
|
+
* Byte formats this subscriber can READ, best first. The broker serves the
|
|
6778
|
+
* chunk's own format when it is in this list and expands to `f32le`
|
|
6779
|
+
* otherwise, so a subscriber is never handed bytes it cannot interpret.
|
|
6780
|
+
*
|
|
6781
|
+
* Absent (or without the source format) means `f32le` — the behaviour every
|
|
6782
|
+
* subscriber had before D455, unchanged. This is the negotiation half of
|
|
6783
|
+
* the source-bytes lever: it is what lets the broker and its consumers
|
|
6784
|
+
* deploy one at a time across three nodes.
|
|
6785
|
+
*/
|
|
6786
|
+
accept: array(AudioChunkFormatSchema).readonly().optional()
|
|
6687
6787
|
});
|
|
6688
6788
|
/** Result of `stream-broker.subscribeAudioChunks`. */
|
|
6689
6789
|
var SubscribeAudioChunksResultSchema = object({
|
|
@@ -7755,7 +7855,7 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
|
|
|
7755
7855
|
* still gives the event loop a chance to drain — useful for breaking
|
|
7756
7856
|
* up tight async loops without changing call-site semantics.
|
|
7757
7857
|
*/
|
|
7758
|
-
function sleep
|
|
7858
|
+
function sleep(ms) {
|
|
7759
7859
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
7760
7860
|
}
|
|
7761
7861
|
//#endregion
|
|
@@ -11508,6 +11608,51 @@ var AudioAnalysisSettingsSchema = object({
|
|
|
11508
11608
|
minConfidence: number().min(0).max(1).default(.3),
|
|
11509
11609
|
allowedClasses: array(string()).default([])
|
|
11510
11610
|
});
|
|
11611
|
+
/**
|
|
11612
|
+
* `attachDevice` — the analyzer PULLS a camera's audio from the broker (D461).
|
|
11613
|
+
*
|
|
11614
|
+
* Until D461 the orchestrator drained the broker's chunk plane, accumulated
|
|
11615
|
+
* ~1 s windows and pushed them back out as `analyseChunk`. It neither produced
|
|
11616
|
+
* nor consumed the audio: the PCM crossed hub-main twice for a process that
|
|
11617
|
+
* only buffered it. `attachDevice` inverts the direction — the analyzer opens
|
|
11618
|
+
* its own `subscribeAudioChunks` against the broker and the subscriber IS the
|
|
11619
|
+
* decoder, so the coded G.711 bytes D455 put on the plane stay coded all the
|
|
11620
|
+
* way to the one expansion that feeds the model.
|
|
11621
|
+
*
|
|
11622
|
+
* The orchestrator still owns the POLICY (the `audioMode` gate, the on-motion
|
|
11623
|
+
* window, the per-device node assignment, the settings read) and therefore
|
|
11624
|
+
* still owns the attach/detach pair. It no longer owns the bytes.
|
|
11625
|
+
*/
|
|
11626
|
+
var AudioAttachDeviceInputSchema = object({
|
|
11627
|
+
deviceId: number(),
|
|
11628
|
+
/** Broker id (`<deviceId>/<camStreamId>`) carrying this camera's audio. */
|
|
11629
|
+
brokerId: string(),
|
|
11630
|
+
/**
|
|
11631
|
+
* `clusterRoles.ingestNode` — the node whose broker owns the source dial.
|
|
11632
|
+
* Every `streamBroker` call the attachment makes is pinned to it, exactly as
|
|
11633
|
+
* the orchestrator's poller pinned them before the move.
|
|
11634
|
+
*/
|
|
11635
|
+
ingestNodeId: string(),
|
|
11636
|
+
/**
|
|
11637
|
+
* Resolved once by the orchestrator at attach time, exactly as it was read
|
|
11638
|
+
* once per subscription before D461. The analyzer does NOT re-resolve per
|
|
11639
|
+
* window: a settings change re-attaches, which is what always happened.
|
|
11640
|
+
*/
|
|
11641
|
+
settings: AudioAnalysisSettingsSchema
|
|
11642
|
+
});
|
|
11643
|
+
var AudioAttachDeviceResultSchema = object({
|
|
11644
|
+
/** False only when the analyzer is shutting down and refused to attach. */
|
|
11645
|
+
attached: boolean(),
|
|
11646
|
+
/**
|
|
11647
|
+
* True when the attachment replaced a live one for the same device. An
|
|
11648
|
+
* attach is idempotent by REPLACEMENT — two pollers on one camera would
|
|
11649
|
+
* double the broker's fanout and neither would know about the other.
|
|
11650
|
+
*/
|
|
11651
|
+
replaced: boolean()
|
|
11652
|
+
});
|
|
11653
|
+
var AudioDetachDeviceResultSchema = object({
|
|
11654
|
+
/** False when no attachment existed — detach is idempotent. */
|
|
11655
|
+
detached: boolean() });
|
|
11511
11656
|
var AudioClassificationResultSchema = object({
|
|
11512
11657
|
labels: array(AudioClassificationLabelSchema).readonly(),
|
|
11513
11658
|
rawLabels: array(AudioClassificationLabelSchema).readonly().optional(),
|
|
@@ -11516,7 +11661,7 @@ var AudioClassificationResultSchema = object({
|
|
|
11516
11661
|
method(object({
|
|
11517
11662
|
chunk: AudioChunkInputSchema,
|
|
11518
11663
|
settings: AudioAnalysisSettingsSchema
|
|
11519
|
-
}), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
|
|
11664
|
+
}), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(AudioAttachDeviceInputSchema, AudioAttachDeviceResultSchema, { kind: "mutation" }), method(object({ deviceId: number() }), AudioDetachDeviceResultSchema, { kind: "mutation" }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
|
|
11520
11665
|
kind: "mutation",
|
|
11521
11666
|
auth: "admin"
|
|
11522
11667
|
});
|
|
@@ -32327,12 +32472,24 @@ Object.freeze({
|
|
|
32327
32472
|
addonId: null,
|
|
32328
32473
|
access: "create"
|
|
32329
32474
|
},
|
|
32475
|
+
"audioAnalyzer.attachDevice": {
|
|
32476
|
+
capName: "audio-analyzer",
|
|
32477
|
+
capScope: "system",
|
|
32478
|
+
addonId: null,
|
|
32479
|
+
access: "create"
|
|
32480
|
+
},
|
|
32330
32481
|
"audioAnalyzer.classify": {
|
|
32331
32482
|
capName: "audio-analyzer",
|
|
32332
32483
|
capScope: "system",
|
|
32333
32484
|
addonId: null,
|
|
32334
32485
|
access: "view"
|
|
32335
32486
|
},
|
|
32487
|
+
"audioAnalyzer.detachDevice": {
|
|
32488
|
+
capName: "audio-analyzer",
|
|
32489
|
+
capScope: "system",
|
|
32490
|
+
addonId: null,
|
|
32491
|
+
access: "create"
|
|
32492
|
+
},
|
|
32336
32493
|
"audioAnalyzer.dispose": {
|
|
32337
32494
|
capName: "audio-analyzer",
|
|
32338
32495
|
capScope: "system",
|
|
@@ -38176,11 +38333,21 @@ Object.freeze({
|
|
|
38176
38333
|
form: "single",
|
|
38177
38334
|
optional: false
|
|
38178
38335
|
}],
|
|
38336
|
+
"audioAnalyzer.attachDevice": [{
|
|
38337
|
+
name: "deviceId",
|
|
38338
|
+
form: "single",
|
|
38339
|
+
optional: false
|
|
38340
|
+
}],
|
|
38179
38341
|
"audioAnalyzer.classify": [{
|
|
38180
38342
|
name: "deviceId",
|
|
38181
38343
|
form: "single",
|
|
38182
38344
|
optional: true
|
|
38183
38345
|
}],
|
|
38346
|
+
"audioAnalyzer.detachDevice": [{
|
|
38347
|
+
name: "deviceId",
|
|
38348
|
+
form: "single",
|
|
38349
|
+
optional: false
|
|
38350
|
+
}],
|
|
38184
38351
|
"audioMetrics.getCurrentSnapshot": [{
|
|
38185
38352
|
name: "deviceId",
|
|
38186
38353
|
form: "single",
|
|
@@ -40032,6 +40199,52 @@ Object.freeze({
|
|
|
40032
40199
|
"network-access": "ingress",
|
|
40033
40200
|
"smtp-provider": "email"
|
|
40034
40201
|
});
|
|
40202
|
+
var G711_SCALE_CORRECTION_DB = {
|
|
40203
|
+
PCMU: 20 * Math.log10(4),
|
|
40204
|
+
PCMA: 20 * Math.log10(8)
|
|
40205
|
+
};
|
|
40206
|
+
/**
|
|
40207
|
+
* Restate a dBFS number that was MEASURED through the pre-epoch decoder as the
|
|
40208
|
+
* same intent on the ITU-T scale (D460).
|
|
40209
|
+
*
|
|
40210
|
+
* ## When this applies, and when it is the wrong thing to reach for
|
|
40211
|
+
*
|
|
40212
|
+
* An absolute-dBFS number in this repo is one of two things, and only one of
|
|
40213
|
+
* them converts:
|
|
40214
|
+
*
|
|
40215
|
+
* - **A statement about the scale** — "-55 dBFS is near silence", "-25 dBFS
|
|
40216
|
+
* is loud". It was true on the ITU-T scale before the epoch and it is true
|
|
40217
|
+
* after. The defect was never in the number; it was that 19 of this hub's
|
|
40218
|
+
* 25 cameras did not obey it. Converting such a number takes something
|
|
40219
|
+
* correct and makes it wrong, in order to preserve a bug.
|
|
40220
|
+
* - **A measurement taken through the old decoder** — a value someone read
|
|
40221
|
+
* off a meter that under-reported by exactly 4× (PCMU) or 8× (PCMA). It
|
|
40222
|
+
* describes a sound that was really {@link G711_SCALE_CORRECTION_DB} dB
|
|
40223
|
+
* louder. That is what this function is for.
|
|
40224
|
+
*
|
|
40225
|
+
* Telling the two apart is a question about PROVENANCE, not about arithmetic,
|
|
40226
|
+
* and it cannot be answered from the number. It is answered by the comment the
|
|
40227
|
+
* author left — which is why `scripts/check-dbfs-era.mts` makes leaving one
|
|
40228
|
+
* mandatory.
|
|
40229
|
+
*
|
|
40230
|
+
* ## Why a function and not a typed-in number
|
|
40231
|
+
*
|
|
40232
|
+
* `-55 + 12.04` written into a source file is, six months later, completely
|
|
40233
|
+
* indistinguishable from a threshold somebody simply preferred. Calling this
|
|
40234
|
+
* keeps the derivation, the law, and the original measurement all visible at
|
|
40235
|
+
* the call site, so a future reader can disagree with the *premise* instead of
|
|
40236
|
+
* having to reverse-engineer the sum.
|
|
40237
|
+
*
|
|
40238
|
+
* **This is not a runtime gain.** It converts an authored CONSTANT once, where
|
|
40239
|
+
* it is declared. It must never be applied to a live sample or a stored
|
|
40240
|
+
* `AudioEvent.dbfs`: the decoder is correct now, and a second authority
|
|
40241
|
+
* adjusting numbers the decoder already got right is the original defect with
|
|
40242
|
+
* an extra place to argue with (D459).
|
|
40243
|
+
*/
|
|
40244
|
+
function ituDbfsFromPreEpoch(law, authoredDbfs) {
|
|
40245
|
+
return authoredDbfs + G711_SCALE_CORRECTION_DB[law];
|
|
40246
|
+
}
|
|
40247
|
+
Math.round(ituDbfsFromPreEpoch("PCMU", -55));
|
|
40035
40248
|
/** Schema defaults — an untouched sub-field must author exactly these. */
|
|
40036
40249
|
var NC_AUDIO_DEFAULTS = {
|
|
40037
40250
|
hitPercent: 60,
|
|
@@ -40743,248 +40956,6 @@ function deviceBackendToFormat(backend) {
|
|
|
40743
40956
|
return DEVICE_BACKEND_TO_FORMAT[backend] ?? "onnx";
|
|
40744
40957
|
}
|
|
40745
40958
|
//#endregion
|
|
40746
|
-
//#region src/audio-chunk-poller.ts
|
|
40747
|
-
/**
|
|
40748
|
-
* `AudioChunkPoller` — the consumer-side poll loop of the decoded audio-chunk
|
|
40749
|
-
* plane (Phase 5 / D9).
|
|
40750
|
-
*
|
|
40751
|
-
* Replaces the live-object `IStreamBroker.onDecodedAudioChunk(cb)` callback
|
|
40752
|
-
* path. A live callback cannot cross a process boundary; once the `pipeline`
|
|
40753
|
-
* group is dissolved (Task 8) the orchestrator runs in a different process
|
|
40754
|
-
* from the broker, so audio delivery must go over tRPC.
|
|
40755
|
-
*
|
|
40756
|
-
* The consumer:
|
|
40757
|
-
*
|
|
40758
|
-
* 1. `subscribeAudioChunks({ brokerId, tag })` over tRPC — the broker
|
|
40759
|
-
* registers a per-subscription bounded FIFO queue and returns a
|
|
40760
|
-
* `subscriptionId`;
|
|
40761
|
-
* 2. polls `pullAudioChunks({ subscriptionId, maxCount })` on a modest
|
|
40762
|
-
* interval, draining `DecodedAudioChunk`s in FIFO arrival order;
|
|
40763
|
-
* 3. feeds each chunk to its downstream audio logic;
|
|
40764
|
-
* 4. on teardown, `unsubscribeAudioChunks`.
|
|
40765
|
-
*
|
|
40766
|
-
* Audio is not latency-critical like video, and chunks arrive only ~every
|
|
40767
|
-
* 500ms (the broker's `AudioCodecSession` window). A modest poll period plus
|
|
40768
|
-
* a small per-poll burst keeps latency low without busy-spinning. The
|
|
40769
|
-
* broker's queue is FIFO/no-drop-sized so a poll-period of jitter never
|
|
40770
|
-
* loses a chunk.
|
|
40771
|
-
*
|
|
40772
|
-
* Boot-race tolerance: the broker for a given camStream may not be registered
|
|
40773
|
-
* yet when the orchestrator wires the subscription (provider addons publish
|
|
40774
|
-
* their cameraStreams asynchronously after their probe completes).
|
|
40775
|
-
* `subscribeAudioChunks` retries with exponential backoff (capped at
|
|
40776
|
-
* `MAX_SUBSCRIBE_RETRY_BACKOFF_MS`) until it succeeds or the returned
|
|
40777
|
-
* teardown closure is invoked. Mirrors the `FrameHandlePoller` recovery
|
|
40778
|
-
* shape so video and audio plumbing self-heal identically.
|
|
40779
|
-
*/
|
|
40780
|
-
/** Poll period — audio chunks arrive ~every 500ms; 200ms keeps latency low. */
|
|
40781
|
-
var POLL_INTERVAL_MS$1 = 200;
|
|
40782
|
-
/** How many chunks to drain per poll — a small burst absorbs jitter. */
|
|
40783
|
-
var PULL_MAX_COUNT = 8;
|
|
40784
|
-
/**
|
|
40785
|
-
* Consecutive pull failures before we attempt to re-subscribe. A single failed
|
|
40786
|
-
* poll is treated as a blip (re-subscribing would leak a broker-side FIFO); a
|
|
40787
|
-
* sustained failure means the broker child restarted and dropped our
|
|
40788
|
-
* subscription, so we re-establish it.
|
|
40789
|
-
*/
|
|
40790
|
-
var RESUBSCRIBE_AFTER_FAILURES = 2;
|
|
40791
|
-
/** Re-subscribe at most once every N ticks while failing (~1s at 200ms). */
|
|
40792
|
-
var RESUBSCRIBE_THROTTLE_TICKS = 5;
|
|
40793
|
-
/** First subscribe-retry delay, doubled on every subsequent failure. */
|
|
40794
|
-
var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
|
|
40795
|
-
/**
|
|
40796
|
-
* Subscribe-retry backoff ceiling. 5 s matches the frame-handle poller — fast
|
|
40797
|
-
* enough to recover within a single reconcile of the orchestrator and slow
|
|
40798
|
-
* enough that a misconfigured camStream costs ~12 op/min, not 5/s.
|
|
40799
|
-
*/
|
|
40800
|
-
var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
|
|
40801
|
-
/**
|
|
40802
|
-
* Attempts after which a still-failing subscribe escalates from the fast 5 s
|
|
40803
|
-
* ceiling to {@link LONG_SUBSCRIBE_RETRY_BACKOFF_MS}. ~15 attempts ≈ one
|
|
40804
|
-
* minute of fast retries — plenty for the boot races the 5 s ceiling exists
|
|
40805
|
-
* for. A broker that is STILL absent after that is a long-lived condition
|
|
40806
|
-
* (e.g. a DISABLED camera, whose broker the stream-broker reconcile refuses
|
|
40807
|
-
* to recreate — 2026-07-23) and retrying every 5 s forever is pure log/RPC
|
|
40808
|
-
* churn. The slow loop stays alive so audio still recovers automatically
|
|
40809
|
-
* (≤60 s) once the camera is re-enabled.
|
|
40810
|
-
*/
|
|
40811
|
-
var PERSISTENT_SUBSCRIBE_FAILURE_ATTEMPTS = 15;
|
|
40812
|
-
var LONG_SUBSCRIBE_RETRY_BACKOFF_MS = 6e4;
|
|
40813
|
-
/**
|
|
40814
|
-
* Subscribe to a broker's decoded audio-chunk stream and start the poll loop.
|
|
40815
|
-
*
|
|
40816
|
-
* Always resolves to a teardown closure — when the broker is not yet
|
|
40817
|
-
* registered the closure cancels the ongoing retry loop; when polling is
|
|
40818
|
-
* active it stops the loop and releases the broker subscription. Mirrors
|
|
40819
|
-
* `startFrameHandlePoller` so video and audio recover identically.
|
|
40820
|
-
*/
|
|
40821
|
-
function startAudioChunkPoller(options) {
|
|
40822
|
-
const lifecycle = {
|
|
40823
|
-
stopped: false,
|
|
40824
|
-
retryTimer: void 0,
|
|
40825
|
-
pollTimer: void 0,
|
|
40826
|
-
activeSubscriptionId: null
|
|
40827
|
-
};
|
|
40828
|
-
const teardown = () => {
|
|
40829
|
-
if (lifecycle.stopped) return;
|
|
40830
|
-
lifecycle.stopped = true;
|
|
40831
|
-
if (lifecycle.retryTimer) {
|
|
40832
|
-
clearTimeout(lifecycle.retryTimer);
|
|
40833
|
-
lifecycle.retryTimer = void 0;
|
|
40834
|
-
}
|
|
40835
|
-
if (lifecycle.pollTimer) {
|
|
40836
|
-
clearTimeout(lifecycle.pollTimer);
|
|
40837
|
-
lifecycle.pollTimer = void 0;
|
|
40838
|
-
}
|
|
40839
|
-
const subId = lifecycle.activeSubscriptionId;
|
|
40840
|
-
if (subId) {
|
|
40841
|
-
lifecycle.activeSubscriptionId = null;
|
|
40842
|
-
options.api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: subId }, nodePin(options.ownerNodeId)).catch((err) => {
|
|
40843
|
-
options.logger.warn("audio-chunk poller: unsubscribeAudioChunks failed", { meta: {
|
|
40844
|
-
brokerId: options.brokerId,
|
|
40845
|
-
subscriptionId: subId,
|
|
40846
|
-
error: errMsg(err)
|
|
40847
|
-
} });
|
|
40848
|
-
});
|
|
40849
|
-
}
|
|
40850
|
-
};
|
|
40851
|
-
subscribeWithRetry(options, lifecycle);
|
|
40852
|
-
return teardown;
|
|
40853
|
-
}
|
|
40854
|
-
/**
|
|
40855
|
-
* Run the subscribe → poll handshake with exponential backoff on subscribe
|
|
40856
|
-
* failures. Resolves once the subscription is acquired (and the poll loop has
|
|
40857
|
-
* been started) or once `lifecycle.stopped` flips, whichever comes first.
|
|
40858
|
-
*/
|
|
40859
|
-
async function subscribeWithRetry(options, lifecycle) {
|
|
40860
|
-
const { api, brokerId, tag, ownerNodeId, logger } = options;
|
|
40861
|
-
const pin = nodePin(ownerNodeId);
|
|
40862
|
-
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
40863
|
-
let attempt = 0;
|
|
40864
|
-
while (!lifecycle.stopped) {
|
|
40865
|
-
attempt += 1;
|
|
40866
|
-
try {
|
|
40867
|
-
const result = await api.streamBroker.subscribeAudioChunks.mutate({
|
|
40868
|
-
brokerId,
|
|
40869
|
-
tag
|
|
40870
|
-
}, pin);
|
|
40871
|
-
if (lifecycle.stopped) {
|
|
40872
|
-
await api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: result.subscriptionId }, pin).catch((err) => {
|
|
40873
|
-
logger.warn("audio-chunk poller: late unsubscribe failed", { meta: {
|
|
40874
|
-
brokerId,
|
|
40875
|
-
subscriptionId: result.subscriptionId,
|
|
40876
|
-
error: errMsg(err)
|
|
40877
|
-
} });
|
|
40878
|
-
});
|
|
40879
|
-
return;
|
|
40880
|
-
}
|
|
40881
|
-
if (attempt > 1) logger.info("audio-chunk poller: subscribeAudioChunks recovered", { meta: {
|
|
40882
|
-
brokerId,
|
|
40883
|
-
tag,
|
|
40884
|
-
attempt
|
|
40885
|
-
} });
|
|
40886
|
-
lifecycle.activeSubscriptionId = result.subscriptionId;
|
|
40887
|
-
startPolling(options, lifecycle);
|
|
40888
|
-
return;
|
|
40889
|
-
} catch (err) {
|
|
40890
|
-
if (lifecycle.stopped) return;
|
|
40891
|
-
if (attempt === 1) logger.warn("audio-chunk poller: subscribeAudioChunks failed, retrying", { meta: {
|
|
40892
|
-
brokerId,
|
|
40893
|
-
tag,
|
|
40894
|
-
error: errMsg(err),
|
|
40895
|
-
nextRetryInMs: backoffMs
|
|
40896
|
-
} });
|
|
40897
|
-
else logger.debug("audio-chunk poller: subscribeAudioChunks still failing", { meta: {
|
|
40898
|
-
brokerId,
|
|
40899
|
-
tag,
|
|
40900
|
-
attempt,
|
|
40901
|
-
error: errMsg(err),
|
|
40902
|
-
nextRetryInMs: backoffMs
|
|
40903
|
-
} });
|
|
40904
|
-
await sleep(backoffMs, lifecycle);
|
|
40905
|
-
backoffMs = Math.min(attempt >= PERSISTENT_SUBSCRIBE_FAILURE_ATTEMPTS ? LONG_SUBSCRIBE_RETRY_BACKOFF_MS : MAX_SUBSCRIBE_RETRY_BACKOFF_MS, backoffMs * 2);
|
|
40906
|
-
}
|
|
40907
|
-
}
|
|
40908
|
-
}
|
|
40909
|
-
/**
|
|
40910
|
-
* Drive the steady-state poll loop. Mid-stream `pullAudioChunks` failures
|
|
40911
|
-
* trigger a fresh `subscribeAudioChunks` via the recovery branch — covers
|
|
40912
|
-
* the broker child restart case where our `subscriptionId` is silently
|
|
40913
|
-
* disowned.
|
|
40914
|
-
*/
|
|
40915
|
-
function startPolling(options, lifecycle) {
|
|
40916
|
-
const { api, brokerId, tag, ownerNodeId, onChunk, logger } = options;
|
|
40917
|
-
const pin = nodePin(ownerNodeId);
|
|
40918
|
-
let consecutiveFailures = 0;
|
|
40919
|
-
const resubscribe = async () => {
|
|
40920
|
-
try {
|
|
40921
|
-
const result = await api.streamBroker.subscribeAudioChunks.mutate({
|
|
40922
|
-
brokerId,
|
|
40923
|
-
tag
|
|
40924
|
-
}, pin);
|
|
40925
|
-
lifecycle.activeSubscriptionId = result.subscriptionId;
|
|
40926
|
-
logger.info("audio-chunk poller: re-subscribed after broker outage", { meta: {
|
|
40927
|
-
brokerId,
|
|
40928
|
-
tag,
|
|
40929
|
-
subscriptionId: result.subscriptionId,
|
|
40930
|
-
afterFailures: consecutiveFailures
|
|
40931
|
-
} });
|
|
40932
|
-
return true;
|
|
40933
|
-
} catch {
|
|
40934
|
-
return false;
|
|
40935
|
-
}
|
|
40936
|
-
};
|
|
40937
|
-
const tick = async () => {
|
|
40938
|
-
if (lifecycle.stopped) return;
|
|
40939
|
-
const subId = lifecycle.activeSubscriptionId;
|
|
40940
|
-
if (!subId) return;
|
|
40941
|
-
try {
|
|
40942
|
-
const chunks = await api.streamBroker.pullAudioChunks.query({
|
|
40943
|
-
subscriptionId: subId,
|
|
40944
|
-
maxCount: PULL_MAX_COUNT
|
|
40945
|
-
}, pin);
|
|
40946
|
-
if (consecutiveFailures > 0) logger.info("audio-chunk poller: stream recovered", { meta: {
|
|
40947
|
-
brokerId,
|
|
40948
|
-
subscriptionId: subId
|
|
40949
|
-
} });
|
|
40950
|
-
consecutiveFailures = 0;
|
|
40951
|
-
for (const chunk of chunks) {
|
|
40952
|
-
if (lifecycle.stopped) break;
|
|
40953
|
-
await onChunk(chunk);
|
|
40954
|
-
}
|
|
40955
|
-
} catch (err) {
|
|
40956
|
-
consecutiveFailures += 1;
|
|
40957
|
-
if (consecutiveFailures === 1) logger.warn("audio-chunk poller: pullAudioChunks failed — attempting recovery", { meta: {
|
|
40958
|
-
brokerId,
|
|
40959
|
-
subscriptionId: subId,
|
|
40960
|
-
error: errMsg(err)
|
|
40961
|
-
} });
|
|
40962
|
-
if (!lifecycle.stopped && consecutiveFailures >= RESUBSCRIBE_AFTER_FAILURES && (consecutiveFailures - RESUBSCRIBE_AFTER_FAILURES) % RESUBSCRIBE_THROTTLE_TICKS === 0) await resubscribe();
|
|
40963
|
-
}
|
|
40964
|
-
if (!lifecycle.stopped) lifecycle.pollTimer = setTimeout(() => void tick(), POLL_INTERVAL_MS$1);
|
|
40965
|
-
};
|
|
40966
|
-
tick();
|
|
40967
|
-
}
|
|
40968
|
-
/**
|
|
40969
|
-
* Cancellable sleep — wakes early when `lifecycle.stopped` flips. We
|
|
40970
|
-
* keep a local wrapper around the shared {@link sleep} helper because
|
|
40971
|
-
* the lifecycle tracks the active retry timer for `teardown()` to
|
|
40972
|
-
* clear; pure `sleep()` would leak the timer if teardown fired while
|
|
40973
|
-
* we were waiting.
|
|
40974
|
-
*/
|
|
40975
|
-
function sleep(ms, lifecycle) {
|
|
40976
|
-
return new Promise((resolve) => {
|
|
40977
|
-
if (lifecycle.stopped) {
|
|
40978
|
-
resolve();
|
|
40979
|
-
return;
|
|
40980
|
-
}
|
|
40981
|
-
lifecycle.retryTimer = setTimeout(() => {
|
|
40982
|
-
lifecycle.retryTimer = void 0;
|
|
40983
|
-
resolve();
|
|
40984
|
-
}, ms);
|
|
40985
|
-
});
|
|
40986
|
-
}
|
|
40987
|
-
//#endregion
|
|
40988
40959
|
//#region src/audio-load-balancer.ts
|
|
40989
40960
|
function balanceAudio(input) {
|
|
40990
40961
|
if (input.nodes.length === 0) return null;
|
|
@@ -41001,278 +40972,6 @@ function balanceAudio(input) {
|
|
|
41001
40972
|
};
|
|
41002
40973
|
}
|
|
41003
40974
|
//#endregion
|
|
41004
|
-
//#region src/orchestrator-types.ts
|
|
41005
|
-
var PHASE_MODE_VALUES = new Set([
|
|
41006
|
-
"disabled",
|
|
41007
|
-
"always-on",
|
|
41008
|
-
"on-motion"
|
|
41009
|
-
]);
|
|
41010
|
-
function isPipelinePhaseMode(v) {
|
|
41011
|
-
return PHASE_MODE_VALUES.has(v);
|
|
41012
|
-
}
|
|
41013
|
-
/**
|
|
41014
|
-
* Periodic sweep interval for `retryPendingDispatches` — re-dispatches cameras
|
|
41015
|
-
* that are KNOWN but UNASSIGNED (pending/over-cap/no-frame-source/load-shed).
|
|
41016
|
-
* `reconcileDispatch` is additive-only and never revisits these, so a slow
|
|
41017
|
-
* safety-net timer + event-driven debounce triggers recover them.
|
|
41018
|
-
*/
|
|
41019
|
-
var PENDING_RETRY_INTERVAL_MS = 6e4;
|
|
41020
|
-
/** Debounce window for `schedulePendingRetry` — coalesces capacity/eligibility/readiness signals. */
|
|
41021
|
-
var PENDING_RETRY_DEBOUNCE_MS = 2e3;
|
|
41022
|
-
/**
|
|
41023
|
-
* Periodic auto-rebalance sweep. New attaches are already load-balanced at
|
|
41024
|
-
* dispatch time; this corrects DRIFT that accumulates over time (uneven
|
|
41025
|
-
* detach, a node returning online, a weight change) so the steady-state
|
|
41026
|
-
* distribution tracks the per-node weights. Only runs with ≥2 enabled detect
|
|
41027
|
-
* nodes, and migrates under hysteresis so a balanced cluster is a no-op.
|
|
41028
|
-
*/
|
|
41029
|
-
var AUTO_REBALANCE_INTERVAL_MS = 6e4;
|
|
41030
|
-
/**
|
|
41031
|
-
* Hysteresis margin (weight-adjusted cameras) for the periodic auto-rebalance:
|
|
41032
|
-
* migrate a camera only when its target node is at least this much less loaded
|
|
41033
|
-
* than its current node. > 1 so equalizing a single-camera gap (which would
|
|
41034
|
-
* only reverse the imbalance) is skipped — prevents periodic churn.
|
|
41035
|
-
*/
|
|
41036
|
-
var AUTO_REBALANCE_MIN_IMPROVEMENT = 1.5;
|
|
41037
|
-
var REMOTE_HEALTH_WINDOW_MS = 60 * 6e4;
|
|
41038
|
-
/**
|
|
41039
|
-
* Device-details keys routed through the orchestrator's pipeline
|
|
41040
|
-
* settings writer instead of the device orchestration store. The
|
|
41041
|
-
* `cameraPipeline` key carries the full `CameraPipelineConfig`
|
|
41042
|
-
* emitted by the `pipeline-editor` ConfigField (Phase 6 Option B).
|
|
41043
|
-
*/
|
|
41044
|
-
var PIPELINE_PATCH_KEYS = ["cameraPipeline"];
|
|
41045
|
-
var DEFAULT_FAILOVER_POLICY = {
|
|
41046
|
-
onDisconnect: "migrate",
|
|
41047
|
-
pinnedOnDisconnect: "leave-pinned",
|
|
41048
|
-
onReconnect: "restore"
|
|
41049
|
-
};
|
|
41050
|
-
/**
|
|
41051
|
-
* Custom-action catalog exposed through `api.addons.custom` (Task 9.1 PoC).
|
|
41052
|
-
*
|
|
41053
|
-
* The orchestrator's cap surface is the contract for all runtime traffic
|
|
41054
|
-
* (assignCamera / unassignCamera / rebalance / getGlobalMetrics etc). This
|
|
41055
|
-
* catalog is reserved for read-only diagnostics that are intentionally
|
|
41056
|
-
* outside the cap — they expose internal state (balancer caches, enabledNodes
|
|
41057
|
-
* set, active detection count) that is useful for admin tooling but does not
|
|
41058
|
-
* belong on the capability contract.
|
|
41059
|
-
*/
|
|
41060
|
-
var OrchestratorDiagnosticsSchema = object({
|
|
41061
|
-
localNodeId: string(),
|
|
41062
|
-
knownRunnerNodes: array(string()),
|
|
41063
|
-
cachedAgentLoadNodeIds: array(string()),
|
|
41064
|
-
enabledNodes: array(string()),
|
|
41065
|
-
enabledDecoderNodes: array(string()),
|
|
41066
|
-
enabledAudioNodes: array(string()),
|
|
41067
|
-
enabledIngestNodes: array(string()),
|
|
41068
|
-
clusterRoles: object({
|
|
41069
|
-
ingestNode: string(),
|
|
41070
|
-
audioNode: string(),
|
|
41071
|
-
motionNode: string()
|
|
41072
|
-
}),
|
|
41073
|
-
assignedDeviceCount: number().int().min(0),
|
|
41074
|
-
cameraConfigCount: number().int().min(0),
|
|
41075
|
-
activeDetectionCount: number().int().min(0)
|
|
41076
|
-
});
|
|
41077
|
-
/**
|
|
41078
|
-
* The node-stress long-term-statistics read surface.
|
|
41079
|
-
*
|
|
41080
|
-
* A custom action rather than a cap method, matching how the orchestrator
|
|
41081
|
-
* already serves `dumpState`: this is a hub-local read over a table the hub
|
|
41082
|
-
* owns, and it ships with one `camstack deploy` instead of a release train.
|
|
41083
|
-
* The MEAN is derived here and returned alongside the addable `sum`/`samples`
|
|
41084
|
-
* — a chart wants the first, a re-bucketing caller wants the second, and a
|
|
41085
|
-
* stored mean is a field that can disagree with both.
|
|
41086
|
-
*/
|
|
41087
|
-
var NodeStressStatsInputSchema = object({
|
|
41088
|
-
/** `node-score` | `node-queue-pressure` | `node-drop-ratio` | `node-fps-deficit`. */
|
|
41089
|
-
series: string().optional(),
|
|
41090
|
-
/** A node id. Omit for every node. */
|
|
41091
|
-
subject: string().optional(),
|
|
41092
|
-
/** Inclusive bucket-start bounds, ms. */
|
|
41093
|
-
from: number().int().optional(),
|
|
41094
|
-
to: number().int().optional(),
|
|
41095
|
-
limit: number().int().positive().max(5e3).optional()
|
|
41096
|
-
});
|
|
41097
|
-
var NodeStressStatsRowSchema = object({
|
|
41098
|
-
subject: string(),
|
|
41099
|
-
series: string(),
|
|
41100
|
-
scope: string(),
|
|
41101
|
-
bucketStart: number(),
|
|
41102
|
-
samples: number(),
|
|
41103
|
-
sum: number(),
|
|
41104
|
-
mean: number(),
|
|
41105
|
-
min: number(),
|
|
41106
|
-
max: number()
|
|
41107
|
-
});
|
|
41108
|
-
var NodeStressStatsOutputSchema = object({
|
|
41109
|
-
rows: array(NodeStressStatsRowSchema).readonly(),
|
|
41110
|
-
/** Buckets still accumulating — "is it running" answerable at once, rather
|
|
41111
|
-
* than after five minutes of indistinguishable silence. */
|
|
41112
|
-
open: array(NodeStressStatsRowSchema).readonly(),
|
|
41113
|
-
/** The durable failover history the anti-flap guards read, newest first.
|
|
41114
|
-
* Exposed for the same reason the heartbeat exists: "nothing moved" has to
|
|
41115
|
-
* be distinguishable from "nothing is watching". */
|
|
41116
|
-
moves: array(object({
|
|
41117
|
-
deviceId: number(),
|
|
41118
|
-
fromNodeId: string(),
|
|
41119
|
-
at: number()
|
|
41120
|
-
})).readonly()
|
|
41121
|
-
});
|
|
41122
|
-
var pipelineOrchestratorActions = defineCustomActions({
|
|
41123
|
-
dumpState: customAction(_void(), OrchestratorDiagnosticsSchema),
|
|
41124
|
-
nodeStressStats: customAction(NodeStressStatsInputSchema, NodeStressStatsOutputSchema, { auth: "admin" })
|
|
41125
|
-
});
|
|
41126
|
-
/**
|
|
41127
|
-
* Sentinel returned by `buildDetectionConfig` when the profile-slot READ
|
|
41128
|
-
* itself failed transiently (e.g. `listAllProfileSlots` discovery timeout
|
|
41129
|
-
* while the stream-broker is (re)starting) — as opposed to `null`, which
|
|
41130
|
-
* means "genuinely no assigned slot / not configured". Callers MUST treat
|
|
41131
|
-
* this differently from `null`: never stop active detection on a transient
|
|
41132
|
-
* read failure (the slots almost certainly still exist), and schedule a retry.
|
|
41133
|
-
*/
|
|
41134
|
-
var TRANSIENT_SLOT_READ = Symbol("transient-slot-read");
|
|
41135
|
-
//#endregion
|
|
41136
|
-
//#region src/audio-window-accumulator.ts
|
|
41137
|
-
function sameFormat(a, b) {
|
|
41138
|
-
return a.sampleRate === b.sampleRate && a.channels === b.channels;
|
|
41139
|
-
}
|
|
41140
|
-
var AudioWindowAccumulator = class {
|
|
41141
|
-
deviceId;
|
|
41142
|
-
onFormat;
|
|
41143
|
-
pcmParts = [];
|
|
41144
|
-
accumulatedBytes = 0;
|
|
41145
|
-
accumulatedMs = 0;
|
|
41146
|
-
windowSampleRate = 0;
|
|
41147
|
-
windowChannels = 0;
|
|
41148
|
-
windowTimestamp = 0;
|
|
41149
|
-
windowOpen = false;
|
|
41150
|
-
/** Last format seen on this subscription — survives a flush, unlike the window anchor. */
|
|
41151
|
-
lastFormat = null;
|
|
41152
|
-
constructor(deviceId, onFormat) {
|
|
41153
|
-
this.deviceId = deviceId;
|
|
41154
|
-
this.onFormat = onFormat;
|
|
41155
|
-
}
|
|
41156
|
-
/**
|
|
41157
|
-
* Append one decoded PCM chunk to the open window. Returns the flushed
|
|
41158
|
-
* `AudioChunkInput` once the accumulated duration reaches
|
|
41159
|
-
* `AUDIO_WINDOW_TARGET_MS` (and resets for the next window), else `null`
|
|
41160
|
-
* (accumulate-only, no flush yet).
|
|
41161
|
-
*/
|
|
41162
|
-
push(chunk) {
|
|
41163
|
-
const byteLength = chunk.data.byteLength;
|
|
41164
|
-
const bytes = new Uint8Array(byteLength);
|
|
41165
|
-
bytes.set(new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, byteLength));
|
|
41166
|
-
this.observeFormat({
|
|
41167
|
-
sampleRate: chunk.sampleRate,
|
|
41168
|
-
channels: chunk.channels
|
|
41169
|
-
});
|
|
41170
|
-
if (!this.windowOpen) {
|
|
41171
|
-
this.windowSampleRate = chunk.sampleRate;
|
|
41172
|
-
this.windowChannels = chunk.channels;
|
|
41173
|
-
this.windowTimestamp = chunk.timestamp;
|
|
41174
|
-
this.windowOpen = true;
|
|
41175
|
-
}
|
|
41176
|
-
this.pcmParts.push(bytes);
|
|
41177
|
-
this.accumulatedBytes += byteLength;
|
|
41178
|
-
const channels = chunk.channels > 0 ? chunk.channels : 1;
|
|
41179
|
-
const framesPerChannel = byteLength / 4 / channels;
|
|
41180
|
-
this.accumulatedMs += framesPerChannel / chunk.sampleRate * 1e3;
|
|
41181
|
-
if (this.accumulatedMs < 1e3) return null;
|
|
41182
|
-
const windowData = new Uint8Array(this.accumulatedBytes);
|
|
41183
|
-
let offset = 0;
|
|
41184
|
-
for (const part of this.pcmParts) {
|
|
41185
|
-
windowData.set(part, offset);
|
|
41186
|
-
offset += part.byteLength;
|
|
41187
|
-
}
|
|
41188
|
-
const flushSampleRate = this.windowSampleRate;
|
|
41189
|
-
const flushChannels = this.windowChannels;
|
|
41190
|
-
const flushTimestamp = this.windowTimestamp;
|
|
41191
|
-
this.reset();
|
|
41192
|
-
return {
|
|
41193
|
-
data: windowData,
|
|
41194
|
-
sampleRate: flushSampleRate,
|
|
41195
|
-
channels: flushChannels,
|
|
41196
|
-
timestamp: flushTimestamp,
|
|
41197
|
-
deviceId: this.deviceId
|
|
41198
|
-
};
|
|
41199
|
-
}
|
|
41200
|
-
/**
|
|
41201
|
-
* Report the first format and every change; a change under an OPEN window
|
|
41202
|
-
* drops the partial window so the flushed bytes never mix two rates.
|
|
41203
|
-
*/
|
|
41204
|
-
observeFormat(format) {
|
|
41205
|
-
const previous = this.lastFormat;
|
|
41206
|
-
if (previous !== null && sameFormat(previous, format)) return;
|
|
41207
|
-
const droppedMs = this.windowOpen ? Math.round(this.accumulatedMs) : 0;
|
|
41208
|
-
if (this.windowOpen) this.reset();
|
|
41209
|
-
this.lastFormat = format;
|
|
41210
|
-
this.onFormat?.({
|
|
41211
|
-
from: previous,
|
|
41212
|
-
to: format,
|
|
41213
|
-
droppedMs
|
|
41214
|
-
});
|
|
41215
|
-
}
|
|
41216
|
-
/**
|
|
41217
|
-
* Drop any partially-accumulated window. Called on unsubscribe / device
|
|
41218
|
-
* audio stop (no trailing flush — a sub-second partial window is not worth
|
|
41219
|
-
* a classify during teardown) and internally after every flush.
|
|
41220
|
-
*/
|
|
41221
|
-
reset() {
|
|
41222
|
-
this.pcmParts = [];
|
|
41223
|
-
this.accumulatedBytes = 0;
|
|
41224
|
-
this.accumulatedMs = 0;
|
|
41225
|
-
this.windowOpen = false;
|
|
41226
|
-
}
|
|
41227
|
-
};
|
|
41228
|
-
/**
|
|
41229
|
-
* Build the canonical `AudioResult` frame from one `analyseChunk` result —
|
|
41230
|
-
* extracted verbatim from `subscribeAudioStream`'s result→event-payload
|
|
41231
|
-
* mapping. The caller wraps this in the `pipeline.audio-inference-result`
|
|
41232
|
-
* event; building the frame is pure and needs no controller state.
|
|
41233
|
-
*/
|
|
41234
|
-
function buildAudioResultFrame(deviceId, result) {
|
|
41235
|
-
const windowId = `${deviceId}-${result.timestamp}`;
|
|
41236
|
-
const audioDetections = [];
|
|
41237
|
-
let idCounter = 0;
|
|
41238
|
-
if (result.classification && result.classification.labels.length > 0) for (const label of result.classification.labels) {
|
|
41239
|
-
idCounter += 1;
|
|
41240
|
-
audioDetections.push({
|
|
41241
|
-
id: `a${idCounter}`,
|
|
41242
|
-
kind: "audio",
|
|
41243
|
-
macroClass: label.className,
|
|
41244
|
-
score: label.score,
|
|
41245
|
-
labels: [{
|
|
41246
|
-
label: label.className,
|
|
41247
|
-
score: label.score
|
|
41248
|
-
}],
|
|
41249
|
-
startMs: 0,
|
|
41250
|
-
endMs: 0
|
|
41251
|
-
});
|
|
41252
|
-
}
|
|
41253
|
-
return {
|
|
41254
|
-
kind: "audio-window",
|
|
41255
|
-
windowId,
|
|
41256
|
-
deviceId,
|
|
41257
|
-
timestamp: result.timestamp,
|
|
41258
|
-
startMs: 0,
|
|
41259
|
-
endMs: 0,
|
|
41260
|
-
level: {
|
|
41261
|
-
rms: result.level.rms,
|
|
41262
|
-
dbfs: result.level.dbfs
|
|
41263
|
-
},
|
|
41264
|
-
detections: audioDetections,
|
|
41265
|
-
debug: result.classification ? {
|
|
41266
|
-
totalInferenceMs: result.classification.inferenceMs,
|
|
41267
|
-
stepTimings: [{
|
|
41268
|
-
source: "audio-analyzer",
|
|
41269
|
-
ms: result.classification.inferenceMs,
|
|
41270
|
-
detectionCount: audioDetections.length
|
|
41271
|
-
}]
|
|
41272
|
-
} : void 0
|
|
41273
|
-
};
|
|
41274
|
-
}
|
|
41275
|
-
//#endregion
|
|
41276
40975
|
//#region src/keyed-async-lock.ts
|
|
41277
40976
|
/**
|
|
41278
40977
|
* Serializes critical sections per key. Different keys run fully
|
|
@@ -41800,73 +41499,41 @@ var AudioSubscriptionController = class {
|
|
|
41800
41499
|
isRemote: isRemoteAudio
|
|
41801
41500
|
}
|
|
41802
41501
|
});
|
|
41803
|
-
const
|
|
41804
|
-
|
|
41805
|
-
sampleRate: change.to.sampleRate,
|
|
41806
|
-
channels: change.to.channels,
|
|
41807
|
-
previous: change.from,
|
|
41808
|
-
droppedMs: change.droppedMs
|
|
41809
|
-
};
|
|
41810
|
-
if (change.from === null) {
|
|
41811
|
-
this.deps.logger.info("audio stream delivering", {
|
|
41812
|
-
tags: { deviceId },
|
|
41813
|
-
meta
|
|
41814
|
-
});
|
|
41815
|
-
return;
|
|
41816
|
-
}
|
|
41817
|
-
this.deps.logger.warn("audio stream format changed — partial window dropped", {
|
|
41818
|
-
tags: { deviceId },
|
|
41819
|
-
meta
|
|
41820
|
-
});
|
|
41821
|
-
});
|
|
41822
|
-
const teardown = startAudioChunkPoller({
|
|
41823
|
-
api,
|
|
41502
|
+
const attach = await api.audioAnalyzer.attachDevice.mutate({
|
|
41503
|
+
deviceId,
|
|
41824
41504
|
brokerId: audioBrokerId,
|
|
41825
|
-
|
|
41826
|
-
|
|
41827
|
-
|
|
41828
|
-
|
|
41829
|
-
|
|
41830
|
-
|
|
41831
|
-
|
|
41832
|
-
|
|
41833
|
-
|
|
41834
|
-
chunk: audioChunkInput,
|
|
41835
|
-
settings,
|
|
41836
|
-
...isRemoteAudio ? { nodeId: audioNodeId } : {}
|
|
41837
|
-
});
|
|
41838
|
-
if (!result) return;
|
|
41839
|
-
const frame = buildAudioResultFrame(deviceId, result);
|
|
41840
|
-
this.deps.eventBus.emit({
|
|
41841
|
-
id: `audio-inference-${deviceId}-${Date.now()}`,
|
|
41842
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
41843
|
-
source: {
|
|
41844
|
-
type: "device",
|
|
41845
|
-
id: deviceId,
|
|
41846
|
-
nodeId: "hub",
|
|
41847
|
-
addonId: "pipeline-orchestrator",
|
|
41848
|
-
deviceId
|
|
41849
|
-
},
|
|
41850
|
-
category: EventCategory.PipelineAudioInferenceResult,
|
|
41851
|
-
data: {
|
|
41852
|
-
deviceId,
|
|
41853
|
-
frame,
|
|
41854
|
-
nodeId: "hub"
|
|
41855
|
-
}
|
|
41856
|
-
});
|
|
41857
|
-
} catch (err) {
|
|
41858
|
-
const msg = errMsg(err);
|
|
41859
|
-
this.deps.logger.error("Audio analysis failed", {
|
|
41860
|
-
tags: { deviceId },
|
|
41861
|
-
meta: { error: msg }
|
|
41862
|
-
});
|
|
41505
|
+
ingestNodeId: this.deps.ingestNode(),
|
|
41506
|
+
settings
|
|
41507
|
+
}, nodePin(audioNodeId));
|
|
41508
|
+
if (!attach.attached) {
|
|
41509
|
+
this.deps.logger.warn("audio subscription REFUSED by the analyzer", {
|
|
41510
|
+
tags: { deviceId },
|
|
41511
|
+
meta: {
|
|
41512
|
+
audioNodeId,
|
|
41513
|
+
brokerId: audioBrokerId
|
|
41863
41514
|
}
|
|
41515
|
+
});
|
|
41516
|
+
return null;
|
|
41517
|
+
}
|
|
41518
|
+
this.deps.logger.info("Audio stream subscribed", {
|
|
41519
|
+
tags: { deviceId },
|
|
41520
|
+
meta: {
|
|
41521
|
+
audioNodeId,
|
|
41522
|
+
brokerId: audioBrokerId,
|
|
41523
|
+
replaced: attach.replaced
|
|
41864
41524
|
}
|
|
41865
41525
|
});
|
|
41866
|
-
this.deps.logger.info("Audio stream subscribed", { tags: { deviceId } });
|
|
41867
41526
|
return () => {
|
|
41868
|
-
|
|
41869
|
-
|
|
41527
|
+
api.audioAnalyzer.detachDevice.mutate({ deviceId }, nodePin(audioNodeId)).catch((err) => {
|
|
41528
|
+
this.deps.logger.warn("audio detachDevice failed — the broker lease will reclaim it", {
|
|
41529
|
+
tags: { deviceId },
|
|
41530
|
+
meta: {
|
|
41531
|
+
audioNodeId,
|
|
41532
|
+
brokerId: audioBrokerId,
|
|
41533
|
+
error: errMsg(err)
|
|
41534
|
+
}
|
|
41535
|
+
});
|
|
41536
|
+
});
|
|
41870
41537
|
};
|
|
41871
41538
|
}
|
|
41872
41539
|
/**
|
|
@@ -44615,6 +44282,138 @@ var DeviceActivitySource = class {
|
|
|
44615
44282
|
}
|
|
44616
44283
|
};
|
|
44617
44284
|
//#endregion
|
|
44285
|
+
//#region src/orchestrator-types.ts
|
|
44286
|
+
var PHASE_MODE_VALUES = new Set([
|
|
44287
|
+
"disabled",
|
|
44288
|
+
"always-on",
|
|
44289
|
+
"on-motion"
|
|
44290
|
+
]);
|
|
44291
|
+
function isPipelinePhaseMode(v) {
|
|
44292
|
+
return PHASE_MODE_VALUES.has(v);
|
|
44293
|
+
}
|
|
44294
|
+
/**
|
|
44295
|
+
* Periodic sweep interval for `retryPendingDispatches` — re-dispatches cameras
|
|
44296
|
+
* that are KNOWN but UNASSIGNED (pending/over-cap/no-frame-source/load-shed).
|
|
44297
|
+
* `reconcileDispatch` is additive-only and never revisits these, so a slow
|
|
44298
|
+
* safety-net timer + event-driven debounce triggers recover them.
|
|
44299
|
+
*/
|
|
44300
|
+
var PENDING_RETRY_INTERVAL_MS = 6e4;
|
|
44301
|
+
/** Debounce window for `schedulePendingRetry` — coalesces capacity/eligibility/readiness signals. */
|
|
44302
|
+
var PENDING_RETRY_DEBOUNCE_MS = 2e3;
|
|
44303
|
+
/**
|
|
44304
|
+
* Periodic auto-rebalance sweep. New attaches are already load-balanced at
|
|
44305
|
+
* dispatch time; this corrects DRIFT that accumulates over time (uneven
|
|
44306
|
+
* detach, a node returning online, a weight change) so the steady-state
|
|
44307
|
+
* distribution tracks the per-node weights. Only runs with ≥2 enabled detect
|
|
44308
|
+
* nodes, and migrates under hysteresis so a balanced cluster is a no-op.
|
|
44309
|
+
*/
|
|
44310
|
+
var AUTO_REBALANCE_INTERVAL_MS = 6e4;
|
|
44311
|
+
/**
|
|
44312
|
+
* Hysteresis margin (weight-adjusted cameras) for the periodic auto-rebalance:
|
|
44313
|
+
* migrate a camera only when its target node is at least this much less loaded
|
|
44314
|
+
* than its current node. > 1 so equalizing a single-camera gap (which would
|
|
44315
|
+
* only reverse the imbalance) is skipped — prevents periodic churn.
|
|
44316
|
+
*/
|
|
44317
|
+
var AUTO_REBALANCE_MIN_IMPROVEMENT = 1.5;
|
|
44318
|
+
var REMOTE_HEALTH_WINDOW_MS = 60 * 6e4;
|
|
44319
|
+
/**
|
|
44320
|
+
* Device-details keys routed through the orchestrator's pipeline
|
|
44321
|
+
* settings writer instead of the device orchestration store. The
|
|
44322
|
+
* `cameraPipeline` key carries the full `CameraPipelineConfig`
|
|
44323
|
+
* emitted by the `pipeline-editor` ConfigField (Phase 6 Option B).
|
|
44324
|
+
*/
|
|
44325
|
+
var PIPELINE_PATCH_KEYS = ["cameraPipeline"];
|
|
44326
|
+
var DEFAULT_FAILOVER_POLICY = {
|
|
44327
|
+
onDisconnect: "migrate",
|
|
44328
|
+
pinnedOnDisconnect: "leave-pinned",
|
|
44329
|
+
onReconnect: "restore"
|
|
44330
|
+
};
|
|
44331
|
+
/**
|
|
44332
|
+
* Custom-action catalog exposed through `api.addons.custom` (Task 9.1 PoC).
|
|
44333
|
+
*
|
|
44334
|
+
* The orchestrator's cap surface is the contract for all runtime traffic
|
|
44335
|
+
* (assignCamera / unassignCamera / rebalance / getGlobalMetrics etc). This
|
|
44336
|
+
* catalog is reserved for read-only diagnostics that are intentionally
|
|
44337
|
+
* outside the cap — they expose internal state (balancer caches, enabledNodes
|
|
44338
|
+
* set, active detection count) that is useful for admin tooling but does not
|
|
44339
|
+
* belong on the capability contract.
|
|
44340
|
+
*/
|
|
44341
|
+
var OrchestratorDiagnosticsSchema = object({
|
|
44342
|
+
localNodeId: string(),
|
|
44343
|
+
knownRunnerNodes: array(string()),
|
|
44344
|
+
cachedAgentLoadNodeIds: array(string()),
|
|
44345
|
+
enabledNodes: array(string()),
|
|
44346
|
+
enabledDecoderNodes: array(string()),
|
|
44347
|
+
enabledAudioNodes: array(string()),
|
|
44348
|
+
enabledIngestNodes: array(string()),
|
|
44349
|
+
clusterRoles: object({
|
|
44350
|
+
ingestNode: string(),
|
|
44351
|
+
audioNode: string(),
|
|
44352
|
+
motionNode: string()
|
|
44353
|
+
}),
|
|
44354
|
+
assignedDeviceCount: number().int().min(0),
|
|
44355
|
+
cameraConfigCount: number().int().min(0),
|
|
44356
|
+
activeDetectionCount: number().int().min(0)
|
|
44357
|
+
});
|
|
44358
|
+
/**
|
|
44359
|
+
* The node-stress long-term-statistics read surface.
|
|
44360
|
+
*
|
|
44361
|
+
* A custom action rather than a cap method, matching how the orchestrator
|
|
44362
|
+
* already serves `dumpState`: this is a hub-local read over a table the hub
|
|
44363
|
+
* owns, and it ships with one `camstack deploy` instead of a release train.
|
|
44364
|
+
* The MEAN is derived here and returned alongside the addable `sum`/`samples`
|
|
44365
|
+
* — a chart wants the first, a re-bucketing caller wants the second, and a
|
|
44366
|
+
* stored mean is a field that can disagree with both.
|
|
44367
|
+
*/
|
|
44368
|
+
var NodeStressStatsInputSchema = object({
|
|
44369
|
+
/** `node-score` | `node-queue-pressure` | `node-drop-ratio` | `node-fps-deficit`. */
|
|
44370
|
+
series: string().optional(),
|
|
44371
|
+
/** A node id. Omit for every node. */
|
|
44372
|
+
subject: string().optional(),
|
|
44373
|
+
/** Inclusive bucket-start bounds, ms. */
|
|
44374
|
+
from: number().int().optional(),
|
|
44375
|
+
to: number().int().optional(),
|
|
44376
|
+
limit: number().int().positive().max(5e3).optional()
|
|
44377
|
+
});
|
|
44378
|
+
var NodeStressStatsRowSchema = object({
|
|
44379
|
+
subject: string(),
|
|
44380
|
+
series: string(),
|
|
44381
|
+
scope: string(),
|
|
44382
|
+
bucketStart: number(),
|
|
44383
|
+
samples: number(),
|
|
44384
|
+
sum: number(),
|
|
44385
|
+
mean: number(),
|
|
44386
|
+
min: number(),
|
|
44387
|
+
max: number()
|
|
44388
|
+
});
|
|
44389
|
+
var NodeStressStatsOutputSchema = object({
|
|
44390
|
+
rows: array(NodeStressStatsRowSchema).readonly(),
|
|
44391
|
+
/** Buckets still accumulating — "is it running" answerable at once, rather
|
|
44392
|
+
* than after five minutes of indistinguishable silence. */
|
|
44393
|
+
open: array(NodeStressStatsRowSchema).readonly(),
|
|
44394
|
+
/** The durable failover history the anti-flap guards read, newest first.
|
|
44395
|
+
* Exposed for the same reason the heartbeat exists: "nothing moved" has to
|
|
44396
|
+
* be distinguishable from "nothing is watching". */
|
|
44397
|
+
moves: array(object({
|
|
44398
|
+
deviceId: number(),
|
|
44399
|
+
fromNodeId: string(),
|
|
44400
|
+
at: number()
|
|
44401
|
+
})).readonly()
|
|
44402
|
+
});
|
|
44403
|
+
var pipelineOrchestratorActions = defineCustomActions({
|
|
44404
|
+
dumpState: customAction(_void(), OrchestratorDiagnosticsSchema),
|
|
44405
|
+
nodeStressStats: customAction(NodeStressStatsInputSchema, NodeStressStatsOutputSchema, { auth: "admin" })
|
|
44406
|
+
});
|
|
44407
|
+
/**
|
|
44408
|
+
* Sentinel returned by `buildDetectionConfig` when the profile-slot READ
|
|
44409
|
+
* itself failed transiently (e.g. `listAllProfileSlots` discovery timeout
|
|
44410
|
+
* while the stream-broker is (re)starting) — as opposed to `null`, which
|
|
44411
|
+
* means "genuinely no assigned slot / not configured". Callers MUST treat
|
|
44412
|
+
* this differently from `null`: never stop active detection on a transient
|
|
44413
|
+
* read failure (the slots almost certainly still exist), and schedule a retry.
|
|
44414
|
+
*/
|
|
44415
|
+
var TRANSIENT_SLOT_READ = Symbol("transient-slot-read");
|
|
44416
|
+
//#endregion
|
|
44618
44417
|
//#region src/device-detection-settings.ts
|
|
44619
44418
|
/** Read a required string leaf out of the hydrated `flat` schema values. */
|
|
44620
44419
|
function mustString(flat, deviceId, key) {
|
|
@@ -48889,6 +48688,10 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
48889
48688
|
const deviceId = event.source.deviceId;
|
|
48890
48689
|
if (typeof deviceId === "number") deps.noteWatchdogSignal(deviceId, "motion");
|
|
48891
48690
|
});
|
|
48691
|
+
const unsubAudioWatchdog = deps.eventBus.subscribe({ category: EventCategory.PipelineAudioInferenceResult }, (event) => {
|
|
48692
|
+
if (!isEvent(event, EventCategory.PipelineAudioInferenceResult)) return;
|
|
48693
|
+
deps.noteWatchdogSignal(event.data.deviceId, "audio");
|
|
48694
|
+
});
|
|
48892
48695
|
const activitySource = new DeviceActivitySource({
|
|
48893
48696
|
logger: deps.logger,
|
|
48894
48697
|
emitMotion: (deviceId, detected, timestamp) => {
|
|
@@ -48944,6 +48747,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
48944
48747
|
unsubSessionMotion();
|
|
48945
48748
|
unsubFrameTracked();
|
|
48946
48749
|
unsubMotionAnalysis();
|
|
48750
|
+
unsubAudioWatchdog();
|
|
48947
48751
|
unsubDeviceActivity();
|
|
48948
48752
|
};
|
|
48949
48753
|
}
|
|
@@ -50054,7 +49858,7 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
50054
49858
|
nodeId
|
|
50055
49859
|
}, { timeoutMs: PipelineSettingsStore.EXECUTOR_RECONCILE_ROUND_MS }).then(() => true, () => false);
|
|
50056
49860
|
if (this.disposed) break;
|
|
50057
|
-
if (readyPerRegistry) await sleep
|
|
49861
|
+
if (readyPerRegistry) await sleep(PipelineSettingsStore.EXECUTOR_READY_PROBE_PACE_MS);
|
|
50058
49862
|
}
|
|
50059
49863
|
return null;
|
|
50060
49864
|
}
|
|
@@ -50082,13 +49886,13 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
50082
49886
|
let agent = (await this.readAgentSettingsMap())[nodeId];
|
|
50083
49887
|
if (!agent) {
|
|
50084
49888
|
if (!await this.seedAgentSettingsFromCatalog(nodeId)) {
|
|
50085
|
-
await sleep
|
|
49889
|
+
await sleep(2e3);
|
|
50086
49890
|
continue;
|
|
50087
49891
|
}
|
|
50088
49892
|
agent = (await this.readAgentSettingsMap())[nodeId];
|
|
50089
49893
|
}
|
|
50090
49894
|
if (!agent) {
|
|
50091
|
-
await sleep
|
|
49895
|
+
await sleep(2e3);
|
|
50092
49896
|
continue;
|
|
50093
49897
|
}
|
|
50094
49898
|
return {
|
|
@@ -53113,7 +52917,6 @@ async function buildOrchestratorControllers(deps) {
|
|
|
53113
52917
|
eventBus: deps.ctx().eventBus,
|
|
53114
52918
|
logger: deps.ctx().logger,
|
|
53115
52919
|
readDeviceStore: async (deviceId) => await deps.ctxIfReady()?.settings?.readDeviceStore(deviceId) ?? {},
|
|
53116
|
-
watchdogNote: (deviceId, stage) => pipelineWatchdog?.noteSignal(deviceId, stage),
|
|
53117
52920
|
probeAudioTrack: (deviceId, camStreamId) => resolveAudioTrackProbe(deviceId, camStreamId),
|
|
53118
52921
|
ingestNode: () => topology.clusterRoles.ingestNode,
|
|
53119
52922
|
localNodeId: () => localNodeId,
|