@camstack/addon-provider-hikvision 1.2.30 → 1.2.32
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/addon.js +420 -95
- package/dist/addon.mjs +420 -95
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -7562,7 +7562,7 @@ var RecordingBandSchema = object({
|
|
|
7562
7562
|
postBufferSec: number().min(0).optional()
|
|
7563
7563
|
});
|
|
7564
7564
|
({
|
|
7565
|
-
preBufferSec:
|
|
7565
|
+
preBufferSec: 15,
|
|
7566
7566
|
postBufferSec: 30
|
|
7567
7567
|
}).postBufferSec * 1e3;
|
|
7568
7568
|
/**
|
|
@@ -13952,128 +13952,103 @@ var COCO_TO_MACRO = {
|
|
|
13952
13952
|
var AUDIO_MACRO_LABELS = [
|
|
13953
13953
|
{
|
|
13954
13954
|
id: "speech",
|
|
13955
|
-
name: "Speech"
|
|
13956
|
-
icon: "🗣️"
|
|
13955
|
+
name: "Speech"
|
|
13957
13956
|
},
|
|
13958
13957
|
{
|
|
13959
13958
|
id: "scream",
|
|
13960
|
-
name: "Scream / Shout"
|
|
13961
|
-
icon: "😱"
|
|
13959
|
+
name: "Scream / Shout"
|
|
13962
13960
|
},
|
|
13963
13961
|
{
|
|
13964
13962
|
id: "crying",
|
|
13965
|
-
name: "Crying / Baby"
|
|
13966
|
-
icon: "😢"
|
|
13963
|
+
name: "Crying / Baby"
|
|
13967
13964
|
},
|
|
13968
13965
|
{
|
|
13969
13966
|
id: "laughter",
|
|
13970
|
-
name: "Laughter"
|
|
13971
|
-
icon: "😂"
|
|
13967
|
+
name: "Laughter"
|
|
13972
13968
|
},
|
|
13973
13969
|
{
|
|
13974
13970
|
id: "music",
|
|
13975
|
-
name: "Music"
|
|
13976
|
-
icon: "🎵"
|
|
13971
|
+
name: "Music"
|
|
13977
13972
|
},
|
|
13978
13973
|
{
|
|
13979
13974
|
id: "dog",
|
|
13980
|
-
name: "Dog"
|
|
13981
|
-
icon: "🐕"
|
|
13975
|
+
name: "Dog"
|
|
13982
13976
|
},
|
|
13983
13977
|
{
|
|
13984
13978
|
id: "cat",
|
|
13985
|
-
name: "Cat"
|
|
13986
|
-
icon: "🐈"
|
|
13979
|
+
name: "Cat"
|
|
13987
13980
|
},
|
|
13988
13981
|
{
|
|
13989
13982
|
id: "bird",
|
|
13990
|
-
name: "Bird"
|
|
13991
|
-
icon: "🐦"
|
|
13983
|
+
name: "Bird"
|
|
13992
13984
|
},
|
|
13993
13985
|
{
|
|
13994
13986
|
id: "animal",
|
|
13995
|
-
name: "Animal (other)"
|
|
13996
|
-
icon: "🐾"
|
|
13987
|
+
name: "Animal (other)"
|
|
13997
13988
|
},
|
|
13998
13989
|
{
|
|
13999
13990
|
id: "alarm",
|
|
14000
|
-
name: "Alarm / Siren"
|
|
14001
|
-
icon: "🚨"
|
|
13991
|
+
name: "Alarm / Siren"
|
|
14002
13992
|
},
|
|
14003
13993
|
{
|
|
14004
13994
|
id: "doorbell",
|
|
14005
|
-
name: "Doorbell / Knock"
|
|
14006
|
-
icon: "🔔"
|
|
13995
|
+
name: "Doorbell / Knock"
|
|
14007
13996
|
},
|
|
14008
13997
|
{
|
|
14009
13998
|
id: "glass_breaking",
|
|
14010
|
-
name: "Glass Breaking"
|
|
14011
|
-
icon: "💥"
|
|
13999
|
+
name: "Glass Breaking"
|
|
14012
14000
|
},
|
|
14013
14001
|
{
|
|
14014
14002
|
id: "gunshot",
|
|
14015
|
-
name: "Gunshot / Explosion"
|
|
14016
|
-
icon: "💣"
|
|
14003
|
+
name: "Gunshot / Explosion"
|
|
14017
14004
|
},
|
|
14018
14005
|
{
|
|
14019
14006
|
id: "vehicle",
|
|
14020
|
-
name: "Vehicle"
|
|
14021
|
-
icon: "🚗"
|
|
14007
|
+
name: "Vehicle"
|
|
14022
14008
|
},
|
|
14023
14009
|
{
|
|
14024
14010
|
id: "siren",
|
|
14025
|
-
name: "Emergency Siren"
|
|
14026
|
-
icon: "🚑"
|
|
14011
|
+
name: "Emergency Siren"
|
|
14027
14012
|
},
|
|
14028
14013
|
{
|
|
14029
14014
|
id: "fire",
|
|
14030
|
-
name: "Fire / Smoke"
|
|
14031
|
-
icon: "🔥"
|
|
14015
|
+
name: "Fire / Smoke"
|
|
14032
14016
|
},
|
|
14033
14017
|
{
|
|
14034
14018
|
id: "water",
|
|
14035
|
-
name: "Water"
|
|
14036
|
-
icon: "💧"
|
|
14019
|
+
name: "Water"
|
|
14037
14020
|
},
|
|
14038
14021
|
{
|
|
14039
14022
|
id: "wind",
|
|
14040
|
-
name: "Wind / Weather"
|
|
14041
|
-
icon: "🌬️"
|
|
14023
|
+
name: "Wind / Weather"
|
|
14042
14024
|
},
|
|
14043
14025
|
{
|
|
14044
14026
|
id: "door",
|
|
14045
|
-
name: "Door"
|
|
14046
|
-
icon: "🚪"
|
|
14027
|
+
name: "Door"
|
|
14047
14028
|
},
|
|
14048
14029
|
{
|
|
14049
14030
|
id: "footsteps",
|
|
14050
|
-
name: "Footsteps"
|
|
14051
|
-
icon: "👣"
|
|
14031
|
+
name: "Footsteps"
|
|
14052
14032
|
},
|
|
14053
14033
|
{
|
|
14054
14034
|
id: "crowd",
|
|
14055
|
-
name: "Crowd / Chatter"
|
|
14056
|
-
icon: "👥"
|
|
14035
|
+
name: "Crowd / Chatter"
|
|
14057
14036
|
},
|
|
14058
14037
|
{
|
|
14059
14038
|
id: "telephone",
|
|
14060
|
-
name: "Telephone"
|
|
14061
|
-
icon: "📞"
|
|
14039
|
+
name: "Telephone"
|
|
14062
14040
|
},
|
|
14063
14041
|
{
|
|
14064
14042
|
id: "engine",
|
|
14065
|
-
name: "Engine / Motor"
|
|
14066
|
-
icon: "⚙️"
|
|
14043
|
+
name: "Engine / Motor"
|
|
14067
14044
|
},
|
|
14068
14045
|
{
|
|
14069
14046
|
id: "tools",
|
|
14070
|
-
name: "Tools / Construction"
|
|
14071
|
-
icon: "🔨"
|
|
14047
|
+
name: "Tools / Construction"
|
|
14072
14048
|
},
|
|
14073
14049
|
{
|
|
14074
14050
|
id: "silence",
|
|
14075
|
-
name: "Silence"
|
|
14076
|
-
icon: "🤫"
|
|
14051
|
+
name: "Silence"
|
|
14077
14052
|
}
|
|
14078
14053
|
];
|
|
14079
14054
|
var YAMNET_TO_MACRO = {
|
|
@@ -16575,7 +16550,7 @@ var TrackEnvelopeSchema = object({
|
|
|
16575
16550
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
16576
16551
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
16577
16552
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
16578
|
-
* zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
|
|
16553
|
+
* zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
|
|
16579
16554
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
16580
16555
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
16581
16556
|
* (`getObjectEvents` et al.).
|
|
@@ -16870,6 +16845,35 @@ var TrackSchema = object({
|
|
|
16870
16845
|
*/
|
|
16871
16846
|
hasFace: boolean().optional(),
|
|
16872
16847
|
/**
|
|
16848
|
+
* This track has a face row IN THE GALLERY: a crop **and** an embedding — a
|
|
16849
|
+
* face an operator could ASSIGN to an identity.
|
|
16850
|
+
*
|
|
16851
|
+
* The STRICT twin of {@link hasFace}, and the pair only earns its keep
|
|
16852
|
+
* because the two disagree. `hasFace` is stamped at the TOP of the face
|
|
16853
|
+
* branch, before every gate, and means no more than "a face detector produced
|
|
16854
|
+
* a face detail". This one is stamped at the single moment the gallery row
|
|
16855
|
+
* LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
|
|
16856
|
+
* past the embedding-magnitude verdict, the `minFacePx` detection gate, the
|
|
16857
|
+
* candidate gate, the imageless-track drop (no crop was ever captured) and
|
|
16858
|
+
* the crop-store drop. Everything between the detector and that insert can
|
|
16859
|
+
* legitimately refuse the face, so a flag written any earlier promises the
|
|
16860
|
+
* operator something to assign and delivers nothing.
|
|
16861
|
+
*
|
|
16862
|
+
* **Independent of recognition.** A face collected but never auto-matched is
|
|
16863
|
+
* still assignable — it is in fact the face an operator most wants to reach —
|
|
16864
|
+
* so this is NOT gated on `recognizedIdentityId`. Recognition lands in
|
|
16865
|
+
* `subLabel`; this says only that the raw material exists.
|
|
16866
|
+
*
|
|
16867
|
+
* **Set once, never cleared.** A track that produced a gallery row produced
|
|
16868
|
+
* one; deleting the row later is the gallery's business, not this flag's.
|
|
16869
|
+
*
|
|
16870
|
+
* **Absent ≠ false**, the same rule as {@link hasFace}: every row written
|
|
16871
|
+
* before the column omits it, and so does every server that predates the
|
|
16872
|
+
* field. A consumer must test `=== true` and render nothing otherwise —
|
|
16873
|
+
* never infer "no assignable face".
|
|
16874
|
+
*/
|
|
16875
|
+
hasEmbeddedFace: boolean().optional(),
|
|
16876
|
+
/**
|
|
16873
16877
|
* This subject CONTAINS a folded rider — a person the rider-pairing step
|
|
16874
16878
|
* ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
|
|
16875
16879
|
* so the passage is tracked once and as a VEHICLE.
|
|
@@ -18259,6 +18263,32 @@ var DetailResultSchema = object({
|
|
|
18259
18263
|
/** FRAME-space bbox (already mapped back from crop space). */
|
|
18260
18264
|
bbox: NativeCropBboxSchema.optional(),
|
|
18261
18265
|
embedding: string().optional(),
|
|
18266
|
+
/**
|
|
18267
|
+
* L2 magnitude of the RAW pre-normalization embedding. Additive and optional
|
|
18268
|
+
* so an older runner that does not send it degrades to "unmeasurable", which
|
|
18269
|
+
* the consuming gate treats as accept — never as reject.
|
|
18270
|
+
*/
|
|
18271
|
+
embeddingMagnitude: number().optional(),
|
|
18272
|
+
/**
|
|
18273
|
+
* Model that produced `embedding` — the SAME contract `labelModelId` has for
|
|
18274
|
+
* `label`, and for the same reason: the producing node is the only side that
|
|
18275
|
+
* knows which model actually ran, and re-deriving it downstream is exactly
|
|
18276
|
+
* the inference this model exists to forbid.
|
|
18277
|
+
*
|
|
18278
|
+
* Additive and optional, so a runner that predates it degrades to
|
|
18279
|
+
* "unmeasurable" rather than to a wrong answer — the consumer falls back to
|
|
18280
|
+
* its previous assumption instead of refusing.
|
|
18281
|
+
*
|
|
18282
|
+
* Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
|
|
18283
|
+
* to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
|
|
18284
|
+
* with nothing on the wire it took the id from the GALLERY, so the gate
|
|
18285
|
+
* compared the gallery against itself and could never fire. That turned the
|
|
18286
|
+
* one guard protecting the face feature space into a tautology, and it is
|
|
18287
|
+
* why swapping `face-embedding`'s model would NOT have stranded the enrolled
|
|
18288
|
+
* gallery as the catalog comment predicts — it would have scored the new
|
|
18289
|
+
* model's vectors against the old model's samples in one cosine space.
|
|
18290
|
+
*/
|
|
18291
|
+
embeddingModelId: string().optional(),
|
|
18262
18292
|
label: string().optional(),
|
|
18263
18293
|
/**
|
|
18264
18294
|
* The tier `label` occupies, copied VERBATIM from the producing step's
|
|
@@ -19134,6 +19164,12 @@ var CameraStatusSchema = object({
|
|
|
19134
19164
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
19135
19165
|
fetchedAt: number()
|
|
19136
19166
|
});
|
|
19167
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
19168
|
+
"disabled",
|
|
19169
|
+
"unavailable",
|
|
19170
|
+
"cannot-host-camera-root",
|
|
19171
|
+
"accelerator-preferred"
|
|
19172
|
+
]);
|
|
19137
19173
|
var NodeInferenceDeviceSchema = object({
|
|
19138
19174
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
19139
19175
|
key: string(),
|
|
@@ -19164,7 +19200,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
19164
19200
|
* available per format; this is the stored selection that becomes the
|
|
19165
19201
|
* default for EVERY camera landing on this accelerator.
|
|
19166
19202
|
*/
|
|
19167
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
19203
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
19204
|
+
/**
|
|
19205
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
19206
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
19207
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
19208
|
+
* never disagree with the election — deriving it in the UI from
|
|
19209
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
19210
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
19211
|
+
* "an accelerator is serving" predicate).
|
|
19212
|
+
*/
|
|
19213
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
19168
19214
|
});
|
|
19169
19215
|
var NodeInferenceDevicesSchema = object({
|
|
19170
19216
|
nodeId: string(),
|
|
@@ -19723,6 +19769,7 @@ var snapshotCapability = {
|
|
|
19723
19769
|
isBattery: boolean(),
|
|
19724
19770
|
reason: _enum([
|
|
19725
19771
|
"disabled",
|
|
19772
|
+
"offline",
|
|
19726
19773
|
"sleeping",
|
|
19727
19774
|
"unreachable",
|
|
19728
19775
|
"waking"
|
|
@@ -19812,9 +19859,17 @@ targets: array(object({
|
|
|
19812
19859
|
sleeping: boolean(),
|
|
19813
19860
|
/** Current device state rendered over the cached frame. State images
|
|
19814
19861
|
* remain authoritative even when their photographic background is
|
|
19815
|
-
* old; null means the link must carry a current camera frame.
|
|
19862
|
+
* old; null means the link must carry a current camera frame.
|
|
19863
|
+
*
|
|
19864
|
+
* **This is the ONLY source a surface may derive an inactive tile
|
|
19865
|
+
* from.** A client that reads a device's `online`/`disabled` flags and
|
|
19866
|
+
* draws its own verdict will disagree with the pixels it is painting
|
|
19867
|
+
* — the frame comes from here, and so must the sentence over it
|
|
19868
|
+
* (D190). Pair with `ageMs` for the age of the photograph behind the
|
|
19869
|
+
* state. */
|
|
19816
19870
|
stateReason: _enum([
|
|
19817
19871
|
"disabled",
|
|
19872
|
+
"offline",
|
|
19818
19873
|
"sleeping",
|
|
19819
19874
|
"unreachable",
|
|
19820
19875
|
"waking"
|
|
@@ -24110,7 +24165,12 @@ var ListResultSchema = object({
|
|
|
24110
24165
|
probedAt: number()
|
|
24111
24166
|
});
|
|
24112
24167
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
24113
|
-
|
|
24168
|
+
/**
|
|
24169
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
24170
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
24171
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
24172
|
+
*/
|
|
24173
|
+
var ConnectionEndpointSchema = object({
|
|
24114
24174
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
24115
24175
|
label: string(),
|
|
24116
24176
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -24153,7 +24213,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
24153
24213
|
* ordering between polls.
|
|
24154
24214
|
*/
|
|
24155
24215
|
priority: number()
|
|
24156
|
-
})
|
|
24216
|
+
});
|
|
24217
|
+
/**
|
|
24218
|
+
* Where the advertised local port came from. Ordered most → least
|
|
24219
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
24220
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
24221
|
+
*/
|
|
24222
|
+
var LocalPortSourceEnum = _enum([
|
|
24223
|
+
"server-config",
|
|
24224
|
+
"server-env",
|
|
24225
|
+
"caller-hint",
|
|
24226
|
+
"default"
|
|
24227
|
+
]);
|
|
24228
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
24229
|
+
var AdvertisedLocalPortSchema = object({
|
|
24230
|
+
port: number().int().min(1).max(65535),
|
|
24231
|
+
source: LocalPortSourceEnum
|
|
24232
|
+
});
|
|
24233
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
24234
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
24235
|
+
/**
|
|
24236
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
24237
|
+
* came from.
|
|
24238
|
+
*
|
|
24239
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
24240
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
24241
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
24242
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
24243
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
24244
|
+
* `caller-hint` is the hub's own socket.
|
|
24245
|
+
*
|
|
24246
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
24247
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
24248
|
+
* `caller-hint`.
|
|
24249
|
+
*/
|
|
24250
|
+
localPort: AdvertisedLocalPortSchema
|
|
24251
|
+
});
|
|
24157
24252
|
/**
|
|
24158
24253
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
24159
24254
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -24174,8 +24269,13 @@ var AllowedAddressesSchema = object({
|
|
|
24174
24269
|
*/
|
|
24175
24270
|
addresses: array(string()).readonly() });
|
|
24176
24271
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
24177
|
-
/**
|
|
24178
|
-
|
|
24272
|
+
/**
|
|
24273
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
24274
|
+
* written against the echoing contract keep working; the hub uses it
|
|
24275
|
+
* only when it cannot read its own port, and says so via
|
|
24276
|
+
* `localPort.source === 'caller-hint'`.
|
|
24277
|
+
*/
|
|
24278
|
+
port: number().int().min(1).max(65535).optional(),
|
|
24179
24279
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24180
24280
|
* candidate. Default `true`. */
|
|
24181
24281
|
includeLoopback: boolean().optional(),
|
|
@@ -30394,11 +30494,63 @@ function startReachabilityPoll(options) {
|
|
|
30394
30494
|
} };
|
|
30395
30495
|
}
|
|
30396
30496
|
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
30497
|
+
/**
|
|
30498
|
+
* How long a bridge stops re-attempting a refresh that did not land.
|
|
30499
|
+
*
|
|
30500
|
+
* Sized against the failure it exists for: a camera whose control plane
|
|
30501
|
+
* is unreachable costs the FULL connect give-up (~3s on Linux when the
|
|
30502
|
+
* neighbour never answers, up to the client's own timeout otherwise) on
|
|
30503
|
+
* every attempt, and a failed refresh never advances `lastFetchedAt`, so
|
|
30504
|
+
* without a cooldown the slice is permanently stale and EVERY read pays
|
|
30505
|
+
* that price. One attempt per minute is enough to notice the camera
|
|
30506
|
+
* coming back; per-read is enough to stall the viewer's first paint.
|
|
30507
|
+
*/
|
|
30508
|
+
var RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS = 6e4;
|
|
30509
|
+
/**
|
|
30510
|
+
* The freshness window for a cap whose value changes ONLY when WE write it.
|
|
30511
|
+
*
|
|
30512
|
+
* The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
|
|
30513
|
+
* battery level, a day/night state that flips at dusk. It is the wrong number
|
|
30514
|
+
* for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
|
|
30515
|
+
* microphone on) change when an operator changes them, through a `setMask` /
|
|
30516
|
+
* `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
|
|
30517
|
+
* A write is therefore its own invalidation: the window never delays an
|
|
30518
|
+
* operator's own change, however long it is.
|
|
30519
|
+
*
|
|
30520
|
+
* What the window really bounds is the ONE case we do not write: somebody
|
|
30521
|
+
* changing the mask in the vendor app or the camera's web UI. Ten minutes is
|
|
30522
|
+
* the trade — that change is visible within ten minutes, and a fleet of 29
|
|
30523
|
+
* cameras behind a page that polls every 5s costs about 3 camera round trips a
|
|
30524
|
+
* minute instead of 696
|
|
30525
|
+
* ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
|
|
30526
|
+
*
|
|
30527
|
+
* Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
|
|
30528
|
+
* On the awaiting default a window this long does not remove the stall, it
|
|
30529
|
+
* merely makes it rarer and just as long.
|
|
30530
|
+
*/
|
|
30531
|
+
var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
|
|
30397
30532
|
function createRuntimeStateBridge(params) {
|
|
30398
30533
|
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
|
|
30399
30534
|
const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
|
|
30535
|
+
const staleRead = params.staleRead ?? "await-refresh";
|
|
30400
30536
|
/** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
|
|
30401
30537
|
let missCooldownUntil = 0;
|
|
30538
|
+
/**
|
|
30539
|
+
* The refresh this bridge currently has in the air, if any.
|
|
30540
|
+
*
|
|
30541
|
+
* Providers single-flight their own camera client, so this is not what stops
|
|
30542
|
+
* a second round trip. What it stops is a POLLED reader JOINING one: under
|
|
30543
|
+
* `'serve-and-revalidate'` a read that finds a refresh already outstanding is
|
|
30544
|
+
* answered from the slice at once — even a cold, empty slice, which reports
|
|
30545
|
+
* UNKNOWN, which is the truth about a camera nobody has reached.
|
|
30546
|
+
*
|
|
30547
|
+
* That distinction is the whole cost of an unreachable camera. Measured on
|
|
30548
|
+
* the live hub (device 3629, an offline battery Reolink): its refresh takes
|
|
30549
|
+
* 23.1s to give up, so without this every 5s poll landing inside those 23s
|
|
30550
|
+
* joined the wait and spent the caller's full 1.2s source budget — five
|
|
30551
|
+
* stalled polls per cooldown cycle, for one camera, forever.
|
|
30552
|
+
*/
|
|
30553
|
+
let refreshInFlight = null;
|
|
30402
30554
|
const readFetchedAt = () => {
|
|
30403
30555
|
const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
|
|
30404
30556
|
return typeof value === "number" ? value : 0;
|
|
@@ -30419,14 +30571,14 @@ function createRuntimeStateBridge(params) {
|
|
|
30419
30571
|
}
|
|
30420
30572
|
});
|
|
30421
30573
|
};
|
|
30422
|
-
|
|
30423
|
-
|
|
30424
|
-
|
|
30425
|
-
|
|
30426
|
-
|
|
30427
|
-
|
|
30428
|
-
|
|
30429
|
-
|
|
30574
|
+
/**
|
|
30575
|
+
* One refresh attempt, plus the LANDED check that decides the cooldown.
|
|
30576
|
+
*
|
|
30577
|
+
* @param fetchedAt What `lastFetchedAt` was before the attempt — the only
|
|
30578
|
+
* evidence the bridge has that the refresh persisted
|
|
30579
|
+
* anything, since providers swallow their own camera errors.
|
|
30580
|
+
*/
|
|
30581
|
+
const runRefresh = async (fetchedAt) => {
|
|
30430
30582
|
try {
|
|
30431
30583
|
await refresh();
|
|
30432
30584
|
} catch (err) {
|
|
@@ -30442,6 +30594,41 @@ function createRuntimeStateBridge(params) {
|
|
|
30442
30594
|
}
|
|
30443
30595
|
openMissCooldown(void 0);
|
|
30444
30596
|
};
|
|
30597
|
+
/**
|
|
30598
|
+
* Start a refresh and remember it, at most one at a time.
|
|
30599
|
+
*
|
|
30600
|
+
* It never rejects: under `'serve-and-revalidate'` the caller is answered
|
|
30601
|
+
* from the slice either way, so a floating rejection would take the process
|
|
30602
|
+
* down for a fault the miss cooldown has already recorded and logged.
|
|
30603
|
+
*/
|
|
30604
|
+
const startRefresh = (fetchedAt) => {
|
|
30605
|
+
const existing = refreshInFlight;
|
|
30606
|
+
if (existing !== null) return existing;
|
|
30607
|
+
const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
|
|
30608
|
+
refreshInFlight = null;
|
|
30609
|
+
});
|
|
30610
|
+
refreshInFlight = started;
|
|
30611
|
+
return started;
|
|
30612
|
+
};
|
|
30613
|
+
const ensureFresh = async () => {
|
|
30614
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
30615
|
+
const fetchedAt = readFetchedAt();
|
|
30616
|
+
if (slice && Date.now() - fetchedAt <= staleMs) {
|
|
30617
|
+
missCooldownUntil = 0;
|
|
30618
|
+
return;
|
|
30619
|
+
}
|
|
30620
|
+
if (Date.now() < missCooldownUntil) return;
|
|
30621
|
+
if (staleRead === "serve-and-revalidate") {
|
|
30622
|
+
if (refreshInFlight !== null) return;
|
|
30623
|
+
if (slice && fetchedAt > 0) {
|
|
30624
|
+
startRefresh(fetchedAt);
|
|
30625
|
+
return;
|
|
30626
|
+
}
|
|
30627
|
+
await startRefresh(fetchedAt);
|
|
30628
|
+
return;
|
|
30629
|
+
}
|
|
30630
|
+
await runRefresh(fetchedAt);
|
|
30631
|
+
};
|
|
30445
30632
|
const projectStatus = () => {
|
|
30446
30633
|
const slice = runtimeState.getCapState(cap.name);
|
|
30447
30634
|
if (!slice) return empty();
|
|
@@ -37840,7 +38027,6 @@ var NC_AUDIO_DEFAULTS = {
|
|
|
37840
38027
|
hitPercent: 60,
|
|
37841
38028
|
samplingSeconds: 10
|
|
37842
38029
|
};
|
|
37843
|
-
new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
|
|
37844
38030
|
new Set(["devices", "classes"]);
|
|
37845
38031
|
NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
|
|
37846
38032
|
/**
|
|
@@ -38074,6 +38260,39 @@ TimelapseRuleInputSchema.extend({
|
|
|
38074
38260
|
createdAt: number(),
|
|
38075
38261
|
updatedAt: number()
|
|
38076
38262
|
});
|
|
38263
|
+
Object.freeze(Object.fromEntries([{
|
|
38264
|
+
stepId: "face-embedding",
|
|
38265
|
+
label: "Face recognition model",
|
|
38266
|
+
defaultModelId: "arcface-r100",
|
|
38267
|
+
indexName: "the enrolled face gallery",
|
|
38268
|
+
options: [
|
|
38269
|
+
{
|
|
38270
|
+
id: "arcface-r100",
|
|
38271
|
+
label: "ArcFace (ResNet34)"
|
|
38272
|
+
},
|
|
38273
|
+
{
|
|
38274
|
+
id: "auraface-r100",
|
|
38275
|
+
label: "AuraFace R100"
|
|
38276
|
+
},
|
|
38277
|
+
{
|
|
38278
|
+
id: "inception-resnet-v1",
|
|
38279
|
+
label: "Inception ResNet V1"
|
|
38280
|
+
}
|
|
38281
|
+
]
|
|
38282
|
+
}, {
|
|
38283
|
+
stepId: "clip-embedding",
|
|
38284
|
+
label: "Semantic search model",
|
|
38285
|
+
defaultModelId: "mobileclip-s1",
|
|
38286
|
+
indexName: "the object semantic-search index",
|
|
38287
|
+
options: [{
|
|
38288
|
+
id: "mobileclip-s1",
|
|
38289
|
+
label: "MobileCLIP S1"
|
|
38290
|
+
}, {
|
|
38291
|
+
id: "mobileclip-s2",
|
|
38292
|
+
label: "MobileCLIP S2"
|
|
38293
|
+
}]
|
|
38294
|
+
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
38295
|
+
string().min(1);
|
|
38077
38296
|
object({
|
|
38078
38297
|
/**
|
|
38079
38298
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -38094,10 +38313,21 @@ object({
|
|
|
38094
38313
|
*/
|
|
38095
38314
|
square: boolean()
|
|
38096
38315
|
});
|
|
38097
|
-
|
|
38316
|
+
/**
|
|
38317
|
+
* The convention in force when nobody has configured one — byte-for-byte the
|
|
38318
|
+
* behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
|
|
38319
|
+
*/
|
|
38320
|
+
var DEFAULT_DETAIL_CROP_CONVENTION = {
|
|
38098
38321
|
paddingRatio: .15,
|
|
38099
38322
|
square: false
|
|
38100
|
-
}
|
|
38323
|
+
};
|
|
38324
|
+
Object.freeze({
|
|
38325
|
+
x: 0,
|
|
38326
|
+
y: 0,
|
|
38327
|
+
w: 1,
|
|
38328
|
+
h: 1
|
|
38329
|
+
});
|
|
38330
|
+
DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
|
|
38101
38331
|
/**
|
|
38102
38332
|
* WHICH delivered frames the decode worker retains a native copy of.
|
|
38103
38333
|
*
|
|
@@ -42255,6 +42485,29 @@ var HikvisionDeviceCacheSchema = object({
|
|
|
42255
42485
|
* `DeviceFeature.TwoWayAudio` + the `intercom` cap registration.
|
|
42256
42486
|
*/
|
|
42257
42487
|
hasIntercom: boolean().optional(),
|
|
42488
|
+
/**
|
|
42489
|
+
* The `privacy-mask` cap's per-camera AVAILABILITY facts, as the last
|
|
42490
|
+
* successful read saw them.
|
|
42491
|
+
*
|
|
42492
|
+
* Persisted for the same reason Reolink persists its `capOptionsSnapshot`:
|
|
42493
|
+
* `privacyMask.getOptions` sits on a POLLED path (the admin Cameras badge
|
|
42494
|
+
* asks it per camera every 5s), and these describe what the model can do —
|
|
42495
|
+
* they change on a firmware update, not during operation. Reading them from
|
|
42496
|
+
* here costs no ISAPI round trip, and it survives an addon restart, which
|
|
42497
|
+
* an in-process value does not.
|
|
42498
|
+
*
|
|
42499
|
+
* They come out of the SAME two reads that build the cap's status slice, so
|
|
42500
|
+
* they cannot drift from it.
|
|
42501
|
+
*/
|
|
42502
|
+
privacyMaskOptions: object({
|
|
42503
|
+
/** Zones the firmware advertises. */
|
|
42504
|
+
maxRegions: number().int().nonnegative(),
|
|
42505
|
+
/** True only when a streaming channel reported an audio flag we can
|
|
42506
|
+
* patch — a switch that writes nothing must never be offered. */
|
|
42507
|
+
supportsAudioMute: boolean(),
|
|
42508
|
+
/** Epoch ms of the read that produced this. */
|
|
42509
|
+
fetchedAt: number()
|
|
42510
|
+
}).optional(),
|
|
42258
42511
|
/** ISAPI codec the firmware reports for the talk channel (e.g.
|
|
42259
42512
|
* `G.711ulaw`, `G.711alaw`). Persisted so the intercom session
|
|
42260
42513
|
* doesn't have to re-discover on every open. Optional — discovery
|
|
@@ -44257,7 +44510,68 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44257
44510
|
registerPrivacyMaskCap(cameraNumber) {
|
|
44258
44511
|
const isapi = this.ensureClient();
|
|
44259
44512
|
const CAP_NAME = "privacy-mask";
|
|
44260
|
-
|
|
44513
|
+
/**
|
|
44514
|
+
* The per-camera AVAILABILITY facts, as the last successful read saw them.
|
|
44515
|
+
*
|
|
44516
|
+
* `getOptions` used to make its own pair of ISAPI calls on every
|
|
44517
|
+
* invocation — and `getCameraStatuses` calls it once per camera per 5s
|
|
44518
|
+
* poll, so on this fleet (20 of 29 cameras are Hikvision) it was the
|
|
44519
|
+
* single largest source of camera traffic on the badge path.
|
|
44520
|
+
*
|
|
44521
|
+
* It is not a second copy of anything: `maxRegions` and the audio-channel
|
|
44522
|
+
* census live nowhere else, and they come out of the SAME two reads that
|
|
44523
|
+
* build the status slice, so they cannot disagree with it. They are kept in
|
|
44524
|
+
* the persisted `deviceCache` rather than only in memory because they must
|
|
44525
|
+
* survive an addon restart — the status slice does (`durability:
|
|
44526
|
+
* 'restored'`), and an options read that did not would be the one caller
|
|
44527
|
+
* still dialling every camera after every deploy.
|
|
44528
|
+
*/
|
|
44529
|
+
const readPersistedOptions = () => {
|
|
44530
|
+
const stored = this.config.get("deviceCache")?.privacyMaskOptions;
|
|
44531
|
+
if (!stored) return null;
|
|
44532
|
+
return {
|
|
44533
|
+
maxRegions: stored.maxRegions,
|
|
44534
|
+
supportedShapes: ["rect", "polygon"],
|
|
44535
|
+
polygonVertices: {
|
|
44536
|
+
min: 4,
|
|
44537
|
+
max: 4
|
|
44538
|
+
},
|
|
44539
|
+
supportsAudioMute: stored.supportsAudioMute
|
|
44540
|
+
};
|
|
44541
|
+
};
|
|
44542
|
+
/**
|
|
44543
|
+
* Epoch ms until which a COLD options probe is not re-attempted.
|
|
44544
|
+
*
|
|
44545
|
+
* Only the cold path below can dial, and only on a camera that has never
|
|
44546
|
+
* produced a descriptor — but "never" and "unreachable" are the same state,
|
|
44547
|
+
* so without this an unreachable camera would be dialled once per poll
|
|
44548
|
+
* forever. Same constant, same reasoning as the bridge's own miss cooldown.
|
|
44549
|
+
*/
|
|
44550
|
+
let optionsProbeCooldownUntil = 0;
|
|
44551
|
+
/** Persist the descriptor, but only when it actually MOVED — this runs on
|
|
44552
|
+
* every refresh and a per-refresh SQLite commit for an unchanged model
|
|
44553
|
+
* capability is pure churn. Best-effort: a failed write costs the next
|
|
44554
|
+
* restart one probe. */
|
|
44555
|
+
const persistOptions = async (next) => {
|
|
44556
|
+
const stored = this.config.get("deviceCache")?.privacyMaskOptions;
|
|
44557
|
+
if (stored && stored.maxRegions === next.maxRegions && stored.supportsAudioMute === next.supportsAudioMute) return;
|
|
44558
|
+
try {
|
|
44559
|
+
const previous = this.config.get("deviceCache") ?? {};
|
|
44560
|
+
await this.config.setAll({ deviceCache: {
|
|
44561
|
+
...previous,
|
|
44562
|
+
privacyMaskOptions: {
|
|
44563
|
+
maxRegions: next.maxRegions,
|
|
44564
|
+
supportsAudioMute: next.supportsAudioMute,
|
|
44565
|
+
fetchedAt: Date.now()
|
|
44566
|
+
}
|
|
44567
|
+
} });
|
|
44568
|
+
} catch (err) {
|
|
44569
|
+
this.ctx.logger.debug("hikvision privacy-mask options persist failed", {
|
|
44570
|
+
tags: { deviceId: this.id },
|
|
44571
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
44572
|
+
});
|
|
44573
|
+
}
|
|
44574
|
+
};
|
|
44261
44575
|
/** Round-trip the privacy-mask config → cap regions, write slice. */
|
|
44262
44576
|
const refreshFromCamera = async () => {
|
|
44263
44577
|
if (this.privacyMaskRefreshInFlight) return this.privacyMaskRefreshInFlight;
|
|
@@ -44265,6 +44579,15 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44265
44579
|
try {
|
|
44266
44580
|
const [mask, audio] = await Promise.all([isapi.getPrivacyMask(cameraNumber), this.readStreamAudioChannels(cameraNumber)]);
|
|
44267
44581
|
if (!mask) return;
|
|
44582
|
+
await persistOptions({
|
|
44583
|
+
maxRegions: mask.maxRegions > 0 ? mask.maxRegions : 4,
|
|
44584
|
+
supportedShapes: ["rect", "polygon"],
|
|
44585
|
+
polygonVertices: {
|
|
44586
|
+
min: 4,
|
|
44587
|
+
max: 4
|
|
44588
|
+
},
|
|
44589
|
+
supportsAudioMute: audio.length > 0
|
|
44590
|
+
});
|
|
44268
44591
|
const regions = mask.regions.map((region, index) => ({
|
|
44269
44592
|
id: index,
|
|
44270
44593
|
enabled: true,
|
|
@@ -44291,22 +44614,25 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44291
44614
|
this.privacyMaskRefreshInFlight = null;
|
|
44292
44615
|
}
|
|
44293
44616
|
};
|
|
44617
|
+
const bridge = createRuntimeStateBridge({
|
|
44618
|
+
runtimeState: this.runtimeState,
|
|
44619
|
+
cap: privacyMaskCapability,
|
|
44620
|
+
ownDeviceId: this.id,
|
|
44621
|
+
refresh: refreshFromCamera,
|
|
44622
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
44623
|
+
staleRead: "serve-and-revalidate",
|
|
44624
|
+
logger: this.ctx.logger,
|
|
44625
|
+
empty: () => ({
|
|
44626
|
+
enabled: false,
|
|
44627
|
+
regions: [],
|
|
44628
|
+
audioEnabled: null,
|
|
44629
|
+
lastFetchedAt: 0
|
|
44630
|
+
})
|
|
44631
|
+
});
|
|
44294
44632
|
const provider = {
|
|
44295
|
-
getStatus:
|
|
44296
|
-
runtimeState: this.runtimeState,
|
|
44297
|
-
cap: privacyMaskCapability,
|
|
44298
|
-
ownDeviceId: this.id,
|
|
44299
|
-
refresh: refreshFromCamera,
|
|
44300
|
-
staleMs: STALE_MS,
|
|
44301
|
-
empty: () => ({
|
|
44302
|
-
enabled: false,
|
|
44303
|
-
regions: [],
|
|
44304
|
-
audioEnabled: null,
|
|
44305
|
-
lastFetchedAt: 0
|
|
44306
|
-
})
|
|
44307
|
-
}).getStatus,
|
|
44633
|
+
getStatus: bridge.getStatus,
|
|
44308
44634
|
getOptions: async ({ deviceId }) => {
|
|
44309
|
-
|
|
44635
|
+
if (deviceId !== this.id) return {
|
|
44310
44636
|
maxRegions: 4,
|
|
44311
44637
|
supportedShapes: ["rect", "polygon"],
|
|
44312
44638
|
polygonVertices: {
|
|
@@ -44315,17 +44641,16 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44315
44641
|
},
|
|
44316
44642
|
supportsAudioMute: false
|
|
44317
44643
|
};
|
|
44318
|
-
|
|
44319
|
-
const
|
|
44320
|
-
return
|
|
44321
|
-
|
|
44322
|
-
|
|
44323
|
-
|
|
44324
|
-
|
|
44325
|
-
|
|
44326
|
-
|
|
44327
|
-
|
|
44328
|
-
};
|
|
44644
|
+
await bridge.ensureFresh();
|
|
44645
|
+
const warm = readPersistedOptions();
|
|
44646
|
+
if (warm !== null) return warm;
|
|
44647
|
+
if (Date.now() >= optionsProbeCooldownUntil) {
|
|
44648
|
+
optionsProbeCooldownUntil = Date.now() + RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS;
|
|
44649
|
+
await refreshFromCamera();
|
|
44650
|
+
const cold = readPersistedOptions();
|
|
44651
|
+
if (cold !== null) return cold;
|
|
44652
|
+
}
|
|
44653
|
+
throw new Error(`device ${String(this.id)}: privacy-mask options unknown — the camera has not answered`);
|
|
44329
44654
|
},
|
|
44330
44655
|
setMask: async ({ deviceId, patch }) => {
|
|
44331
44656
|
if (deviceId !== this.id) return;
|