@camstack/addon-admin-ui 1.2.31 → 1.2.33
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-C8l2xgGA.js → AddLocationWizard-BNLzjRkS.js} +1 -1
- package/dist/assets/{AddonCollectionPage-C6c6iSQR.js → AddonCollectionPage-CvyviITl.js} +1 -1
- package/dist/assets/Addons-B0nG62pB.js +1 -0
- package/dist/assets/{AdminTabs-BlETE4he.js → AdminTabs-V94RuaJP.js} +1 -1
- package/dist/assets/AiProfiles-CNda-xrz.js +1 -0
- package/dist/assets/{Automation-C28vW-50.js → Automation-DYV0gbiV.js} +2 -2
- package/dist/assets/{BrokerForm-DV5P6yLC.js → BrokerForm-DX_RUCNg.js} +1 -1
- package/dist/assets/{Capabilities-I7OecGck.js → Capabilities-Cud2I1p1.js} +1 -1
- package/dist/assets/{CapabilityBadges-CeffiM3_.js → CapabilityBadges-CaKz6Row.js} +1 -1
- package/dist/assets/{Cluster-CtsElqbm.js → Cluster-Cx5dtvDp.js} +1 -1
- package/dist/assets/{ClusterRecordingTab-Cu4e2Sa0.js → ClusterRecordingTab-BShu9AFE.js} +1 -1
- package/dist/assets/{Dashboard-BV2-sKn8.js → Dashboard-CN0qzHHd.js} +1 -1
- package/dist/assets/{Data-D_in2E11.js → Data-Cddnp6Uz.js} +1 -1
- package/dist/assets/DetectionIntelligence-C4g5DaCG.js +1 -0
- package/dist/assets/{DeviceAggregateSection-BM70M394.js → DeviceAggregateSection-H5AVeNJ0.js} +1 -1
- package/dist/assets/DeviceDetail-CLQTZBsf.js +2 -0
- package/dist/assets/{Devices-B3eRW0ZV.js → Devices-Bp2S4_jq.js} +1 -1
- package/dist/assets/EmbedPlayerPage-DVIbQ9_P.js +1 -0
- package/dist/assets/Events-C_TE39Tm.js +1 -0
- package/dist/assets/{FormBuilder-CjOCkLVf.js → FormBuilder-B3G6L5El.js} +1 -1
- package/dist/assets/Identity-D5YIw7nG.js +1 -0
- package/dist/assets/{IntegrationDetail-AqDFph4_.js → IntegrationDetail-Dj2OdD1K.js} +1 -1
- package/dist/assets/{Integrations-5slYYtBd.js → Integrations-DZKddVhD.js} +1 -1
- package/dist/assets/{Integrations-UekIuTxN.js → Integrations-fadRG4Km.js} +1 -1
- package/dist/assets/{Logs-CcRjhQ8q.js → Logs-zJNzZ183.js} +1 -1
- package/dist/assets/{MotionZonesSettings-CeuQ-dQ4.js → MotionZonesSettings-4IW50CRy.js} +1 -1
- package/dist/assets/{MyAccess-C6ZM2f5P.js → MyAccess-CBfHhwSU.js} +1 -1
- package/dist/assets/{Network-D0CKemO3.js → Network-Dx9MFUR0.js} +1 -1
- package/dist/assets/{NodeAddonsSettingsPanel-D2e6v5bt.js → NodeAddonsSettingsPanel-C2NjwPws.js} +1 -1
- package/dist/assets/{NodeInferenceDevices-BCis98Y5.js → NodeInferenceDevices-uPCEdvuj.js} +1 -1
- package/dist/assets/{Notifications-DITbe32-.js → Notifications-C3Ng_pEz.js} +1 -1
- package/dist/assets/Pipeline-Daj8GJqM.js +1 -0
- package/dist/assets/{Plates-BJe-z0Ly.js → Plates-CdBLHSjv.js} +1 -1
- package/dist/assets/{PrivacyMaskSettings-DL1wRk3D.js → PrivacyMaskSettings-DOgtt7Eb.js} +1 -1
- package/dist/assets/{ProviderIcon-DTUqxb3A.js → ProviderIcon-DpSmN8_h.js} +1 -1
- package/dist/assets/{Recordings-DsFS0v_V.js → Recordings-5kVeLC44.js} +1 -1
- package/dist/assets/{ScanNetworkModal-Sh8DKc_Z.js → ScanNetworkModal-CVNem53G.js} +1 -1
- package/dist/assets/{Settings-CX4Nf9gj.js → Settings-Dot1aUpP.js} +1 -1
- package/dist/assets/{Showroom-BMroQnR-.js → Showroom-CE0pD_JA.js} +1 -1
- package/dist/assets/{Terminal-WVzNWGgt.js → Terminal-Jn61QHqq.js} +2 -2
- package/dist/assets/__vite-browser-external-DQHwpgVa.js +64 -0
- package/dist/assets/{_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-ueW3CNXD.js → _virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-DfmS2X5b.js} +1 -1
- package/dist/assets/{build-cluster-graph-BQfyn5-h.js → build-cluster-graph-oxCzI_GT.js} +2 -2
- package/dist/assets/circle-check-big-bqZqQn_F.js +1 -0
- package/dist/assets/clock-BxBK96nq.js +1 -0
- package/dist/assets/{copy-CgqX6jAd.js → copy-BrMo-O0l.js} +1 -1
- package/dist/assets/{debounced-invalidate-Ckd5veqP.js → debounced-invalidate-DCfaNC1r.js} +1 -1
- package/dist/assets/dist-DPJBx5gf.js +7 -0
- package/dist/assets/{dist-CWZqSYg7.js → dist-w7XvHdqJ.js} +1 -1
- package/dist/assets/{download-p0j7iBId.js → download-Bd71YsuA.js} +1 -1
- package/dist/assets/{face-bbox-D1cykRSA.js → face-bbox-ByAacIlM.js} +1 -1
- package/dist/assets/{hostInit-Dt2ASvrJ.js → hostInit-DhSUBWsx.js} +1 -1
- package/dist/assets/index-BIqRUg3I.css +2 -0
- package/dist/assets/index-BZA0u2Kc.js +4 -0
- package/dist/assets/{key-round-Cx7q2or5.js → key-round-DZL9RdNY.js} +1 -1
- package/dist/assets/{mf-entry-bootstrap-0-ba8db39e.js → mf-entry-bootstrap-0-28bb7ad2.js} +2 -2
- package/dist/assets/{pencil-CSOkVyOg.js → pencil-TjwM9NpS.js} +1 -1
- package/dist/assets/{player-overlays-DMopxtfV.js → player-overlays-CkAkhPbQ.js} +1 -1
- package/dist/assets/plus-BgUAuoLj.js +1 -0
- package/dist/assets/{radio-sWK9osbk.js → radio-DOrncLmg.js} +1 -1
- package/dist/assets/{refresh-cw-DvvUyPxz.js → refresh-cw-BYCB135v.js} +1 -1
- package/dist/assets/remoteEntry-2xQrocPD.js +1 -0
- package/dist/assets/{rotate-ccw-C9AgJ2n3.js → rotate-ccw-Db_qQ-Qx.js} +1 -1
- package/dist/assets/rotate-cw-vsmRF4Nv.js +1 -0
- package/dist/assets/search-BopXcdUz.js +1 -0
- package/dist/assets/{send-DHGacLkf.js → send-CosR7D5R.js} +1 -1
- package/dist/assets/{server-BTkuOhTj.js → server-CUNj6w6P.js} +1 -1
- package/dist/assets/square-C0Uc1Hub.js +1 -0
- package/dist/assets/{square-check-big-Bj-h9e8-.js → square-check-big-yxLWkS9v.js} +1 -1
- package/dist/assets/src-Bh5CQXm7.js +7 -0
- package/dist/assets/{src-DMF-vfTM.js → src-DOXkuCPk.js} +10 -10
- package/dist/assets/{upload-DpVq2e2i.js → upload-D0orNbud.js} +1 -1
- package/dist/assets/{useEventInvalidation-d8_SLAo2.js → useEventInvalidation-DJ5h5Wvp.js} +1 -1
- package/dist/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-CxUAKqIn.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-Cg_QybrP.js} +2 -2
- package/dist/assets/{wifi-DTySHt8w.js → wifi-D0ItCC1K.js} +1 -1
- package/dist/assets/{zap-r1jKR3Im.js → zap-CIK4k9g5.js} +1 -1
- package/dist/index.html +7 -7
- package/dist/server/addon.js +910 -30
- package/package.json +1 -1
- package/dist/assets/Addons-BltMkzm5.js +0 -1
- package/dist/assets/AiProfiles-BcPA79i8.js +0 -1
- package/dist/assets/DetectionIntelligence-DoDZdfkQ.js +0 -1
- package/dist/assets/DeviceDetail-BkHiJUVr.js +0 -2
- package/dist/assets/EmbedPlayerPage-BsSzvPTu.js +0 -1
- package/dist/assets/Events-CbDa9mNk.js +0 -1
- package/dist/assets/Identity-BOJJJ9fs.js +0 -1
- package/dist/assets/Pipeline-BuUTm7JS.js +0 -1
- package/dist/assets/__vite-browser-external-NEUUz2mh.js +0 -1
- package/dist/assets/circle-check-big-DeInsepB.js +0 -1
- package/dist/assets/clock-BW_cTo5d.js +0 -1
- package/dist/assets/compat-CbwIzIXq.js +0 -64
- package/dist/assets/dist-QF9uqGQD.js +0 -7
- package/dist/assets/index-BHm1FmD-.css +0 -2
- package/dist/assets/index-CCxo3WRY.js +0 -4
- package/dist/assets/plus-65CWfpkk.js +0 -1
- package/dist/assets/remoteEntry-BG0gquJL.js +0 -1
- package/dist/assets/rotate-cw-DzWdgqzc.js +0 -1
- package/dist/assets/search-C0NOsICx.js +0 -1
- package/dist/assets/square-5WfxRUZm.js +0 -1
- package/dist/assets/src-Bbly4y8Q.js +0 -7
package/dist/server/addon.js
CHANGED
|
@@ -6954,6 +6954,14 @@ var EncodeProfileSchema = object({
|
|
|
6954
6954
|
"main",
|
|
6955
6955
|
"high"
|
|
6956
6956
|
]).optional(),
|
|
6957
|
+
/**
|
|
6958
|
+
* `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
|
|
6959
|
+
* (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
|
|
6960
|
+
* it, or it ships a stream that does not match its own advertisement — the
|
|
6961
|
+
* defect class that kept HomeKit black for a year and that Alexa carried
|
|
6962
|
+
* silently. Optional because a browser negotiates the level itself.
|
|
6963
|
+
*/
|
|
6964
|
+
level: string().optional(),
|
|
6957
6965
|
width: number().int().positive().optional(),
|
|
6958
6966
|
height: number().int().positive().optional(),
|
|
6959
6967
|
fps: number().positive().optional(),
|
|
@@ -7001,6 +7009,29 @@ var EncodeProfileSchema = object({
|
|
|
7001
7009
|
outputArgs: array(string()).optional()
|
|
7002
7010
|
});
|
|
7003
7011
|
/**
|
|
7012
|
+
* The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
|
|
7013
|
+
* Baseline because it is the one profile every consumer in this repo decodes
|
|
7014
|
+
* (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
|
|
7015
|
+
*/
|
|
7016
|
+
var BASE_LIVE_EGRESS_PROFILE = {
|
|
7017
|
+
video: {
|
|
7018
|
+
codec: "h264",
|
|
7019
|
+
profile: "baseline",
|
|
7020
|
+
level: "3.1",
|
|
7021
|
+
width: 1280,
|
|
7022
|
+
height: 720,
|
|
7023
|
+
fps: 25,
|
|
7024
|
+
bitrateKbps: 2500,
|
|
7025
|
+
gopFrames: 25,
|
|
7026
|
+
bf: 0,
|
|
7027
|
+
preset: "veryfast",
|
|
7028
|
+
tune: "zerolatency"
|
|
7029
|
+
},
|
|
7030
|
+
audio: "passthrough"
|
|
7031
|
+
};
|
|
7032
|
+
({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
|
|
7033
|
+
({ ...BASE_LIVE_EGRESS_PROFILE });
|
|
7034
|
+
/**
|
|
7004
7035
|
* Deep wiring healthcheck — snapshot of active reachability probes across
|
|
7005
7036
|
* every declared capability + widget of every installed plugin, on every
|
|
7006
7037
|
* node. Produced by the backend `WiringHealthService` and surfaced via
|
|
@@ -7050,6 +7081,154 @@ object({
|
|
|
7050
7081
|
})
|
|
7051
7082
|
});
|
|
7052
7083
|
/**
|
|
7084
|
+
* Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
|
|
7085
|
+
* pipeline functions an operator thinks in terms of.
|
|
7086
|
+
*
|
|
7087
|
+
* ## This file adds no state
|
|
7088
|
+
*
|
|
7089
|
+
* Every switch here is a VIEW onto an authority that already existed
|
|
7090
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
|
|
7091
|
+
* group is that there is exactly one place each function is turned off, and
|
|
7092
|
+
* the group routes to it:
|
|
7093
|
+
*
|
|
7094
|
+
* | Switch | Authority | Proven "off stops the work" gate |
|
|
7095
|
+
* | --- | --- | --- |
|
|
7096
|
+
* | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
|
|
7097
|
+
* | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
|
|
7098
|
+
* | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
|
|
7099
|
+
* | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
|
|
7100
|
+
* | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
|
|
7101
|
+
* | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
|
|
7102
|
+
* | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
|
|
7103
|
+
*
|
|
7104
|
+
* ## The two switches whose authority is not on this server
|
|
7105
|
+
*
|
|
7106
|
+
* `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
|
|
7107
|
+
* in "the group stores nothing" — it is the purest form of it: the camera
|
|
7108
|
+
* holds the fact, every read is a read-through, and there is no server-side
|
|
7109
|
+
* copy that could drift. Their availability therefore cannot come from
|
|
7110
|
+
* `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
|
|
7111
|
+
* is filtered out there); it comes from the cap's own camera-probed
|
|
7112
|
+
* `privacyMask.getOptions()`, which is strictly more honest — it answers for
|
|
7113
|
+
* THIS camera rather than for the device type
|
|
7114
|
+
* ([D74](../../../../docs/decisions/adr-0074.md)).
|
|
7115
|
+
*
|
|
7116
|
+
* ## `privacy-mask` is the one row whose ON is not "the function is working"
|
|
7117
|
+
*
|
|
7118
|
+
* Every other switch means *this camera's function is doing its job*, so
|
|
7119
|
+
* `enabled: false` is a thing an operator took away. `privacy-mask` means **the
|
|
7120
|
+
* MASK is active** — `enabled: true` is video deliberately obscured. The
|
|
7121
|
+
* polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
|
|
7122
|
+
* (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
|
|
7123
|
+
* a HomeKit toggle that disagreed with the app's toggle for the same camera is
|
|
7124
|
+
* worse than either surface not having one.
|
|
7125
|
+
*
|
|
7126
|
+
* Two consequences follow and both are load-bearing:
|
|
7127
|
+
*
|
|
7128
|
+
* - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
|
|
7129
|
+
* exactly this row. With the polarity above, every camera that has NOT drawn
|
|
7130
|
+
* a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
|
|
7131
|
+
* normal, healthy state of most cameras rendered as an operator disablement.
|
|
7132
|
+
* - **Its cost line names BOTH directions.** `costWhenOff` is rendered
|
|
7133
|
+
* unconditionally by both clients, so for this row it has to read correctly
|
|
7134
|
+
* whichever way the switch is sitting.
|
|
7135
|
+
*
|
|
7136
|
+
* The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
|
|
7137
|
+
* migrated the legacy `audioEnabled` / `pipelineEnabled` /
|
|
7138
|
+
* `motionDetectionEnabled` booleans ONTO `setWrapperActive`. The group is the
|
|
7139
|
+
* surface that decision never got.
|
|
7140
|
+
*
|
|
7141
|
+
* ## Two rules that are load-bearing
|
|
7142
|
+
*
|
|
7143
|
+
* - **Recording's switch is `enabled`, never the bands.** `bands` is the only
|
|
7144
|
+
* authored intent and `mode` is derived from it (`deriveRecordingMode`).
|
|
7145
|
+
* Expressing "off" by clearing bands destroys the operator's schedule and
|
|
7146
|
+
* turning the camera back on would then silently record nothing.
|
|
7147
|
+
* - **A switch that is off must be reported as off**, not merely produce
|
|
7148
|
+
* nothing. {@link CameraSwitch.enabled} is what a status surface renders as
|
|
7149
|
+
* "disabled by an operator" instead of "broken" — see
|
|
7150
|
+
* `CameraStatus.switchedOff`.
|
|
7151
|
+
*/
|
|
7152
|
+
/**
|
|
7153
|
+
* The functions the operator named — five on 2026-08-05, plus the camera's own
|
|
7154
|
+
* microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
|
|
7155
|
+
* recognition and plate/LPR are per-step toggles on
|
|
7156
|
+
* `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
|
|
7157
|
+
* editor, not in a safety group.
|
|
7158
|
+
*/
|
|
7159
|
+
var CameraSwitchIdSchema = _enum([
|
|
7160
|
+
"stream-broker",
|
|
7161
|
+
"object-detection",
|
|
7162
|
+
"privacy-mask",
|
|
7163
|
+
"device-audio",
|
|
7164
|
+
"audio-analysis",
|
|
7165
|
+
"recording",
|
|
7166
|
+
"notifications"
|
|
7167
|
+
]);
|
|
7168
|
+
/**
|
|
7169
|
+
* WHERE the switch's state actually lives. A discriminated union rather than a
|
|
7170
|
+
* string so both the writer (the orchestrator's `setCameraSwitch`) and any
|
|
7171
|
+
* reader can exhaustively narrow — and so "the group added a parallel map" is
|
|
7172
|
+
* a compile error rather than a review comment.
|
|
7173
|
+
*/
|
|
7174
|
+
var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
|
|
7175
|
+
object({ kind: literal("device-disabled") }),
|
|
7176
|
+
object({
|
|
7177
|
+
kind: literal("wrapper-binding"),
|
|
7178
|
+
capName: string()
|
|
7179
|
+
}),
|
|
7180
|
+
object({ kind: literal("recording-config") }),
|
|
7181
|
+
object({ kind: literal("notification-mute") }),
|
|
7182
|
+
object({
|
|
7183
|
+
kind: literal("camera-audio"),
|
|
7184
|
+
capName: string()
|
|
7185
|
+
}),
|
|
7186
|
+
object({
|
|
7187
|
+
kind: literal("camera-mask"),
|
|
7188
|
+
capName: string()
|
|
7189
|
+
})
|
|
7190
|
+
]);
|
|
7191
|
+
/**
|
|
7192
|
+
* Why a switch is not offered for this camera. Rendered instead of the
|
|
7193
|
+
* control, never as a dead control — an absent function and a broken one must
|
|
7194
|
+
* not look the same.
|
|
7195
|
+
*/
|
|
7196
|
+
var CameraSwitchUnavailableReasonSchema = _enum([
|
|
7197
|
+
"no-provider",
|
|
7198
|
+
"source-unreachable",
|
|
7199
|
+
"not-configured"
|
|
7200
|
+
]);
|
|
7201
|
+
/**
|
|
7202
|
+
* One switch, resolved for one camera.
|
|
7203
|
+
*
|
|
7204
|
+
* `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
|
|
7205
|
+
* client-side: the viewer is a separate repository that does not import
|
|
7206
|
+
* `@camstack/types`, and a cost line duplicated in two clients is a cost line
|
|
7207
|
+
* that will disagree with itself. Five rows per camera is nothing.
|
|
7208
|
+
*/
|
|
7209
|
+
var CameraSwitchSchema = object({
|
|
7210
|
+
id: CameraSwitchIdSchema,
|
|
7211
|
+
label: string(),
|
|
7212
|
+
/**
|
|
7213
|
+
* What the operator LOSES while this is off, in one sentence. Required, not
|
|
7214
|
+
* optional: a switch that cannot say what it costs should not ship.
|
|
7215
|
+
*/
|
|
7216
|
+
costWhenOff: string(),
|
|
7217
|
+
/** False = do not render a control. `unavailableReason` says why. */
|
|
7218
|
+
available: boolean(),
|
|
7219
|
+
unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
|
|
7220
|
+
/** Current state. Meaningless when `available` is false — read it as `true`. */
|
|
7221
|
+
enabled: boolean(),
|
|
7222
|
+
authority: CameraSwitchAuthoritySchema
|
|
7223
|
+
});
|
|
7224
|
+
/** The whole group for one camera. */
|
|
7225
|
+
var CameraSwitchGroupSchema = object({
|
|
7226
|
+
deviceId: number().int(),
|
|
7227
|
+
switches: array(CameraSwitchSchema).readonly(),
|
|
7228
|
+
/** Unix ms when the group was composed server-side. */
|
|
7229
|
+
fetchedAt: number()
|
|
7230
|
+
});
|
|
7231
|
+
/**
|
|
7053
7232
|
* Ops-log — the durable, append-only operations audit shared by the
|
|
7054
7233
|
* recordings and events management surfaces.
|
|
7055
7234
|
*
|
|
@@ -7068,14 +7247,16 @@ var OpsLogOpSchema = _enum([
|
|
|
7068
7247
|
"manual-delete",
|
|
7069
7248
|
"rescan",
|
|
7070
7249
|
"retention-run",
|
|
7071
|
-
"relocate"
|
|
7250
|
+
"relocate",
|
|
7251
|
+
"orphan-audit"
|
|
7072
7252
|
]);
|
|
7073
7253
|
/** Why the operation ran. */
|
|
7074
7254
|
var OpsLogReasonSchema = _enum([
|
|
7075
7255
|
"retention",
|
|
7076
7256
|
"quota",
|
|
7077
7257
|
"manual",
|
|
7078
|
-
"operator"
|
|
7258
|
+
"operator",
|
|
7259
|
+
"maintenance"
|
|
7079
7260
|
]);
|
|
7080
7261
|
/** One audit row, shared verbatim by both domains. */
|
|
7081
7262
|
var OpsLogEntrySchema = object({
|
|
@@ -8936,6 +9117,126 @@ var RtpSourceSchema = object({
|
|
|
8936
9117
|
encoder: string(),
|
|
8937
9118
|
pipelineKey: string()
|
|
8938
9119
|
});
|
|
9120
|
+
/**
|
|
9121
|
+
* The encode request — **structured and serialisable, with NO raw-flag escape
|
|
9122
|
+
* hatch.** This is deliberate and it is the one lesson taken from
|
|
9123
|
+
* `getStreamWithCodec`: that method's `outputArgs: string[]` is simultaneously
|
|
9124
|
+
* its extensibility mechanism AND part of `pipelineKeyFor`'s sharing key, so
|
|
9125
|
+
* adding a flag silently forks the shared child, and two consumers that mean
|
|
9126
|
+
* the same thing but spell it differently never share. Here every knob is a
|
|
9127
|
+
* NAMED field: a new requirement becomes a schema field (and a codegen run),
|
|
9128
|
+
* never an opaque array.
|
|
9129
|
+
*
|
|
9130
|
+
* `inputArgs` / `outputArgs` are omitted from the profile for the same reason.
|
|
9131
|
+
* The operator-facing derived-stream transform editor still has them — that is
|
|
9132
|
+
* a different surface (`publishCameraStream({ kind: 'derived' })`) with a
|
|
9133
|
+
* different purpose (reshaping a badly-behaved SOURCE), and it is unchanged.
|
|
9134
|
+
*/
|
|
9135
|
+
var EgressEncodeSchema = EncodeProfileSchema.omit({
|
|
9136
|
+
inputArgs: true,
|
|
9137
|
+
outputArgs: true
|
|
9138
|
+
});
|
|
9139
|
+
/**
|
|
9140
|
+
* How the encoder is bounded. `'tight'` is a one-second VBV window for a
|
|
9141
|
+
* consumer whose budget is enforced per second (HomeKit); `'relaxed'` is two
|
|
9142
|
+
* seconds, letting a keyframe spike borrow from the next second (a browser,
|
|
9143
|
+
* an Echo). Named rather than numeric so the INTENT survives.
|
|
9144
|
+
*/
|
|
9145
|
+
var EgressRateControlSchema = _enum(["tight", "relaxed"]);
|
|
9146
|
+
var EgressTranscodeRequestSchema = object({
|
|
9147
|
+
deviceId: number().int().nonnegative(),
|
|
9148
|
+
/** Which published stream to read. */
|
|
9149
|
+
source: discriminatedUnion("kind", [object({
|
|
9150
|
+
kind: literal("profile"),
|
|
9151
|
+
profile: CamProfileSchema
|
|
9152
|
+
}), object({
|
|
9153
|
+
kind: literal("cam-stream"),
|
|
9154
|
+
camStreamId: string().min(1)
|
|
9155
|
+
})]),
|
|
9156
|
+
encode: EgressEncodeSchema,
|
|
9157
|
+
rateControl: EgressRateControlSchema.optional(),
|
|
9158
|
+
/**
|
|
9159
|
+
* `-bsf:v`. A consumer that negotiates its OWN SDP (HomeKit) cannot carry
|
|
9160
|
+
* out-of-band extradata and needs `dump_extra` on both the copy and encode
|
|
9161
|
+
* branches. Enumerated, not free text.
|
|
9162
|
+
*/
|
|
9163
|
+
bitstreamFilter: _enum([
|
|
9164
|
+
"dump_extra",
|
|
9165
|
+
"h264_mp4toannexb",
|
|
9166
|
+
"hevc_mp4toannexb"
|
|
9167
|
+
]).optional(),
|
|
9168
|
+
/**
|
|
9169
|
+
* Publish the transcode as a LOCAL push cam stream, instead of leaving the
|
|
9170
|
+
* consumer to dial the returned url. The broker picks the id and returns it
|
|
9171
|
+
* as `camStreamId` — a caller-supplied one would be circular, since the
|
|
9172
|
+
* sharing key is computed FROM this request.
|
|
9173
|
+
*
|
|
9174
|
+
* The url is still returned and still the contract for a transcode pinned to
|
|
9175
|
+
* another node. But dialling it locally costs an RTSP round trip that changes
|
|
9176
|
+
* the transport underneath the consumer: a dialled stream is an RTP source,
|
|
9177
|
+
* so `isRtpSource()` is true and the session takes the RTP-passthrough +
|
|
9178
|
+
* repacketizer branch. The push branch — the one the derived mechanism has
|
|
9179
|
+
* live hours on — is never reached. Measured on Alexa: broker registered, RTP
|
|
9180
|
+
* arriving, key frame arriving, black screen, on a chain healthy at every
|
|
9181
|
+
* other point.
|
|
9182
|
+
*
|
|
9183
|
+
* Same idea the transport already applies to CALLS, where `classifyCapRoute`
|
|
9184
|
+
* gives priority to `hub-in-process` so a local call never leaves the node.
|
|
9185
|
+
* This is that rule for media.
|
|
9186
|
+
*/
|
|
9187
|
+
publishLocally: boolean().optional(),
|
|
9188
|
+
pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
|
|
9189
|
+
/**
|
|
9190
|
+
* Operator/consumer override for decode hardware. ABSENT is the normal case
|
|
9191
|
+
* and the one that matters: the broker then resolves the backend from the
|
|
9192
|
+
* DECODER ADDON's per-node `probedBestHwaccel` (see
|
|
9193
|
+
* `@camstack/types` `ffmpeg/hwaccel.ts`), which is the ranking known to work
|
|
9194
|
+
* on this hardware — never the raw kernel resolver's qsv-first order.
|
|
9195
|
+
*/
|
|
9196
|
+
decodeHwAccel: _enum([
|
|
9197
|
+
"auto",
|
|
9198
|
+
"none",
|
|
9199
|
+
"videotoolbox",
|
|
9200
|
+
"vaapi",
|
|
9201
|
+
"qsv",
|
|
9202
|
+
"cuda"
|
|
9203
|
+
]).optional(),
|
|
9204
|
+
/**
|
|
9205
|
+
* Host to embed in the returned restream `url`. The broker mints hub-local
|
|
9206
|
+
* `127.0.0.1` URLs; a consumer on another node passes a cluster-resolvable
|
|
9207
|
+
* host (`NodeTopologyService.reachableHostByNode`) so the returned URL is
|
|
9208
|
+
* dialable from there. Same contract as `getStreamWithCodec.hostname` —
|
|
9209
|
+
* `substituteRtspHost` rewrites only the dial address, never the restreamer.
|
|
9210
|
+
*/
|
|
9211
|
+
hostname: string().optional(),
|
|
9212
|
+
/** Attribution for the broker panel. Never part of the sharing key. */
|
|
9213
|
+
tag: string().optional()
|
|
9214
|
+
});
|
|
9215
|
+
var EgressTranscodeSchema = object({
|
|
9216
|
+
/** Dial-able RTSP url (host-substituted when `hostname` was supplied). */
|
|
9217
|
+
url: string(),
|
|
9218
|
+
/** Release handle. Refcounted — the child dies when the last holder releases. */
|
|
9219
|
+
pipelineKey: string(),
|
|
9220
|
+
videoCodec: _enum(["H264", "H265"]),
|
|
9221
|
+
resolution: object({
|
|
9222
|
+
width: number().int().positive(),
|
|
9223
|
+
height: number().int().positive()
|
|
9224
|
+
}),
|
|
9225
|
+
transcoded: boolean(),
|
|
9226
|
+
encoder: string(),
|
|
9227
|
+
/**
|
|
9228
|
+
* The decode backend the child ACTUALLY ran with — `null` for software.
|
|
9229
|
+
* Returned rather than assumed: a consumer that asked for hardware and got
|
|
9230
|
+
* software needs to be able to see that without reading the broker's logs.
|
|
9231
|
+
*/
|
|
9232
|
+
decodeHwAccel: string().nullable(),
|
|
9233
|
+
/**
|
|
9234
|
+
* Set when `publishLocally` was honoured: attach to THIS instead of dialling
|
|
9235
|
+
* `url`, and the session takes the push/deframe transport rather than the
|
|
9236
|
+
* RTP-passthrough one. `null` means the consumer must dial.
|
|
9237
|
+
*/
|
|
9238
|
+
camStreamId: string().nullable()
|
|
9239
|
+
});
|
|
8939
9240
|
method(object({
|
|
8940
9241
|
deviceId: number().int().nonnegative(),
|
|
8941
9242
|
camStreamId: string().min(1),
|
|
@@ -9045,6 +9346,15 @@ method(object({
|
|
|
9045
9346
|
}), {
|
|
9046
9347
|
kind: "mutation",
|
|
9047
9348
|
auth: "admin"
|
|
9349
|
+
}), method(EgressTranscodeRequestSchema, EgressTranscodeSchema, {
|
|
9350
|
+
kind: "mutation",
|
|
9351
|
+
auth: "admin"
|
|
9352
|
+
}), method(object({ pipelineKey: string() }), object({
|
|
9353
|
+
released: boolean(),
|
|
9354
|
+
refcount: number().int().nonnegative()
|
|
9355
|
+
}), {
|
|
9356
|
+
kind: "mutation",
|
|
9357
|
+
auth: "admin"
|
|
9048
9358
|
}), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
|
|
9049
9359
|
subscriptionId: string(),
|
|
9050
9360
|
maxCount: number().int().positive().default(8)
|
|
@@ -13371,12 +13681,13 @@ var NcConditionsSchema = object({
|
|
|
13371
13681
|
* source; otherwise the subject's source must equal it. Legacy records
|
|
13372
13682
|
* with no stamped source are treated as `pipeline`. The union spans both
|
|
13373
13683
|
* record kinds — object events carry `pipeline` | `onboard`, synthetic
|
|
13374
|
-
* tracks carry `sensor
|
|
13684
|
+
* tracks carry `sensor` (a linked device) or `audio` (a D62 audio marker).
|
|
13375
13685
|
*/
|
|
13376
13686
|
source: _enum([
|
|
13377
13687
|
"pipeline",
|
|
13378
13688
|
"onboard",
|
|
13379
13689
|
"sensor",
|
|
13690
|
+
"audio",
|
|
13380
13691
|
"any"
|
|
13381
13692
|
]).optional(),
|
|
13382
13693
|
/**
|
|
@@ -13952,6 +14263,12 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
13952
14263
|
}), object({ success: literal(true) }), {
|
|
13953
14264
|
kind: "mutation",
|
|
13954
14265
|
auth: "admin"
|
|
14266
|
+
}), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
|
|
14267
|
+
deviceId: number().int(),
|
|
14268
|
+
muted: boolean()
|
|
14269
|
+
}), object({ success: literal(true) }), {
|
|
14270
|
+
kind: "mutation",
|
|
14271
|
+
auth: "admin"
|
|
13955
14272
|
}), method(object({
|
|
13956
14273
|
rule: NcRuleInputSchema,
|
|
13957
14274
|
lookbackMinutes: number().int().min(1).max(1440).default(60)
|
|
@@ -14290,12 +14607,60 @@ var TrackAudioLabelSchema = object({
|
|
|
14290
14607
|
});
|
|
14291
14608
|
/**
|
|
14292
14609
|
* How a track was produced. `pipeline` (default / absent) = the spatial
|
|
14293
|
-
* detection+tracking pipeline.
|
|
14294
|
-
*
|
|
14295
|
-
*
|
|
14296
|
-
*
|
|
14610
|
+
* detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection —
|
|
14611
|
+
* no positions, a single snapshot, and no bbox trajectory at all:
|
|
14612
|
+
*
|
|
14613
|
+
* - `sensor` — a linked sensor/control device state change.
|
|
14614
|
+
* - `audio` — an audio event on the camera itself that was anomalous for
|
|
14615
|
+
* THAT camera, loud, and heard while nothing visual was happening (D62).
|
|
14616
|
+
*
|
|
14617
|
+
* The spatial subsystems (tracker association, occupancy count, re-id /
|
|
14618
|
+
* embedding, resurrection) MUST skip every synthetic source. Test for that
|
|
14619
|
+
* with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
|
|
14620
|
+
* check silently readmits every source added after it was written.
|
|
14621
|
+
*/
|
|
14622
|
+
var TrackSourceSchema = _enum([
|
|
14623
|
+
"pipeline",
|
|
14624
|
+
"sensor",
|
|
14625
|
+
"audio"
|
|
14626
|
+
]);
|
|
14627
|
+
/**
|
|
14628
|
+
* Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
|
|
14629
|
+
* by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
|
|
14630
|
+
* so the two surfaces cannot drift.
|
|
14631
|
+
*
|
|
14632
|
+
* **Absent ≠ false.** A track that has never been touched omits the field; an
|
|
14633
|
+
* explicitly un-flagged track carries `false`. Legacy rows written before the
|
|
14634
|
+
* columns existed read as absent, and a consumer that needs a boolean should say
|
|
14635
|
+
* `flag === true`, not `flag !== false`.
|
|
14636
|
+
*
|
|
14637
|
+
* What the flags DO is deliberately UNDEFINED at the time of writing: they are
|
|
14638
|
+
* operator curation, and the behaviour they drive will be specified separately.
|
|
14639
|
+
* In particular a `markForTrain` track is NOT pinned against retention — see
|
|
14640
|
+
* `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
|
|
14641
|
+
*/
|
|
14642
|
+
var TrackFlagFields = {
|
|
14643
|
+
/** Operator marked this track as training material. */
|
|
14644
|
+
markForTrain: boolean().optional(),
|
|
14645
|
+
/** Operator marked this track for diagnostic attention. */
|
|
14646
|
+
debug: boolean().optional()
|
|
14647
|
+
};
|
|
14648
|
+
/**
|
|
14649
|
+
* The write half: a PARTIAL patch. An omitted key is left untouched, so setting
|
|
14650
|
+
* one flag can never clear the other — the toggles are independent and are
|
|
14651
|
+
* driven from three surfaces that do not know about each other.
|
|
14297
14652
|
*/
|
|
14298
|
-
var
|
|
14653
|
+
var TrackFlagsPatchSchema = object(TrackFlagFields);
|
|
14654
|
+
/**
|
|
14655
|
+
* The resolved flag state after a write. Both fields are REQUIRED here (absent
|
|
14656
|
+
* collapses to `false`) so a caller can drive a toggle's checked state off the
|
|
14657
|
+
* mutation result without a re-fetch.
|
|
14658
|
+
*/
|
|
14659
|
+
var TrackFlagsSchema = object({
|
|
14660
|
+
trackId: string(),
|
|
14661
|
+
markForTrain: boolean(),
|
|
14662
|
+
debug: boolean()
|
|
14663
|
+
});
|
|
14299
14664
|
var TrackSchema = object({
|
|
14300
14665
|
trackId: string(),
|
|
14301
14666
|
deviceId: number(),
|
|
@@ -14338,7 +14703,8 @@ var TrackSchema = object({
|
|
|
14338
14703
|
/** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
|
|
14339
14704
|
* Populated from the persisted envelope columns on historical reads;
|
|
14340
14705
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
14341
|
-
envelope: TrackEnvelopeSchema.optional()
|
|
14706
|
+
envelope: TrackEnvelopeSchema.optional(),
|
|
14707
|
+
...TrackFlagFields
|
|
14342
14708
|
});
|
|
14343
14709
|
var BaseEventFields = {
|
|
14344
14710
|
id: string(),
|
|
@@ -14551,7 +14917,8 @@ var KeyEventSchema = object({
|
|
|
14551
14917
|
/** Highest-confidence ObjectEvent id for the track (empty when none). */
|
|
14552
14918
|
bestEventId: string(),
|
|
14553
14919
|
/** Track lifetime in ms (lastSeen - firstSeen). */
|
|
14554
|
-
windowMs: number().optional()
|
|
14920
|
+
windowMs: number().optional(),
|
|
14921
|
+
...TrackFlagFields
|
|
14555
14922
|
});
|
|
14556
14923
|
object({
|
|
14557
14924
|
trackId: string(),
|
|
@@ -14622,6 +14989,104 @@ var EventPruneCountsSchema = object({
|
|
|
14622
14989
|
object: number().int(),
|
|
14623
14990
|
audio: number().int()
|
|
14624
14991
|
});
|
|
14992
|
+
/**
|
|
14993
|
+
* Re-embed stored tracks from their key frames.
|
|
14994
|
+
*
|
|
14995
|
+
* The reason this is an operator-callable method and not a migration script:
|
|
14996
|
+
* every knob that decides what a vector MEANS — encoder model, crop margin,
|
|
14997
|
+
* squaring — is only changeable if the existing vectors can be regenerated.
|
|
14998
|
+
* Mixing feature spaces in one index makes cosine scores incomparable, and the
|
|
14999
|
+
* symptom is a quality regression with no visible cause.
|
|
15000
|
+
*/
|
|
15001
|
+
var RebuildObjectEmbeddingsInput = object({
|
|
15002
|
+
/** Restrict to one camera. Omit for the whole fleet. */
|
|
15003
|
+
deviceId: number().optional(),
|
|
15004
|
+
since: number().optional(),
|
|
15005
|
+
until: number().optional(),
|
|
15006
|
+
/** Stop after this many tracks; the result reports whether more remain. */
|
|
15007
|
+
maxTracks: number().int().positive().optional(),
|
|
15008
|
+
/**
|
|
15009
|
+
* Run every embedding on THIS node instead of round-robining the fleet.
|
|
15010
|
+
*
|
|
15011
|
+
* Named `executeOnNodeId` and not `nodeId` on purpose: an inline `nodeId`
|
|
15012
|
+
* field in cap args is read by `parent-unowned-call.ts` as a ROUTING PIN, so
|
|
15013
|
+
* calling it that would pin the rebuild REQUEST itself to that node — the
|
|
15014
|
+
* rebuild orchestration lives on the hub, and only the per-track step runs
|
|
15015
|
+
* remotely. This field is data; the per-track pin is applied inside.
|
|
15016
|
+
*
|
|
15017
|
+
* Absent ⇒ round-robin over every online node whose runner can serve the
|
|
15018
|
+
* pinned model.
|
|
15019
|
+
*/
|
|
15020
|
+
executeOnNodeId: string().optional(),
|
|
15021
|
+
/**
|
|
15022
|
+
* Milliseconds to wait between tracks; omit for the built-in default, `0` to
|
|
15023
|
+
* run flat out.
|
|
15024
|
+
*
|
|
15025
|
+
* A rebuild is bulk maintenance on hub-main's single thread. Measured
|
|
15026
|
+
* 2026-08-06, an unpaced pass held that thread busy 82.2 s out of 120 and
|
|
15027
|
+
* pushed `nodes.topology` from 0.25 s to 26 s for 43 minutes. The value in
|
|
15028
|
+
* force is logged at start and finish so a deliberately slow pass reads
|
|
15029
|
+
* differently from a stalled one.
|
|
15030
|
+
*/
|
|
15031
|
+
pacingMs: number().int().nonnegative().optional()
|
|
15032
|
+
});
|
|
15033
|
+
/**
|
|
15034
|
+
* Result of emptying the CLIP index.
|
|
15035
|
+
*
|
|
15036
|
+
* The clean slate before a policy change: a new crop margin or encoder model
|
|
15037
|
+
* leaves two feature spaces in one index whose cosine scores are not
|
|
15038
|
+
* comparable, so wiping and rebuilding is the only way to be sure every vector
|
|
15039
|
+
* means the same thing.
|
|
15040
|
+
*/
|
|
15041
|
+
var WipeObjectEmbeddingsResultSchema = object({ deleted: number() });
|
|
15042
|
+
/**
|
|
15043
|
+
* Acknowledgement that a rebuild STARTED.
|
|
15044
|
+
*
|
|
15045
|
+
* The pass costs ~1s per track — 45 minutes for a 2,600-track fleet — so it
|
|
15046
|
+
* runs detached and this returns immediately. Waiting for it made the client
|
|
15047
|
+
* time out while the work carried on server-side, which is the worst of both:
|
|
15048
|
+
* no result and no way to know it was still going. Poll
|
|
15049
|
+
* `getObjectEmbeddingRebuildStatus` for progress.
|
|
15050
|
+
*/
|
|
15051
|
+
var RebuildObjectEmbeddingsResultSchema = object({
|
|
15052
|
+
started: boolean(),
|
|
15053
|
+
/** True when a pass was already running; the new request is ignored. */
|
|
15054
|
+
alreadyRunning: boolean()
|
|
15055
|
+
});
|
|
15056
|
+
var RebuildStatusSchema = object({
|
|
15057
|
+
running: boolean(),
|
|
15058
|
+
scanned: number(),
|
|
15059
|
+
rebuilt: number(),
|
|
15060
|
+
/** Tracks whose key frame is gone — nothing to re-embed from. */
|
|
15061
|
+
missingKeyFrame: number(),
|
|
15062
|
+
/** Tracks with no usable detection box. */
|
|
15063
|
+
missingBbox: number(),
|
|
15064
|
+
/**
|
|
15065
|
+
* Tracks an executing node REFUSED rather than broke on — an unreadable key
|
|
15066
|
+
* frame, a step that threw. Separate from `failed` because the remedy is
|
|
15067
|
+
* different, and because a whole camera silently contributing zero vectors
|
|
15068
|
+
* is the shape of failure a rebuild must never hide.
|
|
15069
|
+
*/
|
|
15070
|
+
notRunnable: number(),
|
|
15071
|
+
/**
|
|
15072
|
+
* The pass stopped because NO node could serve the pinned model.
|
|
15073
|
+
*
|
|
15074
|
+
* Distinct from `notRunnable` on purpose: that one says "this track was
|
|
15075
|
+
* refused", this one says "the cluster cannot do this work at all" — every
|
|
15076
|
+
* candidate node either lacks the `clip-embedding` step, lacks a build of the
|
|
15077
|
+
* pinned model for its engine format, or dropped out. The remedy is a model /
|
|
15078
|
+
* engine change, not a per-camera one. Non-zero here always comes with
|
|
15079
|
+
* `complete: false`.
|
|
15080
|
+
*/
|
|
15081
|
+
noCapableNode: number(),
|
|
15082
|
+
failed: number(),
|
|
15083
|
+
/** Set once a pass ends: true only when EVERYTHING was covered. */
|
|
15084
|
+
complete: boolean().nullable(),
|
|
15085
|
+
startedAtMs: number().nullable(),
|
|
15086
|
+
finishedAtMs: number().nullable(),
|
|
15087
|
+
/** Present when the pass ended by throwing. */
|
|
15088
|
+
error: string().nullable()
|
|
15089
|
+
});
|
|
14625
15090
|
DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
|
|
14626
15091
|
deviceId: number(),
|
|
14627
15092
|
trackId: string()
|
|
@@ -14685,7 +15150,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
14685
15150
|
}), {
|
|
14686
15151
|
kind: "mutation",
|
|
14687
15152
|
auth: "admin"
|
|
14688
|
-
}), method(object({
|
|
15153
|
+
}), method(object({
|
|
15154
|
+
/** Log/audit scope only — the trackId is globally unique on its own. */
|
|
15155
|
+
deviceId: number(),
|
|
15156
|
+
trackId: string(),
|
|
15157
|
+
flags: TrackFlagsPatchSchema
|
|
15158
|
+
}), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
|
|
14689
15159
|
kind: "query",
|
|
14690
15160
|
auth: "admin"
|
|
14691
15161
|
}), method(object({
|
|
@@ -14715,7 +15185,13 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
14715
15185
|
}), array(MediaFileSchema).readonly()), method(object({
|
|
14716
15186
|
trackId: string(),
|
|
14717
15187
|
kinds: array(MediaFileKindEnum).optional()
|
|
14718
|
-
}), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
|
|
15188
|
+
}), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
|
|
15189
|
+
kind: "mutation",
|
|
15190
|
+
auth: "admin"
|
|
15191
|
+
}), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
|
|
15192
|
+
kind: "mutation",
|
|
15193
|
+
auth: "admin"
|
|
15194
|
+
}), method(object({}), RebuildStatusSchema), object({
|
|
14719
15195
|
deviceId: number(),
|
|
14720
15196
|
timestamp: number(),
|
|
14721
15197
|
frameWidth: number(),
|
|
@@ -15289,6 +15765,53 @@ var DetailResultSchema = object({
|
|
|
15289
15765
|
nativeFaceShortSidePx: number().optional()
|
|
15290
15766
|
});
|
|
15291
15767
|
/**
|
|
15768
|
+
* Why an executing node REFUSED a stateless step run (`runStatelessStep`).
|
|
15769
|
+
*
|
|
15770
|
+
* A refusal is a first-class answer, not an error, because the caller's next
|
|
15771
|
+
* move depends on WHICH one it is — and because "the pass produced nothing"
|
|
15772
|
+
* must never be reachable without a named, counted cause. The two tiers:
|
|
15773
|
+
*
|
|
15774
|
+
* - **node-level** (`unknown-step`, `model-not-servable`) — this node can
|
|
15775
|
+
* never serve this (step, model) pair. The caller drops it from its rotation
|
|
15776
|
+
* and retries the same work elsewhere; nothing about the work changes.
|
|
15777
|
+
* - **work-level** (`unreadable-frame`, `execution-failed`) — this node is
|
|
15778
|
+
* fine, this one request is not. Retrying it on another node would only
|
|
15779
|
+
* spread the same failure.
|
|
15780
|
+
*/
|
|
15781
|
+
var StatelessStepRefusalSchema = _enum([
|
|
15782
|
+
"unknown-step",
|
|
15783
|
+
"model-not-servable",
|
|
15784
|
+
"unreadable-frame",
|
|
15785
|
+
"execution-failed"
|
|
15786
|
+
]);
|
|
15787
|
+
/**
|
|
15788
|
+
* Answer to `runStatelessStep` — a discriminated union rather than a nullable
|
|
15789
|
+
* result, because `null` is exactly what made the camera-bound detail path
|
|
15790
|
+
* unable to tell "refused" from "never asked".
|
|
15791
|
+
*/
|
|
15792
|
+
var RunStatelessStepResultSchema = discriminatedUnion("kind", [object({
|
|
15793
|
+
kind: literal("ran"),
|
|
15794
|
+
/** The node that actually executed it — the pin, echoed back for the log. */
|
|
15795
|
+
nodeId: string(),
|
|
15796
|
+
/**
|
|
15797
|
+
* The model the step ran with.
|
|
15798
|
+
*
|
|
15799
|
+
* The node verified this exact id has a build for the format it dispatched
|
|
15800
|
+
* on BEFORE running, so the executor's format resolution returns it
|
|
15801
|
+
* unchanged. A caller that pinned a model must compare this field and
|
|
15802
|
+
* treat a mismatch as a refusal — the whole point of the pin is that a
|
|
15803
|
+
* pass writes one feature space.
|
|
15804
|
+
*/
|
|
15805
|
+
modelId: string(),
|
|
15806
|
+
details: array(DetailResultSchema)
|
|
15807
|
+
}), object({
|
|
15808
|
+
kind: literal("refused"),
|
|
15809
|
+
nodeId: string(),
|
|
15810
|
+
reason: StatelessStepRefusalSchema,
|
|
15811
|
+
/** Human-readable specifics — the format tried, the formats shipped, etc. */
|
|
15812
|
+
detail: string()
|
|
15813
|
+
})]);
|
|
15814
|
+
/**
|
|
15292
15815
|
* Per-camera tunable ranges + defaults. Single source of truth used
|
|
15293
15816
|
* by both the Zod data schema (validation + default fallback) and
|
|
15294
15817
|
* the device settings UI (slider min/max/step). Touch one place and
|
|
@@ -15624,10 +16147,46 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
|
|
|
15624
16147
|
}), NativeCropResultSchema.nullable()), method(object({
|
|
15625
16148
|
deviceId: number(),
|
|
15626
16149
|
frameHandle: FrameHandleSchema.optional(),
|
|
16150
|
+
/**
|
|
16151
|
+
* FULL FRAME (base64 JPEG). The runner derives the crop rectangle from
|
|
16152
|
+
* `parent.bbox` with the cluster crop convention and cuts it itself —
|
|
16153
|
+
* do NOT pre-crop for this field, that is what `cropJpeg` is.
|
|
16154
|
+
*/
|
|
16155
|
+
frameJpeg: string().optional(),
|
|
16156
|
+
/**
|
|
16157
|
+
* PRE-CUT tile (base64 JPEG), used verbatim — NO padding is applied.
|
|
16158
|
+
* The fallback when the lease/session backing the frame is gone and the
|
|
16159
|
+
* caller already holds a crop.
|
|
16160
|
+
*/
|
|
15627
16161
|
cropJpeg: string().optional(),
|
|
15628
16162
|
parent: DetailParentSchema,
|
|
15629
16163
|
steps: array(string()).optional()
|
|
15630
|
-
}), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" })
|
|
16164
|
+
}), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" }), method(object({
|
|
16165
|
+
/** Catalog step id, e.g. `clip-embedding`. */
|
|
16166
|
+
stepId: string(),
|
|
16167
|
+
/**
|
|
16168
|
+
* REQUIRED model pin. The node runs this exact model or refuses with
|
|
16169
|
+
* `model-not-servable` — it never substitutes a format default, because
|
|
16170
|
+
* a fleet pass that round-robins across nodes would then fill one index
|
|
16171
|
+
* from several encoders.
|
|
16172
|
+
*/
|
|
16173
|
+
modelId: string(),
|
|
16174
|
+
/** FULL FRAME, base64 JPEG. The runner cuts — do NOT pre-crop. */
|
|
16175
|
+
frameJpeg: string(),
|
|
16176
|
+
/**
|
|
16177
|
+
* The subject box, NORMALISED [0,1] against `frameJpeg`. Normalised on
|
|
16178
|
+
* purpose: the caller stores boxes against a downscaled analysis frame
|
|
16179
|
+
* while the stored key frame is native-resolution, and the only side
|
|
16180
|
+
* that reliably knows the image's pixel dimensions is the side that
|
|
16181
|
+
* decodes it. Denormalising here removes a second reader of the
|
|
16182
|
+
* dimensions and the class of mismatch that comes with it.
|
|
16183
|
+
*/
|
|
16184
|
+
bbox: NativeCropBboxSchema,
|
|
16185
|
+
/** Parent class of the subject (`person`, `vehicle`, …) — carried into the result. */
|
|
16186
|
+
className: string(),
|
|
16187
|
+
/** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
|
|
16188
|
+
sourceDeviceId: number()
|
|
16189
|
+
}), RunStatelessStepResultSchema, { kind: "mutation" });
|
|
15631
16190
|
var CameraPipelineConfigSchema = object({
|
|
15632
16191
|
engine: PipelineEngineChoiceSchema.optional(),
|
|
15633
16192
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
@@ -15925,6 +16484,20 @@ var CameraStatusSchema = object({
|
|
|
15925
16484
|
detection: CameraDetectionStatusSchema.nullable(),
|
|
15926
16485
|
audio: CameraAudioStatusSchema.nullable(),
|
|
15927
16486
|
recording: CameraRecordingStatusSchema.nullable(),
|
|
16487
|
+
/**
|
|
16488
|
+
* Per-camera function switches an OPERATOR has turned off
|
|
16489
|
+
* ([D61](../../../../docs/decisions/adr-0067.md)).
|
|
16490
|
+
*
|
|
16491
|
+
* This is the difference between DISABLED and BROKEN. A camera whose
|
|
16492
|
+
* `detection` block reports zero fps and whose `switchedOff` contains
|
|
16493
|
+
* `'object-detection'` was switched off by a person; the same camera with an
|
|
16494
|
+
* empty list is failing. Every status surface must render the two
|
|
16495
|
+
* differently — a quiet camera that looks identical to a dead one is the
|
|
16496
|
+
* silence-reads-as-never-happened trap this repo keeps paying for.
|
|
16497
|
+
*
|
|
16498
|
+
* Empty when nothing is off. Never contains a switch no provider offers.
|
|
16499
|
+
*/
|
|
16500
|
+
switchedOff: array(CameraSwitchIdSchema).readonly(),
|
|
15928
16501
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
15929
16502
|
fetchedAt: number()
|
|
15930
16503
|
});
|
|
@@ -16093,7 +16666,14 @@ method(object({
|
|
|
16093
16666
|
}), method(object({
|
|
16094
16667
|
deviceId: number(),
|
|
16095
16668
|
agentNodeId: string().optional()
|
|
16096
|
-
}), CameraPipelineConfigSchema), method(object({ deviceId: number() }),
|
|
16669
|
+
}), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraSwitchGroupSchema), method(object({
|
|
16670
|
+
deviceId: number(),
|
|
16671
|
+
switchId: CameraSwitchIdSchema,
|
|
16672
|
+
enabled: boolean()
|
|
16673
|
+
}), CameraSwitchGroupSchema, {
|
|
16674
|
+
kind: "mutation",
|
|
16675
|
+
auth: "admin"
|
|
16676
|
+
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
|
|
16097
16677
|
name: string(),
|
|
16098
16678
|
description: string().optional(),
|
|
16099
16679
|
config: CameraPipelineConfigSchema
|
|
@@ -16416,9 +16996,15 @@ DeviceType.Camera, method(object({
|
|
|
16416
16996
|
* Bypass the cache freshness check and fetch directly from the
|
|
16417
16997
|
* native (or stream-broker fallback). Triggered by the UI's
|
|
16418
16998
|
* "refresh" button so an operator can force a fresh frame
|
|
16419
|
-
* even when the cache is well within
|
|
16420
|
-
*
|
|
16421
|
-
*
|
|
16999
|
+
* even when the cache is well within the device's
|
|
17000
|
+
* `snapshotMaxAgeS` window.
|
|
17001
|
+
*
|
|
17002
|
+
* **`force` is an OPERATOR signal, not a freshness preference.** On a
|
|
17003
|
+
* battery camera it is the one thing that walks past the wrapper's
|
|
17004
|
+
* sleep gate and wakes the camera, so a background caller — a poller,
|
|
17005
|
+
* an event handler, a thumbnail — must NEVER set it. Every such caller
|
|
17006
|
+
* gets the cached frame, which on a sleeping battery camera is the
|
|
17007
|
+
* correct answer: stale but honest beats woken.
|
|
16422
17008
|
*/
|
|
16423
17009
|
force: boolean().optional()
|
|
16424
17010
|
}), SnapshotImageSchema.nullable()), method(object({ deviceId: number() }), _void(), {
|
|
@@ -16949,6 +17535,24 @@ var VectorDeleteByFilterInputSchema = object({
|
|
|
16949
17535
|
filter: VectorFilterSchema
|
|
16950
17536
|
});
|
|
16951
17537
|
var VectorDeleteResultSchema = object({ deleted: number() });
|
|
17538
|
+
var VectorGetInputSchema = object({
|
|
17539
|
+
index: string(),
|
|
17540
|
+
ids: array(string())
|
|
17541
|
+
});
|
|
17542
|
+
/**
|
|
17543
|
+
* Metadata for the requested ids, WITHOUT their vectors.
|
|
17544
|
+
*
|
|
17545
|
+
* The only caller is a best-of gate that compares a candidate's confidence
|
|
17546
|
+
* against the stored one, and shipping 512 floats back to answer "is 0.91 >
|
|
17547
|
+
* 0.87" would undo the point of the compact encoding. Ids with no row are
|
|
17548
|
+
* simply absent — a caller distinguishing "not stored" from "stored" reads the
|
|
17549
|
+
* length, and a null placeholder would invite a `?? 0` that treats a missing
|
|
17550
|
+
* row as confidence zero.
|
|
17551
|
+
*/
|
|
17552
|
+
var VectorGetResultSchema = object({ items: array(object({
|
|
17553
|
+
id: string(),
|
|
17554
|
+
metadata: VectorMetadataSchema
|
|
17555
|
+
})) });
|
|
16952
17556
|
var VectorStatsInputSchema = object({ index: string() });
|
|
16953
17557
|
var VectorStatsResultSchema = object({
|
|
16954
17558
|
/** Provider id, so an operator can tell brute force from an ANN index. */
|
|
@@ -16961,7 +17565,7 @@ var VectorStatsResultSchema = object({
|
|
|
16961
17565
|
/** False when the backend ranks approximately. */
|
|
16962
17566
|
exact: boolean()
|
|
16963
17567
|
});
|
|
16964
|
-
method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
|
|
17568
|
+
method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
|
|
16965
17569
|
/**
|
|
16966
17570
|
* `videoclips` — the unified, navigable-clip surface for a camera.
|
|
16967
17571
|
*
|
|
@@ -20682,12 +21286,30 @@ object({
|
|
|
20682
21286
|
});
|
|
20683
21287
|
DeviceType.Sensor;
|
|
20684
21288
|
/**
|
|
20685
|
-
*
|
|
20686
|
-
*
|
|
20687
|
-
*
|
|
20688
|
-
*
|
|
20689
|
-
*
|
|
20690
|
-
*
|
|
21289
|
+
* PRIVACY — what the camera deliberately does not capture. Two planes:
|
|
21290
|
+
*
|
|
21291
|
+
* - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
|
|
21292
|
+
* grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
|
|
21293
|
+
* `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
|
|
21294
|
+
* 4 vertices, not necessarily axis-aligned). The cap composes the shared
|
|
21295
|
+
* rect|polygon subset of the MaskShape vocabulary. All coords are
|
|
21296
|
+
* normalized 0..1 (top-left origin).
|
|
21297
|
+
* - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
|
|
21298
|
+
* camera encoding an audio track at all, so EVERY consumer — live view,
|
|
21299
|
+
* recording, the audio analyzer, an export — sees silent video. There is
|
|
21300
|
+
* no server-side copy of this fact; the camera is the store and every read
|
|
21301
|
+
* is a read-through, which is why a switch over it cannot drift
|
|
21302
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)).
|
|
21303
|
+
*
|
|
21304
|
+
* Both belong here for one reason: they are the two things an operator turns
|
|
21305
|
+
* off when the answer to "what is this camera allowed to record" changes, and
|
|
21306
|
+
* both are applied ON the device, before anything leaves it.
|
|
21307
|
+
*
|
|
21308
|
+
* **The audio flag has exactly one writer.** `stream-params` used to carry a
|
|
21309
|
+
* per-profile `audio` in its patch schema — reachable from no UI and honoured
|
|
21310
|
+
* by one provider — and it was removed when this landed. A second writer onto
|
|
21311
|
+
* one device register is the shape of every knob this repo has shipped that
|
|
21312
|
+
* disagreed with the one the reader read.
|
|
20691
21313
|
*/
|
|
20692
21314
|
/** A privacy-mask region's geometry — rectangle or free polygon. */
|
|
20693
21315
|
var PrivacyMaskShapeSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
|
|
@@ -20703,16 +21325,40 @@ object({
|
|
|
20703
21325
|
enabled: boolean(),
|
|
20704
21326
|
/** Active zones (normalized 0..1). Length ≤ maxRegions. */
|
|
20705
21327
|
regions: array(PrivacyMaskRegionSchema),
|
|
21328
|
+
/**
|
|
21329
|
+
* Is the camera capturing sound right now? Read from the camera, never from
|
|
21330
|
+
* a server-side mirror.
|
|
21331
|
+
*
|
|
21332
|
+
* `null` means "no answer" — either this camera exposes no controllable
|
|
21333
|
+
* microphone (`getOptions().supportsAudioMute === false`) or the read
|
|
21334
|
+
* failed. A consumer must render `null` as UNKNOWN and never as `false`:
|
|
21335
|
+
* "the microphone is off" and "we could not ask" look identical to an
|
|
21336
|
+
* operator only until one of them is wrong.
|
|
21337
|
+
*
|
|
21338
|
+
* On a camera whose profiles carry the flag independently (Reolink writes
|
|
21339
|
+
* it per stream), `true` means AT LEAST ONE profile still carries audio —
|
|
21340
|
+
* privacy is only satisfied when every one of them is silent.
|
|
21341
|
+
*/
|
|
21342
|
+
audioEnabled: boolean().nullable(),
|
|
20706
21343
|
lastFetchedAt: number()
|
|
20707
21344
|
});
|
|
20708
|
-
/** Per-camera availability. */
|
|
21345
|
+
/** Per-camera availability. Probed, never assumed from the model name. */
|
|
20709
21346
|
var PrivacyMaskOptionsSchema = object({
|
|
20710
21347
|
/** Maximum number of supported zones. */
|
|
20711
21348
|
maxRegions: number(),
|
|
20712
21349
|
/** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
|
|
20713
21350
|
supportedShapes: array(MaskShapeKindSchema),
|
|
20714
21351
|
/** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
|
|
20715
|
-
polygonVertices: MaskPolygonVerticesSchema.optional()
|
|
21352
|
+
polygonVertices: MaskPolygonVerticesSchema.optional(),
|
|
21353
|
+
/**
|
|
21354
|
+
* Does this camera expose a microphone switch we can actually write?
|
|
21355
|
+
*
|
|
21356
|
+
* Camera-probed: `true` only when the firmware answered with an audio flag
|
|
21357
|
+
* we know how to patch. A camera that never answered is `false` — a control
|
|
21358
|
+
* the operator can press that changes nothing is worse than no control, and
|
|
21359
|
+
* the switch group renders "not available" instead.
|
|
21360
|
+
*/
|
|
21361
|
+
supportsAudioMute: boolean()
|
|
20716
21362
|
});
|
|
20717
21363
|
/** Partial change — every field optional. */
|
|
20718
21364
|
var PrivacyMaskPatchSchema = object({
|
|
@@ -20725,6 +21371,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), PrivacyMaskOptionsSche
|
|
|
20725
21371
|
}), _void(), {
|
|
20726
21372
|
kind: "mutation",
|
|
20727
21373
|
auth: "admin"
|
|
21374
|
+
}), method(object({
|
|
21375
|
+
deviceId: number(),
|
|
21376
|
+
enabled: boolean()
|
|
21377
|
+
}), _void(), {
|
|
21378
|
+
kind: "mutation",
|
|
21379
|
+
auth: "admin"
|
|
20728
21380
|
});
|
|
20729
21381
|
var PtzPresetSchema = object({
|
|
20730
21382
|
id: string(),
|
|
@@ -20934,6 +21586,21 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
|
|
|
20934
21586
|
})]);
|
|
20935
21587
|
/** Raw bytes of one finalized footage segment (read off disk on the recording node). */
|
|
20936
21588
|
var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
|
|
21589
|
+
/**
|
|
21590
|
+
* One GOP of a finalized segment, cut by byte range through the segment's own
|
|
21591
|
+
* `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
|
|
21592
|
+
* the single `moof`+`mdat` covering the requested instant — standalone-
|
|
21593
|
+
* demuxable, never the whole file. When the segment's index cannot be parsed
|
|
21594
|
+
* the provider degrades INSIDE the mechanism to the whole segment (still one
|
|
21595
|
+
* `data`, `gopStartMs` = the segment start) — a worse read, not another path.
|
|
21596
|
+
*/
|
|
21597
|
+
var ReadGopBytesResultSchema = object({
|
|
21598
|
+
data: _instanceof(Uint8Array),
|
|
21599
|
+
/** Absolute epoch ms of the returned fragment's first sample. */
|
|
21600
|
+
gopStartMs: number(),
|
|
21601
|
+
/** Media ms the returned fragment covers. */
|
|
21602
|
+
gopDurMs: number()
|
|
21603
|
+
});
|
|
20937
21604
|
method(object({
|
|
20938
21605
|
deviceId: number(),
|
|
20939
21606
|
fromMs: number(),
|
|
@@ -20976,6 +21643,14 @@ method(object({
|
|
|
20976
21643
|
}), ReadSegmentBytesResultSchema, {
|
|
20977
21644
|
kind: "query",
|
|
20978
21645
|
auth: "admin"
|
|
21646
|
+
}), method(object({
|
|
21647
|
+
deviceId: number(),
|
|
21648
|
+
profile: string(),
|
|
21649
|
+
startMs: number(),
|
|
21650
|
+
epochMs: number()
|
|
21651
|
+
}), ReadGopBytesResultSchema, {
|
|
21652
|
+
kind: "query",
|
|
21653
|
+
auth: "admin"
|
|
20979
21654
|
}), method(object({
|
|
20980
21655
|
deviceId: number(),
|
|
20981
21656
|
config: RecordingConfigSchema
|
|
@@ -21456,6 +22131,16 @@ var StreamProfileConfigSchema = object({
|
|
|
21456
22131
|
"baseline"
|
|
21457
22132
|
]).optional(),
|
|
21458
22133
|
gop: number().optional(),
|
|
22134
|
+
/**
|
|
22135
|
+
* Whether THIS profile currently carries an audio track. READ-ONLY here.
|
|
22136
|
+
*
|
|
22137
|
+
* There is no matching field on {@link StreamProfilePatchSchema}: the
|
|
22138
|
+
* camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
|
|
22139
|
+
* writes every profile at once so "audio off" means silent everywhere. A
|
|
22140
|
+
* per-profile writer beside it would let a camera be half-muted and would be
|
|
22141
|
+
* a second knob onto one device register — the failure D62 exists to
|
|
22142
|
+
* prevent. Absent when the firmware does not report the flag.
|
|
22143
|
+
*/
|
|
21459
22144
|
audio: boolean().optional()
|
|
21460
22145
|
});
|
|
21461
22146
|
object({
|
|
@@ -21496,7 +22181,13 @@ var StreamParamsOptionsSchema = object({
|
|
|
21496
22181
|
ext: StreamProfileOptionsSchema.optional()
|
|
21497
22182
|
});
|
|
21498
22183
|
/** A partial change to one profile — every field optional; a provider
|
|
21499
|
-
* ignores fields it doesn't support.
|
|
22184
|
+
* ignores fields it doesn't support.
|
|
22185
|
+
*
|
|
22186
|
+
* There is deliberately NO `audio` here. It existed until 2026-08-07,
|
|
22187
|
+
* reachable from no form and honoured by exactly one provider, while the
|
|
22188
|
+
* camera's microphone is a whole-device fact. It now has one writer,
|
|
22189
|
+
* `privacyMask.setAudioEnabled`, which writes every profile — see
|
|
22190
|
+
* `privacy-mask.cap.ts`. */
|
|
21500
22191
|
var StreamProfilePatchSchema = object({
|
|
21501
22192
|
width: number().optional(),
|
|
21502
22193
|
height: number().optional(),
|
|
@@ -21509,8 +22200,7 @@ var StreamProfilePatchSchema = object({
|
|
|
21509
22200
|
"main",
|
|
21510
22201
|
"baseline"
|
|
21511
22202
|
]).optional(),
|
|
21512
|
-
gop: number().optional()
|
|
21513
|
-
audio: boolean().optional()
|
|
22203
|
+
gop: number().optional()
|
|
21514
22204
|
});
|
|
21515
22205
|
DeviceType.Camera, method(object({ deviceId: number() }), StreamParamsOptionsSchema), method(object({
|
|
21516
22206
|
deviceId: number(),
|
|
@@ -25032,6 +25722,12 @@ Object.freeze({
|
|
|
25032
25722
|
addonId: null,
|
|
25033
25723
|
access: "view"
|
|
25034
25724
|
},
|
|
25725
|
+
"notificationRules.listDeviceMutes": {
|
|
25726
|
+
capName: "notification-rules",
|
|
25727
|
+
capScope: "system",
|
|
25728
|
+
addonId: null,
|
|
25729
|
+
access: "view"
|
|
25730
|
+
},
|
|
25035
25731
|
"notificationRules.listRules": {
|
|
25036
25732
|
capName: "notification-rules",
|
|
25037
25733
|
capScope: "system",
|
|
@@ -25050,6 +25746,12 @@ Object.freeze({
|
|
|
25050
25746
|
addonId: null,
|
|
25051
25747
|
access: "create"
|
|
25052
25748
|
},
|
|
25749
|
+
"notificationRules.setDeviceMuted": {
|
|
25750
|
+
capName: "notification-rules",
|
|
25751
|
+
capScope: "system",
|
|
25752
|
+
addonId: null,
|
|
25753
|
+
access: "create"
|
|
25754
|
+
},
|
|
25053
25755
|
"notificationRules.setRuleEnabled": {
|
|
25054
25756
|
capName: "notification-rules",
|
|
25055
25757
|
capScope: "system",
|
|
@@ -25224,6 +25926,12 @@ Object.freeze({
|
|
|
25224
25926
|
addonId: null,
|
|
25225
25927
|
access: "view"
|
|
25226
25928
|
},
|
|
25929
|
+
"pipelineAnalytics.getObjectEmbeddingRebuildStatus": {
|
|
25930
|
+
capName: "pipeline-analytics",
|
|
25931
|
+
capScope: "device",
|
|
25932
|
+
addonId: null,
|
|
25933
|
+
access: "view"
|
|
25934
|
+
},
|
|
25227
25935
|
"pipelineAnalytics.getObjectEvents": {
|
|
25228
25936
|
capName: "pipeline-analytics",
|
|
25229
25937
|
capScope: "device",
|
|
@@ -25302,6 +26010,12 @@ Object.freeze({
|
|
|
25302
26010
|
addonId: null,
|
|
25303
26011
|
access: "create"
|
|
25304
26012
|
},
|
|
26013
|
+
"pipelineAnalytics.rebuildObjectEmbeddings": {
|
|
26014
|
+
capName: "pipeline-analytics",
|
|
26015
|
+
capScope: "device",
|
|
26016
|
+
addonId: null,
|
|
26017
|
+
access: "create"
|
|
26018
|
+
},
|
|
25305
26019
|
"pipelineAnalytics.relocateMedia": {
|
|
25306
26020
|
capName: "pipeline-analytics",
|
|
25307
26021
|
capScope: "device",
|
|
@@ -25314,12 +26028,24 @@ Object.freeze({
|
|
|
25314
26028
|
addonId: null,
|
|
25315
26029
|
access: "view"
|
|
25316
26030
|
},
|
|
26031
|
+
"pipelineAnalytics.setTrackFlags": {
|
|
26032
|
+
capName: "pipeline-analytics",
|
|
26033
|
+
capScope: "device",
|
|
26034
|
+
addonId: null,
|
|
26035
|
+
access: "create"
|
|
26036
|
+
},
|
|
25317
26037
|
"pipelineAnalytics.wipeAllAnalytics": {
|
|
25318
26038
|
capName: "pipeline-analytics",
|
|
25319
26039
|
capScope: "device",
|
|
25320
26040
|
addonId: null,
|
|
25321
26041
|
access: "delete"
|
|
25322
26042
|
},
|
|
26043
|
+
"pipelineAnalytics.wipeObjectEmbeddings": {
|
|
26044
|
+
capName: "pipeline-analytics",
|
|
26045
|
+
capScope: "device",
|
|
26046
|
+
addonId: null,
|
|
26047
|
+
access: "delete"
|
|
26048
|
+
},
|
|
25323
26049
|
"pipelineExecutor.cacheFrameInPool": {
|
|
25324
26050
|
capName: "pipeline-executor",
|
|
25325
26051
|
capScope: "system",
|
|
@@ -25614,6 +26340,12 @@ Object.freeze({
|
|
|
25614
26340
|
addonId: null,
|
|
25615
26341
|
access: "view"
|
|
25616
26342
|
},
|
|
26343
|
+
"pipelineOrchestrator.getCameraSwitches": {
|
|
26344
|
+
capName: "pipeline-orchestrator",
|
|
26345
|
+
capScope: "system",
|
|
26346
|
+
addonId: null,
|
|
26347
|
+
access: "view"
|
|
26348
|
+
},
|
|
25617
26349
|
"pipelineOrchestrator.getCapabilityBindings": {
|
|
25618
26350
|
capName: "pipeline-orchestrator",
|
|
25619
26351
|
capScope: "system",
|
|
@@ -25746,6 +26478,12 @@ Object.freeze({
|
|
|
25746
26478
|
addonId: null,
|
|
25747
26479
|
access: "create"
|
|
25748
26480
|
},
|
|
26481
|
+
"pipelineOrchestrator.setCameraSwitch": {
|
|
26482
|
+
capName: "pipeline-orchestrator",
|
|
26483
|
+
capScope: "system",
|
|
26484
|
+
addonId: null,
|
|
26485
|
+
access: "create"
|
|
26486
|
+
},
|
|
25749
26487
|
"pipelineOrchestrator.setCapabilityBinding": {
|
|
25750
26488
|
capName: "pipeline-orchestrator",
|
|
25751
26489
|
capScope: "system",
|
|
@@ -25836,6 +26574,12 @@ Object.freeze({
|
|
|
25836
26574
|
addonId: null,
|
|
25837
26575
|
access: "create"
|
|
25838
26576
|
},
|
|
26577
|
+
"pipelineRunner.runStatelessStep": {
|
|
26578
|
+
capName: "pipeline-runner",
|
|
26579
|
+
capScope: "system",
|
|
26580
|
+
addonId: null,
|
|
26581
|
+
access: "create"
|
|
26582
|
+
},
|
|
25839
26583
|
"plateGallery.assignPlate": {
|
|
25840
26584
|
capName: "plate-gallery",
|
|
25841
26585
|
capScope: "system",
|
|
@@ -25968,6 +26712,12 @@ Object.freeze({
|
|
|
25968
26712
|
addonId: null,
|
|
25969
26713
|
access: "view"
|
|
25970
26714
|
},
|
|
26715
|
+
"privacyMask.setAudioEnabled": {
|
|
26716
|
+
capName: "privacy-mask",
|
|
26717
|
+
capScope: "device",
|
|
26718
|
+
addonId: null,
|
|
26719
|
+
access: "create"
|
|
26720
|
+
},
|
|
25971
26721
|
"privacyMask.setMask": {
|
|
25972
26722
|
capName: "privacy-mask",
|
|
25973
26723
|
capScope: "device",
|
|
@@ -26136,6 +26886,12 @@ Object.freeze({
|
|
|
26136
26886
|
addonId: null,
|
|
26137
26887
|
access: "create"
|
|
26138
26888
|
},
|
|
26889
|
+
"recording.readGopBytes": {
|
|
26890
|
+
capName: "recording",
|
|
26891
|
+
capScope: "system",
|
|
26892
|
+
addonId: null,
|
|
26893
|
+
access: "view"
|
|
26894
|
+
},
|
|
26139
26895
|
"recording.readSegmentBytes": {
|
|
26140
26896
|
capName: "recording",
|
|
26141
26897
|
capScope: "system",
|
|
@@ -26652,6 +27408,12 @@ Object.freeze({
|
|
|
26652
27408
|
addonId: null,
|
|
26653
27409
|
access: "create"
|
|
26654
27410
|
},
|
|
27411
|
+
"streamBroker.acquireEgressTranscode": {
|
|
27412
|
+
capName: "stream-broker",
|
|
27413
|
+
capScope: "system",
|
|
27414
|
+
addonId: null,
|
|
27415
|
+
access: "create"
|
|
27416
|
+
},
|
|
26655
27417
|
"streamBroker.assignProfile": {
|
|
26656
27418
|
capName: "stream-broker",
|
|
26657
27419
|
capScope: "system",
|
|
@@ -26760,6 +27522,12 @@ Object.freeze({
|
|
|
26760
27522
|
addonId: null,
|
|
26761
27523
|
access: "create"
|
|
26762
27524
|
},
|
|
27525
|
+
"streamBroker.releaseEgressTranscode": {
|
|
27526
|
+
capName: "stream-broker",
|
|
27527
|
+
capScope: "system",
|
|
27528
|
+
addonId: null,
|
|
27529
|
+
access: "create"
|
|
27530
|
+
},
|
|
26763
27531
|
"streamBroker.releaseStreamWithCodec": {
|
|
26764
27532
|
capName: "stream-broker",
|
|
26765
27533
|
capScope: "system",
|
|
@@ -27240,6 +28008,12 @@ Object.freeze({
|
|
|
27240
28008
|
addonId: null,
|
|
27241
28009
|
access: "delete"
|
|
27242
28010
|
},
|
|
28011
|
+
"vectorStore.getByIds": {
|
|
28012
|
+
capName: "vector-store",
|
|
28013
|
+
capScope: "system",
|
|
28014
|
+
addonId: null,
|
|
28015
|
+
access: "view"
|
|
28016
|
+
},
|
|
27243
28017
|
"vectorStore.query": {
|
|
27244
28018
|
capName: "vector-store",
|
|
27245
28019
|
capScope: "system",
|
|
@@ -27514,6 +28288,112 @@ TimelapseRuleInputSchema.extend({
|
|
|
27514
28288
|
createdAt: number(),
|
|
27515
28289
|
updatedAt: number()
|
|
27516
28290
|
});
|
|
28291
|
+
object({
|
|
28292
|
+
/**
|
|
28293
|
+
* Fraction of the box's own size added on EACH side before cutting.
|
|
28294
|
+
*
|
|
28295
|
+
* CLIP is trained on natural images WITH surroundings; a pixel-tight crop
|
|
28296
|
+
* removes exactly the context it is strongest on (a dog cut to its outline
|
|
28297
|
+
* is a dark blob). The right value is an empirical question, which is why it
|
|
28298
|
+
* is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
|
|
28299
|
+
*/
|
|
28300
|
+
paddingRatio: number().min(0).max(4),
|
|
28301
|
+
/**
|
|
28302
|
+
* Square the window (in PIXELS) before cutting.
|
|
28303
|
+
*
|
|
28304
|
+
* CLIP's input is square, so a tall bbox resized straight to NxN is squashed
|
|
28305
|
+
* — a standing person becomes a shape the model never saw. Squaring costs
|
|
28306
|
+
* extra background, which is context the model wants anyway. Off by default
|
|
28307
|
+
* because the live path has never squared and the stored index reflects that.
|
|
28308
|
+
*/
|
|
28309
|
+
square: boolean()
|
|
28310
|
+
});
|
|
28311
|
+
({
|
|
28312
|
+
paddingRatio: .15,
|
|
28313
|
+
square: false
|
|
28314
|
+
}).paddingRatio;
|
|
28315
|
+
/**
|
|
28316
|
+
* WHICH delivered frames the decode worker retains a native copy of.
|
|
28317
|
+
*
|
|
28318
|
+
* - `all` — every frame the worker delivered to the runner. The shipped
|
|
28319
|
+
* behaviour, and the only correct one if something can ask for a crop of a
|
|
28320
|
+
* frame the runner never sent to inference.
|
|
28321
|
+
* - `inferred` — only the frames the runner ADMITTED to its detection queue.
|
|
28322
|
+
* A native-crop request always names a `frameId` that rode an inference
|
|
28323
|
+
* result, so that is the only set a request can name. How much it drops is
|
|
28324
|
+
* the two-plane governor's admit ratio and nothing else: measured at ~50% on
|
|
28325
|
+
* this cluster, not the ~80% the design sketch assumed, because the governor
|
|
28326
|
+
* was not throttling as hard as the sketch supposed. Read
|
|
28327
|
+
* `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
|
|
28328
|
+
* of you rather than quoting a number from here. The newest delivered frame is
|
|
28329
|
+
* croppable regardless — it is still the worker's reserved slot, not a lease —
|
|
28330
|
+
* which covers the one-frame race between a mark and the supersede that
|
|
28331
|
+
* consumes it.
|
|
28332
|
+
*/
|
|
28333
|
+
var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
|
|
28334
|
+
object({
|
|
28335
|
+
/**
|
|
28336
|
+
* How long a retained native frame is served before it counts as a miss.
|
|
28337
|
+
*
|
|
28338
|
+
* Must cover the FULL late-crop horizon: detection inference + the
|
|
28339
|
+
* cross-process inference-result hop to hub post-analysis + tracking + the
|
|
28340
|
+
* tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
|
|
28341
|
+
* outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
|
|
28342
|
+
* RAM per busy camera grows linearly with no measured hit-rate gain.
|
|
28343
|
+
*/
|
|
28344
|
+
ttlMs: number().int().min(250).max(1e4),
|
|
28345
|
+
/**
|
|
28346
|
+
* Hard per-decode-worker RAM ceiling for retained native frames, in MB.
|
|
28347
|
+
*
|
|
28348
|
+
* Intended as a SAFETY ceiling with the TTL as the effective cap — but check
|
|
28349
|
+
* which one is actually binding before reasoning from that. At the shipped
|
|
28350
|
+
* 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
|
|
28351
|
+
* at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
|
|
28352
|
+
* `leaseFrames` on the metrics line say which. When the ceiling binds, a
|
|
28353
|
+
* change that admits fewer frames buys retention WINDOW at constant RAM
|
|
28354
|
+
* rather than giving RAM back — lower this knob if RAM is what you wanted.
|
|
28355
|
+
* `0` DISABLES the lease entirely and falls the worker back to the tiny
|
|
28356
|
+
* leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
|
|
28357
|
+
* to replace).
|
|
28358
|
+
*/
|
|
28359
|
+
budgetMb: number().int().min(0).max(4096),
|
|
28360
|
+
/**
|
|
28361
|
+
* Demand window: eager per-frame native retention runs only within this many
|
|
28362
|
+
* ms of the last native-crop request (or of the dial starting).
|
|
28363
|
+
*
|
|
28364
|
+
* `0` means ALWAYS ON — it disables the gate, it does not disable retention.
|
|
28365
|
+
* That is the legacy behaviour that saturated an N100 (24 native-4K downloads
|
|
28366
|
+
* per second on a camera with zero crop demand), so leave it non-zero unless
|
|
28367
|
+
* you are reproducing that.
|
|
28368
|
+
*/
|
|
28369
|
+
activityMs: number().int().min(0).max(12e4),
|
|
28370
|
+
/**
|
|
28371
|
+
* Which delivered frames are retained at all — see
|
|
28372
|
+
* {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
|
|
28373
|
+
* changes WHAT is kept rather than for how long, so it is also the only one
|
|
28374
|
+
* that can turn a crop that used to hit into a miss. The worker counts every
|
|
28375
|
+
* crop request naming a frame it did NOT see marked
|
|
28376
|
+
* (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
|
|
28377
|
+
* there is the signal that some caller names frames outside the inference set
|
|
28378
|
+
* and that this must go back to `all`.
|
|
28379
|
+
*/
|
|
28380
|
+
admission: NativeLeaseAdmissionSchema
|
|
28381
|
+
});
|
|
28382
|
+
/**
|
|
28383
|
+
* The values in force when the operator has set nothing — byte-for-byte the
|
|
28384
|
+
* constants the decode worker shipped with as env-var defaults, so making these
|
|
28385
|
+
* settings changed no behaviour on the day it landed.
|
|
28386
|
+
*/
|
|
28387
|
+
var DEFAULT_NATIVE_LEASE_SETTINGS = {
|
|
28388
|
+
ttlMs: 1200,
|
|
28389
|
+
budgetMb: 1024,
|
|
28390
|
+
activityMs: 15e3,
|
|
28391
|
+
admission: "inferred"
|
|
28392
|
+
};
|
|
28393
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
|
|
28394
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
28395
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
28396
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
27517
28397
|
//#endregion
|
|
27518
28398
|
//#region src/server/addon.ts
|
|
27519
28399
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -27533,7 +28413,7 @@ var AdminUIAddon = class extends BaseAddon {
|
|
|
27533
28413
|
capability: adminUiCapability,
|
|
27534
28414
|
provider: {
|
|
27535
28415
|
getStaticDir: async () => ({ staticDir: resolveAdminUiDistDir() }),
|
|
27536
|
-
getVersion: async () => ({ version: "1.2.
|
|
28416
|
+
getVersion: async () => ({ version: "1.2.33" })
|
|
27537
28417
|
}
|
|
27538
28418
|
}];
|
|
27539
28419
|
}
|