@camstack/addon-pipeline-orchestrator 1.2.222 → 1.2.223
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 +32 -32
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-Ba6QA7CL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-OXfq7-7N.mjs} +2 -2
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CJKRvrm0.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-lkkSZHH0.mjs +26 -0
- package/dist/{hostInit-Be3_uqeJ.mjs → hostInit-DKf_umlV.mjs} +2 -2
- package/dist/index.js +442 -33
- package/dist/index.mjs +442 -33
- 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-SM4f3ulJ.mjs +0 -26
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BI7e6C_J.mjs +0 -26
package/dist/index.js
CHANGED
|
@@ -5391,7 +5391,7 @@ var ZodIssueCode = {
|
|
|
5391
5391
|
var ZodFirstPartyTypeKind;
|
|
5392
5392
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5393
5393
|
//#endregion
|
|
5394
|
-
//#region ../types/dist/sleep-
|
|
5394
|
+
//#region ../types/dist/sleep-CiR4z7r9.mjs
|
|
5395
5395
|
/**
|
|
5396
5396
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5397
5397
|
* window to float samples (D455).
|
|
@@ -6549,6 +6549,23 @@ var CamProfileSchema = _enum([
|
|
|
6549
6549
|
"mid",
|
|
6550
6550
|
"low"
|
|
6551
6551
|
]);
|
|
6552
|
+
/** Canonical ordering. Hard-coded; never sorted. */
|
|
6553
|
+
var CAM_PROFILE_ORDER = [
|
|
6554
|
+
"high",
|
|
6555
|
+
"mid",
|
|
6556
|
+
"low"
|
|
6557
|
+
];
|
|
6558
|
+
/**
|
|
6559
|
+
* Which rung a per-camera consumer listens on: an explicit profile, or `auto`.
|
|
6560
|
+
*
|
|
6561
|
+
* `auto` is a REAL value, not an absent one — it means "resolve it by the rule
|
|
6562
|
+
* that consumer owns". It lives beside `CamProfile` because a rung is the only
|
|
6563
|
+
* operator-facing way to name a stream: a cam-stream id is broker-internal and
|
|
6564
|
+
* moves when a slot is reassigned, so a stored id silently stops meaning what
|
|
6565
|
+
* the operator picked. Today's one user is the audio analyser, whose rule is
|
|
6566
|
+
* "the lowest AVAILABLE native rung" (D537).
|
|
6567
|
+
*/
|
|
6568
|
+
var AudioStreamPickSchema = union([CamProfileSchema, literal("auto")]);
|
|
6552
6569
|
var CamStreamKindSchema = _enum([
|
|
6553
6570
|
"pull-rtsp",
|
|
6554
6571
|
"pull-rtmp",
|
|
@@ -21032,6 +21049,7 @@ var MediaFileKindEnum = _enum([
|
|
|
21032
21049
|
"keyFrameSmall",
|
|
21033
21050
|
"thumbnailSmall",
|
|
21034
21051
|
"confirmationCrop",
|
|
21052
|
+
"debugCrop",
|
|
21035
21053
|
"sceneFrame"
|
|
21036
21054
|
]);
|
|
21037
21055
|
/**
|
|
@@ -22696,6 +22714,18 @@ var RunnerCameraConfigSchema = object({
|
|
|
22696
22714
|
motionStreamId: string(),
|
|
22697
22715
|
detectionStreamId: string(),
|
|
22698
22716
|
/**
|
|
22717
|
+
* Which rung this camera's audio analysis listens on — `auto` (the lowest
|
|
22718
|
+
* AVAILABLE native rung) or an explicit one.
|
|
22719
|
+
*
|
|
22720
|
+
* Not consumed by the runner: carried here so it travels with the camera's
|
|
22721
|
+
* config the way `audioMotionWindowMs` does, and so the orchestrator's
|
|
22722
|
+
* watchdog resolves the same stream the audio subscription did. OPTIONAL
|
|
22723
|
+
* with no default — absent means `auto`, which is what every camera written
|
|
22724
|
+
* before this field had, and an absent optional adds nothing to the
|
|
22725
|
+
* generated cap-input defaults (D537).
|
|
22726
|
+
*/
|
|
22727
|
+
audioStreamPick: AudioStreamPickSchema.optional(),
|
|
22728
|
+
/**
|
|
22699
22729
|
* Motion sources that can activate the pipeline. At least one must
|
|
22700
22730
|
* be present. Multiple sources OR-ed: any one firing arms the
|
|
22701
22731
|
* cooldown timer and transitions watching → active. The same
|
|
@@ -25718,6 +25748,104 @@ var WebrtcStreamChoiceSchema = object({
|
|
|
25718
25748
|
/** Bitrate in kbps (null for adaptive). */
|
|
25719
25749
|
bitrateKbps: number().nullable()
|
|
25720
25750
|
});
|
|
25751
|
+
/**
|
|
25752
|
+
* The last move the adaptive ladder made on this session, and the numbers it
|
|
25753
|
+
* rested on. Mirrors `LadderDecision` in the stream-broker's
|
|
25754
|
+
* `adaptive-controller.ts` — the log line's evidence, made readable.
|
|
25755
|
+
*
|
|
25756
|
+
* Nullability is load-bearing: `lossRatio: null` means the loss tracker had
|
|
25757
|
+
* no window to report, which is NOT a link with zero loss.
|
|
25758
|
+
*/
|
|
25759
|
+
var WebrtcLadderDecisionSchema = object({
|
|
25760
|
+
/** `loss` — RTCP / client-reported packet loss; `stall` — the picture
|
|
25761
|
+
* stopped moving; `healthy` — a promotion. */
|
|
25762
|
+
cause: _enum([
|
|
25763
|
+
"loss",
|
|
25764
|
+
"stall",
|
|
25765
|
+
"healthy"
|
|
25766
|
+
]),
|
|
25767
|
+
/** The rung the ladder moved TO. Null for a move that changed no rung
|
|
25768
|
+
* (an MTU reduction, a forced transcode-down at the bottom). */
|
|
25769
|
+
tier: CamProfileSchema.nullable(),
|
|
25770
|
+
/** Whether that move forced the egress transcode-down rung. */
|
|
25771
|
+
forcedTranscodeDown: boolean(),
|
|
25772
|
+
/** Loss ratio over the tracker's window at the moment of the decision. */
|
|
25773
|
+
lossRatio: number().nullable(),
|
|
25774
|
+
/** The client's reported downlink at that moment, in kbps. */
|
|
25775
|
+
clientKbps: number().nullable(),
|
|
25776
|
+
/** How long the stall detector had been seeing nothing arrive. */
|
|
25777
|
+
stalledForMs: number().nullable(),
|
|
25778
|
+
/** Whether the broker WAS sending while that was measured — the half that
|
|
25779
|
+
* separates "the camera stopped" from "the link stopped". */
|
|
25780
|
+
serverSending: boolean().nullable(),
|
|
25781
|
+
/** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
|
|
25782
|
+
* must not read like one from this second. */
|
|
25783
|
+
at: number()
|
|
25784
|
+
});
|
|
25785
|
+
/** Per-tier count of MEASURED downgrades this viewer session has suffered. */
|
|
25786
|
+
var WebrtcTierFailureSchema = object({
|
|
25787
|
+
tier: CamProfileSchema,
|
|
25788
|
+
failures: number().int().nonnegative()
|
|
25789
|
+
});
|
|
25790
|
+
/**
|
|
25791
|
+
* What the SERVER decided for one WebRTC session — the half the client cannot
|
|
25792
|
+
* measure. See `getSessionDebug`.
|
|
25793
|
+
*/
|
|
25794
|
+
var WebrtcSessionDebugSchema = object({
|
|
25795
|
+
sessionId: string(),
|
|
25796
|
+
/** Whether the session still exists here. Everything below is null when
|
|
25797
|
+
* it does not — a dead session has no measurements, not zero ones. */
|
|
25798
|
+
alive: boolean(),
|
|
25799
|
+
/** Epoch ms this snapshot was taken, so the overlay can show its age. */
|
|
25800
|
+
observedAt: number(),
|
|
25801
|
+
/** The broker actually feeding this session (`<deviceId>/<camStreamId>`). */
|
|
25802
|
+
brokerId: string().nullable(),
|
|
25803
|
+
camStreamId: string().nullable(),
|
|
25804
|
+
/** `derived` = this hub BUILT the rung being served (a transcode/scale of
|
|
25805
|
+
* another stream); `native` = the camera's own encode. */
|
|
25806
|
+
sourceKind: _enum([
|
|
25807
|
+
"native",
|
|
25808
|
+
"derived",
|
|
25809
|
+
"unknown"
|
|
25810
|
+
]),
|
|
25811
|
+
/** Whether the ladder is running at all (an `adaptive` target). */
|
|
25812
|
+
adaptive: boolean(),
|
|
25813
|
+
/** The rung `auto` ACTUALLY resolved to right now. Null when the broker
|
|
25814
|
+
* is bound to no profile slot (a manually-activated stream). */
|
|
25815
|
+
servedTier: CamProfileSchema.nullable(),
|
|
25816
|
+
/** Which node ingests the source. Null when unresolved. */
|
|
25817
|
+
ingestNodeId: string().nullable(),
|
|
25818
|
+
/** Whether that node is a remote one (the media crosses a node boundary
|
|
25819
|
+
* before it reaches this session). Null when unresolved. */
|
|
25820
|
+
ingestIsRemote: boolean().nullable(),
|
|
25821
|
+
/** Codec off the source, and the one negotiated with this client. */
|
|
25822
|
+
sourceCodec: string().nullable(),
|
|
25823
|
+
negotiatedCodec: string().nullable(),
|
|
25824
|
+
/** Whether the EGRESS leg re-encodes for this client or forwards the
|
|
25825
|
+
* source bytes untouched. */
|
|
25826
|
+
egressLeg: _enum([
|
|
25827
|
+
"copy",
|
|
25828
|
+
"transcode",
|
|
25829
|
+
"unknown"
|
|
25830
|
+
]),
|
|
25831
|
+
/** What the broker itself reports about the rung it is serving. */
|
|
25832
|
+
brokerStatus: string().nullable(),
|
|
25833
|
+
brokerCodec: string().nullable(),
|
|
25834
|
+
brokerBitrateKbps: number().nullable(),
|
|
25835
|
+
brokerInputFps: number().nullable(),
|
|
25836
|
+
brokerUptimeMs: number().nullable(),
|
|
25837
|
+
/** RTP packets this session has sent — the server's own "am I sending?". */
|
|
25838
|
+
rtpPacketsSent: number().nullable(),
|
|
25839
|
+
/** D42: the track may be carrying the recorded feeder, not live. The ladder
|
|
25840
|
+
* is suspended there, so a frozen picture in `recorded` mode means
|
|
25841
|
+
* something entirely different. */
|
|
25842
|
+
trackMode: _enum(["live", "recorded"]).nullable(),
|
|
25843
|
+
lastDecision: WebrtcLadderDecisionSchema.nullable(),
|
|
25844
|
+
tierFailures: array(WebrtcTierFailureSchema),
|
|
25845
|
+
/** Successful promotions in this viewer session (drives the progressive
|
|
25846
|
+
* healthy timer). Null when no ladder is running. */
|
|
25847
|
+
upgradeCount: number().nullable()
|
|
25848
|
+
});
|
|
25721
25849
|
DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
|
|
25722
25850
|
deviceId: number().int().nonnegative(),
|
|
25723
25851
|
target: WebrtcStreamTargetSchema,
|
|
@@ -25882,7 +26010,10 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
|
|
|
25882
26010
|
* the same answer as "it ended": either way nothing is holding it up.
|
|
25883
26011
|
*/
|
|
25884
26012
|
alive: boolean()
|
|
25885
|
-
}))
|
|
26013
|
+
})), method(object({
|
|
26014
|
+
deviceId: number().int().nonnegative(),
|
|
26015
|
+
sessionId: string()
|
|
26016
|
+
}), WebrtcSessionDebugSchema, { kind: "query" });
|
|
25886
26017
|
object({
|
|
25887
26018
|
/** All accessory children of the parent. */
|
|
25888
26019
|
childDeviceIds: array(number()).readonly(),
|
|
@@ -28970,16 +29101,37 @@ var GridProfileOfferSchema = object({
|
|
|
28970
29101
|
]),
|
|
28971
29102
|
offered: boolean(),
|
|
28972
29103
|
/** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
|
|
28973
|
-
missingSources: array(number().int().positive())
|
|
29104
|
+
missingSources: array(number().int().positive()),
|
|
29105
|
+
/**
|
|
29106
|
+
* The canvas this profile composes onto, `WxH`, or empty when it is not
|
|
29107
|
+
* offered. DERIVED from the cells and the sources' own size at this profile —
|
|
29108
|
+
* it is reported because nothing else in the system would ever say what the
|
|
29109
|
+
* grid came out as, and because it is the number an operator would otherwise
|
|
29110
|
+
* expect to type.
|
|
29111
|
+
*/
|
|
29112
|
+
canvas: string(),
|
|
29113
|
+
/**
|
|
29114
|
+
* Whether this profile is PUBLISHED, of the ones the grid could serve.
|
|
29115
|
+
*
|
|
29116
|
+
* A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
|
|
29117
|
+
* a 4K canvas built from 4K decodes — something to opt into, not something a
|
|
29118
|
+
* viewer's adaptive should be handed by climbing to the top rung it can see.
|
|
29119
|
+
* Default is `mid` + `low`.
|
|
29120
|
+
*/
|
|
29121
|
+
published: boolean()
|
|
28974
29122
|
});
|
|
28975
29123
|
var GridLayoutViewSchema = object({
|
|
28976
29124
|
/** The persisted grid row this camera was declared from. */
|
|
28977
29125
|
instanceId: string(),
|
|
28978
29126
|
deviceId: number().int().nonnegative(),
|
|
28979
29127
|
name: string(),
|
|
28980
|
-
/**
|
|
28981
|
-
|
|
28982
|
-
|
|
29128
|
+
/**
|
|
29129
|
+
* NO canvas size. A grid's resolution is not authored: each profile derives
|
|
29130
|
+
* its own from the cells and its sources' dimensions. The two numbers that
|
|
29131
|
+
* used to be here were a text field that silently decided both how much the
|
|
29132
|
+
* composite cost and how sharp it was — see `profiles[].canvas` for what it
|
|
29133
|
+
* came out as.
|
|
29134
|
+
*/
|
|
28983
29135
|
fps: number().int(),
|
|
28984
29136
|
cells: array(GridLayoutCellSchema),
|
|
28985
29137
|
/** What the catalog will publish, and what it refuses to. Read-only. */
|
|
@@ -28988,9 +29140,13 @@ var GridLayoutViewSchema = object({
|
|
|
28988
29140
|
var GridLayoutPatchSchema = object({
|
|
28989
29141
|
deviceId: number().int().nonnegative(),
|
|
28990
29142
|
name: string().min(1).max(160).optional(),
|
|
28991
|
-
width: number().int().min(160).max(7680).optional(),
|
|
28992
|
-
height: number().int().min(120).max(4320).optional(),
|
|
28993
29143
|
fps: number().int().min(1).max(60).optional(),
|
|
29144
|
+
/** Which profiles to publish. See `GridProfileOffer.published`. */
|
|
29145
|
+
publishedProfiles: array(_enum([
|
|
29146
|
+
"high",
|
|
29147
|
+
"mid",
|
|
29148
|
+
"low"
|
|
29149
|
+
])).max(3).optional(),
|
|
28994
29150
|
/**
|
|
28995
29151
|
* The whole cell list at once. A per-cell patch would need an ordering the
|
|
28996
29152
|
* editor does not have, and a half-applied layout is a picture nobody asked
|
|
@@ -39659,6 +39815,12 @@ Object.freeze({
|
|
|
39659
39815
|
addonId: null,
|
|
39660
39816
|
access: "view"
|
|
39661
39817
|
},
|
|
39818
|
+
"webrtcSession.getSessionDebug": {
|
|
39819
|
+
capName: "webrtc-session",
|
|
39820
|
+
capScope: "device",
|
|
39821
|
+
addonId: null,
|
|
39822
|
+
access: "view"
|
|
39823
|
+
},
|
|
39662
39824
|
"webrtcSession.getSessionState": {
|
|
39663
39825
|
capName: "webrtc-session",
|
|
39664
39826
|
capScope: "device",
|
|
@@ -41642,6 +41804,11 @@ Object.freeze({
|
|
|
41642
41804
|
form: "single",
|
|
41643
41805
|
optional: false
|
|
41644
41806
|
}],
|
|
41807
|
+
"webrtcSession.getSessionDebug": [{
|
|
41808
|
+
name: "deviceId",
|
|
41809
|
+
form: "single",
|
|
41810
|
+
optional: false
|
|
41811
|
+
}],
|
|
41645
41812
|
"webrtcSession.getSessionState": [{
|
|
41646
41813
|
name: "deviceId",
|
|
41647
41814
|
form: "single",
|
|
@@ -42505,6 +42672,106 @@ function balanceAudio(input) {
|
|
|
42505
42672
|
};
|
|
42506
42673
|
}
|
|
42507
42674
|
//#endregion
|
|
42675
|
+
//#region src/audio-stream-selection.ts
|
|
42676
|
+
/**
|
|
42677
|
+
* Rung order for an audio consumer: cheapest first. The inverse of
|
|
42678
|
+
* `CAM_PROFILE_ORDER` (`high → mid → low`), derived from it rather than
|
|
42679
|
+
* re-spelled so the two can never disagree about which rungs exist.
|
|
42680
|
+
*/
|
|
42681
|
+
var AUDIO_RUNG_PREFERENCE = [...CAM_PROFILE_ORDER].reverse();
|
|
42682
|
+
/**
|
|
42683
|
+
* True when a cam-stream id names a broker-spawned transcode plane.
|
|
42684
|
+
*
|
|
42685
|
+
* The `derived:` prefix is the repo-wide convention (`stream-broker-manager`'s
|
|
42686
|
+
* `derivedDefinition`, `snapshot-grab-pick`, `prefer-live-adaptive-candidates`,
|
|
42687
|
+
* the device-scoped stream pickers). Judged from the ID because that is what
|
|
42688
|
+
* the caller holds: the profile-slot table carries `sourceCamStreamId`, not the
|
|
42689
|
+
* published `CameraStream.kind`, and asking the broker for the whole stream
|
|
42690
|
+
* catalogue to re-derive a fact the id already states would add an RPC to every
|
|
42691
|
+
* audio subscribe.
|
|
42692
|
+
*/
|
|
42693
|
+
function isDerivedCamStream(camStreamId) {
|
|
42694
|
+
return camStreamId.startsWith("derived:");
|
|
42695
|
+
}
|
|
42696
|
+
/** Every value the per-camera setting may hold, in the order the UI offers them. */
|
|
42697
|
+
var AUDIO_STREAM_PICKS = ["auto", ...CAM_PROFILE_ORDER];
|
|
42698
|
+
/**
|
|
42699
|
+
* Narrow a stored settings value to an {@link AudioStreamPick}.
|
|
42700
|
+
*
|
|
42701
|
+
* Anything that is not one of the four known values — absent, empty, a rung
|
|
42702
|
+
* from a schema this build does not have — reads as `auto`. That is the
|
|
42703
|
+
* behaviour every camera already has, so an unreadable value degrades to
|
|
42704
|
+
* today's rather than throwing the camera's whole detection-settings read.
|
|
42705
|
+
*/
|
|
42706
|
+
function toAudioStreamPick(value) {
|
|
42707
|
+
return AUDIO_STREAM_PICKS.find((pick) => pick === value) ?? "auto";
|
|
42708
|
+
}
|
|
42709
|
+
/**
|
|
42710
|
+
* Choose the cam-stream this camera's audio analysis attaches to.
|
|
42711
|
+
*
|
|
42712
|
+
* Pure. Every branch is either a selection carrying the reason, or a refusal
|
|
42713
|
+
* carrying the cause — there is no third answer and no silent default.
|
|
42714
|
+
*/
|
|
42715
|
+
function selectAudioCamStream(input) {
|
|
42716
|
+
const { pick, motionStreamId, assigned } = input;
|
|
42717
|
+
if (pick !== "auto") {
|
|
42718
|
+
if (assigned === null) return {
|
|
42719
|
+
kind: "refused",
|
|
42720
|
+
reason: "pick-unresolvable",
|
|
42721
|
+
pick,
|
|
42722
|
+
rejectedCamStreamId: null
|
|
42723
|
+
};
|
|
42724
|
+
const picked = assigned.get(pick);
|
|
42725
|
+
if (picked === void 0) return {
|
|
42726
|
+
kind: "refused",
|
|
42727
|
+
reason: "pick-unassigned",
|
|
42728
|
+
pick,
|
|
42729
|
+
rejectedCamStreamId: null
|
|
42730
|
+
};
|
|
42731
|
+
return {
|
|
42732
|
+
kind: "selected",
|
|
42733
|
+
camStreamId: picked,
|
|
42734
|
+
profile: pick,
|
|
42735
|
+
selectedBy: "operator-pick",
|
|
42736
|
+
derived: isDerivedCamStream(picked)
|
|
42737
|
+
};
|
|
42738
|
+
}
|
|
42739
|
+
if (assigned === null) {
|
|
42740
|
+
if (isDerivedCamStream(motionStreamId)) return {
|
|
42741
|
+
kind: "refused",
|
|
42742
|
+
reason: "slots-unknown-and-fallback-derived",
|
|
42743
|
+
pick,
|
|
42744
|
+
rejectedCamStreamId: motionStreamId
|
|
42745
|
+
};
|
|
42746
|
+
return {
|
|
42747
|
+
kind: "selected",
|
|
42748
|
+
camStreamId: motionStreamId,
|
|
42749
|
+
profile: null,
|
|
42750
|
+
selectedBy: "motion-stream-fallback",
|
|
42751
|
+
derived: false
|
|
42752
|
+
};
|
|
42753
|
+
}
|
|
42754
|
+
for (const profile of AUDIO_RUNG_PREFERENCE) {
|
|
42755
|
+
const camStreamId = assigned.get(profile);
|
|
42756
|
+
if (camStreamId === void 0) continue;
|
|
42757
|
+
if (isDerivedCamStream(camStreamId)) continue;
|
|
42758
|
+
return {
|
|
42759
|
+
kind: "selected",
|
|
42760
|
+
camStreamId,
|
|
42761
|
+
profile,
|
|
42762
|
+
selectedBy: "lowest-native-rung",
|
|
42763
|
+
derived: false
|
|
42764
|
+
};
|
|
42765
|
+
}
|
|
42766
|
+
const anyAssigned = AUDIO_RUNG_PREFERENCE.some((profile) => assigned.has(profile));
|
|
42767
|
+
return {
|
|
42768
|
+
kind: "refused",
|
|
42769
|
+
reason: anyAssigned ? "all-rungs-derived" : "no-assigned-rung",
|
|
42770
|
+
pick,
|
|
42771
|
+
rejectedCamStreamId: anyAssigned ? motionStreamId : null
|
|
42772
|
+
};
|
|
42773
|
+
}
|
|
42774
|
+
//#endregion
|
|
42508
42775
|
//#region src/keyed-async-lock.ts
|
|
42509
42776
|
/**
|
|
42510
42777
|
* Serializes critical sections per key. Different keys run fully
|
|
@@ -43004,15 +43271,46 @@ var AudioSubscriptionController = class {
|
|
|
43004
43271
|
this.deps.logger.info("audio subscribe deferred: audioMode=on-motion, no window open", { tags: { deviceId } });
|
|
43005
43272
|
return null;
|
|
43006
43273
|
}
|
|
43007
|
-
const
|
|
43274
|
+
const selection = selectAudioCamStream({
|
|
43275
|
+
pick: config.audioStreamPick ?? "auto",
|
|
43276
|
+
motionStreamId: config.motionStreamId,
|
|
43277
|
+
assigned: await this.deps.assignedProfiles(deviceId)
|
|
43278
|
+
});
|
|
43279
|
+
if (selection.kind === "refused") {
|
|
43280
|
+
this.deps.logger.warn("audio subscription REFUSED — no native rung to listen on (an audio-only consumer never causes a video transcode)", {
|
|
43281
|
+
tags: { deviceId },
|
|
43282
|
+
meta: {
|
|
43283
|
+
reason: selection.reason,
|
|
43284
|
+
pick: selection.pick,
|
|
43285
|
+
rejectedCamStreamId: selection.rejectedCamStreamId,
|
|
43286
|
+
motionStreamId: config.motionStreamId,
|
|
43287
|
+
hint: selection.reason === "pick-unassigned" ? "this camera no longer publishes the rung its audio is pinned to — repoint the Audio stream setting, or set it back to Auto" : "assign a native (non-derived) stream to one of this camera’s profile slots, or pick a rung explicitly"
|
|
43288
|
+
}
|
|
43289
|
+
});
|
|
43290
|
+
return null;
|
|
43291
|
+
}
|
|
43292
|
+
const audioStream = selection.camStreamId;
|
|
43008
43293
|
const audioBrokerId = makeSourceBrokerId(deviceId, audioStream);
|
|
43294
|
+
this.deps.logger.info("audio stream selected", {
|
|
43295
|
+
tags: { deviceId },
|
|
43296
|
+
meta: {
|
|
43297
|
+
camStreamId: audioStream,
|
|
43298
|
+
profile: selection.profile,
|
|
43299
|
+
selectedBy: selection.selectedBy,
|
|
43300
|
+
derived: selection.derived,
|
|
43301
|
+
pick: config.audioStreamPick ?? "auto",
|
|
43302
|
+
motionStreamId: config.motionStreamId,
|
|
43303
|
+
...selection.derived ? { note: "this pick is a derived stream — the audio subscription keeps its transcode feeder alive" } : {}
|
|
43304
|
+
}
|
|
43305
|
+
});
|
|
43009
43306
|
if ((await this.deps.probeAudioTrack(deviceId, audioStream)).kind === "absent") {
|
|
43010
43307
|
this.deps.logger.warn("audio subscription REFUSED — this stream carries no audio track", {
|
|
43011
43308
|
tags: { deviceId },
|
|
43012
43309
|
meta: {
|
|
43013
43310
|
camStreamId: audioStream,
|
|
43014
43311
|
brokerId: audioBrokerId,
|
|
43015
|
-
selectedBy:
|
|
43312
|
+
selectedBy: selection.selectedBy,
|
|
43313
|
+
profile: selection.profile,
|
|
43016
43314
|
hint: "point the camera’s audio at a stream that has an audio track — no stream is substituted automatically"
|
|
43017
43315
|
}
|
|
43018
43316
|
});
|
|
@@ -45948,6 +46246,39 @@ var pipelineOrchestratorActions = defineCustomActions({
|
|
|
45948
46246
|
var TRANSIENT_SLOT_READ = Symbol("transient-slot-read");
|
|
45949
46247
|
//#endregion
|
|
45950
46248
|
//#region src/device-detection-settings.ts
|
|
46249
|
+
/**
|
|
46250
|
+
* Pure device-detection-settings derivation for the pipeline orchestrator.
|
|
46251
|
+
*
|
|
46252
|
+
* Extracted from `index.ts`'s `resolveDeviceDetectionSettings` (S3 refactor
|
|
46253
|
+
* step — zero behavior change). That ~185-line method interleaved I/O
|
|
46254
|
+
* (device-store read, binding/feature lookups) with a 5-branch
|
|
46255
|
+
* `motionSources` default ladder and several other operator-wins→profile→
|
|
46256
|
+
* fallback resolutions. The class-side method now does ONLY the I/O
|
|
46257
|
+
* gathering (`readDeviceStore`, `hydrateSchema`, `lookupDeviceFeatures`,
|
|
46258
|
+
* `deviceHasOnboardMotionCap`, `isDetectionPipelineActive`,
|
|
46259
|
+
* `isMotionDetectionActive`) and hands the raw materials to
|
|
46260
|
+
* `resolveDetectionSettings`, the pure decision half that used to be
|
|
46261
|
+
* interleaved with that I/O.
|
|
46262
|
+
*
|
|
46263
|
+
* Also carries:
|
|
46264
|
+
* - `resolveAssignedProfile` — the profile-slot fallback chain (was already
|
|
46265
|
+
* pure; moved verbatim, no `this` usage in the original).
|
|
46266
|
+
* - `detectionConfigEquals` — the field-by-field `CameraDetectionConfig`
|
|
46267
|
+
* comparator (was already a pure `static`; moved verbatim).
|
|
46268
|
+
* - `buildDetectionConfigFromInputs` — the pure tail of
|
|
46269
|
+
* `buildDetectionConfig`: given the resolved settings + the assigned
|
|
46270
|
+
* profile-slot map, resolves motion/detection profiles and shapes the
|
|
46271
|
+
* `CameraDetectionConfig` result.
|
|
46272
|
+
* - `buildRunnerConfig` — a single-source-of-truth `RunnerCameraConfig`
|
|
46273
|
+
* literal builder (smell #10: the literal was built field-by-field in 3
|
|
46274
|
+
* places — `startDetection`, `ensureMotionWatch`,
|
|
46275
|
+
* `dispatchDetectionSession` — differing only in `motionSources` /
|
|
46276
|
+
* `detectionMode` / `audioMode` / `steps` / `audio`). Wired ONLY into
|
|
46277
|
+
* `startDetection`'s always-on/config-build branch in this step; the two
|
|
46278
|
+
* Phase-3 session-time sites intentionally still build their own literals
|
|
46279
|
+
* inline — a later refactor step migrates them so this step never touches
|
|
46280
|
+
* the session-dispatch path.
|
|
46281
|
+
*/
|
|
45951
46282
|
/** Read a required string leaf out of the hydrated `flat` schema values. */
|
|
45952
46283
|
function mustString(flat, deviceId, key) {
|
|
45953
46284
|
const v = flat[key];
|
|
@@ -46024,6 +46355,7 @@ function resolveDetectionSettings(input) {
|
|
|
46024
46355
|
}
|
|
46025
46356
|
const userDetectionMode = raw["detectionMode"];
|
|
46026
46357
|
const detectionMode = typeof userDetectionMode === "string" && isPipelinePhaseMode(userDetectionMode) ? userDetectionMode : profile?.defaults.detectionMode ?? "on-motion";
|
|
46358
|
+
const audioStreamPick = toAudioStreamPick(raw["audioStreamPick"]);
|
|
46027
46359
|
const userAudioMode = raw["audioMode"];
|
|
46028
46360
|
const audioMode = typeof userAudioMode === "string" && isPipelinePhaseMode(userAudioMode) ? userAudioMode : profile?.defaults.audioMode ?? "always-on";
|
|
46029
46361
|
const userOnboardMotionDrivesAnalyzer = raw["onboardMotionDrivesAnalyzer"];
|
|
@@ -46049,6 +46381,7 @@ function resolveDetectionSettings(input) {
|
|
|
46049
46381
|
motionCooldownMs: numberOrDefault(flat, "motionCooldownMs"),
|
|
46050
46382
|
maxSessionHoldMs: numberOrDefault(flat, "maxSessionHoldMs"),
|
|
46051
46383
|
audioMotionWindowMs: numberOrDefault(flat, "audioMotionWindowMs"),
|
|
46384
|
+
audioStreamPick,
|
|
46052
46385
|
detectionMode,
|
|
46053
46386
|
audioMode,
|
|
46054
46387
|
onboardMotionDrivesAnalyzer,
|
|
@@ -46109,7 +46442,7 @@ function buildDetectionConfigFromInputs(resolved, assigned) {
|
|
|
46109
46442
|
motionCooldownMs: resolved.motionCooldownMs,
|
|
46110
46443
|
maxSessionHoldMs: resolved.maxSessionHoldMs,
|
|
46111
46444
|
audioMotionWindowMs: resolved.audioMotionWindowMs,
|
|
46112
|
-
|
|
46445
|
+
audioStreamPick: resolved.audioStreamPick,
|
|
46113
46446
|
detectionMode: resolved.detectionMode,
|
|
46114
46447
|
audioMode: resolved.audioMode,
|
|
46115
46448
|
onboardMotionDrivesAnalyzer: resolved.onboardMotionDrivesAnalyzer,
|
|
@@ -46137,7 +46470,7 @@ function detectionConfigEquals(a, b) {
|
|
|
46137
46470
|
if (a.motionCooldownMs !== b.motionCooldownMs) return false;
|
|
46138
46471
|
if (a.maxSessionHoldMs !== b.maxSessionHoldMs) return false;
|
|
46139
46472
|
if (a.audioMotionWindowMs !== b.audioMotionWindowMs) return false;
|
|
46140
|
-
if (a.
|
|
46473
|
+
if (a.audioStreamPick !== b.audioStreamPick) return false;
|
|
46141
46474
|
if (a.onboardMotionDrivesAnalyzer !== b.onboardMotionDrivesAnalyzer) return false;
|
|
46142
46475
|
if (a.occupancyRecheckEnabled !== b.occupancyRecheckEnabled) return false;
|
|
46143
46476
|
if (a.occupancyRecheckSec !== b.occupancyRecheckSec) return false;
|
|
@@ -46167,6 +46500,7 @@ function buildRunnerConfig(base, overrides) {
|
|
|
46167
46500
|
detectionFps: config.detectionFps,
|
|
46168
46501
|
motionStreamId: config.motionStreamId,
|
|
46169
46502
|
detectionStreamId: config.detectionStreamId,
|
|
46503
|
+
...config.audioStreamPick !== void 0 ? { audioStreamPick: config.audioStreamPick } : {},
|
|
46170
46504
|
motionSources: overrides?.motionSources ?? config.motionSources,
|
|
46171
46505
|
...(() => {
|
|
46172
46506
|
const sources = overrides?.detectionSources ?? config.detectionSources;
|
|
@@ -46669,8 +47003,15 @@ async function reconcileOnboardForwarding(input, deps) {
|
|
|
46669
47003
|
/**
|
|
46670
47004
|
* Derive the watchdog camera descriptor from a runner config. Captures which
|
|
46671
47005
|
* stages are expected to flow CONTINUOUSLY (always-on audio, analyzer motion,
|
|
46672
|
-
* always-on detection) → the camStreamId feeding each.
|
|
46673
|
-
*
|
|
47006
|
+
* always-on detection) → the camStreamId feeding each.
|
|
47007
|
+
*
|
|
47008
|
+
* Audio no longer shares the motion stream id. It rides the camera's lowest
|
|
47009
|
+
* NATIVE rung (`selectAudioCamStream`) because an audio-only consumer must
|
|
47010
|
+
* never cause a video transcode, and on this fleet the motion rung IS a
|
|
47011
|
+
* `derived:*` transcode. The caller therefore hands the resolved audio stream
|
|
47012
|
+
* in {@link WatchdogAudioSource} — reading `motionStreamId` here would label
|
|
47013
|
+
* and RECOVER the wrong stream, which is how camera 618's audio stage watched
|
|
47014
|
+
* a plane the analyser was not listening to.
|
|
46674
47015
|
*
|
|
46675
47016
|
* `pipelineEnabled === false` (the `object-detection` switch, D62/ADR-0109)
|
|
46676
47017
|
* arms NOTHING: `startDetection` deliberately performs no attach and opens no
|
|
@@ -46679,12 +47020,14 @@ async function reconcileOnboardForwarding(input, deps) {
|
|
|
46679
47020
|
* its watchdog row stays visible — a switched-off camera must read as off, not
|
|
46680
47021
|
* as broken.
|
|
46681
47022
|
*
|
|
46682
|
-
* `
|
|
47023
|
+
* `audio.hasTrack` is the caller's evidence that the AUDIO stream actually
|
|
46683
47024
|
* carries an audio track (`BrokerStats.audio !== null`). It fails OPEN: an
|
|
46684
47025
|
* unknown or failed read passes `true`, because the audio stage is diagnostic
|
|
46685
|
-
* and a false negative silently stops watching a working camera.
|
|
47026
|
+
* and a false negative silently stops watching a working camera. A `null`
|
|
47027
|
+
* `audio.camStreamId` means the subscription itself was refused (no native
|
|
47028
|
+
* rung, or `audioMode` cannot arm the stage) — there is nothing to watch.
|
|
46686
47029
|
*/
|
|
46687
|
-
function buildWatchdogCamera(config, deviceName,
|
|
47030
|
+
function buildWatchdogCamera(config, deviceName, audio) {
|
|
46688
47031
|
const continuousStages = /* @__PURE__ */ new Map();
|
|
46689
47032
|
if (config.pipelineEnabled === false) return {
|
|
46690
47033
|
deviceId: config.deviceId,
|
|
@@ -46694,7 +47037,7 @@ function buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
|
46694
47037
|
motionSources: config.motionSources,
|
|
46695
47038
|
continuousStages
|
|
46696
47039
|
};
|
|
46697
|
-
if (config.audioMode === "always-on" &&
|
|
47040
|
+
if (config.audioMode === "always-on" && audio.camStreamId !== null && audio.hasTrack) continuousStages.set("audio", audio.camStreamId);
|
|
46698
47041
|
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
46699
47042
|
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
46700
47043
|
return {
|
|
@@ -46879,6 +47222,11 @@ var DetectionWiringController = class {
|
|
|
46879
47222
|
* `CamProfile → sourceCamStreamId` so callers can both check which
|
|
46880
47223
|
* profiles are live AND resolve a profile to the cam-stream-keyed
|
|
46881
47224
|
* brokerId (`${deviceId}/${camStreamId}`) used by the broker.
|
|
47225
|
+
*
|
|
47226
|
+
* Public because the AUDIO plane needs the same answer, from the same read:
|
|
47227
|
+
* `selectAudioCamStream` picks the lowest NATIVE rung, and a second reader
|
|
47228
|
+
* of the slot table would be a second authority on which rung is backed by
|
|
47229
|
+
* which cam-stream.
|
|
46882
47230
|
*/
|
|
46883
47231
|
async fetchAssignedProfiles(deviceId) {
|
|
46884
47232
|
const api = this.deps.api();
|
|
@@ -47258,10 +47606,10 @@ var DetectionWiringController = class {
|
|
|
47258
47606
|
*
|
|
47259
47607
|
* Public — `PlacementService`'s `buildWatchdogCamera` closure (wired in
|
|
47260
47608
|
* `orchestrator-bootstrap.ts`) calls straight through to this method; the
|
|
47261
|
-
* caller resolves
|
|
47609
|
+
* caller resolves the {@link WatchdogAudioSource} once per dispatch.
|
|
47262
47610
|
*/
|
|
47263
|
-
buildWatchdogCamera(config, deviceName,
|
|
47264
|
-
return buildWatchdogCamera(config, deviceName,
|
|
47611
|
+
buildWatchdogCamera(config, deviceName, audio) {
|
|
47612
|
+
return buildWatchdogCamera(config, deviceName, audio);
|
|
47265
47613
|
}
|
|
47266
47614
|
/**
|
|
47267
47615
|
* Watchdog recovery callback. Invoked when a camera's continuous stage
|
|
@@ -48196,8 +48544,8 @@ var PlacementService = class {
|
|
|
48196
48544
|
await this.attachOn(targetNodeId, attachConfig);
|
|
48197
48545
|
this.deps.ledger.setConfig(runnerConfig.deviceId, attachConfig);
|
|
48198
48546
|
if (targetNodeId === this.deps.localNodeId()) {
|
|
48199
|
-
const
|
|
48200
|
-
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(runnerConfig, String(runnerConfig.deviceId),
|
|
48547
|
+
const audioSource = await this.resolveAudioSource(runnerConfig);
|
|
48548
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(runnerConfig, String(runnerConfig.deviceId), audioSource));
|
|
48201
48549
|
}
|
|
48202
48550
|
this.deps.ledger.recordAssignment(runnerConfig.deviceId, targetNodeId, reason, reason === "manual");
|
|
48203
48551
|
} finally {
|
|
@@ -48625,19 +48973,35 @@ var PlacementService = class {
|
|
|
48625
48973
|
}
|
|
48626
48974
|
await this.attachOn(nodeId, cached);
|
|
48627
48975
|
if (nodeId === this.deps.localNodeId()) {
|
|
48628
|
-
const
|
|
48629
|
-
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(cached, String(cached.deviceId),
|
|
48976
|
+
const audioSource = await this.resolveAudioSource(cached);
|
|
48977
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(cached, String(cached.deviceId), audioSource));
|
|
48630
48978
|
}
|
|
48631
48979
|
}
|
|
48632
48980
|
/**
|
|
48633
|
-
* Resolve the watchdog's audio
|
|
48634
|
-
*
|
|
48635
|
-
*
|
|
48636
|
-
* audio costs no broker round-trip.
|
|
48981
|
+
* Resolve the watchdog's audio plane for ONE dispatch: which stream audio
|
|
48982
|
+
* rides, and whether it carries a track. Skipped entirely (and answered
|
|
48983
|
+
* "nothing to watch") when `audioMode` could not arm the stage anyway, so a
|
|
48984
|
+
* camera with on-motion audio costs no broker round-trip.
|
|
48985
|
+
*
|
|
48986
|
+
* The stream comes from the SAME selection the subscription makes — reading
|
|
48987
|
+
* `motionStreamId` here would arm the stage on the derived rung the audio
|
|
48988
|
+
* analyser deliberately does not listen to, and both the label and the
|
|
48989
|
+
* recovery would name a stream nobody is on.
|
|
48637
48990
|
*/
|
|
48638
|
-
async
|
|
48639
|
-
if (config.audioMode !== "always-on") return
|
|
48640
|
-
|
|
48991
|
+
async resolveAudioSource(config) {
|
|
48992
|
+
if (config.audioMode !== "always-on") return {
|
|
48993
|
+
camStreamId: null,
|
|
48994
|
+
hasTrack: false
|
|
48995
|
+
};
|
|
48996
|
+
const camStreamId = await this.deps.audioCamStreamFor(config);
|
|
48997
|
+
if (camStreamId === null) return {
|
|
48998
|
+
camStreamId: null,
|
|
48999
|
+
hasTrack: false
|
|
49000
|
+
};
|
|
49001
|
+
return {
|
|
49002
|
+
camStreamId,
|
|
49003
|
+
hasTrack: await this.deps.hasAudioSource(config.deviceId, camStreamId)
|
|
49004
|
+
};
|
|
48641
49005
|
}
|
|
48642
49006
|
/**
|
|
48643
49007
|
* Normalizes the `decision.kind === 'pending'` → `setPendingReason` +
|
|
@@ -54596,6 +54960,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
54596
54960
|
logger: deps.ctx().logger,
|
|
54597
54961
|
readDeviceStore: async (deviceId) => await deps.ctxIfReady()?.settings?.readDeviceStore(deviceId) ?? {},
|
|
54598
54962
|
probeAudioTrack: (deviceId, camStreamId) => resolveAudioTrackProbe(deviceId, camStreamId),
|
|
54963
|
+
assignedProfiles: (deviceId) => detectionWiring.fetchAssignedProfiles(deviceId),
|
|
54599
54964
|
ingestNode: () => topology.clusterRoles.ingestNode,
|
|
54600
54965
|
localNodeId: () => localNodeId,
|
|
54601
54966
|
enabledAudioNodes: () => topology.enabledAudioNodes,
|
|
@@ -54696,6 +55061,21 @@ async function buildOrchestratorControllers(deps) {
|
|
|
54696
55061
|
* stage that never produced one signal, with one line.
|
|
54697
55062
|
*/
|
|
54698
55063
|
const resolveHasAudioSource = async (deviceId, camStreamId) => (await resolveAudioTrackProbe(deviceId, camStreamId)).kind !== "absent";
|
|
55064
|
+
/**
|
|
55065
|
+
* The cam-stream a camera's audio subscription rides — `selectAudioCamStream`
|
|
55066
|
+
* over the same profile-slot read the detection plane uses. `null` when the
|
|
55067
|
+
* selection refuses (every rung is a `derived:*` transcode, or nothing is
|
|
55068
|
+
* assigned): an audio-only consumer never causes a video transcode, so there
|
|
55069
|
+
* is genuinely no stream and the watchdog must not arm its audio stage.
|
|
55070
|
+
*/
|
|
55071
|
+
const resolveAudioCamStream = async (config) => {
|
|
55072
|
+
const selection = selectAudioCamStream({
|
|
55073
|
+
pick: config.audioStreamPick ?? "auto",
|
|
55074
|
+
motionStreamId: config.motionStreamId,
|
|
55075
|
+
assigned: await detectionWiring.fetchAssignedProfiles(config.deviceId)
|
|
55076
|
+
});
|
|
55077
|
+
return selection.kind === "selected" ? selection.camStreamId : null;
|
|
55078
|
+
};
|
|
54699
55079
|
const placement = new PlacementService({
|
|
54700
55080
|
ctx: () => deps.ctx(),
|
|
54701
55081
|
ctxIfReady: () => deps.ctxIfReady(),
|
|
@@ -54712,8 +55092,9 @@ async function buildOrchestratorControllers(deps) {
|
|
|
54712
55092
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
54713
55093
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
54714
55094
|
inferenceRotation,
|
|
54715
|
-
buildWatchdogCamera: (config, deviceName,
|
|
55095
|
+
buildWatchdogCamera: (config, deviceName, audio) => detectionWiring.buildWatchdogCamera(config, deviceName, audio),
|
|
54716
55096
|
hasAudioSource: (deviceId, camStreamId) => resolveHasAudioSource(deviceId, camStreamId),
|
|
55097
|
+
audioCamStreamFor: (config) => resolveAudioCamStream(config),
|
|
54717
55098
|
isIdleSessionCamera: (deviceId) => deps.isIdleSessionCamera(deviceId),
|
|
54718
55099
|
activeSessionNode: (deviceId) => session.activeSessionNode(deviceId),
|
|
54719
55100
|
closeActiveSession: (deviceId) => session.closeSessionForReassign(deviceId),
|
|
@@ -55516,6 +55897,34 @@ function buildDeviceSettingsSections(nodeOptions) {
|
|
|
55516
55897
|
label: "On motion (firmware/PIR/SW)"
|
|
55517
55898
|
}
|
|
55518
55899
|
]
|
|
55900
|
+
}, {
|
|
55901
|
+
key: "audioStreamPick",
|
|
55902
|
+
type: "select",
|
|
55903
|
+
label: "Audio stream",
|
|
55904
|
+
default: "auto",
|
|
55905
|
+
description: "Which camera stream the audio classifier listens on. Auto picks the lowest stream the camera serves natively — a transcoded stream is never started just to hear a camera.",
|
|
55906
|
+
options: [
|
|
55907
|
+
{
|
|
55908
|
+
value: "auto",
|
|
55909
|
+
label: "Auto (lowest native)"
|
|
55910
|
+
},
|
|
55911
|
+
{
|
|
55912
|
+
value: "high",
|
|
55913
|
+
label: "High"
|
|
55914
|
+
},
|
|
55915
|
+
{
|
|
55916
|
+
value: "mid",
|
|
55917
|
+
label: "Medium"
|
|
55918
|
+
},
|
|
55919
|
+
{
|
|
55920
|
+
value: "low",
|
|
55921
|
+
label: "Low"
|
|
55922
|
+
}
|
|
55923
|
+
],
|
|
55924
|
+
showWhen: {
|
|
55925
|
+
field: "audioMode",
|
|
55926
|
+
notEquals: "disabled"
|
|
55927
|
+
}
|
|
55519
55928
|
}]
|
|
55520
55929
|
},
|
|
55521
55930
|
{
|