@camstack/addon-osd-manager 0.1.3 → 0.1.4
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/_stub.js +1094 -1023
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-BBrLl2CB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-CL-YG2or.mjs} +2 -2
- package/dist/addon-osd-manager.css +1 -1
- package/dist/{hostInit-COP9TmCI.mjs → hostInit-BCB8KzuM.mjs} +1 -1
- package/dist/index.js +199 -3
- package/dist/index.mjs +199 -3
- package/dist/remoteEntry.js +1 -1
- package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-Bx-1SwyE.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-CdG2yVd0.mjs} +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -24802,6 +24802,7 @@ var FaceInfoSchema = object({
|
|
|
24802
24802
|
var FaceFilterEnum = _enum([
|
|
24803
24803
|
"unassigned",
|
|
24804
24804
|
"recognized",
|
|
24805
|
+
"identified",
|
|
24805
24806
|
"all"
|
|
24806
24807
|
]);
|
|
24807
24808
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -24841,6 +24842,8 @@ var faceGalleryCapability = {
|
|
|
24841
24842
|
auth: "admin"
|
|
24842
24843
|
}),
|
|
24843
24844
|
listRecentFaces: method(object({
|
|
24845
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
24846
|
+
deviceId: number().int().optional(),
|
|
24844
24847
|
limit: number().int().positive().optional(),
|
|
24845
24848
|
filter: FaceFilterEnum.optional(),
|
|
24846
24849
|
/**
|
|
@@ -27384,6 +27387,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
27384
27387
|
capName: string().min(1).max(64),
|
|
27385
27388
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
27386
27389
|
valuePath: string().min(1).max(64)
|
|
27390
|
+
}),
|
|
27391
|
+
object({
|
|
27392
|
+
kind: literal("latest-recognition"),
|
|
27393
|
+
recognition: _enum(["person", "plate"])
|
|
27387
27394
|
})
|
|
27388
27395
|
]);
|
|
27389
27396
|
var OsdSlotBindingSchema = object({
|
|
@@ -27522,6 +27529,21 @@ var osdManagerCapability = {
|
|
|
27522
27529
|
auth: "admin"
|
|
27523
27530
|
}),
|
|
27524
27531
|
/**
|
|
27532
|
+
* Replace one camera's binding configuration from another. Writable source
|
|
27533
|
+
* slots are mapped by ordinal onto writable target slots so different
|
|
27534
|
+
* provider slot ids do not make the copied configuration disappear.
|
|
27535
|
+
*/
|
|
27536
|
+
copyDeviceConfiguration: method(object({
|
|
27537
|
+
sourceDeviceId: number().int(),
|
|
27538
|
+
targetDeviceId: number().int()
|
|
27539
|
+
}), object({
|
|
27540
|
+
copied: number().int().nonnegative(),
|
|
27541
|
+
skipped: number().int().nonnegative()
|
|
27542
|
+
}), {
|
|
27543
|
+
kind: "mutation",
|
|
27544
|
+
auth: "admin"
|
|
27545
|
+
}),
|
|
27546
|
+
/**
|
|
27525
27547
|
* Render without writing. `binding` overrides the stored one so an
|
|
27526
27548
|
* editor can preview an unsaved change. Mutation kind only to carry
|
|
27527
27549
|
* the binding object safely; no side effects.
|
|
@@ -33706,6 +33728,12 @@ Object.freeze({
|
|
|
33706
33728
|
addonId: null,
|
|
33707
33729
|
access: "delete"
|
|
33708
33730
|
},
|
|
33731
|
+
"osdManager.copyDeviceConfiguration": {
|
|
33732
|
+
capName: "osd-manager",
|
|
33733
|
+
capScope: "system",
|
|
33734
|
+
addonId: null,
|
|
33735
|
+
access: "create"
|
|
33736
|
+
},
|
|
33709
33737
|
"osdManager.getConditionSupport": {
|
|
33710
33738
|
capName: "osd-manager",
|
|
33711
33739
|
capScope: "system",
|
|
@@ -36589,7 +36617,8 @@ var OsdBindingsStore = class {
|
|
|
36589
36617
|
* Widening it means giving `evaluateGate` the facts to answer it — a
|
|
36590
36618
|
* live detection subject would unlock `classes`, `zones`, `minConfidence`.
|
|
36591
36619
|
*/
|
|
36592
|
-
var OSD_SUPPORTED_CONDITIONS = ["
|
|
36620
|
+
var OSD_SUPPORTED_CONDITIONS = ["deviceState"];
|
|
36621
|
+
var OSD_RUNTIME_CONDITIONS = ["devices", ...OSD_SUPPORTED_CONDITIONS];
|
|
36593
36622
|
var OPEN = { open: true };
|
|
36594
36623
|
/**
|
|
36595
36624
|
* Which condition keys are set on `conditions` but outside the supported
|
|
@@ -36597,7 +36626,7 @@ var OPEN = { open: true };
|
|
|
36597
36626
|
*/
|
|
36598
36627
|
function unsupportedConditionKeys(conditions) {
|
|
36599
36628
|
if (conditions === void 0) return [];
|
|
36600
|
-
const supported = new Set(
|
|
36629
|
+
const supported = new Set(OSD_RUNTIME_CONDITIONS);
|
|
36601
36630
|
const present = [];
|
|
36602
36631
|
for (const [key, value] of Object.entries(conditions)) {
|
|
36603
36632
|
if (value === void 0) continue;
|
|
@@ -36840,6 +36869,10 @@ function resolveSource(binding, cameraDeviceId, facts) {
|
|
|
36840
36869
|
const source = binding.source;
|
|
36841
36870
|
if (source.kind === "static") return { value: { raw: source.text } };
|
|
36842
36871
|
if (source.kind === "clock") return { value: { raw: formatClock(facts.atMs, source.pattern, source.timezone) } };
|
|
36872
|
+
if (source.kind === "latest-recognition") {
|
|
36873
|
+
const value = facts.recognitionByDevice.get(cameraDeviceId)?.[source.recognition];
|
|
36874
|
+
return value === void 0 ? { reason: `no recognized ${source.recognition} has been persisted for device ${cameraDeviceId}` } : { value: { raw: value } };
|
|
36875
|
+
}
|
|
36843
36876
|
const deviceId = source.deviceId ?? cameraDeviceId;
|
|
36844
36877
|
const deviceState = facts.stateByDevice.get(deviceId);
|
|
36845
36878
|
if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
|
|
@@ -36963,6 +36996,10 @@ var OsdManager = class {
|
|
|
36963
36996
|
lastWritten = /* @__PURE__ */ new Map();
|
|
36964
36997
|
/** Cameras with at least one binding — the loop's work list. */
|
|
36965
36998
|
boundDevices = /* @__PURE__ */ new Set();
|
|
36999
|
+
recognitions = /* @__PURE__ */ new Map();
|
|
37000
|
+
recognitionHydration = /* @__PURE__ */ new Map();
|
|
37001
|
+
recognitionGeneration = /* @__PURE__ */ new Map();
|
|
37002
|
+
recognitionInvalidatedAt = /* @__PURE__ */ new Map();
|
|
36966
37003
|
timer = null;
|
|
36967
37004
|
constructor(deps) {
|
|
36968
37005
|
this.deps = deps;
|
|
@@ -36990,6 +37027,36 @@ var OsdManager = class {
|
|
|
36990
37027
|
this.lastWritten.clear();
|
|
36991
37028
|
}
|
|
36992
37029
|
/**
|
|
37030
|
+
* Keep recognition overlays event-fresh. The persisted-track hydration below
|
|
37031
|
+
* remains the restart/cold-start authority; lifecycle events only advance it.
|
|
37032
|
+
*/
|
|
37033
|
+
onTrackLifecycle(payload) {
|
|
37034
|
+
let changed = false;
|
|
37035
|
+
if (payload.label !== void 0 && (payload.bestClassName === "person" || payload.classes.includes("person"))) changed = this.mergeRecognition(payload.deviceId, "person", payload.label, payload.lastSeen, payload.firstSeen) || changed;
|
|
37036
|
+
if (payload.plateText !== void 0) changed = this.mergeRecognition(payload.deviceId, "plate", payload.plateText, payload.lastSeen, payload.firstSeen) || changed;
|
|
37037
|
+
if (!changed || !this.boundDevices.has(payload.deviceId)) return;
|
|
37038
|
+
this.renderDeviceInternal(payload.deviceId, null).catch((err) => {
|
|
37039
|
+
this.logger.warn("osd-manager: recognition-triggered render failed", {
|
|
37040
|
+
tags: { deviceId: payload.deviceId },
|
|
37041
|
+
meta: { error: String(err) }
|
|
37042
|
+
});
|
|
37043
|
+
});
|
|
37044
|
+
}
|
|
37045
|
+
/** Gallery rows are authoritative for corrections, assignments and deletion. */
|
|
37046
|
+
onRecognitionGalleryChanged(deviceId) {
|
|
37047
|
+
this.recognitionGeneration.set(deviceId, (this.recognitionGeneration.get(deviceId) ?? 0) + 1);
|
|
37048
|
+
this.recognitionInvalidatedAt.set(deviceId, this.now());
|
|
37049
|
+
this.recognitions.delete(deviceId);
|
|
37050
|
+
this.recognitionHydration.delete(deviceId);
|
|
37051
|
+
if (!this.boundDevices.has(deviceId)) return;
|
|
37052
|
+
this.renderDeviceInternal(deviceId, null).catch((err) => {
|
|
37053
|
+
this.logger.warn("osd-manager: gallery-triggered render failed", {
|
|
37054
|
+
tags: { deviceId },
|
|
37055
|
+
meta: { error: String(err) }
|
|
37056
|
+
});
|
|
37057
|
+
});
|
|
37058
|
+
}
|
|
37059
|
+
/**
|
|
36993
37060
|
* Reconcile the work list against the store rather than trusting an event
|
|
36994
37061
|
* to have arrived (D8). Cheap: one settings read per camera, on the tick
|
|
36995
37062
|
* cadence, not per frame.
|
|
@@ -37083,6 +37150,54 @@ var OsdManager = class {
|
|
|
37083
37150
|
});
|
|
37084
37151
|
return { success: true };
|
|
37085
37152
|
};
|
|
37153
|
+
copyDeviceConfiguration = async ({ sourceDeviceId, targetDeviceId }) => {
|
|
37154
|
+
if (sourceDeviceId === targetDeviceId) throw new Error("osd-manager: source and target camera must be different");
|
|
37155
|
+
const [sourceStatus, targetStatus, sourceBindings] = await Promise.all([
|
|
37156
|
+
this.deps.readOsdStatus(sourceDeviceId),
|
|
37157
|
+
this.deps.readOsdStatus(targetDeviceId),
|
|
37158
|
+
this.readBindings(sourceDeviceId)
|
|
37159
|
+
]);
|
|
37160
|
+
if (sourceStatus === null) throw new Error(`osd-manager: source device ${sourceDeviceId} exposes no overlay slots`);
|
|
37161
|
+
if (targetStatus === null) throw new Error(`osd-manager: target device ${targetDeviceId} exposes no overlay slots`);
|
|
37162
|
+
const sourceSlots = sourceStatus.overlays.filter((slot) => slot.readOnly !== true && sourceBindings[slot.id] !== void 0);
|
|
37163
|
+
if (sourceSlots.length === 0) throw new Error(`osd-manager: source device ${sourceDeviceId} has no configured writable slots`);
|
|
37164
|
+
const targetSlots = targetStatus.overlays.filter((slot) => slot.readOnly !== true);
|
|
37165
|
+
const copiedBindings = {};
|
|
37166
|
+
const copyCount = Math.min(sourceSlots.length, targetSlots.length);
|
|
37167
|
+
for (let index = 0; index < copyCount; index += 1) {
|
|
37168
|
+
const sourceSlot = sourceSlots[index];
|
|
37169
|
+
const targetSlot = targetSlots[index];
|
|
37170
|
+
if (sourceSlot === void 0 || targetSlot === void 0) continue;
|
|
37171
|
+
const binding = sourceBindings[sourceSlot.id];
|
|
37172
|
+
if (binding === void 0) continue;
|
|
37173
|
+
const conditions = binding.conditions;
|
|
37174
|
+
const normalizedConditions = conditions === void 0 ? void 0 : (() => {
|
|
37175
|
+
const { devices: _cameraScope, ...rest } = conditions;
|
|
37176
|
+
return Object.keys(rest).length === 0 ? void 0 : rest;
|
|
37177
|
+
})();
|
|
37178
|
+
copiedBindings[targetSlot.id] = {
|
|
37179
|
+
...binding,
|
|
37180
|
+
conditions: normalizedConditions
|
|
37181
|
+
};
|
|
37182
|
+
}
|
|
37183
|
+
await this.deps.bindings(targetDeviceId).set(copiedBindings);
|
|
37184
|
+
for (const key of [...this.lastWritten.keys()]) if (key.startsWith(`${targetDeviceId}:`)) this.lastWritten.delete(key);
|
|
37185
|
+
if (Object.keys(copiedBindings).length > 0) this.boundDevices.add(targetDeviceId);
|
|
37186
|
+
else this.boundDevices.delete(targetDeviceId);
|
|
37187
|
+
await this.renderDeviceInternal(targetDeviceId, null);
|
|
37188
|
+
this.logger.info("osd-manager: camera configuration copied", {
|
|
37189
|
+
tags: { deviceId: targetDeviceId },
|
|
37190
|
+
meta: {
|
|
37191
|
+
sourceDeviceId,
|
|
37192
|
+
copied: Object.keys(copiedBindings).length,
|
|
37193
|
+
skipped: Math.max(0, sourceSlots.length - targetSlots.length)
|
|
37194
|
+
}
|
|
37195
|
+
});
|
|
37196
|
+
return {
|
|
37197
|
+
copied: Object.keys(copiedBindings).length,
|
|
37198
|
+
skipped: Math.max(0, sourceSlots.length - targetSlots.length)
|
|
37199
|
+
};
|
|
37200
|
+
};
|
|
37086
37201
|
previewSlot = async ({ deviceId, slotId, binding }) => {
|
|
37087
37202
|
if (binding !== void 0) assertSupportedConditions(binding.conditions);
|
|
37088
37203
|
const effective = binding ?? (await this.readBindings(deviceId))[slotId] ?? null;
|
|
@@ -37157,6 +37272,7 @@ var OsdManager = class {
|
|
|
37157
37272
|
*/
|
|
37158
37273
|
async collectFacts(cameraDeviceId, bindings) {
|
|
37159
37274
|
const wanted = new Set([cameraDeviceId]);
|
|
37275
|
+
const needsRecognition = Object.values(bindings).some((binding) => binding.source.kind === "latest-recognition");
|
|
37160
37276
|
for (const binding of Object.values(bindings)) {
|
|
37161
37277
|
if (binding.source.kind === "device-state" && binding.source.deviceId !== void 0) wanted.add(binding.source.deviceId);
|
|
37162
37278
|
const gated = binding.conditions?.deviceState?.deviceId;
|
|
@@ -37175,12 +37291,59 @@ var OsdManager = class {
|
|
|
37175
37291
|
meta: { error: String(err) }
|
|
37176
37292
|
});
|
|
37177
37293
|
}
|
|
37294
|
+
if (needsRecognition) await this.hydrateRecognitions(cameraDeviceId);
|
|
37295
|
+
const recognitionByDevice = /* @__PURE__ */ new Map();
|
|
37296
|
+
const known = this.recognitions.get(cameraDeviceId);
|
|
37297
|
+
if (known !== void 0) recognitionByDevice.set(cameraDeviceId, {
|
|
37298
|
+
...known.person !== void 0 ? { person: known.person.text } : {},
|
|
37299
|
+
...known.plate !== void 0 ? { plate: known.plate.text } : {}
|
|
37300
|
+
});
|
|
37178
37301
|
return {
|
|
37179
37302
|
atMs: this.now(),
|
|
37180
37303
|
stateByDevice,
|
|
37304
|
+
recognitionByDevice,
|
|
37181
37305
|
deviceState: (id) => gateState.get(id)
|
|
37182
37306
|
};
|
|
37183
37307
|
}
|
|
37308
|
+
async hydrateRecognitions(deviceId) {
|
|
37309
|
+
const existing = this.recognitionHydration.get(deviceId);
|
|
37310
|
+
if (existing !== void 0) return existing;
|
|
37311
|
+
const generation = this.recognitionGeneration.get(deviceId) ?? 0;
|
|
37312
|
+
const hydration = (async () => {
|
|
37313
|
+
try {
|
|
37314
|
+
const latest = await this.deps.readLatestRecognitions(deviceId);
|
|
37315
|
+
if ((this.recognitionGeneration.get(deviceId) ?? 0) !== generation) return;
|
|
37316
|
+
if (latest.person !== void 0) this.mergeRecognition(deviceId, "person", latest.person.text, latest.person.atMs);
|
|
37317
|
+
if (latest.plate !== void 0) this.mergeRecognition(deviceId, "plate", latest.plate.text, latest.plate.atMs);
|
|
37318
|
+
} catch (err) {
|
|
37319
|
+
this.logger.warn("osd-manager: recognition history hydration failed", {
|
|
37320
|
+
tags: { deviceId },
|
|
37321
|
+
meta: { error: String(err) }
|
|
37322
|
+
});
|
|
37323
|
+
this.recognitionHydration.delete(deviceId);
|
|
37324
|
+
}
|
|
37325
|
+
})();
|
|
37326
|
+
this.recognitionHydration.set(deviceId, hydration);
|
|
37327
|
+
await hydration;
|
|
37328
|
+
}
|
|
37329
|
+
mergeRecognition(deviceId, kind, text, atMs, trackFirstSeen) {
|
|
37330
|
+
const normalized = text.trim();
|
|
37331
|
+
if (normalized === "") return false;
|
|
37332
|
+
const invalidatedAt = this.recognitionInvalidatedAt.get(deviceId);
|
|
37333
|
+
if (trackFirstSeen !== void 0 && invalidatedAt !== void 0 && trackFirstSeen <= invalidatedAt) return false;
|
|
37334
|
+
const current = this.recognitions.get(deviceId) ?? {};
|
|
37335
|
+
const previous = current[kind];
|
|
37336
|
+
if (previous !== void 0 && previous.atMs > atMs) return false;
|
|
37337
|
+
if (previous?.atMs === atMs && previous.text === normalized) return false;
|
|
37338
|
+
this.recognitions.set(deviceId, {
|
|
37339
|
+
...current,
|
|
37340
|
+
[kind]: {
|
|
37341
|
+
text: normalized,
|
|
37342
|
+
atMs
|
|
37343
|
+
}
|
|
37344
|
+
});
|
|
37345
|
+
return true;
|
|
37346
|
+
}
|
|
37184
37347
|
async readBindings(deviceId) {
|
|
37185
37348
|
try {
|
|
37186
37349
|
return await this.deps.bindings(deviceId).get();
|
|
@@ -37240,7 +37403,8 @@ var OsdManagerAddon = class extends BaseAddon {
|
|
|
37240
37403
|
icon: "monitor",
|
|
37241
37404
|
path: "/addon/osd-manager",
|
|
37242
37405
|
remoteName: "addon_osd_manager_page",
|
|
37243
|
-
bundle: "remoteEntry.js"
|
|
37406
|
+
bundle: "remoteEntry.js",
|
|
37407
|
+
section: "detection"
|
|
37244
37408
|
}];
|
|
37245
37409
|
constructor() {
|
|
37246
37410
|
super({ ...DEFAULTS });
|
|
@@ -37288,9 +37452,41 @@ var OsdManagerAddon = class extends BaseAddon {
|
|
|
37288
37452
|
const bindings = await ctx.api.deviceManager.getBindings.query({ deviceId });
|
|
37289
37453
|
return [...new Set(bindings.entries.map((e) => e.capName))];
|
|
37290
37454
|
},
|
|
37455
|
+
readLatestRecognitions: async (deviceId) => {
|
|
37456
|
+
const [faces, plates] = await Promise.all([ctx.api.faceGallery.listRecentFaces.query({
|
|
37457
|
+
deviceId,
|
|
37458
|
+
limit: 1,
|
|
37459
|
+
filter: "identified",
|
|
37460
|
+
includeCrops: false
|
|
37461
|
+
}), ctx.api.plateGallery.listPlates.query({
|
|
37462
|
+
deviceId,
|
|
37463
|
+
limit: 1
|
|
37464
|
+
})]);
|
|
37465
|
+
const face = faces[0];
|
|
37466
|
+
const plate = plates[0];
|
|
37467
|
+
return {
|
|
37468
|
+
...face?.identityName !== void 0 ? { person: {
|
|
37469
|
+
text: face.identityName,
|
|
37470
|
+
atMs: face.timestamp
|
|
37471
|
+
} } : {},
|
|
37472
|
+
...plate !== void 0 ? { plate: {
|
|
37473
|
+
text: plate.text,
|
|
37474
|
+
atMs: plate.timestamp
|
|
37475
|
+
} } : {}
|
|
37476
|
+
};
|
|
37477
|
+
},
|
|
37291
37478
|
bindings: (deviceId) => store.forDevice(deviceId)
|
|
37292
37479
|
});
|
|
37293
37480
|
this.manager = manager;
|
|
37481
|
+
this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {
|
|
37482
|
+
manager.onTrackLifecycle(event.data);
|
|
37483
|
+
});
|
|
37484
|
+
this.subscribe({ category: EventCategory.PipelineAnalyticsFaceGalleryChanged }, (event) => {
|
|
37485
|
+
manager.onRecognitionGalleryChanged(event.data.deviceId);
|
|
37486
|
+
});
|
|
37487
|
+
this.subscribe({ category: EventCategory.PipelineAnalyticsPlateGalleryChanged }, (event) => {
|
|
37488
|
+
manager.onRecognitionGalleryChanged(event.data.deviceId);
|
|
37489
|
+
});
|
|
37294
37490
|
await manager.start(Math.max(1, this.config.tickSeconds) * 1e3);
|
|
37295
37491
|
const pagesProvider = {
|
|
37296
37492
|
id: this.id,
|
package/dist/index.mjs
CHANGED
|
@@ -24798,6 +24798,7 @@ var FaceInfoSchema = object({
|
|
|
24798
24798
|
var FaceFilterEnum = _enum([
|
|
24799
24799
|
"unassigned",
|
|
24800
24800
|
"recognized",
|
|
24801
|
+
"identified",
|
|
24801
24802
|
"all"
|
|
24802
24803
|
]);
|
|
24803
24804
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -24837,6 +24838,8 @@ var faceGalleryCapability = {
|
|
|
24837
24838
|
auth: "admin"
|
|
24838
24839
|
}),
|
|
24839
24840
|
listRecentFaces: method(object({
|
|
24841
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
24842
|
+
deviceId: number().int().optional(),
|
|
24840
24843
|
limit: number().int().positive().optional(),
|
|
24841
24844
|
filter: FaceFilterEnum.optional(),
|
|
24842
24845
|
/**
|
|
@@ -27380,6 +27383,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
27380
27383
|
capName: string().min(1).max(64),
|
|
27381
27384
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
27382
27385
|
valuePath: string().min(1).max(64)
|
|
27386
|
+
}),
|
|
27387
|
+
object({
|
|
27388
|
+
kind: literal("latest-recognition"),
|
|
27389
|
+
recognition: _enum(["person", "plate"])
|
|
27383
27390
|
})
|
|
27384
27391
|
]);
|
|
27385
27392
|
var OsdSlotBindingSchema = object({
|
|
@@ -27518,6 +27525,21 @@ var osdManagerCapability = {
|
|
|
27518
27525
|
auth: "admin"
|
|
27519
27526
|
}),
|
|
27520
27527
|
/**
|
|
27528
|
+
* Replace one camera's binding configuration from another. Writable source
|
|
27529
|
+
* slots are mapped by ordinal onto writable target slots so different
|
|
27530
|
+
* provider slot ids do not make the copied configuration disappear.
|
|
27531
|
+
*/
|
|
27532
|
+
copyDeviceConfiguration: method(object({
|
|
27533
|
+
sourceDeviceId: number().int(),
|
|
27534
|
+
targetDeviceId: number().int()
|
|
27535
|
+
}), object({
|
|
27536
|
+
copied: number().int().nonnegative(),
|
|
27537
|
+
skipped: number().int().nonnegative()
|
|
27538
|
+
}), {
|
|
27539
|
+
kind: "mutation",
|
|
27540
|
+
auth: "admin"
|
|
27541
|
+
}),
|
|
27542
|
+
/**
|
|
27521
27543
|
* Render without writing. `binding` overrides the stored one so an
|
|
27522
27544
|
* editor can preview an unsaved change. Mutation kind only to carry
|
|
27523
27545
|
* the binding object safely; no side effects.
|
|
@@ -33702,6 +33724,12 @@ Object.freeze({
|
|
|
33702
33724
|
addonId: null,
|
|
33703
33725
|
access: "delete"
|
|
33704
33726
|
},
|
|
33727
|
+
"osdManager.copyDeviceConfiguration": {
|
|
33728
|
+
capName: "osd-manager",
|
|
33729
|
+
capScope: "system",
|
|
33730
|
+
addonId: null,
|
|
33731
|
+
access: "create"
|
|
33732
|
+
},
|
|
33705
33733
|
"osdManager.getConditionSupport": {
|
|
33706
33734
|
capName: "osd-manager",
|
|
33707
33735
|
capScope: "system",
|
|
@@ -36585,7 +36613,8 @@ var OsdBindingsStore = class {
|
|
|
36585
36613
|
* Widening it means giving `evaluateGate` the facts to answer it — a
|
|
36586
36614
|
* live detection subject would unlock `classes`, `zones`, `minConfidence`.
|
|
36587
36615
|
*/
|
|
36588
|
-
var OSD_SUPPORTED_CONDITIONS = ["
|
|
36616
|
+
var OSD_SUPPORTED_CONDITIONS = ["deviceState"];
|
|
36617
|
+
var OSD_RUNTIME_CONDITIONS = ["devices", ...OSD_SUPPORTED_CONDITIONS];
|
|
36589
36618
|
var OPEN = { open: true };
|
|
36590
36619
|
/**
|
|
36591
36620
|
* Which condition keys are set on `conditions` but outside the supported
|
|
@@ -36593,7 +36622,7 @@ var OPEN = { open: true };
|
|
|
36593
36622
|
*/
|
|
36594
36623
|
function unsupportedConditionKeys(conditions) {
|
|
36595
36624
|
if (conditions === void 0) return [];
|
|
36596
|
-
const supported = new Set(
|
|
36625
|
+
const supported = new Set(OSD_RUNTIME_CONDITIONS);
|
|
36597
36626
|
const present = [];
|
|
36598
36627
|
for (const [key, value] of Object.entries(conditions)) {
|
|
36599
36628
|
if (value === void 0) continue;
|
|
@@ -36836,6 +36865,10 @@ function resolveSource(binding, cameraDeviceId, facts) {
|
|
|
36836
36865
|
const source = binding.source;
|
|
36837
36866
|
if (source.kind === "static") return { value: { raw: source.text } };
|
|
36838
36867
|
if (source.kind === "clock") return { value: { raw: formatClock(facts.atMs, source.pattern, source.timezone) } };
|
|
36868
|
+
if (source.kind === "latest-recognition") {
|
|
36869
|
+
const value = facts.recognitionByDevice.get(cameraDeviceId)?.[source.recognition];
|
|
36870
|
+
return value === void 0 ? { reason: `no recognized ${source.recognition} has been persisted for device ${cameraDeviceId}` } : { value: { raw: value } };
|
|
36871
|
+
}
|
|
36839
36872
|
const deviceId = source.deviceId ?? cameraDeviceId;
|
|
36840
36873
|
const deviceState = facts.stateByDevice.get(deviceId);
|
|
36841
36874
|
if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
|
|
@@ -36959,6 +36992,10 @@ var OsdManager = class {
|
|
|
36959
36992
|
lastWritten = /* @__PURE__ */ new Map();
|
|
36960
36993
|
/** Cameras with at least one binding — the loop's work list. */
|
|
36961
36994
|
boundDevices = /* @__PURE__ */ new Set();
|
|
36995
|
+
recognitions = /* @__PURE__ */ new Map();
|
|
36996
|
+
recognitionHydration = /* @__PURE__ */ new Map();
|
|
36997
|
+
recognitionGeneration = /* @__PURE__ */ new Map();
|
|
36998
|
+
recognitionInvalidatedAt = /* @__PURE__ */ new Map();
|
|
36962
36999
|
timer = null;
|
|
36963
37000
|
constructor(deps) {
|
|
36964
37001
|
this.deps = deps;
|
|
@@ -36986,6 +37023,36 @@ var OsdManager = class {
|
|
|
36986
37023
|
this.lastWritten.clear();
|
|
36987
37024
|
}
|
|
36988
37025
|
/**
|
|
37026
|
+
* Keep recognition overlays event-fresh. The persisted-track hydration below
|
|
37027
|
+
* remains the restart/cold-start authority; lifecycle events only advance it.
|
|
37028
|
+
*/
|
|
37029
|
+
onTrackLifecycle(payload) {
|
|
37030
|
+
let changed = false;
|
|
37031
|
+
if (payload.label !== void 0 && (payload.bestClassName === "person" || payload.classes.includes("person"))) changed = this.mergeRecognition(payload.deviceId, "person", payload.label, payload.lastSeen, payload.firstSeen) || changed;
|
|
37032
|
+
if (payload.plateText !== void 0) changed = this.mergeRecognition(payload.deviceId, "plate", payload.plateText, payload.lastSeen, payload.firstSeen) || changed;
|
|
37033
|
+
if (!changed || !this.boundDevices.has(payload.deviceId)) return;
|
|
37034
|
+
this.renderDeviceInternal(payload.deviceId, null).catch((err) => {
|
|
37035
|
+
this.logger.warn("osd-manager: recognition-triggered render failed", {
|
|
37036
|
+
tags: { deviceId: payload.deviceId },
|
|
37037
|
+
meta: { error: String(err) }
|
|
37038
|
+
});
|
|
37039
|
+
});
|
|
37040
|
+
}
|
|
37041
|
+
/** Gallery rows are authoritative for corrections, assignments and deletion. */
|
|
37042
|
+
onRecognitionGalleryChanged(deviceId) {
|
|
37043
|
+
this.recognitionGeneration.set(deviceId, (this.recognitionGeneration.get(deviceId) ?? 0) + 1);
|
|
37044
|
+
this.recognitionInvalidatedAt.set(deviceId, this.now());
|
|
37045
|
+
this.recognitions.delete(deviceId);
|
|
37046
|
+
this.recognitionHydration.delete(deviceId);
|
|
37047
|
+
if (!this.boundDevices.has(deviceId)) return;
|
|
37048
|
+
this.renderDeviceInternal(deviceId, null).catch((err) => {
|
|
37049
|
+
this.logger.warn("osd-manager: gallery-triggered render failed", {
|
|
37050
|
+
tags: { deviceId },
|
|
37051
|
+
meta: { error: String(err) }
|
|
37052
|
+
});
|
|
37053
|
+
});
|
|
37054
|
+
}
|
|
37055
|
+
/**
|
|
36989
37056
|
* Reconcile the work list against the store rather than trusting an event
|
|
36990
37057
|
* to have arrived (D8). Cheap: one settings read per camera, on the tick
|
|
36991
37058
|
* cadence, not per frame.
|
|
@@ -37079,6 +37146,54 @@ var OsdManager = class {
|
|
|
37079
37146
|
});
|
|
37080
37147
|
return { success: true };
|
|
37081
37148
|
};
|
|
37149
|
+
copyDeviceConfiguration = async ({ sourceDeviceId, targetDeviceId }) => {
|
|
37150
|
+
if (sourceDeviceId === targetDeviceId) throw new Error("osd-manager: source and target camera must be different");
|
|
37151
|
+
const [sourceStatus, targetStatus, sourceBindings] = await Promise.all([
|
|
37152
|
+
this.deps.readOsdStatus(sourceDeviceId),
|
|
37153
|
+
this.deps.readOsdStatus(targetDeviceId),
|
|
37154
|
+
this.readBindings(sourceDeviceId)
|
|
37155
|
+
]);
|
|
37156
|
+
if (sourceStatus === null) throw new Error(`osd-manager: source device ${sourceDeviceId} exposes no overlay slots`);
|
|
37157
|
+
if (targetStatus === null) throw new Error(`osd-manager: target device ${targetDeviceId} exposes no overlay slots`);
|
|
37158
|
+
const sourceSlots = sourceStatus.overlays.filter((slot) => slot.readOnly !== true && sourceBindings[slot.id] !== void 0);
|
|
37159
|
+
if (sourceSlots.length === 0) throw new Error(`osd-manager: source device ${sourceDeviceId} has no configured writable slots`);
|
|
37160
|
+
const targetSlots = targetStatus.overlays.filter((slot) => slot.readOnly !== true);
|
|
37161
|
+
const copiedBindings = {};
|
|
37162
|
+
const copyCount = Math.min(sourceSlots.length, targetSlots.length);
|
|
37163
|
+
for (let index = 0; index < copyCount; index += 1) {
|
|
37164
|
+
const sourceSlot = sourceSlots[index];
|
|
37165
|
+
const targetSlot = targetSlots[index];
|
|
37166
|
+
if (sourceSlot === void 0 || targetSlot === void 0) continue;
|
|
37167
|
+
const binding = sourceBindings[sourceSlot.id];
|
|
37168
|
+
if (binding === void 0) continue;
|
|
37169
|
+
const conditions = binding.conditions;
|
|
37170
|
+
const normalizedConditions = conditions === void 0 ? void 0 : (() => {
|
|
37171
|
+
const { devices: _cameraScope, ...rest } = conditions;
|
|
37172
|
+
return Object.keys(rest).length === 0 ? void 0 : rest;
|
|
37173
|
+
})();
|
|
37174
|
+
copiedBindings[targetSlot.id] = {
|
|
37175
|
+
...binding,
|
|
37176
|
+
conditions: normalizedConditions
|
|
37177
|
+
};
|
|
37178
|
+
}
|
|
37179
|
+
await this.deps.bindings(targetDeviceId).set(copiedBindings);
|
|
37180
|
+
for (const key of [...this.lastWritten.keys()]) if (key.startsWith(`${targetDeviceId}:`)) this.lastWritten.delete(key);
|
|
37181
|
+
if (Object.keys(copiedBindings).length > 0) this.boundDevices.add(targetDeviceId);
|
|
37182
|
+
else this.boundDevices.delete(targetDeviceId);
|
|
37183
|
+
await this.renderDeviceInternal(targetDeviceId, null);
|
|
37184
|
+
this.logger.info("osd-manager: camera configuration copied", {
|
|
37185
|
+
tags: { deviceId: targetDeviceId },
|
|
37186
|
+
meta: {
|
|
37187
|
+
sourceDeviceId,
|
|
37188
|
+
copied: Object.keys(copiedBindings).length,
|
|
37189
|
+
skipped: Math.max(0, sourceSlots.length - targetSlots.length)
|
|
37190
|
+
}
|
|
37191
|
+
});
|
|
37192
|
+
return {
|
|
37193
|
+
copied: Object.keys(copiedBindings).length,
|
|
37194
|
+
skipped: Math.max(0, sourceSlots.length - targetSlots.length)
|
|
37195
|
+
};
|
|
37196
|
+
};
|
|
37082
37197
|
previewSlot = async ({ deviceId, slotId, binding }) => {
|
|
37083
37198
|
if (binding !== void 0) assertSupportedConditions(binding.conditions);
|
|
37084
37199
|
const effective = binding ?? (await this.readBindings(deviceId))[slotId] ?? null;
|
|
@@ -37153,6 +37268,7 @@ var OsdManager = class {
|
|
|
37153
37268
|
*/
|
|
37154
37269
|
async collectFacts(cameraDeviceId, bindings) {
|
|
37155
37270
|
const wanted = new Set([cameraDeviceId]);
|
|
37271
|
+
const needsRecognition = Object.values(bindings).some((binding) => binding.source.kind === "latest-recognition");
|
|
37156
37272
|
for (const binding of Object.values(bindings)) {
|
|
37157
37273
|
if (binding.source.kind === "device-state" && binding.source.deviceId !== void 0) wanted.add(binding.source.deviceId);
|
|
37158
37274
|
const gated = binding.conditions?.deviceState?.deviceId;
|
|
@@ -37171,12 +37287,59 @@ var OsdManager = class {
|
|
|
37171
37287
|
meta: { error: String(err) }
|
|
37172
37288
|
});
|
|
37173
37289
|
}
|
|
37290
|
+
if (needsRecognition) await this.hydrateRecognitions(cameraDeviceId);
|
|
37291
|
+
const recognitionByDevice = /* @__PURE__ */ new Map();
|
|
37292
|
+
const known = this.recognitions.get(cameraDeviceId);
|
|
37293
|
+
if (known !== void 0) recognitionByDevice.set(cameraDeviceId, {
|
|
37294
|
+
...known.person !== void 0 ? { person: known.person.text } : {},
|
|
37295
|
+
...known.plate !== void 0 ? { plate: known.plate.text } : {}
|
|
37296
|
+
});
|
|
37174
37297
|
return {
|
|
37175
37298
|
atMs: this.now(),
|
|
37176
37299
|
stateByDevice,
|
|
37300
|
+
recognitionByDevice,
|
|
37177
37301
|
deviceState: (id) => gateState.get(id)
|
|
37178
37302
|
};
|
|
37179
37303
|
}
|
|
37304
|
+
async hydrateRecognitions(deviceId) {
|
|
37305
|
+
const existing = this.recognitionHydration.get(deviceId);
|
|
37306
|
+
if (existing !== void 0) return existing;
|
|
37307
|
+
const generation = this.recognitionGeneration.get(deviceId) ?? 0;
|
|
37308
|
+
const hydration = (async () => {
|
|
37309
|
+
try {
|
|
37310
|
+
const latest = await this.deps.readLatestRecognitions(deviceId);
|
|
37311
|
+
if ((this.recognitionGeneration.get(deviceId) ?? 0) !== generation) return;
|
|
37312
|
+
if (latest.person !== void 0) this.mergeRecognition(deviceId, "person", latest.person.text, latest.person.atMs);
|
|
37313
|
+
if (latest.plate !== void 0) this.mergeRecognition(deviceId, "plate", latest.plate.text, latest.plate.atMs);
|
|
37314
|
+
} catch (err) {
|
|
37315
|
+
this.logger.warn("osd-manager: recognition history hydration failed", {
|
|
37316
|
+
tags: { deviceId },
|
|
37317
|
+
meta: { error: String(err) }
|
|
37318
|
+
});
|
|
37319
|
+
this.recognitionHydration.delete(deviceId);
|
|
37320
|
+
}
|
|
37321
|
+
})();
|
|
37322
|
+
this.recognitionHydration.set(deviceId, hydration);
|
|
37323
|
+
await hydration;
|
|
37324
|
+
}
|
|
37325
|
+
mergeRecognition(deviceId, kind, text, atMs, trackFirstSeen) {
|
|
37326
|
+
const normalized = text.trim();
|
|
37327
|
+
if (normalized === "") return false;
|
|
37328
|
+
const invalidatedAt = this.recognitionInvalidatedAt.get(deviceId);
|
|
37329
|
+
if (trackFirstSeen !== void 0 && invalidatedAt !== void 0 && trackFirstSeen <= invalidatedAt) return false;
|
|
37330
|
+
const current = this.recognitions.get(deviceId) ?? {};
|
|
37331
|
+
const previous = current[kind];
|
|
37332
|
+
if (previous !== void 0 && previous.atMs > atMs) return false;
|
|
37333
|
+
if (previous?.atMs === atMs && previous.text === normalized) return false;
|
|
37334
|
+
this.recognitions.set(deviceId, {
|
|
37335
|
+
...current,
|
|
37336
|
+
[kind]: {
|
|
37337
|
+
text: normalized,
|
|
37338
|
+
atMs
|
|
37339
|
+
}
|
|
37340
|
+
});
|
|
37341
|
+
return true;
|
|
37342
|
+
}
|
|
37180
37343
|
async readBindings(deviceId) {
|
|
37181
37344
|
try {
|
|
37182
37345
|
return await this.deps.bindings(deviceId).get();
|
|
@@ -37236,7 +37399,8 @@ var OsdManagerAddon = class extends BaseAddon {
|
|
|
37236
37399
|
icon: "monitor",
|
|
37237
37400
|
path: "/addon/osd-manager",
|
|
37238
37401
|
remoteName: "addon_osd_manager_page",
|
|
37239
|
-
bundle: "remoteEntry.js"
|
|
37402
|
+
bundle: "remoteEntry.js",
|
|
37403
|
+
section: "detection"
|
|
37240
37404
|
}];
|
|
37241
37405
|
constructor() {
|
|
37242
37406
|
super({ ...DEFAULTS });
|
|
@@ -37284,9 +37448,41 @@ var OsdManagerAddon = class extends BaseAddon {
|
|
|
37284
37448
|
const bindings = await ctx.api.deviceManager.getBindings.query({ deviceId });
|
|
37285
37449
|
return [...new Set(bindings.entries.map((e) => e.capName))];
|
|
37286
37450
|
},
|
|
37451
|
+
readLatestRecognitions: async (deviceId) => {
|
|
37452
|
+
const [faces, plates] = await Promise.all([ctx.api.faceGallery.listRecentFaces.query({
|
|
37453
|
+
deviceId,
|
|
37454
|
+
limit: 1,
|
|
37455
|
+
filter: "identified",
|
|
37456
|
+
includeCrops: false
|
|
37457
|
+
}), ctx.api.plateGallery.listPlates.query({
|
|
37458
|
+
deviceId,
|
|
37459
|
+
limit: 1
|
|
37460
|
+
})]);
|
|
37461
|
+
const face = faces[0];
|
|
37462
|
+
const plate = plates[0];
|
|
37463
|
+
return {
|
|
37464
|
+
...face?.identityName !== void 0 ? { person: {
|
|
37465
|
+
text: face.identityName,
|
|
37466
|
+
atMs: face.timestamp
|
|
37467
|
+
} } : {},
|
|
37468
|
+
...plate !== void 0 ? { plate: {
|
|
37469
|
+
text: plate.text,
|
|
37470
|
+
atMs: plate.timestamp
|
|
37471
|
+
} } : {}
|
|
37472
|
+
};
|
|
37473
|
+
},
|
|
37287
37474
|
bindings: (deviceId) => store.forDevice(deviceId)
|
|
37288
37475
|
});
|
|
37289
37476
|
this.manager = manager;
|
|
37477
|
+
this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {
|
|
37478
|
+
manager.onTrackLifecycle(event.data);
|
|
37479
|
+
});
|
|
37480
|
+
this.subscribe({ category: EventCategory.PipelineAnalyticsFaceGalleryChanged }, (event) => {
|
|
37481
|
+
manager.onRecognitionGalleryChanged(event.data.deviceId);
|
|
37482
|
+
});
|
|
37483
|
+
this.subscribe({ category: EventCategory.PipelineAnalyticsPlateGalleryChanged }, (event) => {
|
|
37484
|
+
manager.onRecognitionGalleryChanged(event.data.deviceId);
|
|
37485
|
+
});
|
|
37290
37486
|
await manager.start(Math.max(1, this.config.tickSeconds) * 1e3);
|
|
37291
37487
|
const pagesProvider = {
|
|
37292
37488
|
id: this.id,
|
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-CdG2yVd0.mjs";
|
|
2
2
|
export { t as get, e as init };
|
|
@@ -2753,7 +2753,7 @@ async function rr(e) {
|
|
|
2753
2753
|
}
|
|
2754
2754
|
}
|
|
2755
2755
|
async function ir() {
|
|
2756
|
-
return tr ||= rr(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-
|
|
2756
|
+
return tr ||= rr(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-CL-YG2or.mjs")).catch((e) => {
|
|
2757
2757
|
throw tr = void 0, e;
|
|
2758
2758
|
}), tr;
|
|
2759
2759
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-osd-manager",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Binds camera on-screen-display slots to live
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Binds camera on-screen-display slots to live state and recognitions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
7
7
|
"addon",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"category": "system",
|
|
34
34
|
"name": "OSD Manager",
|
|
35
35
|
"version": "0.1.0",
|
|
36
|
-
"description": "Drives the text a camera burns into its own video. Each overlay slot
|
|
36
|
+
"description": "Drives the text a camera burns into its own video. Each overlay slot can be bound to device state, the latest recognized person or plate, a clock, or a literal. Writes nothing when the rendered text has not changed.",
|
|
37
37
|
"entry": "./dist/index.js",
|
|
38
38
|
"icon": "assets/icon.svg",
|
|
39
39
|
"color": "#f59e0b",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"icon": "monitor",
|
|
58
58
|
"path": "/addon/osd-manager",
|
|
59
59
|
"remoteName": "addon_osd_manager_page",
|
|
60
|
-
"bundle": "remoteEntry.js"
|
|
60
|
+
"bundle": "remoteEntry.js",
|
|
61
|
+
"section": "detection"
|
|
61
62
|
}
|
|
62
63
|
]
|
|
63
64
|
}
|