@camstack/addon-osd-manager 0.1.15 → 0.1.17
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/{MotionZonesSettings-DDk0KstC.mjs → MotionZonesSettings-DlZtvCFl.mjs} +2 -2
- package/dist/{PrivacyMaskSettings-CsXnMGKT.mjs → PrivacyMaskSettings-lyGUWV_l.mjs} +4 -4
- package/dist/{SceneMonitorEditor-8C9vWyuL.mjs → SceneMonitorEditor-dYpiaZEg.mjs} +3 -3
- package/dist/_stub.js +196 -196
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-DAdBzUD3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-DPWjcIBk.mjs} +4 -4
- package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CoDIESQw.mjs +26 -0
- package/dist/{hostInit-BB5iEVB7.mjs → hostInit-B9K1gn0U.mjs} +3 -3
- package/dist/index.js +359 -74
- package/dist/index.mjs +359 -74
- package/dist/{player-overlays-C5TVZDIL.mjs → player-overlays-DIXY-7_H.mjs} +1 -1
- package/dist/remoteEntry.js +1 -1
- package/dist/{responsive-B6685e2G.mjs → responsive-7SgEY_K5.mjs} +1 -1
- package/dist/{square-ezK9FWt6.mjs → square-qJlpvL3B.mjs} +1 -1
- package/dist/{trash-2-BoPZpFI0.mjs → trash-2-CZ_J-874.mjs} +1 -1
- package/dist/{use-device-snapshot-BDmozw2y.mjs → use-device-snapshot--AcJ2F4s.mjs} +1 -1
- package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-7VFK3Vnq.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-CFOj1Hwh.mjs} +1 -1
- package/package.json +1 -1
- package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C9zRtW6C.mjs +0 -26
package/dist/index.js
CHANGED
|
@@ -7641,7 +7641,7 @@ var RecordingBandSchema = object({
|
|
|
7641
7641
|
postBufferSec: number().min(0).optional()
|
|
7642
7642
|
});
|
|
7643
7643
|
({
|
|
7644
|
-
preBufferSec:
|
|
7644
|
+
preBufferSec: 15,
|
|
7645
7645
|
postBufferSec: 30
|
|
7646
7646
|
}).postBufferSec * 1e3;
|
|
7647
7647
|
/**
|
|
@@ -15322,128 +15322,103 @@ var COCO_TO_MACRO = {
|
|
|
15322
15322
|
var AUDIO_MACRO_LABELS = [
|
|
15323
15323
|
{
|
|
15324
15324
|
id: "speech",
|
|
15325
|
-
name: "Speech"
|
|
15326
|
-
icon: "🗣️"
|
|
15325
|
+
name: "Speech"
|
|
15327
15326
|
},
|
|
15328
15327
|
{
|
|
15329
15328
|
id: "scream",
|
|
15330
|
-
name: "Scream / Shout"
|
|
15331
|
-
icon: "😱"
|
|
15329
|
+
name: "Scream / Shout"
|
|
15332
15330
|
},
|
|
15333
15331
|
{
|
|
15334
15332
|
id: "crying",
|
|
15335
|
-
name: "Crying / Baby"
|
|
15336
|
-
icon: "😢"
|
|
15333
|
+
name: "Crying / Baby"
|
|
15337
15334
|
},
|
|
15338
15335
|
{
|
|
15339
15336
|
id: "laughter",
|
|
15340
|
-
name: "Laughter"
|
|
15341
|
-
icon: "😂"
|
|
15337
|
+
name: "Laughter"
|
|
15342
15338
|
},
|
|
15343
15339
|
{
|
|
15344
15340
|
id: "music",
|
|
15345
|
-
name: "Music"
|
|
15346
|
-
icon: "🎵"
|
|
15341
|
+
name: "Music"
|
|
15347
15342
|
},
|
|
15348
15343
|
{
|
|
15349
15344
|
id: "dog",
|
|
15350
|
-
name: "Dog"
|
|
15351
|
-
icon: "🐕"
|
|
15345
|
+
name: "Dog"
|
|
15352
15346
|
},
|
|
15353
15347
|
{
|
|
15354
15348
|
id: "cat",
|
|
15355
|
-
name: "Cat"
|
|
15356
|
-
icon: "🐈"
|
|
15349
|
+
name: "Cat"
|
|
15357
15350
|
},
|
|
15358
15351
|
{
|
|
15359
15352
|
id: "bird",
|
|
15360
|
-
name: "Bird"
|
|
15361
|
-
icon: "🐦"
|
|
15353
|
+
name: "Bird"
|
|
15362
15354
|
},
|
|
15363
15355
|
{
|
|
15364
15356
|
id: "animal",
|
|
15365
|
-
name: "Animal (other)"
|
|
15366
|
-
icon: "🐾"
|
|
15357
|
+
name: "Animal (other)"
|
|
15367
15358
|
},
|
|
15368
15359
|
{
|
|
15369
15360
|
id: "alarm",
|
|
15370
|
-
name: "Alarm / Siren"
|
|
15371
|
-
icon: "🚨"
|
|
15361
|
+
name: "Alarm / Siren"
|
|
15372
15362
|
},
|
|
15373
15363
|
{
|
|
15374
15364
|
id: "doorbell",
|
|
15375
|
-
name: "Doorbell / Knock"
|
|
15376
|
-
icon: "🔔"
|
|
15365
|
+
name: "Doorbell / Knock"
|
|
15377
15366
|
},
|
|
15378
15367
|
{
|
|
15379
15368
|
id: "glass_breaking",
|
|
15380
|
-
name: "Glass Breaking"
|
|
15381
|
-
icon: "💥"
|
|
15369
|
+
name: "Glass Breaking"
|
|
15382
15370
|
},
|
|
15383
15371
|
{
|
|
15384
15372
|
id: "gunshot",
|
|
15385
|
-
name: "Gunshot / Explosion"
|
|
15386
|
-
icon: "💣"
|
|
15373
|
+
name: "Gunshot / Explosion"
|
|
15387
15374
|
},
|
|
15388
15375
|
{
|
|
15389
15376
|
id: "vehicle",
|
|
15390
|
-
name: "Vehicle"
|
|
15391
|
-
icon: "🚗"
|
|
15377
|
+
name: "Vehicle"
|
|
15392
15378
|
},
|
|
15393
15379
|
{
|
|
15394
15380
|
id: "siren",
|
|
15395
|
-
name: "Emergency Siren"
|
|
15396
|
-
icon: "🚑"
|
|
15381
|
+
name: "Emergency Siren"
|
|
15397
15382
|
},
|
|
15398
15383
|
{
|
|
15399
15384
|
id: "fire",
|
|
15400
|
-
name: "Fire / Smoke"
|
|
15401
|
-
icon: "🔥"
|
|
15385
|
+
name: "Fire / Smoke"
|
|
15402
15386
|
},
|
|
15403
15387
|
{
|
|
15404
15388
|
id: "water",
|
|
15405
|
-
name: "Water"
|
|
15406
|
-
icon: "💧"
|
|
15389
|
+
name: "Water"
|
|
15407
15390
|
},
|
|
15408
15391
|
{
|
|
15409
15392
|
id: "wind",
|
|
15410
|
-
name: "Wind / Weather"
|
|
15411
|
-
icon: "🌬️"
|
|
15393
|
+
name: "Wind / Weather"
|
|
15412
15394
|
},
|
|
15413
15395
|
{
|
|
15414
15396
|
id: "door",
|
|
15415
|
-
name: "Door"
|
|
15416
|
-
icon: "🚪"
|
|
15397
|
+
name: "Door"
|
|
15417
15398
|
},
|
|
15418
15399
|
{
|
|
15419
15400
|
id: "footsteps",
|
|
15420
|
-
name: "Footsteps"
|
|
15421
|
-
icon: "👣"
|
|
15401
|
+
name: "Footsteps"
|
|
15422
15402
|
},
|
|
15423
15403
|
{
|
|
15424
15404
|
id: "crowd",
|
|
15425
|
-
name: "Crowd / Chatter"
|
|
15426
|
-
icon: "👥"
|
|
15405
|
+
name: "Crowd / Chatter"
|
|
15427
15406
|
},
|
|
15428
15407
|
{
|
|
15429
15408
|
id: "telephone",
|
|
15430
|
-
name: "Telephone"
|
|
15431
|
-
icon: "📞"
|
|
15409
|
+
name: "Telephone"
|
|
15432
15410
|
},
|
|
15433
15411
|
{
|
|
15434
15412
|
id: "engine",
|
|
15435
|
-
name: "Engine / Motor"
|
|
15436
|
-
icon: "⚙️"
|
|
15413
|
+
name: "Engine / Motor"
|
|
15437
15414
|
},
|
|
15438
15415
|
{
|
|
15439
15416
|
id: "tools",
|
|
15440
|
-
name: "Tools / Construction"
|
|
15441
|
-
icon: "🔨"
|
|
15417
|
+
name: "Tools / Construction"
|
|
15442
15418
|
},
|
|
15443
15419
|
{
|
|
15444
15420
|
id: "silence",
|
|
15445
|
-
name: "Silence"
|
|
15446
|
-
icon: "🤫"
|
|
15421
|
+
name: "Silence"
|
|
15447
15422
|
}
|
|
15448
15423
|
];
|
|
15449
15424
|
var YAMNET_TO_MACRO = {
|
|
@@ -18496,7 +18471,7 @@ var TrackEnvelopeSchema = object({
|
|
|
18496
18471
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
18497
18472
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
18498
18473
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
18499
|
-
* zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
|
|
18474
|
+
* zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
|
|
18500
18475
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
18501
18476
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
18502
18477
|
* (`getObjectEvents` et al.).
|
|
@@ -18791,6 +18766,35 @@ var TrackSchema = object({
|
|
|
18791
18766
|
*/
|
|
18792
18767
|
hasFace: boolean().optional(),
|
|
18793
18768
|
/**
|
|
18769
|
+
* This track has a face row IN THE GALLERY: a crop **and** an embedding — a
|
|
18770
|
+
* face an operator could ASSIGN to an identity.
|
|
18771
|
+
*
|
|
18772
|
+
* The STRICT twin of {@link hasFace}, and the pair only earns its keep
|
|
18773
|
+
* because the two disagree. `hasFace` is stamped at the TOP of the face
|
|
18774
|
+
* branch, before every gate, and means no more than "a face detector produced
|
|
18775
|
+
* a face detail". This one is stamped at the single moment the gallery row
|
|
18776
|
+
* LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
|
|
18777
|
+
* past the embedding-magnitude verdict, the `minFacePx` detection gate, the
|
|
18778
|
+
* candidate gate, the imageless-track drop (no crop was ever captured) and
|
|
18779
|
+
* the crop-store drop. Everything between the detector and that insert can
|
|
18780
|
+
* legitimately refuse the face, so a flag written any earlier promises the
|
|
18781
|
+
* operator something to assign and delivers nothing.
|
|
18782
|
+
*
|
|
18783
|
+
* **Independent of recognition.** A face collected but never auto-matched is
|
|
18784
|
+
* still assignable — it is in fact the face an operator most wants to reach —
|
|
18785
|
+
* so this is NOT gated on `recognizedIdentityId`. Recognition lands in
|
|
18786
|
+
* `subLabel`; this says only that the raw material exists.
|
|
18787
|
+
*
|
|
18788
|
+
* **Set once, never cleared.** A track that produced a gallery row produced
|
|
18789
|
+
* one; deleting the row later is the gallery's business, not this flag's.
|
|
18790
|
+
*
|
|
18791
|
+
* **Absent ≠ false**, the same rule as {@link hasFace}: every row written
|
|
18792
|
+
* before the column omits it, and so does every server that predates the
|
|
18793
|
+
* field. A consumer must test `=== true` and render nothing otherwise —
|
|
18794
|
+
* never infer "no assignable face".
|
|
18795
|
+
*/
|
|
18796
|
+
hasEmbeddedFace: boolean().optional(),
|
|
18797
|
+
/**
|
|
18794
18798
|
* This subject CONTAINS a folded rider — a person the rider-pairing step
|
|
18795
18799
|
* ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
|
|
18796
18800
|
* so the passage is tracked once and as a VEHICLE.
|
|
@@ -20733,6 +20737,32 @@ var DetailResultSchema = object({
|
|
|
20733
20737
|
/** FRAME-space bbox (already mapped back from crop space). */
|
|
20734
20738
|
bbox: NativeCropBboxSchema.optional(),
|
|
20735
20739
|
embedding: string().optional(),
|
|
20740
|
+
/**
|
|
20741
|
+
* L2 magnitude of the RAW pre-normalization embedding. Additive and optional
|
|
20742
|
+
* so an older runner that does not send it degrades to "unmeasurable", which
|
|
20743
|
+
* the consuming gate treats as accept — never as reject.
|
|
20744
|
+
*/
|
|
20745
|
+
embeddingMagnitude: number().optional(),
|
|
20746
|
+
/**
|
|
20747
|
+
* Model that produced `embedding` — the SAME contract `labelModelId` has for
|
|
20748
|
+
* `label`, and for the same reason: the producing node is the only side that
|
|
20749
|
+
* knows which model actually ran, and re-deriving it downstream is exactly
|
|
20750
|
+
* the inference this model exists to forbid.
|
|
20751
|
+
*
|
|
20752
|
+
* Additive and optional, so a runner that predates it degrades to
|
|
20753
|
+
* "unmeasurable" rather than to a wrong answer — the consumer falls back to
|
|
20754
|
+
* its previous assumption instead of refusing.
|
|
20755
|
+
*
|
|
20756
|
+
* Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
|
|
20757
|
+
* to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
|
|
20758
|
+
* with nothing on the wire it took the id from the GALLERY, so the gate
|
|
20759
|
+
* compared the gallery against itself and could never fire. That turned the
|
|
20760
|
+
* one guard protecting the face feature space into a tautology, and it is
|
|
20761
|
+
* why swapping `face-embedding`'s model would NOT have stranded the enrolled
|
|
20762
|
+
* gallery as the catalog comment predicts — it would have scored the new
|
|
20763
|
+
* model's vectors against the old model's samples in one cosine space.
|
|
20764
|
+
*/
|
|
20765
|
+
embeddingModelId: string().optional(),
|
|
20736
20766
|
label: string().optional(),
|
|
20737
20767
|
/**
|
|
20738
20768
|
* The tier `label` occupies, copied VERBATIM from the producing step's
|
|
@@ -22695,6 +22725,7 @@ var snapshotCapability = {
|
|
|
22695
22725
|
isBattery: boolean(),
|
|
22696
22726
|
reason: _enum([
|
|
22697
22727
|
"disabled",
|
|
22728
|
+
"offline",
|
|
22698
22729
|
"sleeping",
|
|
22699
22730
|
"unreachable",
|
|
22700
22731
|
"waking"
|
|
@@ -22784,9 +22815,17 @@ targets: array(object({
|
|
|
22784
22815
|
sleeping: boolean(),
|
|
22785
22816
|
/** Current device state rendered over the cached frame. State images
|
|
22786
22817
|
* remain authoritative even when their photographic background is
|
|
22787
|
-
* old; null means the link must carry a current camera frame.
|
|
22818
|
+
* old; null means the link must carry a current camera frame.
|
|
22819
|
+
*
|
|
22820
|
+
* **This is the ONLY source a surface may derive an inactive tile
|
|
22821
|
+
* from.** A client that reads a device's `online`/`disabled` flags and
|
|
22822
|
+
* draws its own verdict will disagree with the pixels it is painting
|
|
22823
|
+
* — the frame comes from here, and so must the sentence over it
|
|
22824
|
+
* (D190). Pair with `ageMs` for the age of the photograph behind the
|
|
22825
|
+
* state. */
|
|
22788
22826
|
stateReason: _enum([
|
|
22789
22827
|
"disabled",
|
|
22828
|
+
"offline",
|
|
22790
22829
|
"sleeping",
|
|
22791
22830
|
"unreachable",
|
|
22792
22831
|
"waking"
|
|
@@ -40780,7 +40819,6 @@ var NC_AUDIO_DEFAULTS = {
|
|
|
40780
40819
|
hitPercent: 60,
|
|
40781
40820
|
samplingSeconds: 10
|
|
40782
40821
|
};
|
|
40783
|
-
new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
|
|
40784
40822
|
new Set(["devices", "classes"]);
|
|
40785
40823
|
NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
|
|
40786
40824
|
var WEEKDAY_TO_DAY = {
|
|
@@ -41074,6 +41112,39 @@ TimelapseRuleInputSchema.extend({
|
|
|
41074
41112
|
createdAt: number(),
|
|
41075
41113
|
updatedAt: number()
|
|
41076
41114
|
});
|
|
41115
|
+
Object.freeze(Object.fromEntries([{
|
|
41116
|
+
stepId: "face-embedding",
|
|
41117
|
+
label: "Face recognition model",
|
|
41118
|
+
defaultModelId: "arcface-r100",
|
|
41119
|
+
indexName: "the enrolled face gallery",
|
|
41120
|
+
options: [
|
|
41121
|
+
{
|
|
41122
|
+
id: "arcface-r100",
|
|
41123
|
+
label: "ArcFace (ResNet34)"
|
|
41124
|
+
},
|
|
41125
|
+
{
|
|
41126
|
+
id: "auraface-r100",
|
|
41127
|
+
label: "AuraFace R100"
|
|
41128
|
+
},
|
|
41129
|
+
{
|
|
41130
|
+
id: "inception-resnet-v1",
|
|
41131
|
+
label: "Inception ResNet V1"
|
|
41132
|
+
}
|
|
41133
|
+
]
|
|
41134
|
+
}, {
|
|
41135
|
+
stepId: "clip-embedding",
|
|
41136
|
+
label: "Semantic search model",
|
|
41137
|
+
defaultModelId: "mobileclip-s1",
|
|
41138
|
+
indexName: "the object semantic-search index",
|
|
41139
|
+
options: [{
|
|
41140
|
+
id: "mobileclip-s1",
|
|
41141
|
+
label: "MobileCLIP S1"
|
|
41142
|
+
}, {
|
|
41143
|
+
id: "mobileclip-s2",
|
|
41144
|
+
label: "MobileCLIP S2"
|
|
41145
|
+
}]
|
|
41146
|
+
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
41147
|
+
string().min(1);
|
|
41077
41148
|
object({
|
|
41078
41149
|
/**
|
|
41079
41150
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -41094,10 +41165,21 @@ object({
|
|
|
41094
41165
|
*/
|
|
41095
41166
|
square: boolean()
|
|
41096
41167
|
});
|
|
41097
|
-
|
|
41168
|
+
/**
|
|
41169
|
+
* The convention in force when nobody has configured one — byte-for-byte the
|
|
41170
|
+
* behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
|
|
41171
|
+
*/
|
|
41172
|
+
var DEFAULT_DETAIL_CROP_CONVENTION = {
|
|
41098
41173
|
paddingRatio: .15,
|
|
41099
41174
|
square: false
|
|
41100
|
-
}
|
|
41175
|
+
};
|
|
41176
|
+
Object.freeze({
|
|
41177
|
+
x: 0,
|
|
41178
|
+
y: 0,
|
|
41179
|
+
w: 1,
|
|
41180
|
+
h: 1
|
|
41181
|
+
});
|
|
41182
|
+
DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
|
|
41101
41183
|
/**
|
|
41102
41184
|
* WHICH delivered frames the decode worker retains a native copy of.
|
|
41103
41185
|
*
|
|
@@ -41255,6 +41337,37 @@ var OsdBindingsStore = class {
|
|
|
41255
41337
|
}
|
|
41256
41338
|
};
|
|
41257
41339
|
//#endregion
|
|
41340
|
+
//#region src/bound-index.ts
|
|
41341
|
+
/**
|
|
41342
|
+
* The durable index of cameras that have at least one OSD binding.
|
|
41343
|
+
*
|
|
41344
|
+
* WHY IT EXISTS. The render loop's work list used to be DERIVED at boot: list
|
|
41345
|
+
* every device in the directory, read each one's binding block, keep the ones
|
|
41346
|
+
* that answered non-empty. Every one of those reads is a UDS round-trip to the
|
|
41347
|
+
* settings store, and they ran one at a time. Measured on the boot of
|
|
41348
|
+
* 2026-08-19: 976 devices × 559 ms under boot load = 546 s in which
|
|
41349
|
+
* `OsdManager.start()` had not returned — and `start()` was awaited inside
|
|
41350
|
+
* `onInitialize()`, so for nine minutes the `osd-manager` and
|
|
41351
|
+
* `addon-pages-source` capabilities did not exist ANYWHERE in the cluster
|
|
41352
|
+
* (docs/architecture/addon-lifecycle.md). The answer it took nine minutes to
|
|
41353
|
+
* compute was `boundDevices = 0`.
|
|
41354
|
+
*
|
|
41355
|
+
* Filtering to cameras and fanning the reads out makes that scan cheap, but it
|
|
41356
|
+
* is still a scan. The index is the actual fix: the work list is WRITTEN where
|
|
41357
|
+
* bindings are written, so boot reads one key and is done in milliseconds. The
|
|
41358
|
+
* scan stays, in the background, as the reconcile — because an index nothing
|
|
41359
|
+
* checks is an index that eventually lies.
|
|
41360
|
+
*
|
|
41361
|
+
* The value is the SET of bound camera ids, serialised as a sorted array.
|
|
41362
|
+
* `null` is not "no cameras are bound", it is "this hub has never written the
|
|
41363
|
+
* index" — the two must stay distinguishable, because the first is authoritative
|
|
41364
|
+
* and the second means seed it from a scan. An empty ARRAY is the authoritative
|
|
41365
|
+
* "nothing is bound".
|
|
41366
|
+
*/
|
|
41367
|
+
/** Store key inside the addon's own (global) store. */
|
|
41368
|
+
var OSD_BOUND_INDEX_KEY = "boundDeviceIds";
|
|
41369
|
+
var OsdBoundIndexSchema = array(number().int().positive()).nullable();
|
|
41370
|
+
//#endregion
|
|
41258
41371
|
//#region src/gate.ts
|
|
41259
41372
|
/**
|
|
41260
41373
|
* The display gate — "should this slot show its value right now?".
|
|
@@ -41331,6 +41444,27 @@ function evaluateGate(input) {
|
|
|
41331
41444
|
return OPEN;
|
|
41332
41445
|
}
|
|
41333
41446
|
//#endregion
|
|
41447
|
+
//#region src/lane-pool.ts
|
|
41448
|
+
/**
|
|
41449
|
+
* Apply `mapper` to every item with at most `limit` in flight at once.
|
|
41450
|
+
*
|
|
41451
|
+
* A rejecting mapper rejects the whole call (same contract as `Promise.all`);
|
|
41452
|
+
* a caller that must not fail the sweep for one bad unit catches INSIDE the
|
|
41453
|
+
* mapper — which is what every caller in this package does, because a unit that
|
|
41454
|
+
* threw must never be the reason the next camera is not reconciled.
|
|
41455
|
+
*/
|
|
41456
|
+
async function mapWithConcurrency(items, limit, mapper) {
|
|
41457
|
+
const results = new Array(items.length);
|
|
41458
|
+
if (items.length === 0) return results;
|
|
41459
|
+
const lanes = Math.max(1, Math.min(Math.trunc(limit), items.length));
|
|
41460
|
+
const entries = items.entries();
|
|
41461
|
+
const lane = async () => {
|
|
41462
|
+
for (const [index, item] of entries) results[index] = await mapper(item, index);
|
|
41463
|
+
};
|
|
41464
|
+
await Promise.all(Array.from({ length: lanes }, () => lane()));
|
|
41465
|
+
return results;
|
|
41466
|
+
}
|
|
41467
|
+
//#endregion
|
|
41334
41468
|
//#region src/format.ts
|
|
41335
41469
|
/** Tokens `formatClock` understands. Anything else in the pattern is literal. */
|
|
41336
41470
|
var CLOCK_TOKENS = [
|
|
@@ -41663,31 +41797,61 @@ var OsdManager = class {
|
|
|
41663
41797
|
recognitionGeneration = /* @__PURE__ */ new Map();
|
|
41664
41798
|
recognitionInvalidatedAt = /* @__PURE__ */ new Map();
|
|
41665
41799
|
timer = null;
|
|
41800
|
+
/** The background reconcile, while one is running. Held so a second arming
|
|
41801
|
+
* cannot overlap the first — two concurrent passes would race on the index
|
|
41802
|
+
* write and could persist the older of the two answers. */
|
|
41803
|
+
reconcile = null;
|
|
41666
41804
|
constructor(deps) {
|
|
41667
41805
|
this.deps = deps;
|
|
41668
41806
|
this.logger = deps.logger;
|
|
41669
41807
|
this.now = deps.now ?? (() => Date.now());
|
|
41670
41808
|
}
|
|
41671
41809
|
/**
|
|
41672
|
-
*
|
|
41673
|
-
*
|
|
41674
|
-
*
|
|
41810
|
+
* Read the durable work list, start ticking, and arm the reconcile BEHIND
|
|
41811
|
+
* both.
|
|
41812
|
+
*
|
|
41813
|
+
* This used to derive the work list first: list every device, read every
|
|
41814
|
+
* device's binding block, one at a time. On 2026-08-19 that was 976 devices ×
|
|
41815
|
+
* 559 ms = 546 s, awaited inside `onInitialize`, so every capability this
|
|
41816
|
+
* addon declares was absent cluster-wide for nine minutes — and the answer was
|
|
41817
|
+
* `boundDevices = 0`. A camera with no binding still costs nothing at runtime;
|
|
41818
|
+
* what it used to cost was the boot.
|
|
41819
|
+
*
|
|
41820
|
+
* The scan did not go away, it moved: it is the reconcile, it runs in the
|
|
41821
|
+
* background, and it is the only thing that keeps the index from silently
|
|
41822
|
+
* lying.
|
|
41675
41823
|
*/
|
|
41676
41824
|
async start(tickMs) {
|
|
41677
|
-
await this.
|
|
41825
|
+
const stored = await this.readBoundIndex();
|
|
41826
|
+
if (stored !== null) {
|
|
41827
|
+
this.boundDevices.clear();
|
|
41828
|
+
for (const deviceId of stored) this.boundDevices.add(deviceId);
|
|
41829
|
+
}
|
|
41678
41830
|
this.timer = setInterval(() => {
|
|
41679
41831
|
this.renderAllBound();
|
|
41680
41832
|
}, tickMs);
|
|
41681
41833
|
this.logger.info("osd-manager: render loop started", { meta: {
|
|
41682
41834
|
tickMs,
|
|
41683
|
-
boundDevices: this.boundDevices.size
|
|
41835
|
+
boundDevices: this.boundDevices.size,
|
|
41836
|
+
source: stored === null ? "seed" : "durable-index"
|
|
41684
41837
|
} });
|
|
41838
|
+
this.armReconcile(stored === null ? "seed" : "reconcile");
|
|
41685
41839
|
}
|
|
41686
41840
|
stop() {
|
|
41687
41841
|
if (this.timer !== null) clearInterval(this.timer);
|
|
41688
41842
|
this.timer = null;
|
|
41689
41843
|
this.lastWritten.clear();
|
|
41690
41844
|
}
|
|
41845
|
+
/** Resolves when the armed reconcile has settled. Tests await it; production
|
|
41846
|
+
* never does — that is the entire point of arming it. */
|
|
41847
|
+
async settleReconcile() {
|
|
41848
|
+
await this.reconcile;
|
|
41849
|
+
}
|
|
41850
|
+
/** Every camera currently on the work list, sorted — the value persisted as
|
|
41851
|
+
* the durable index. */
|
|
41852
|
+
get boundDeviceIds() {
|
|
41853
|
+
return [...this.boundDevices].sort((a, b) => a - b);
|
|
41854
|
+
}
|
|
41691
41855
|
/**
|
|
41692
41856
|
* Keep recognition overlays event-fresh. The persisted-track hydration below
|
|
41693
41857
|
* remains the restart/cold-start authority; lifecycle events only advance it.
|
|
@@ -41719,19 +41883,124 @@ var OsdManager = class {
|
|
|
41719
41883
|
});
|
|
41720
41884
|
}
|
|
41721
41885
|
/**
|
|
41722
|
-
* Reconcile the
|
|
41723
|
-
*
|
|
41724
|
-
*
|
|
41886
|
+
* Reconcile the durable index against the store rather than trusting it to be
|
|
41887
|
+
* right (D8 — an index nothing checks is an index that eventually lies).
|
|
41888
|
+
*
|
|
41889
|
+
* Background work, and BOUNDED: cameras only, four reads in flight. Two rules
|
|
41890
|
+
* keep it from destroying work:
|
|
41891
|
+
*
|
|
41892
|
+
* - a binding read that FAILED decides nothing — the camera keeps whatever the
|
|
41893
|
+
* index already said (D49);
|
|
41894
|
+
* - a camera absent from the listing is left alone. The device restore pass
|
|
41895
|
+
* runs for minutes after boot (D167), so "not published yet" and "deleted"
|
|
41896
|
+
* look identical from here, and only one of them may unbind a camera. A
|
|
41897
|
+
* real removal comes from `clearSlotBinding`, which is authoritative.
|
|
41898
|
+
*
|
|
41899
|
+
* Every correction is logged with its `deviceId`, because a silent repair is
|
|
41900
|
+
* indistinguishable from an index that was right all along.
|
|
41901
|
+
*/
|
|
41902
|
+
async reconcileBoundDevices(mode = "reconcile") {
|
|
41903
|
+
const cameras = await this.deps.listCameras();
|
|
41904
|
+
const probes = await mapWithConcurrency(cameras, this.deps.reconcileConcurrency ?? 4, async (camera) => ({
|
|
41905
|
+
deviceId: camera.id,
|
|
41906
|
+
bindings: await this.probeBindings(camera.id)
|
|
41907
|
+
}));
|
|
41908
|
+
const listed = new Set(cameras.map((camera) => camera.id));
|
|
41909
|
+
let unreadable = 0;
|
|
41910
|
+
const added = [];
|
|
41911
|
+
const removed = [];
|
|
41912
|
+
for (const probe of probes) {
|
|
41913
|
+
if (probe.bindings === null) {
|
|
41914
|
+
unreadable += 1;
|
|
41915
|
+
continue;
|
|
41916
|
+
}
|
|
41917
|
+
const bound = Object.keys(probe.bindings).length > 0;
|
|
41918
|
+
if (bound && !this.boundDevices.has(probe.deviceId)) {
|
|
41919
|
+
this.boundDevices.add(probe.deviceId);
|
|
41920
|
+
added.push(probe.deviceId);
|
|
41921
|
+
} else if (!bound && this.boundDevices.has(probe.deviceId)) {
|
|
41922
|
+
this.boundDevices.delete(probe.deviceId);
|
|
41923
|
+
removed.push(probe.deviceId);
|
|
41924
|
+
}
|
|
41925
|
+
}
|
|
41926
|
+
if (mode === "reconcile") {
|
|
41927
|
+
for (const deviceId of added) this.logger.warn("osd-manager: bound-device index was missing a camera that HAS bindings — corrected", {
|
|
41928
|
+
tags: { deviceId },
|
|
41929
|
+
meta: { correction: "added" }
|
|
41930
|
+
});
|
|
41931
|
+
for (const deviceId of removed) this.logger.warn("osd-manager: bound-device index claimed a camera with NO bindings — corrected", {
|
|
41932
|
+
tags: { deviceId },
|
|
41933
|
+
meta: { correction: "removed" }
|
|
41934
|
+
});
|
|
41935
|
+
}
|
|
41936
|
+
const notListed = [...this.boundDevices].filter((deviceId) => !listed.has(deviceId)).length;
|
|
41937
|
+
this.logger.info("osd-manager: bound-device reconcile complete", { meta: {
|
|
41938
|
+
mode,
|
|
41939
|
+
cameras: cameras.length,
|
|
41940
|
+
bound: this.boundDevices.size,
|
|
41941
|
+
added: added.length,
|
|
41942
|
+
removed: removed.length,
|
|
41943
|
+
unreadable,
|
|
41944
|
+
notListed
|
|
41945
|
+
} });
|
|
41946
|
+
if (mode === "seed" || added.length > 0 || removed.length > 0) await this.persistBoundIndex(null);
|
|
41947
|
+
}
|
|
41948
|
+
/**
|
|
41949
|
+
* Run the reconcile in the BACKGROUND, once at a time.
|
|
41950
|
+
*
|
|
41951
|
+
* Nothing on the boot path may await it — that await is what took the
|
|
41952
|
+
* capability graph down for nine minutes. A second arming while one is in
|
|
41953
|
+
* flight is dropped rather than queued: both passes would compute over the
|
|
41954
|
+
* same store and race on the index write.
|
|
41955
|
+
*/
|
|
41956
|
+
armReconcile(mode) {
|
|
41957
|
+
if (this.reconcile !== null) return;
|
|
41958
|
+
this.reconcile = this.reconcileBoundDevices(mode).catch((err) => {
|
|
41959
|
+
this.logger.warn("osd-manager: bound-device reconcile failed — the index keeps its current contents", { meta: {
|
|
41960
|
+
mode,
|
|
41961
|
+
error: String(err)
|
|
41962
|
+
} });
|
|
41963
|
+
}).finally(() => {
|
|
41964
|
+
this.reconcile = null;
|
|
41965
|
+
});
|
|
41966
|
+
}
|
|
41967
|
+
/** The persisted index, or `null` when it has never been written (or could
|
|
41968
|
+
* not be read — both mean "derive it from a scan", which is safe). */
|
|
41969
|
+
async readBoundIndex() {
|
|
41970
|
+
try {
|
|
41971
|
+
return await this.deps.boundIndex.get();
|
|
41972
|
+
} catch (err) {
|
|
41973
|
+
this.logger.warn("osd-manager: bound-device index unreadable — seeding from a camera scan", { meta: { error: String(err) } });
|
|
41974
|
+
return null;
|
|
41975
|
+
}
|
|
41976
|
+
}
|
|
41977
|
+
/**
|
|
41978
|
+
* Persist the work list. Called from every place a binding is written, so the
|
|
41979
|
+
* index cannot drift from the bindings without the write that caused the drift
|
|
41980
|
+
* also failing — and that failure is logged, and repaired by the reconcile.
|
|
41725
41981
|
*/
|
|
41726
|
-
async
|
|
41727
|
-
|
|
41728
|
-
|
|
41729
|
-
|
|
41730
|
-
|
|
41731
|
-
|
|
41982
|
+
async persistBoundIndex(deviceId) {
|
|
41983
|
+
try {
|
|
41984
|
+
await this.deps.boundIndex.set([...this.boundDeviceIds]);
|
|
41985
|
+
} catch (err) {
|
|
41986
|
+
this.logger.warn("osd-manager: bound-device index write failed — the next reconcile repairs it", {
|
|
41987
|
+
...deviceId === null ? {} : { tags: { deviceId } },
|
|
41988
|
+
meta: { error: String(err) }
|
|
41989
|
+
});
|
|
41990
|
+
}
|
|
41991
|
+
}
|
|
41992
|
+
/** One camera's bindings, or `null` when the read FAILED. Distinct from an
|
|
41993
|
+
* empty map on purpose: only the second may unbind a camera (D49). */
|
|
41994
|
+
async probeBindings(deviceId) {
|
|
41995
|
+
try {
|
|
41996
|
+
return await this.deps.bindings(deviceId).get();
|
|
41997
|
+
} catch (err) {
|
|
41998
|
+
this.logger.warn("osd-manager: binding read failed during reconcile — this camera keeps its index entry", {
|
|
41999
|
+
tags: { deviceId },
|
|
42000
|
+
meta: { error: String(err) }
|
|
42001
|
+
});
|
|
42002
|
+
return null;
|
|
41732
42003
|
}
|
|
41733
|
-
this.boundDevices.clear();
|
|
41734
|
-
for (const id of next) this.boundDevices.add(id);
|
|
41735
42004
|
}
|
|
41736
42005
|
getDeviceOsd = async ({ deviceId }) => {
|
|
41737
42006
|
const status = await this.deps.readOsdStatus(deviceId);
|
|
@@ -41782,6 +42051,7 @@ var OsdManager = class {
|
|
|
41782
42051
|
await this.assertSlotExists(deviceId, slotId);
|
|
41783
42052
|
await this.deps.bindings(deviceId).update((prev) => withBinding(prev, slotId, binding));
|
|
41784
42053
|
this.boundDevices.add(deviceId);
|
|
42054
|
+
await this.persistBoundIndex(deviceId);
|
|
41785
42055
|
this.logger.info("osd-manager: slot binding saved", {
|
|
41786
42056
|
tags: { deviceId },
|
|
41787
42057
|
meta: {
|
|
@@ -41806,6 +42076,7 @@ var OsdManager = class {
|
|
|
41806
42076
|
this.lastWritten.delete(cacheKey(deviceId, slotId));
|
|
41807
42077
|
const remaining = await this.readBindings(deviceId);
|
|
41808
42078
|
if (Object.keys(remaining).length === 0) this.boundDevices.delete(deviceId);
|
|
42079
|
+
await this.persistBoundIndex(deviceId);
|
|
41809
42080
|
this.logger.info("osd-manager: slot binding cleared — slot keeps its last text", {
|
|
41810
42081
|
tags: { deviceId },
|
|
41811
42082
|
meta: { slotId }
|
|
@@ -41846,6 +42117,7 @@ var OsdManager = class {
|
|
|
41846
42117
|
for (const key of [...this.lastWritten.keys()]) if (key.startsWith(`${targetDeviceId}:`)) this.lastWritten.delete(key);
|
|
41847
42118
|
if (Object.keys(copiedBindings).length > 0) this.boundDevices.add(targetDeviceId);
|
|
41848
42119
|
else this.boundDevices.delete(targetDeviceId);
|
|
42120
|
+
await this.persistBoundIndex(targetDeviceId);
|
|
41849
42121
|
await this.renderDeviceInternal(targetDeviceId, null);
|
|
41850
42122
|
this.logger.info("osd-manager: camera configuration copied", {
|
|
41851
42123
|
tags: { deviceId: targetDeviceId },
|
|
@@ -42074,6 +42346,7 @@ var OsdManagerAddon = class extends BaseAddon {
|
|
|
42074
42346
|
async onInitialize() {
|
|
42075
42347
|
const ctx = this.ctx;
|
|
42076
42348
|
const store = new OsdBindingsStore((deviceId, key, schema, fallback) => this.deviceState(deviceId, key, schema, fallback));
|
|
42349
|
+
const boundIndex = this.state(OSD_BOUND_INDEX_KEY, OsdBoundIndexSchema, null);
|
|
42077
42350
|
const manager = new OsdManager({
|
|
42078
42351
|
logger: ctx.logger,
|
|
42079
42352
|
readOsdStatus: async (deviceId) => {
|
|
@@ -42110,6 +42383,15 @@ var OsdManagerAddon = class extends BaseAddon {
|
|
|
42110
42383
|
name: d.name
|
|
42111
42384
|
}));
|
|
42112
42385
|
},
|
|
42386
|
+
listCameras: async () => {
|
|
42387
|
+
return (await ctx.api.deviceManager.listAll.query({
|
|
42388
|
+
isCamera: true,
|
|
42389
|
+
projection: "slim"
|
|
42390
|
+
})).map((d) => ({
|
|
42391
|
+
id: d.id,
|
|
42392
|
+
name: d.name
|
|
42393
|
+
}));
|
|
42394
|
+
},
|
|
42113
42395
|
listDeviceCaps: async (deviceId) => {
|
|
42114
42396
|
const bindings = await ctx.api.deviceManager.getBindings.query({ deviceId });
|
|
42115
42397
|
return [...new Set(bindings.entries.map((e) => e.capName))];
|
|
@@ -42137,7 +42419,8 @@ var OsdManagerAddon = class extends BaseAddon {
|
|
|
42137
42419
|
} } : {}
|
|
42138
42420
|
};
|
|
42139
42421
|
},
|
|
42140
|
-
bindings: (deviceId) => store.forDevice(deviceId)
|
|
42422
|
+
bindings: (deviceId) => store.forDevice(deviceId),
|
|
42423
|
+
boundIndex
|
|
42141
42424
|
});
|
|
42142
42425
|
this.manager = manager;
|
|
42143
42426
|
this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {
|
|
@@ -42149,7 +42432,9 @@ var OsdManagerAddon = class extends BaseAddon {
|
|
|
42149
42432
|
this.subscribe({ category: EventCategory.PipelineAnalyticsPlateGalleryChanged }, (event) => {
|
|
42150
42433
|
manager.onRecognitionGalleryChanged(event.data.deviceId);
|
|
42151
42434
|
});
|
|
42152
|
-
|
|
42435
|
+
manager.start(Math.max(1, this.config.tickSeconds) * 1e3).catch((err) => {
|
|
42436
|
+
ctx.logger.error("osd-manager: render loop failed to start — no camera will be re-rendered", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
42437
|
+
});
|
|
42153
42438
|
const pagesProvider = {
|
|
42154
42439
|
id: this.id,
|
|
42155
42440
|
listPages: () => this.pages
|