@camstack/addon-admin-ui 1.2.32 → 1.2.34
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/assets/{AddLocationWizard-Bqn51iVc.js → AddLocationWizard-DzP-CYOi.js} +1 -1
- package/dist/assets/{AddonCollectionPage-D9EE6p80.js → AddonCollectionPage-B5t3SHZE.js} +1 -1
- package/dist/assets/{Addons-Bp55KXZR.js → Addons-DCR8DMgD.js} +1 -1
- package/dist/assets/{AdminTabs-DB9TozBh.js → AdminTabs-DTECFIAe.js} +1 -1
- package/dist/assets/{AiProfiles-C4ji0awJ.js → AiProfiles-D0jpUF4v.js} +1 -1
- package/dist/assets/{Automation-BmDGNoTh.js → Automation-BX0bNbpp.js} +1 -1
- package/dist/assets/{BrokerForm-CtjETVv8.js → BrokerForm-CROZPAB-.js} +1 -1
- package/dist/assets/{Capabilities-BlEUfcGN.js → Capabilities-DIGQ9fiv.js} +1 -1
- package/dist/assets/{CapabilityBadges-CTZ7zqWM.js → CapabilityBadges-B4vHw0qw.js} +1 -1
- package/dist/assets/{Cluster-CkGq5KVh.js → Cluster-BnTT5Wsb.js} +1 -1
- package/dist/assets/{ClusterRecordingTab-nszIK7qG.js → ClusterRecordingTab-BVkyOEdI.js} +1 -1
- package/dist/assets/{Dashboard-CO5PTZ9n.js → Dashboard-BlkR2NIh.js} +1 -1
- package/dist/assets/{Data-CYwE4-K2.js → Data-CECcPfPj.js} +1 -1
- package/dist/assets/{DetectionIntelligence-MaxZ_sn8.js → DetectionIntelligence-CzMadCA-.js} +1 -1
- package/dist/assets/{DeviceAggregateSection-C6sscd95.js → DeviceAggregateSection-CbCIksD2.js} +1 -1
- package/dist/assets/DeviceDetail-oWNRSfva.js +2 -0
- package/dist/assets/{Devices-xlmwhKxI.js → Devices-COMVrlGU.js} +1 -1
- package/dist/assets/{EmbedPlayerPage-CLGFNg5C.js → EmbedPlayerPage-B4Y1MxZ9.js} +1 -1
- package/dist/assets/Events-BIZWrXMY.js +1 -0
- package/dist/assets/{FormBuilder-ZuE9B0w9.js → FormBuilder-Cogt0U0A.js} +1 -1
- package/dist/assets/{Identity-CKR7bmrK.js → Identity-i_LhUVHo.js} +1 -1
- package/dist/assets/{IntegrationDetail-DUkC3dFN.js → IntegrationDetail-CPE4VRwU.js} +1 -1
- package/dist/assets/{Integrations-t8-3dqN4.js → Integrations-DhMaG2V3.js} +1 -1
- package/dist/assets/{Integrations-BuhF2y9p.js → Integrations-qNIS4emE.js} +1 -1
- package/dist/assets/{Logs-D1sMKNHu.js → Logs-rH1aVfpt.js} +1 -1
- package/dist/assets/{MotionZonesSettings-JUP-5dky.js → MotionZonesSettings-DDSnsNc9.js} +1 -1
- package/dist/assets/{MyAccess-BSkJTbz8.js → MyAccess-J8p1dKF3.js} +1 -1
- package/dist/assets/{Network-CXKyrgFS.js → Network-C6bXHvxd.js} +1 -1
- package/dist/assets/{NodeAddonsSettingsPanel-1C9rZfoa.js → NodeAddonsSettingsPanel-6Dzcc5in.js} +1 -1
- package/dist/assets/{NodeInferenceDevices-D1yIrxri.js → NodeInferenceDevices-DraQNeS_.js} +1 -1
- package/dist/assets/{Notifications-gurR5fDh.js → Notifications-BL4Cp1b2.js} +1 -1
- package/dist/assets/{Pipeline-DTKomVZV.js → Pipeline-Bf8P_Drx.js} +1 -1
- package/dist/assets/{Plates-CQ4aoncO.js → Plates-BkYxLI16.js} +1 -1
- package/dist/assets/{PrivacyMaskSettings-CfvJZ3tP.js → PrivacyMaskSettings-ByZ6t3aH.js} +1 -1
- package/dist/assets/{ProviderIcon-ClXY1rL4.js → ProviderIcon-9BELU-vo.js} +1 -1
- package/dist/assets/{Recordings-DglOV8NW.js → Recordings-OoJn-xbL.js} +1 -1
- package/dist/assets/{ScanNetworkModal-DmZyDQIW.js → ScanNetworkModal-DGnLpsYA.js} +1 -1
- package/dist/assets/{Settings-BVw2pnr7.js → Settings-DhIPBQcW.js} +1 -1
- package/dist/assets/{Showroom-9BtpR6nD.js → Showroom-C7QnFCQp.js} +1 -1
- package/dist/assets/{Terminal-C-v8B3we.js → Terminal-DVjWHgq3.js} +1 -1
- package/dist/assets/{_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-BtA4eogJ.js → _virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-DkbcXBBJ.js} +1 -1
- package/dist/assets/{build-cluster-graph-6UMcPt27.js → build-cluster-graph-BDFGSfk8.js} +1 -1
- package/dist/assets/circle-check-big-DX7P42Tw.js +1 -0
- package/dist/assets/clock-DMJpZrGI.js +1 -0
- package/dist/assets/{copy-BObtPXKC.js → copy-oDpB_CYv.js} +1 -1
- package/dist/assets/{debounced-invalidate-Sygznr_T.js → debounced-invalidate-CPxCM-Lt.js} +1 -1
- package/dist/assets/dist-CekX4Hra.js +7 -0
- package/dist/assets/{dist-W1UImz79.js → dist-Dx0ACB1O.js} +1 -1
- package/dist/assets/{download-DsIysR_y.js → download-c60BtWes.js} +1 -1
- package/dist/assets/{face-bbox-DjqvdhYZ.js → face-bbox-nkg8OmFa.js} +1 -1
- package/dist/assets/{hostInit-BUHMjqwF.js → hostInit-BpyXzSDR.js} +1 -1
- package/dist/assets/{index-BqCgqp9V.js → index-2QX2zxyn.js} +3 -3
- package/dist/assets/index-BIqRUg3I.css +2 -0
- package/dist/assets/{key-round-DF1dcL0w.js → key-round-BlSZq_ro.js} +1 -1
- package/dist/assets/{mf-entry-bootstrap-0-62a13374.js → mf-entry-bootstrap-0-3845b4a0.js} +2 -2
- package/dist/assets/{pencil-CQwfEybQ.js → pencil-DUgcFYZU.js} +1 -1
- package/dist/assets/{player-overlays-CJ2EDXmI.js → player-overlays-CzjKRHin.js} +1 -1
- package/dist/assets/plus-C7qYd7DF.js +1 -0
- package/dist/assets/{radio-C754NrLM.js → radio-eY0YHItP.js} +1 -1
- package/dist/assets/{refresh-cw-DAm7QoFj.js → refresh-cw-D_fKxMIe.js} +1 -1
- package/dist/assets/remoteEntry-CFMi51wk.js +1 -0
- package/dist/assets/{rotate-ccw-ildFCltM.js → rotate-ccw-DU4_rBQq.js} +1 -1
- package/dist/assets/rotate-cw-CktlNgLW.js +1 -0
- package/dist/assets/search-CY5Hv7Yv.js +1 -0
- package/dist/assets/{send-BMpnpOJi.js → send-B9r8Lohp.js} +1 -1
- package/dist/assets/{server-2ReeYjUz.js → server-BhdxNAPC.js} +1 -1
- package/dist/assets/square-S_5NH0Gl.js +1 -0
- package/dist/assets/{square-check-big-DUv4JqM3.js → square-check-big-DMenPwwp.js} +1 -1
- package/dist/assets/{src-DCsdXZpS.js → src-BBe6PrXe.js} +10 -10
- package/dist/assets/src-DSZBcu6j.js +7 -0
- package/dist/assets/{upload-Dvv16Zg3.js → upload-AwB-upJP.js} +1 -1
- package/dist/assets/{useEventInvalidation-BI2Lc7mp.js → useEventInvalidation-qhbuBjhg.js} +1 -1
- package/dist/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-D3wOFHTg.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-CPcVkZQ0.js} +2 -2
- package/dist/assets/{wifi-cBAahSiZ.js → wifi-DyeEKGI-.js} +1 -1
- package/dist/assets/{zap-C86yuB6j.js → zap-C32Ix0E1.js} +1 -1
- package/dist/index.html +6 -6
- package/dist/server/addon.js +642 -32
- package/package.json +1 -1
- package/dist/assets/DeviceDetail-KHB2dWaR.js +0 -2
- package/dist/assets/Events-EfdVWQGS.js +0 -1
- package/dist/assets/circle-check-big-5J4vhHcH.js +0 -1
- package/dist/assets/clock-D19QkJv2.js +0 -1
- package/dist/assets/dist-Bso6V1TI.js +0 -7
- package/dist/assets/index-BH_HqZVJ.css +0 -2
- package/dist/assets/plus-BIR1e9Y7.js +0 -1
- package/dist/assets/remoteEntry-EVtyXnE6.js +0 -1
- package/dist/assets/rotate-cw-DyrhB96Y.js +0 -1
- package/dist/assets/search-BFU3RCdH.js +0 -1
- package/dist/assets/square-CjeJk_qb.js +0 -1
- package/dist/assets/src-YgttllUb.js +0 -7
package/dist/server/addon.js
CHANGED
|
@@ -6462,7 +6462,20 @@ var BrokerStatsSchema = object({
|
|
|
6462
6462
|
sampleRate: number(),
|
|
6463
6463
|
channels: number(),
|
|
6464
6464
|
supported: boolean()
|
|
6465
|
-
}).nullable().optional()
|
|
6465
|
+
}).nullable().optional(),
|
|
6466
|
+
/**
|
|
6467
|
+
* BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
|
|
6468
|
+
* distributing none of the device's audio, on live or recording.
|
|
6469
|
+
*
|
|
6470
|
+
* Present so a silent camera can be told apart from a broken one on the
|
|
6471
|
+
* stream panel itself, without cross-referencing the switch group: a
|
|
6472
|
+
* broker holding an `audio` track descriptor while `audioMuted` is true is
|
|
6473
|
+
* working exactly as asked. `audioMutedDropped` counts the audio units
|
|
6474
|
+
* thrown away since the current dial — it is how you confirm from stats
|
|
6475
|
+
* alone that the mute is on the packet path and not merely persisted.
|
|
6476
|
+
*/
|
|
6477
|
+
audioMuted: boolean().optional(),
|
|
6478
|
+
audioMutedDropped: number().optional()
|
|
6466
6479
|
});
|
|
6467
6480
|
/**
|
|
6468
6481
|
* Exporter-facing "profile restream" entry. Returned by
|
|
@@ -7087,7 +7100,7 @@ object({
|
|
|
7087
7100
|
* ## This file adds no state
|
|
7088
7101
|
*
|
|
7089
7102
|
* Every switch here is a VIEW onto an authority that already existed
|
|
7090
|
-
* ([
|
|
7103
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
|
|
7091
7104
|
* group is that there is exactly one place each function is turned off, and
|
|
7092
7105
|
* the group routes to it:
|
|
7093
7106
|
*
|
|
@@ -7098,6 +7111,53 @@ object({
|
|
|
7098
7111
|
* | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
|
|
7099
7112
|
* | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
|
|
7100
7113
|
* | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
|
|
7114
|
+
* | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
|
|
7115
|
+
* | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
|
|
7116
|
+
* | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
|
|
7117
|
+
*
|
|
7118
|
+
* ## `device-audio` and `broker-audio` are two functions, not two knobs
|
|
7119
|
+
*
|
|
7120
|
+
* They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
|
|
7121
|
+
* `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
|
|
7122
|
+
* genuinely stops, it survives CamStack entirely, and it costs a multi-second
|
|
7123
|
+
* encoder restart on every flip. `broker-audio` writes THIS server, so it is
|
|
7124
|
+
* instant, vendor-independent and reversible without touching the camera, and
|
|
7125
|
+
* a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
|
|
7126
|
+
* D62 forbids a second switch that *disagrees* with the first; these two
|
|
7127
|
+
* cannot disagree, because neither reads the other's store — the camera holds
|
|
7128
|
+
* one, the broker holds the other, and each reports its own fact.
|
|
7129
|
+
*
|
|
7130
|
+
* ## The two switches whose authority is not on this server
|
|
7131
|
+
*
|
|
7132
|
+
* `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
|
|
7133
|
+
* in "the group stores nothing" — it is the purest form of it: the camera
|
|
7134
|
+
* holds the fact, every read is a read-through, and there is no server-side
|
|
7135
|
+
* copy that could drift. Their availability therefore cannot come from
|
|
7136
|
+
* `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
|
|
7137
|
+
* is filtered out there); it comes from the cap's own camera-probed
|
|
7138
|
+
* `privacyMask.getOptions()`, which is strictly more honest — it answers for
|
|
7139
|
+
* THIS camera rather than for the device type
|
|
7140
|
+
* ([D74](../../../../docs/decisions/adr-0074.md)).
|
|
7141
|
+
*
|
|
7142
|
+
* ## `privacy-mask` is the one row whose ON is not "the function is working"
|
|
7143
|
+
*
|
|
7144
|
+
* Every other switch means *this camera's function is doing its job*, so
|
|
7145
|
+
* `enabled: false` is a thing an operator took away. `privacy-mask` means **the
|
|
7146
|
+
* MASK is active** — `enabled: true` is video deliberately obscured. The
|
|
7147
|
+
* polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
|
|
7148
|
+
* (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
|
|
7149
|
+
* a HomeKit toggle that disagreed with the app's toggle for the same camera is
|
|
7150
|
+
* worse than either surface not having one.
|
|
7151
|
+
*
|
|
7152
|
+
* Two consequences follow and both are load-bearing:
|
|
7153
|
+
*
|
|
7154
|
+
* - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
|
|
7155
|
+
* exactly this row. With the polarity above, every camera that has NOT drawn
|
|
7156
|
+
* a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
|
|
7157
|
+
* normal, healthy state of most cameras rendered as an operator disablement.
|
|
7158
|
+
* - **Its cost line names BOTH directions.** `costWhenOff` is rendered
|
|
7159
|
+
* unconditionally by both clients, so for this row it has to read correctly
|
|
7160
|
+
* whichever way the switch is sitting.
|
|
7101
7161
|
*
|
|
7102
7162
|
* The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
|
|
7103
7163
|
* migrated the legacy `audioEnabled` / `pipelineEnabled` /
|
|
@@ -7116,14 +7176,18 @@ object({
|
|
|
7116
7176
|
* `CameraStatus.switchedOff`.
|
|
7117
7177
|
*/
|
|
7118
7178
|
/**
|
|
7119
|
-
* The
|
|
7120
|
-
*
|
|
7179
|
+
* The functions the operator named — five on 2026-08-05, plus the camera's own
|
|
7180
|
+
* microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
|
|
7181
|
+
* recognition and plate/LPR are per-step toggles on
|
|
7121
7182
|
* `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
|
|
7122
|
-
* editor, not in a
|
|
7183
|
+
* editor, not in a safety group.
|
|
7123
7184
|
*/
|
|
7124
7185
|
var CameraSwitchIdSchema = _enum([
|
|
7125
7186
|
"stream-broker",
|
|
7126
7187
|
"object-detection",
|
|
7188
|
+
"privacy-mask",
|
|
7189
|
+
"device-audio",
|
|
7190
|
+
"broker-audio",
|
|
7127
7191
|
"audio-analysis",
|
|
7128
7192
|
"recording",
|
|
7129
7193
|
"notifications"
|
|
@@ -7141,14 +7205,27 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
|
|
|
7141
7205
|
capName: string()
|
|
7142
7206
|
}),
|
|
7143
7207
|
object({ kind: literal("recording-config") }),
|
|
7144
|
-
object({ kind: literal("notification-mute") })
|
|
7208
|
+
object({ kind: literal("notification-mute") }),
|
|
7209
|
+
object({
|
|
7210
|
+
kind: literal("camera-audio"),
|
|
7211
|
+
capName: string()
|
|
7212
|
+
}),
|
|
7213
|
+
object({
|
|
7214
|
+
kind: literal("camera-mask"),
|
|
7215
|
+
capName: string()
|
|
7216
|
+
}),
|
|
7217
|
+
object({ kind: literal("broker-audio-mute") })
|
|
7145
7218
|
]);
|
|
7146
7219
|
/**
|
|
7147
7220
|
* Why a switch is not offered for this camera. Rendered instead of the
|
|
7148
7221
|
* control, never as a dead control — an absent function and a broken one must
|
|
7149
7222
|
* not look the same.
|
|
7150
7223
|
*/
|
|
7151
|
-
var CameraSwitchUnavailableReasonSchema = _enum([
|
|
7224
|
+
var CameraSwitchUnavailableReasonSchema = _enum([
|
|
7225
|
+
"no-provider",
|
|
7226
|
+
"source-unreachable",
|
|
7227
|
+
"not-configured"
|
|
7228
|
+
]);
|
|
7152
7229
|
/**
|
|
7153
7230
|
* One switch, resolved for one camera.
|
|
7154
7231
|
*
|
|
@@ -9116,6 +9193,26 @@ var EgressTranscodeRequestSchema = object({
|
|
|
9116
9193
|
"h264_mp4toannexb",
|
|
9117
9194
|
"hevc_mp4toannexb"
|
|
9118
9195
|
]).optional(),
|
|
9196
|
+
/**
|
|
9197
|
+
* Publish the transcode as a LOCAL push cam stream, instead of leaving the
|
|
9198
|
+
* consumer to dial the returned url. The broker picks the id and returns it
|
|
9199
|
+
* as `camStreamId` — a caller-supplied one would be circular, since the
|
|
9200
|
+
* sharing key is computed FROM this request.
|
|
9201
|
+
*
|
|
9202
|
+
* The url is still returned and still the contract for a transcode pinned to
|
|
9203
|
+
* another node. But dialling it locally costs an RTSP round trip that changes
|
|
9204
|
+
* the transport underneath the consumer: a dialled stream is an RTP source,
|
|
9205
|
+
* so `isRtpSource()` is true and the session takes the RTP-passthrough +
|
|
9206
|
+
* repacketizer branch. The push branch — the one the derived mechanism has
|
|
9207
|
+
* live hours on — is never reached. Measured on Alexa: broker registered, RTP
|
|
9208
|
+
* arriving, key frame arriving, black screen, on a chain healthy at every
|
|
9209
|
+
* other point.
|
|
9210
|
+
*
|
|
9211
|
+
* Same idea the transport already applies to CALLS, where `classifyCapRoute`
|
|
9212
|
+
* gives priority to `hub-in-process` so a local call never leaves the node.
|
|
9213
|
+
* This is that rule for media.
|
|
9214
|
+
*/
|
|
9215
|
+
publishLocally: boolean().optional(),
|
|
9119
9216
|
pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
|
|
9120
9217
|
/**
|
|
9121
9218
|
* Operator/consumer override for decode hardware. ABSENT is the normal case
|
|
@@ -9160,7 +9257,13 @@ var EgressTranscodeSchema = object({
|
|
|
9160
9257
|
* Returned rather than assumed: a consumer that asked for hardware and got
|
|
9161
9258
|
* software needs to be able to see that without reading the broker's logs.
|
|
9162
9259
|
*/
|
|
9163
|
-
decodeHwAccel: string().nullable()
|
|
9260
|
+
decodeHwAccel: string().nullable(),
|
|
9261
|
+
/**
|
|
9262
|
+
* Set when `publishLocally` was honoured: attach to THIS instead of dialling
|
|
9263
|
+
* `url`, and the session takes the push/deframe transport rather than the
|
|
9264
|
+
* RTP-passthrough one. `null` means the consumer must dial.
|
|
9265
|
+
*/
|
|
9266
|
+
camStreamId: string().nullable()
|
|
9164
9267
|
});
|
|
9165
9268
|
method(object({
|
|
9166
9269
|
deviceId: number().int().nonnegative(),
|
|
@@ -9308,7 +9411,25 @@ method(object({
|
|
|
9308
9411
|
}), _void(), {
|
|
9309
9412
|
kind: "mutation",
|
|
9310
9413
|
auth: "admin"
|
|
9311
|
-
}), method(object({ brokerId: string() }), boolean()), object({
|
|
9414
|
+
}), method(object({ brokerId: string() }), boolean()), method(object({ deviceId: number().int() }), object({
|
|
9415
|
+
muted: boolean(),
|
|
9416
|
+
/**
|
|
9417
|
+
* How many live non-derived brokers currently hold the mute. Purely
|
|
9418
|
+
* diagnostic: `muted` is the policy and is authoritative on its own
|
|
9419
|
+
* (it applies to brokers that do not exist yet), while this says
|
|
9420
|
+
* whether anything is presently being silenced.
|
|
9421
|
+
*/
|
|
9422
|
+
appliedBrokers: number().int().nonnegative()
|
|
9423
|
+
})), method(object({
|
|
9424
|
+
deviceId: number().int(),
|
|
9425
|
+
muted: boolean()
|
|
9426
|
+
}), object({
|
|
9427
|
+
muted: boolean(),
|
|
9428
|
+
appliedBrokers: number().int().nonnegative()
|
|
9429
|
+
}), {
|
|
9430
|
+
kind: "mutation",
|
|
9431
|
+
auth: "admin"
|
|
9432
|
+
}), object({
|
|
9312
9433
|
deviceId: number().int().nonnegative(),
|
|
9313
9434
|
camStreamId: string(),
|
|
9314
9435
|
profile: CamProfileSchema
|
|
@@ -14550,6 +14671,30 @@ var TrackSourceSchema = _enum([
|
|
|
14550
14671
|
"audio"
|
|
14551
14672
|
]);
|
|
14552
14673
|
/**
|
|
14674
|
+
* Where a track sits in the RETRAIN lifecycle (D81).
|
|
14675
|
+
*
|
|
14676
|
+
* - `none` — never marked, or un-marked. Evictable.
|
|
14677
|
+
* - `staging` — the operator wants this track as training material and has not
|
|
14678
|
+
* finished with it. **This is the only state retention holds**: the track and
|
|
14679
|
+
* everything it owns (object events, crops, keyframes, CLIP vector) survive
|
|
14680
|
+
* the device's age window.
|
|
14681
|
+
* - `trained` — the retrain page has taken what it needed. The frames it chose
|
|
14682
|
+
* were COPIED into the retrain dataset at selection time, so the dataset no
|
|
14683
|
+
* longer depends on the track's media and the track becomes EVICTABLE again.
|
|
14684
|
+
* Terminal for the plain `markForTrain` toggle: returning it to `staging` is
|
|
14685
|
+
* a deliberate action of the retrain page, not a side effect of a checkbox.
|
|
14686
|
+
*
|
|
14687
|
+
* There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
|
|
14688
|
+
* the store's filter language has only positive equality and `whereIn` — no
|
|
14689
|
+
* negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
|
|
14690
|
+
* would make the entire pre-column history immortal in one deploy.
|
|
14691
|
+
*/
|
|
14692
|
+
var RetrainStatusSchema = _enum([
|
|
14693
|
+
"none",
|
|
14694
|
+
"staging",
|
|
14695
|
+
"trained"
|
|
14696
|
+
]);
|
|
14697
|
+
/**
|
|
14553
14698
|
* Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
|
|
14554
14699
|
* by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
|
|
14555
14700
|
* so the two surfaces cannot drift.
|
|
@@ -14559,18 +14704,31 @@ var TrackSourceSchema = _enum([
|
|
|
14559
14704
|
* columns existed read as absent, and a consumer that needs a boolean should say
|
|
14560
14705
|
* `flag === true`, not `flag !== false`.
|
|
14561
14706
|
*
|
|
14562
|
-
*
|
|
14563
|
-
*
|
|
14564
|
-
*
|
|
14565
|
-
* `
|
|
14707
|
+
* `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
|
|
14708
|
+
* it is exactly `retrainStatus === 'staging'`, in both directions. Writing
|
|
14709
|
+
* `true` moves `none → staging`, writing `false` moves `staging → none`, and a
|
|
14710
|
+
* `trained` track reports `false` while refusing both writes. The boolean is
|
|
14711
|
+
* kept because three surfaces drive a toggle off it; anything that needs to tell
|
|
14712
|
+
* "never marked" from "already trained" must read `retrainStatus`.
|
|
14713
|
+
*
|
|
14714
|
+
* `debug` does NOT pin; it is attention, not durability.
|
|
14566
14715
|
*/
|
|
14567
14716
|
var TrackFlagFields = {
|
|
14568
|
-
/** Operator marked this track as training material.
|
|
14717
|
+
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
14718
|
+
* `'staging'`. */
|
|
14569
14719
|
markForTrain: boolean().optional(),
|
|
14570
14720
|
/** Operator marked this track for diagnostic attention. */
|
|
14571
14721
|
debug: boolean().optional()
|
|
14572
14722
|
};
|
|
14573
14723
|
/**
|
|
14724
|
+
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
14725
|
+
* Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
|
|
14726
|
+
* write patch, and the status is not something the toggle sets — it is what the
|
|
14727
|
+
* toggle's boolean is derived from. Absent on an in-RAM track never touched;
|
|
14728
|
+
* always present on a persisted row (the column default materialises `'none'`).
|
|
14729
|
+
*/
|
|
14730
|
+
var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
|
|
14731
|
+
/**
|
|
14574
14732
|
* The write half: a PARTIAL patch. An omitted key is left untouched, so setting
|
|
14575
14733
|
* one flag can never clear the other — the toggles are independent and are
|
|
14576
14734
|
* driven from three surfaces that do not know about each other.
|
|
@@ -14584,7 +14742,32 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
|
|
|
14584
14742
|
var TrackFlagsSchema = object({
|
|
14585
14743
|
trackId: string(),
|
|
14586
14744
|
markForTrain: boolean(),
|
|
14587
|
-
debug: boolean()
|
|
14745
|
+
debug: boolean(),
|
|
14746
|
+
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
14747
|
+
* a track row) because this shape is only ever produced by the write body,
|
|
14748
|
+
* which always knows it — and a surface that has just written needs to render
|
|
14749
|
+
* `trained` without a re-fetch. */
|
|
14750
|
+
retrainStatus: RetrainStatusSchema
|
|
14751
|
+
});
|
|
14752
|
+
/** Per-camera slice of a training-export estimate. */
|
|
14753
|
+
var TrainingExportDeviceTotalsSchema = object({
|
|
14754
|
+
deviceId: number(),
|
|
14755
|
+
tracks: number().int(),
|
|
14756
|
+
files: number().int(),
|
|
14757
|
+
bytes: number().int()
|
|
14758
|
+
});
|
|
14759
|
+
/**
|
|
14760
|
+
* What a training export WOULD contain. Computed from media index rows only —
|
|
14761
|
+
* no blob is read to produce this.
|
|
14762
|
+
*/
|
|
14763
|
+
var TrainingExportSummarySchema = object({
|
|
14764
|
+
generatedAt: number(),
|
|
14765
|
+
trackCount: number().int(),
|
|
14766
|
+
fileCount: number().int(),
|
|
14767
|
+
byteCount: number().int(),
|
|
14768
|
+
/** More marked tracks exist than a single pass carries. */
|
|
14769
|
+
truncated: boolean(),
|
|
14770
|
+
devices: array(TrainingExportDeviceTotalsSchema).readonly()
|
|
14588
14771
|
});
|
|
14589
14772
|
var TrackSchema = object({
|
|
14590
14773
|
trackId: string(),
|
|
@@ -14629,7 +14812,8 @@ var TrackSchema = object({
|
|
|
14629
14812
|
* Populated from the persisted envelope columns on historical reads;
|
|
14630
14813
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
14631
14814
|
envelope: TrackEnvelopeSchema.optional(),
|
|
14632
|
-
...TrackFlagFields
|
|
14815
|
+
...TrackFlagFields,
|
|
14816
|
+
...TrackRetrainFields
|
|
14633
14817
|
});
|
|
14634
14818
|
var BaseEventFields = {
|
|
14635
14819
|
id: string(),
|
|
@@ -14843,7 +15027,8 @@ var KeyEventSchema = object({
|
|
|
14843
15027
|
bestEventId: string(),
|
|
14844
15028
|
/** Track lifetime in ms (lastSeen - firstSeen). */
|
|
14845
15029
|
windowMs: number().optional(),
|
|
14846
|
-
...TrackFlagFields
|
|
15030
|
+
...TrackFlagFields,
|
|
15031
|
+
...TrackRetrainFields
|
|
14847
15032
|
});
|
|
14848
15033
|
object({
|
|
14849
15034
|
trackId: string(),
|
|
@@ -15104,6 +15289,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
15104
15289
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
15105
15290
|
kind: "query",
|
|
15106
15291
|
auth: "admin"
|
|
15292
|
+
}), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
|
|
15293
|
+
kind: "query",
|
|
15294
|
+
auth: "admin"
|
|
15295
|
+
}), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
|
|
15296
|
+
kind: "query",
|
|
15297
|
+
auth: "admin"
|
|
15107
15298
|
}), method(object({
|
|
15108
15299
|
eventId: string(),
|
|
15109
15300
|
kind: MediaFileKindEnum.optional()
|
|
@@ -16921,9 +17112,15 @@ DeviceType.Camera, method(object({
|
|
|
16921
17112
|
* Bypass the cache freshness check and fetch directly from the
|
|
16922
17113
|
* native (or stream-broker fallback). Triggered by the UI's
|
|
16923
17114
|
* "refresh" button so an operator can force a fresh frame
|
|
16924
|
-
* even when the cache is well within
|
|
16925
|
-
*
|
|
16926
|
-
*
|
|
17115
|
+
* even when the cache is well within the device's
|
|
17116
|
+
* `snapshotMaxAgeS` window.
|
|
17117
|
+
*
|
|
17118
|
+
* **`force` is an OPERATOR signal, not a freshness preference.** On a
|
|
17119
|
+
* battery camera it is the one thing that walks past the wrapper's
|
|
17120
|
+
* sleep gate and wakes the camera, so a background caller — a poller,
|
|
17121
|
+
* an event handler, a thumbnail — must NEVER set it. Every such caller
|
|
17122
|
+
* gets the cached frame, which on a sleeping battery camera is the
|
|
17123
|
+
* correct answer: stale but honest beats woken.
|
|
16927
17124
|
*/
|
|
16928
17125
|
force: boolean().optional()
|
|
16929
17126
|
}), SnapshotImageSchema.nullable()), method(object({ deviceId: number() }), _void(), {
|
|
@@ -20758,6 +20955,173 @@ DeviceType.Camera, method(object({
|
|
|
20758
20955
|
status: OsdStatusSchema
|
|
20759
20956
|
});
|
|
20760
20957
|
/**
|
|
20958
|
+
* `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
|
|
20959
|
+
*
|
|
20960
|
+
* The `osd` cap is the firmware contract: it probes a camera's overlay
|
|
20961
|
+
* SLOTS and writes literal text into one. It has no idea WHERE that text
|
|
20962
|
+
* comes from, and it must not — a driver that grew a "show the temperature
|
|
20963
|
+
* here" feature would grow it once per vendor.
|
|
20964
|
+
*
|
|
20965
|
+
* This cap owns the other half: a per-(camera, slot) BINDING that says
|
|
20966
|
+
* which value feeds the slot, how it is formatted, and under which
|
|
20967
|
+
* conditions it is shown at all. One addon renders every binding on every
|
|
20968
|
+
* camera, so a new source costs zero driver code.
|
|
20969
|
+
*
|
|
20970
|
+
* Three deliberate choices, each with a rejected alternative:
|
|
20971
|
+
*
|
|
20972
|
+
* 1. A source is `(capName, valuePath)` over the kernel's device
|
|
20973
|
+
* runtime-state mirror — NOT a closed enum of source kinds. Every
|
|
20974
|
+
* cap-keyed slice a device publishes is bindable the day the cap
|
|
20975
|
+
* ships. The rejected alternative (one enum member per source, with
|
|
20976
|
+
* a resolver branch each) is what makes "add the humidity too" a
|
|
20977
|
+
* code change.
|
|
20978
|
+
* 2. The display gate reuses `NcConditionsSchema` verbatim — the
|
|
20979
|
+
* notification centre's condition vocabulary — rather than a parallel
|
|
20980
|
+
* model. An operator who has learned one condition editor has learned
|
|
20981
|
+
* both.
|
|
20982
|
+
* 3. Because the renderer's facts are device STATE and not a detection
|
|
20983
|
+
* record, only a SUBSET of that vocabulary can be answered here.
|
|
20984
|
+
* `setSlotBinding` REJECTS the rest at write time (see
|
|
20985
|
+
* `getConditionSupport`). It does not accept-then-fail-closed: a
|
|
20986
|
+
* condition that can never be true renders a permanently blank
|
|
20987
|
+
* overlay, and a blank overlay looks exactly like a broken camera.
|
|
20988
|
+
*/
|
|
20989
|
+
/** Where a slot's value comes from. */
|
|
20990
|
+
var OsdSourceSchema = discriminatedUnion("kind", [
|
|
20991
|
+
object({
|
|
20992
|
+
kind: literal("static"),
|
|
20993
|
+
text: string().max(64)
|
|
20994
|
+
}),
|
|
20995
|
+
object({
|
|
20996
|
+
kind: literal("clock"),
|
|
20997
|
+
/** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
|
|
20998
|
+
pattern: string().min(1).max(32).default("HH:mm"),
|
|
20999
|
+
/** IANA zone. Omitted = the server's zone. */
|
|
21000
|
+
timezone: string().min(1).max(64).optional()
|
|
21001
|
+
}),
|
|
21002
|
+
object({
|
|
21003
|
+
kind: literal("device-state"),
|
|
21004
|
+
deviceId: number().int().optional(),
|
|
21005
|
+
capName: string().min(1).max(64),
|
|
21006
|
+
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
21007
|
+
valuePath: string().min(1).max(64)
|
|
21008
|
+
})
|
|
21009
|
+
]);
|
|
21010
|
+
var OsdSlotBindingSchema = object({
|
|
21011
|
+
/** Off = the manager stops driving this slot. It does NOT clear it. */
|
|
21012
|
+
enabled: boolean().default(true),
|
|
21013
|
+
source: OsdSourceSchema,
|
|
21014
|
+
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
21015
|
+
template: string().max(96).default("${value}"),
|
|
21016
|
+
/** Truncate with an ellipsis past this length. Absent = no limit. */
|
|
21017
|
+
maxCharacters: number().int().min(4).max(64).optional(),
|
|
21018
|
+
/**
|
|
21019
|
+
* Decimal places for a numeric value. `0` yields an integer — the
|
|
21020
|
+
* documented workaround for firmwares that reject `.` in overlay text.
|
|
21021
|
+
*/
|
|
21022
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
21023
|
+
/** Appended via `${unit}`. The state mirror does not carry units. */
|
|
21024
|
+
unitLabel: string().max(8).optional(),
|
|
21025
|
+
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
21026
|
+
valueMap: record(string(), string()).optional(),
|
|
21027
|
+
/** Time windows in which the slot is shown. Absent = always. */
|
|
21028
|
+
schedule: NcScheduleSchema.optional(),
|
|
21029
|
+
/**
|
|
21030
|
+
* Display gate, in the notification centre's condition vocabulary.
|
|
21031
|
+
* Only the keys reported by `getConditionSupport` are accepted.
|
|
21032
|
+
*/
|
|
21033
|
+
conditions: NcConditionsSchema.optional(),
|
|
21034
|
+
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
21035
|
+
fallbackText: string().max(64).default("")
|
|
21036
|
+
});
|
|
21037
|
+
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
21038
|
+
var OsdSlotViewSchema = object({
|
|
21039
|
+
slotId: string(),
|
|
21040
|
+
kind: OsdOverlayKindEnum,
|
|
21041
|
+
/** Firmware refuses text edits (a timestamp, the channel name). */
|
|
21042
|
+
readOnly: boolean(),
|
|
21043
|
+
cameraEnabled: boolean(),
|
|
21044
|
+
cameraText: string().optional(),
|
|
21045
|
+
binding: OsdSlotBindingSchema.nullable()
|
|
21046
|
+
});
|
|
21047
|
+
/**
|
|
21048
|
+
* What happened to one slot on one render pass. `unchanged` exists so the
|
|
21049
|
+
* operator can tell "we are driving this and the value is steady" from
|
|
21050
|
+
* "we never got there" — and so the loop can prove it is not rewriting
|
|
21051
|
+
* identical text to the camera every tick.
|
|
21052
|
+
*/
|
|
21053
|
+
var OsdRenderOutcomeEnum = _enum([
|
|
21054
|
+
"written",
|
|
21055
|
+
"unchanged",
|
|
21056
|
+
"gated",
|
|
21057
|
+
"unreadable",
|
|
21058
|
+
"disabled",
|
|
21059
|
+
"unbound",
|
|
21060
|
+
"failed"
|
|
21061
|
+
]);
|
|
21062
|
+
var OsdRenderResultSchema = object({
|
|
21063
|
+
slotId: string(),
|
|
21064
|
+
outcome: OsdRenderOutcomeEnum,
|
|
21065
|
+
/** The text the slot should carry. Empty = the slot is switched off. */
|
|
21066
|
+
text: string(),
|
|
21067
|
+
/** Why, whenever the outcome is not a plain write. Never silent. */
|
|
21068
|
+
reason: string().optional()
|
|
21069
|
+
});
|
|
21070
|
+
var OsdSourceValueTypeEnum = _enum([
|
|
21071
|
+
"number",
|
|
21072
|
+
"boolean",
|
|
21073
|
+
"string",
|
|
21074
|
+
"enum"
|
|
21075
|
+
]);
|
|
21076
|
+
/**
|
|
21077
|
+
* One bindable value, derived from a cap's `runtimeState` schema — never
|
|
21078
|
+
* hand-listed. The editor renders from this, so a cap that ships a new
|
|
21079
|
+
* state field becomes bindable with no UI change.
|
|
21080
|
+
*/
|
|
21081
|
+
var OsdSourceOptionSchema = object({
|
|
21082
|
+
deviceId: number().int(),
|
|
21083
|
+
deviceName: string(),
|
|
21084
|
+
capName: string(),
|
|
21085
|
+
valuePath: string(),
|
|
21086
|
+
label: string(),
|
|
21087
|
+
valueType: OsdSourceValueTypeEnum,
|
|
21088
|
+
/** Present for `enum`; the editor offers these as `valueMap` keys. */
|
|
21089
|
+
enumValues: array(string()).readonly().optional()
|
|
21090
|
+
});
|
|
21091
|
+
method(object({ deviceId: number().int() }), object({
|
|
21092
|
+
supported: boolean(),
|
|
21093
|
+
slots: array(OsdSlotViewSchema)
|
|
21094
|
+
}), { auth: "admin" }), method(object({ deviceId: number().int() }), object({ sources: array(OsdSourceOptionSchema) }), { auth: "admin" }), method(object({}), object({
|
|
21095
|
+
supported: array(string()),
|
|
21096
|
+
catalog: array(NcConditionDescriptorSchema)
|
|
21097
|
+
}), { auth: "admin" }), method(object({
|
|
21098
|
+
deviceId: number().int(),
|
|
21099
|
+
slotId: string().min(1),
|
|
21100
|
+
binding: OsdSlotBindingSchema
|
|
21101
|
+
}), object({
|
|
21102
|
+
slot: OsdSlotViewSchema,
|
|
21103
|
+
render: OsdRenderResultSchema
|
|
21104
|
+
}), {
|
|
21105
|
+
kind: "mutation",
|
|
21106
|
+
auth: "admin"
|
|
21107
|
+
}), method(object({
|
|
21108
|
+
deviceId: number().int(),
|
|
21109
|
+
slotId: string().min(1)
|
|
21110
|
+
}), object({ success: literal(true) }), {
|
|
21111
|
+
kind: "mutation",
|
|
21112
|
+
auth: "admin"
|
|
21113
|
+
}), method(object({
|
|
21114
|
+
deviceId: number().int(),
|
|
21115
|
+
slotId: string().min(1),
|
|
21116
|
+
binding: OsdSlotBindingSchema.optional()
|
|
21117
|
+
}), OsdRenderResultSchema, {
|
|
21118
|
+
kind: "mutation",
|
|
21119
|
+
auth: "admin"
|
|
21120
|
+
}), method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
|
|
21121
|
+
kind: "mutation",
|
|
21122
|
+
auth: "admin"
|
|
21123
|
+
});
|
|
21124
|
+
/**
|
|
20761
21125
|
* Feeder connectivity / power status — mirrors the HA petkit device-status
|
|
20762
21126
|
* enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
|
|
20763
21127
|
* `on_batteries` (running on battery backup). `null` until first reported.
|
|
@@ -21205,12 +21569,30 @@ object({
|
|
|
21205
21569
|
});
|
|
21206
21570
|
DeviceType.Sensor;
|
|
21207
21571
|
/**
|
|
21208
|
-
*
|
|
21209
|
-
*
|
|
21210
|
-
*
|
|
21211
|
-
*
|
|
21212
|
-
*
|
|
21213
|
-
*
|
|
21572
|
+
* PRIVACY — what the camera deliberately does not capture. Two planes:
|
|
21573
|
+
*
|
|
21574
|
+
* - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
|
|
21575
|
+
* grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
|
|
21576
|
+
* `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
|
|
21577
|
+
* 4 vertices, not necessarily axis-aligned). The cap composes the shared
|
|
21578
|
+
* rect|polygon subset of the MaskShape vocabulary. All coords are
|
|
21579
|
+
* normalized 0..1 (top-left origin).
|
|
21580
|
+
* - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
|
|
21581
|
+
* camera encoding an audio track at all, so EVERY consumer — live view,
|
|
21582
|
+
* recording, the audio analyzer, an export — sees silent video. There is
|
|
21583
|
+
* no server-side copy of this fact; the camera is the store and every read
|
|
21584
|
+
* is a read-through, which is why a switch over it cannot drift
|
|
21585
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)).
|
|
21586
|
+
*
|
|
21587
|
+
* Both belong here for one reason: they are the two things an operator turns
|
|
21588
|
+
* off when the answer to "what is this camera allowed to record" changes, and
|
|
21589
|
+
* both are applied ON the device, before anything leaves it.
|
|
21590
|
+
*
|
|
21591
|
+
* **The audio flag has exactly one writer.** `stream-params` used to carry a
|
|
21592
|
+
* per-profile `audio` in its patch schema — reachable from no UI and honoured
|
|
21593
|
+
* by one provider — and it was removed when this landed. A second writer onto
|
|
21594
|
+
* one device register is the shape of every knob this repo has shipped that
|
|
21595
|
+
* disagreed with the one the reader read.
|
|
21214
21596
|
*/
|
|
21215
21597
|
/** A privacy-mask region's geometry — rectangle or free polygon. */
|
|
21216
21598
|
var PrivacyMaskShapeSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
|
|
@@ -21226,16 +21608,40 @@ object({
|
|
|
21226
21608
|
enabled: boolean(),
|
|
21227
21609
|
/** Active zones (normalized 0..1). Length ≤ maxRegions. */
|
|
21228
21610
|
regions: array(PrivacyMaskRegionSchema),
|
|
21611
|
+
/**
|
|
21612
|
+
* Is the camera capturing sound right now? Read from the camera, never from
|
|
21613
|
+
* a server-side mirror.
|
|
21614
|
+
*
|
|
21615
|
+
* `null` means "no answer" — either this camera exposes no controllable
|
|
21616
|
+
* microphone (`getOptions().supportsAudioMute === false`) or the read
|
|
21617
|
+
* failed. A consumer must render `null` as UNKNOWN and never as `false`:
|
|
21618
|
+
* "the microphone is off" and "we could not ask" look identical to an
|
|
21619
|
+
* operator only until one of them is wrong.
|
|
21620
|
+
*
|
|
21621
|
+
* On a camera whose profiles carry the flag independently (Reolink writes
|
|
21622
|
+
* it per stream), `true` means AT LEAST ONE profile still carries audio —
|
|
21623
|
+
* privacy is only satisfied when every one of them is silent.
|
|
21624
|
+
*/
|
|
21625
|
+
audioEnabled: boolean().nullable(),
|
|
21229
21626
|
lastFetchedAt: number()
|
|
21230
21627
|
});
|
|
21231
|
-
/** Per-camera availability. */
|
|
21628
|
+
/** Per-camera availability. Probed, never assumed from the model name. */
|
|
21232
21629
|
var PrivacyMaskOptionsSchema = object({
|
|
21233
21630
|
/** Maximum number of supported zones. */
|
|
21234
21631
|
maxRegions: number(),
|
|
21235
21632
|
/** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
|
|
21236
21633
|
supportedShapes: array(MaskShapeKindSchema),
|
|
21237
21634
|
/** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
|
|
21238
|
-
polygonVertices: MaskPolygonVerticesSchema.optional()
|
|
21635
|
+
polygonVertices: MaskPolygonVerticesSchema.optional(),
|
|
21636
|
+
/**
|
|
21637
|
+
* Does this camera expose a microphone switch we can actually write?
|
|
21638
|
+
*
|
|
21639
|
+
* Camera-probed: `true` only when the firmware answered with an audio flag
|
|
21640
|
+
* we know how to patch. A camera that never answered is `false` — a control
|
|
21641
|
+
* the operator can press that changes nothing is worse than no control, and
|
|
21642
|
+
* the switch group renders "not available" instead.
|
|
21643
|
+
*/
|
|
21644
|
+
supportsAudioMute: boolean()
|
|
21239
21645
|
});
|
|
21240
21646
|
/** Partial change — every field optional. */
|
|
21241
21647
|
var PrivacyMaskPatchSchema = object({
|
|
@@ -21248,6 +21654,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), PrivacyMaskOptionsSche
|
|
|
21248
21654
|
}), _void(), {
|
|
21249
21655
|
kind: "mutation",
|
|
21250
21656
|
auth: "admin"
|
|
21657
|
+
}), method(object({
|
|
21658
|
+
deviceId: number(),
|
|
21659
|
+
enabled: boolean()
|
|
21660
|
+
}), _void(), {
|
|
21661
|
+
kind: "mutation",
|
|
21662
|
+
auth: "admin"
|
|
21251
21663
|
});
|
|
21252
21664
|
var PtzPresetSchema = object({
|
|
21253
21665
|
id: string(),
|
|
@@ -21457,6 +21869,21 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
|
|
|
21457
21869
|
})]);
|
|
21458
21870
|
/** Raw bytes of one finalized footage segment (read off disk on the recording node). */
|
|
21459
21871
|
var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
|
|
21872
|
+
/**
|
|
21873
|
+
* One GOP of a finalized segment, cut by byte range through the segment's own
|
|
21874
|
+
* `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
|
|
21875
|
+
* the single `moof`+`mdat` covering the requested instant — standalone-
|
|
21876
|
+
* demuxable, never the whole file. When the segment's index cannot be parsed
|
|
21877
|
+
* the provider degrades INSIDE the mechanism to the whole segment (still one
|
|
21878
|
+
* `data`, `gopStartMs` = the segment start) — a worse read, not another path.
|
|
21879
|
+
*/
|
|
21880
|
+
var ReadGopBytesResultSchema = object({
|
|
21881
|
+
data: _instanceof(Uint8Array),
|
|
21882
|
+
/** Absolute epoch ms of the returned fragment's first sample. */
|
|
21883
|
+
gopStartMs: number(),
|
|
21884
|
+
/** Media ms the returned fragment covers. */
|
|
21885
|
+
gopDurMs: number()
|
|
21886
|
+
});
|
|
21460
21887
|
method(object({
|
|
21461
21888
|
deviceId: number(),
|
|
21462
21889
|
fromMs: number(),
|
|
@@ -21499,6 +21926,14 @@ method(object({
|
|
|
21499
21926
|
}), ReadSegmentBytesResultSchema, {
|
|
21500
21927
|
kind: "query",
|
|
21501
21928
|
auth: "admin"
|
|
21929
|
+
}), method(object({
|
|
21930
|
+
deviceId: number(),
|
|
21931
|
+
profile: string(),
|
|
21932
|
+
startMs: number(),
|
|
21933
|
+
epochMs: number()
|
|
21934
|
+
}), ReadGopBytesResultSchema, {
|
|
21935
|
+
kind: "query",
|
|
21936
|
+
auth: "admin"
|
|
21502
21937
|
}), method(object({
|
|
21503
21938
|
deviceId: number(),
|
|
21504
21939
|
config: RecordingConfigSchema
|
|
@@ -21979,6 +22414,16 @@ var StreamProfileConfigSchema = object({
|
|
|
21979
22414
|
"baseline"
|
|
21980
22415
|
]).optional(),
|
|
21981
22416
|
gop: number().optional(),
|
|
22417
|
+
/**
|
|
22418
|
+
* Whether THIS profile currently carries an audio track. READ-ONLY here.
|
|
22419
|
+
*
|
|
22420
|
+
* There is no matching field on {@link StreamProfilePatchSchema}: the
|
|
22421
|
+
* camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
|
|
22422
|
+
* writes every profile at once so "audio off" means silent everywhere. A
|
|
22423
|
+
* per-profile writer beside it would let a camera be half-muted and would be
|
|
22424
|
+
* a second knob onto one device register — the failure D62 exists to
|
|
22425
|
+
* prevent. Absent when the firmware does not report the flag.
|
|
22426
|
+
*/
|
|
21982
22427
|
audio: boolean().optional()
|
|
21983
22428
|
});
|
|
21984
22429
|
object({
|
|
@@ -22019,7 +22464,13 @@ var StreamParamsOptionsSchema = object({
|
|
|
22019
22464
|
ext: StreamProfileOptionsSchema.optional()
|
|
22020
22465
|
});
|
|
22021
22466
|
/** A partial change to one profile — every field optional; a provider
|
|
22022
|
-
* ignores fields it doesn't support.
|
|
22467
|
+
* ignores fields it doesn't support.
|
|
22468
|
+
*
|
|
22469
|
+
* There is deliberately NO `audio` here. It existed until 2026-08-07,
|
|
22470
|
+
* reachable from no form and honoured by exactly one provider, while the
|
|
22471
|
+
* camera's microphone is a whole-device fact. It now has one writer,
|
|
22472
|
+
* `privacyMask.setAudioEnabled`, which writes every profile — see
|
|
22473
|
+
* `privacy-mask.cap.ts`. */
|
|
22023
22474
|
var StreamProfilePatchSchema = object({
|
|
22024
22475
|
width: number().optional(),
|
|
22025
22476
|
height: number().optional(),
|
|
@@ -22032,8 +22483,7 @@ var StreamProfilePatchSchema = object({
|
|
|
22032
22483
|
"main",
|
|
22033
22484
|
"baseline"
|
|
22034
22485
|
]).optional(),
|
|
22035
|
-
gop: number().optional()
|
|
22036
|
-
audio: boolean().optional()
|
|
22486
|
+
gop: number().optional()
|
|
22037
22487
|
});
|
|
22038
22488
|
DeviceType.Camera, method(object({ deviceId: number() }), StreamParamsOptionsSchema), method(object({
|
|
22039
22489
|
deviceId: number(),
|
|
@@ -25627,6 +26077,48 @@ Object.freeze({
|
|
|
25627
26077
|
addonId: null,
|
|
25628
26078
|
access: "create"
|
|
25629
26079
|
},
|
|
26080
|
+
"osdManager.clearSlotBinding": {
|
|
26081
|
+
capName: "osd-manager",
|
|
26082
|
+
capScope: "system",
|
|
26083
|
+
addonId: null,
|
|
26084
|
+
access: "delete"
|
|
26085
|
+
},
|
|
26086
|
+
"osdManager.getConditionSupport": {
|
|
26087
|
+
capName: "osd-manager",
|
|
26088
|
+
capScope: "system",
|
|
26089
|
+
addonId: null,
|
|
26090
|
+
access: "view"
|
|
26091
|
+
},
|
|
26092
|
+
"osdManager.getDeviceOsd": {
|
|
26093
|
+
capName: "osd-manager",
|
|
26094
|
+
capScope: "system",
|
|
26095
|
+
addonId: null,
|
|
26096
|
+
access: "view"
|
|
26097
|
+
},
|
|
26098
|
+
"osdManager.getSourceCatalog": {
|
|
26099
|
+
capName: "osd-manager",
|
|
26100
|
+
capScope: "system",
|
|
26101
|
+
addonId: null,
|
|
26102
|
+
access: "view"
|
|
26103
|
+
},
|
|
26104
|
+
"osdManager.previewSlot": {
|
|
26105
|
+
capName: "osd-manager",
|
|
26106
|
+
capScope: "system",
|
|
26107
|
+
addonId: null,
|
|
26108
|
+
access: "create"
|
|
26109
|
+
},
|
|
26110
|
+
"osdManager.renderDevice": {
|
|
26111
|
+
capName: "osd-manager",
|
|
26112
|
+
capScope: "system",
|
|
26113
|
+
addonId: null,
|
|
26114
|
+
access: "create"
|
|
26115
|
+
},
|
|
26116
|
+
"osdManager.setSlotBinding": {
|
|
26117
|
+
capName: "osd-manager",
|
|
26118
|
+
capScope: "system",
|
|
26119
|
+
addonId: null,
|
|
26120
|
+
access: "create"
|
|
26121
|
+
},
|
|
25630
26122
|
"petFeeder.callPet": {
|
|
25631
26123
|
capName: "pet-feeder",
|
|
25632
26124
|
capScope: "device",
|
|
@@ -25789,6 +26281,18 @@ Object.freeze({
|
|
|
25789
26281
|
addonId: null,
|
|
25790
26282
|
access: "view"
|
|
25791
26283
|
},
|
|
26284
|
+
"pipelineAnalytics.getTrainingExportSummary": {
|
|
26285
|
+
capName: "pipeline-analytics",
|
|
26286
|
+
capScope: "device",
|
|
26287
|
+
addonId: null,
|
|
26288
|
+
access: "view"
|
|
26289
|
+
},
|
|
26290
|
+
"pipelineAnalytics.getTrainingExportUrl": {
|
|
26291
|
+
capName: "pipeline-analytics",
|
|
26292
|
+
capScope: "device",
|
|
26293
|
+
addonId: null,
|
|
26294
|
+
access: "view"
|
|
26295
|
+
},
|
|
25792
26296
|
"pipelineAnalytics.listEventKinds": {
|
|
25793
26297
|
capName: "pipeline-analytics",
|
|
25794
26298
|
capScope: "device",
|
|
@@ -26545,6 +27049,12 @@ Object.freeze({
|
|
|
26545
27049
|
addonId: null,
|
|
26546
27050
|
access: "view"
|
|
26547
27051
|
},
|
|
27052
|
+
"privacyMask.setAudioEnabled": {
|
|
27053
|
+
capName: "privacy-mask",
|
|
27054
|
+
capScope: "device",
|
|
27055
|
+
addonId: null,
|
|
27056
|
+
access: "create"
|
|
27057
|
+
},
|
|
26548
27058
|
"privacyMask.setMask": {
|
|
26549
27059
|
capName: "privacy-mask",
|
|
26550
27060
|
capScope: "device",
|
|
@@ -26713,6 +27223,12 @@ Object.freeze({
|
|
|
26713
27223
|
addonId: null,
|
|
26714
27224
|
access: "create"
|
|
26715
27225
|
},
|
|
27226
|
+
"recording.readGopBytes": {
|
|
27227
|
+
capName: "recording",
|
|
27228
|
+
capScope: "system",
|
|
27229
|
+
addonId: null,
|
|
27230
|
+
access: "view"
|
|
27231
|
+
},
|
|
26716
27232
|
"recording.readSegmentBytes": {
|
|
26717
27233
|
capName: "recording",
|
|
26718
27234
|
capScope: "system",
|
|
@@ -27253,6 +27769,12 @@ Object.freeze({
|
|
|
27253
27769
|
addonId: null,
|
|
27254
27770
|
access: "view"
|
|
27255
27771
|
},
|
|
27772
|
+
"streamBroker.getDeviceAudioMute": {
|
|
27773
|
+
capName: "stream-broker",
|
|
27774
|
+
capScope: "system",
|
|
27775
|
+
addonId: null,
|
|
27776
|
+
access: "view"
|
|
27777
|
+
},
|
|
27256
27778
|
"streamBroker.getPreBufferInfo": {
|
|
27257
27779
|
capName: "stream-broker",
|
|
27258
27780
|
capScope: "system",
|
|
@@ -27373,6 +27895,12 @@ Object.freeze({
|
|
|
27373
27895
|
addonId: null,
|
|
27374
27896
|
access: "create"
|
|
27375
27897
|
},
|
|
27898
|
+
"streamBroker.setDeviceAudioMute": {
|
|
27899
|
+
capName: "stream-broker",
|
|
27900
|
+
capScope: "system",
|
|
27901
|
+
addonId: null,
|
|
27902
|
+
access: "create"
|
|
27903
|
+
},
|
|
27376
27904
|
"streamBroker.setPreBufferDuration": {
|
|
27377
27905
|
capName: "stream-broker",
|
|
27378
27906
|
capScope: "system",
|
|
@@ -28133,6 +28661,88 @@ object({
|
|
|
28133
28661
|
paddingRatio: .15,
|
|
28134
28662
|
square: false
|
|
28135
28663
|
}).paddingRatio;
|
|
28664
|
+
/**
|
|
28665
|
+
* WHICH delivered frames the decode worker retains a native copy of.
|
|
28666
|
+
*
|
|
28667
|
+
* - `all` — every frame the worker delivered to the runner. The shipped
|
|
28668
|
+
* behaviour, and the only correct one if something can ask for a crop of a
|
|
28669
|
+
* frame the runner never sent to inference.
|
|
28670
|
+
* - `inferred` — only the frames the runner ADMITTED to its detection queue.
|
|
28671
|
+
* A native-crop request always names a `frameId` that rode an inference
|
|
28672
|
+
* result, so that is the only set a request can name. How much it drops is
|
|
28673
|
+
* the two-plane governor's admit ratio and nothing else: measured at ~50% on
|
|
28674
|
+
* this cluster, not the ~80% the design sketch assumed, because the governor
|
|
28675
|
+
* was not throttling as hard as the sketch supposed. Read
|
|
28676
|
+
* `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
|
|
28677
|
+
* of you rather than quoting a number from here. The newest delivered frame is
|
|
28678
|
+
* croppable regardless — it is still the worker's reserved slot, not a lease —
|
|
28679
|
+
* which covers the one-frame race between a mark and the supersede that
|
|
28680
|
+
* consumes it.
|
|
28681
|
+
*/
|
|
28682
|
+
var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
|
|
28683
|
+
object({
|
|
28684
|
+
/**
|
|
28685
|
+
* How long a retained native frame is served before it counts as a miss.
|
|
28686
|
+
*
|
|
28687
|
+
* Must cover the FULL late-crop horizon: detection inference + the
|
|
28688
|
+
* cross-process inference-result hop to hub post-analysis + tracking + the
|
|
28689
|
+
* tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
|
|
28690
|
+
* outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
|
|
28691
|
+
* RAM per busy camera grows linearly with no measured hit-rate gain.
|
|
28692
|
+
*/
|
|
28693
|
+
ttlMs: number().int().min(250).max(1e4),
|
|
28694
|
+
/**
|
|
28695
|
+
* Hard per-decode-worker RAM ceiling for retained native frames, in MB.
|
|
28696
|
+
*
|
|
28697
|
+
* Intended as a SAFETY ceiling with the TTL as the effective cap — but check
|
|
28698
|
+
* which one is actually binding before reasoning from that. At the shipped
|
|
28699
|
+
* 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
|
|
28700
|
+
* at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
|
|
28701
|
+
* `leaseFrames` on the metrics line say which. When the ceiling binds, a
|
|
28702
|
+
* change that admits fewer frames buys retention WINDOW at constant RAM
|
|
28703
|
+
* rather than giving RAM back — lower this knob if RAM is what you wanted.
|
|
28704
|
+
* `0` DISABLES the lease entirely and falls the worker back to the tiny
|
|
28705
|
+
* leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
|
|
28706
|
+
* to replace).
|
|
28707
|
+
*/
|
|
28708
|
+
budgetMb: number().int().min(0).max(4096),
|
|
28709
|
+
/**
|
|
28710
|
+
* Demand window: eager per-frame native retention runs only within this many
|
|
28711
|
+
* ms of the last native-crop request (or of the dial starting).
|
|
28712
|
+
*
|
|
28713
|
+
* `0` means ALWAYS ON — it disables the gate, it does not disable retention.
|
|
28714
|
+
* That is the legacy behaviour that saturated an N100 (24 native-4K downloads
|
|
28715
|
+
* per second on a camera with zero crop demand), so leave it non-zero unless
|
|
28716
|
+
* you are reproducing that.
|
|
28717
|
+
*/
|
|
28718
|
+
activityMs: number().int().min(0).max(12e4),
|
|
28719
|
+
/**
|
|
28720
|
+
* Which delivered frames are retained at all — see
|
|
28721
|
+
* {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
|
|
28722
|
+
* changes WHAT is kept rather than for how long, so it is also the only one
|
|
28723
|
+
* that can turn a crop that used to hit into a miss. The worker counts every
|
|
28724
|
+
* crop request naming a frame it did NOT see marked
|
|
28725
|
+
* (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
|
|
28726
|
+
* there is the signal that some caller names frames outside the inference set
|
|
28727
|
+
* and that this must go back to `all`.
|
|
28728
|
+
*/
|
|
28729
|
+
admission: NativeLeaseAdmissionSchema
|
|
28730
|
+
});
|
|
28731
|
+
/**
|
|
28732
|
+
* The values in force when the operator has set nothing — byte-for-byte the
|
|
28733
|
+
* constants the decode worker shipped with as env-var defaults, so making these
|
|
28734
|
+
* settings changed no behaviour on the day it landed.
|
|
28735
|
+
*/
|
|
28736
|
+
var DEFAULT_NATIVE_LEASE_SETTINGS = {
|
|
28737
|
+
ttlMs: 1200,
|
|
28738
|
+
budgetMb: 1024,
|
|
28739
|
+
activityMs: 15e3,
|
|
28740
|
+
admission: "inferred"
|
|
28741
|
+
};
|
|
28742
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
|
|
28743
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
28744
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
28745
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
28136
28746
|
//#endregion
|
|
28137
28747
|
//#region src/server/addon.ts
|
|
28138
28748
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -28152,7 +28762,7 @@ var AdminUIAddon = class extends BaseAddon {
|
|
|
28152
28762
|
capability: adminUiCapability,
|
|
28153
28763
|
provider: {
|
|
28154
28764
|
getStaticDir: async () => ({ staticDir: resolveAdminUiDistDir() }),
|
|
28155
|
-
getVersion: async () => ({ version: "1.2.
|
|
28765
|
+
getVersion: async () => ({ version: "1.2.34" })
|
|
28156
28766
|
}
|
|
28157
28767
|
}];
|
|
28158
28768
|
}
|