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