@camstack/addon-pipeline 1.2.203 → 1.2.205
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/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-ClY8fk-K.mjs → default-detection-model-23wBHkOQ.mjs} +1 -1
- package/dist/{default-detection-model-CfIEdJop.js → default-detection-model-BGsoboIK.js} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-Czm3CW8M.js → dist-CR3_dWir.js} +17 -7
- package/dist/{dist-B5x2ogXC.mjs → dist-FaCzok3C.mjs} +17 -7
- package/dist/{lazy-sharp-AhHg_Hb-.js → lazy-sharp-CKXcDP-V.js} +1 -1
- package/dist/motion-wasm/index.js +296 -18
- package/dist/motion-wasm/index.mjs +295 -17
- package/dist/{node-u0M7kQe-.js → node-Re3uLz8K.js} +1 -1
- package/dist/{node-Bx76sJbU.mjs → node-f4fUlGHQ.mjs} +1 -1
- package/dist/pipeline-runner/index.js +5 -5
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-D5JcMSDA.js → process-memory-DNheSoSc.js} +1 -1
- package/dist/{process-memory-iiE4Q2mt.mjs → process-memory-MC7wcxqk.mjs} +1 -1
- package/dist/recorder/index.js +61 -13
- package/dist/recorder/index.mjs +61 -13
- package/dist/{segment-demux-js-CSt1T2Vi.js → segment-demux-js-CQYpP1SU.js} +1 -1
- package/dist/{segment-demux-js-BcbB_hcJ.mjs → segment-demux-js-DUpovUKi.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BSWELhoJ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D_m12d-s.mjs} +3 -3
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-BJlv_j8i.mjs → hostInit-Dfu865rx.mjs} +3 -3
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-ClwpoGoP.mjs → worker-protocol-Dz6_u2y6.mjs} +1 -1
- package/dist/{worker-protocol-B4C4seRh.js → worker-protocol-lIjbPNbH.js} +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,182 @@
|
|
|
1
|
-
import { Jt as hydrateSchema, Kt as DeviceType, Ut as BaseAddon, X as evaluateZoneRules, ft as motionDetectionCapability } from "../dist-
|
|
1
|
+
import { Jt as hydrateSchema, Kt as DeviceType, Ut as BaseAddon, X as evaluateZoneRules, ft as motionDetectionCapability } from "../dist-FaCzok3C.mjs";
|
|
2
2
|
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
//#region src/motion-wasm/region-grouping.ts
|
|
6
|
+
/**
|
|
7
|
+
* Default area bar, as a fraction of the motion view.
|
|
8
|
+
*
|
|
9
|
+
* Chosen from the measured separation between true-positive group areas and
|
|
10
|
+
* quiet-footage group areas — see `docs/benchmarks/motion-region-area.md`.
|
|
11
|
+
* On the 320×180 production motion view this is ~490 px, against the 1500 px
|
|
12
|
+
* absolute filter it replaces.
|
|
13
|
+
*/
|
|
14
|
+
var DEFAULT_MIN_AREA_FRACTION = .0085;
|
|
15
|
+
/**
|
|
16
|
+
* The DEFAULT is the rule that shipped before D394: a 1500 px absolute bar per
|
|
17
|
+
* component. The measured rule is per-camera opt-in (`minArea: 0`), because
|
|
18
|
+
* the measurement says duty cycle rises between ×1.45 and ×2.82 and ten fleet
|
|
19
|
+
* cameras turn that into recorded hours that cannot be measured from their own
|
|
20
|
+
* footage. The end state is still one relative constant for the fleet; the
|
|
21
|
+
* path there is one camera at a time.
|
|
22
|
+
*/
|
|
23
|
+
var DEFAULT_MIN_AREA = 1500;
|
|
24
|
+
/**
|
|
25
|
+
* Default storm bar. A group of changed pixels covering more than this
|
|
26
|
+
* fraction of the view is not a subject — it is weather, an exposure step or
|
|
27
|
+
* an IR cut. See the storm section of `docs/benchmarks/motion-region-area.md`.
|
|
28
|
+
*/
|
|
29
|
+
var DEFAULT_MAX_COVERAGE_FRACTION = .5;
|
|
30
|
+
/** The bar in pixels: the absolute one when set, the relative rule otherwise. */
|
|
31
|
+
function resolveMinGroupArea(config) {
|
|
32
|
+
if (config.minArea > 0) return config.minArea;
|
|
33
|
+
const area = Math.max(0, config.viewWidth) * Math.max(0, config.viewHeight);
|
|
34
|
+
return Math.round(config.minAreaFraction * area);
|
|
35
|
+
}
|
|
36
|
+
function grow(region, radius) {
|
|
37
|
+
const cx = region.x + region.w / 2;
|
|
38
|
+
const cy = region.y + region.h / 2;
|
|
39
|
+
const halfW = Math.max(region.w, radius) / 2;
|
|
40
|
+
const halfH = Math.max(region.h, radius) / 2;
|
|
41
|
+
return {
|
|
42
|
+
x0: cx - halfW,
|
|
43
|
+
y0: cy - halfH,
|
|
44
|
+
x1: cx + halfW,
|
|
45
|
+
y1: cy + halfH
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function overlaps(a, b) {
|
|
49
|
+
return a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Merge fragments into subjects and judge each subject on its own summed area.
|
|
53
|
+
*
|
|
54
|
+
* Groups come back in the order of their first member, so the frame's reading
|
|
55
|
+
* order is stable across calls — a UI overlay does not reshuffle.
|
|
56
|
+
*
|
|
57
|
+
* A group over `maxCoverageFraction` is a storm and does not pass as a group;
|
|
58
|
+
* its components are then judged individually, so a subject the storm merged
|
|
59
|
+
* with is not silenced along with it.
|
|
60
|
+
*
|
|
61
|
+
* A positive `config.minArea` short-circuits all of it and runs the pre-D394
|
|
62
|
+
* rule instead — see {@link RegionGroupingConfig.minArea}.
|
|
63
|
+
*/
|
|
64
|
+
function groupMotionRegions(regions, config) {
|
|
65
|
+
if (regions.length === 0) return {
|
|
66
|
+
groups: [],
|
|
67
|
+
passed: [],
|
|
68
|
+
suppressedByCoverage: 0
|
|
69
|
+
};
|
|
70
|
+
if (config.minArea > 0) {
|
|
71
|
+
const legacy = regions.map((r) => ({
|
|
72
|
+
x: r.x,
|
|
73
|
+
y: r.y,
|
|
74
|
+
w: r.w,
|
|
75
|
+
h: r.h,
|
|
76
|
+
pixels: r.pixels,
|
|
77
|
+
members: 1
|
|
78
|
+
}));
|
|
79
|
+
return {
|
|
80
|
+
groups: legacy,
|
|
81
|
+
passed: legacy.filter((g) => g.pixels >= config.minArea),
|
|
82
|
+
suppressedByCoverage: 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const radius = Math.max(1, Math.floor(Math.min(config.viewWidth, config.viewHeight) / Math.max(1, config.mergeRadiusDivisor)));
|
|
86
|
+
const grown = regions.map((r) => grow(r, radius));
|
|
87
|
+
const parent = regions.map((_, i) => i);
|
|
88
|
+
const find = (i) => {
|
|
89
|
+
let root = i;
|
|
90
|
+
while (parent[root] !== root) root = parent[root];
|
|
91
|
+
let cur = i;
|
|
92
|
+
while (parent[cur] !== root) {
|
|
93
|
+
const next = parent[cur];
|
|
94
|
+
parent[cur] = root;
|
|
95
|
+
cur = next;
|
|
96
|
+
}
|
|
97
|
+
return root;
|
|
98
|
+
};
|
|
99
|
+
for (let i = 0; i < regions.length; i++) for (let j = i + 1; j < regions.length; j++) {
|
|
100
|
+
if (!overlaps(grown[i], grown[j])) continue;
|
|
101
|
+
const ri = find(i);
|
|
102
|
+
const rj = find(j);
|
|
103
|
+
if (ri !== rj) parent[rj] = ri;
|
|
104
|
+
}
|
|
105
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
106
|
+
const order = [];
|
|
107
|
+
for (let i = 0; i < regions.length; i++) {
|
|
108
|
+
const region = regions[i];
|
|
109
|
+
const root = find(i);
|
|
110
|
+
const existing = byRoot.get(root);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
order.push(root);
|
|
113
|
+
byRoot.set(root, {
|
|
114
|
+
x0: region.x,
|
|
115
|
+
y0: region.y,
|
|
116
|
+
x1: region.x + region.w,
|
|
117
|
+
y1: region.y + region.h,
|
|
118
|
+
pixels: region.pixels,
|
|
119
|
+
members: 1
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
existing.x0 = Math.min(existing.x0, region.x);
|
|
124
|
+
existing.y0 = Math.min(existing.y0, region.y);
|
|
125
|
+
existing.x1 = Math.max(existing.x1, region.x + region.w);
|
|
126
|
+
existing.y1 = Math.max(existing.y1, region.y + region.h);
|
|
127
|
+
existing.pixels += region.pixels;
|
|
128
|
+
existing.members += 1;
|
|
129
|
+
}
|
|
130
|
+
const groups = order.map((root) => {
|
|
131
|
+
const a = byRoot.get(root);
|
|
132
|
+
return {
|
|
133
|
+
x: a.x0,
|
|
134
|
+
y: a.y0,
|
|
135
|
+
w: a.x1 - a.x0,
|
|
136
|
+
h: a.y1 - a.y0,
|
|
137
|
+
pixels: a.pixels,
|
|
138
|
+
members: a.members
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
const bar = resolveMinGroupArea(config);
|
|
142
|
+
const viewArea = Math.max(0, config.viewWidth) * Math.max(0, config.viewHeight);
|
|
143
|
+
const stormBar = config.maxCoverageFraction >= 1 ? Infinity : config.maxCoverageFraction * viewArea;
|
|
144
|
+
const membersByGroup = /* @__PURE__ */ new Map();
|
|
145
|
+
for (let i = 0; i < regions.length; i++) {
|
|
146
|
+
const root = find(i);
|
|
147
|
+
const list = membersByGroup.get(root) ?? [];
|
|
148
|
+
list.push(regions[i]);
|
|
149
|
+
membersByGroup.set(root, list);
|
|
150
|
+
}
|
|
151
|
+
const passed = [];
|
|
152
|
+
let suppressedByCoverage = 0;
|
|
153
|
+
for (let g = 0; g < groups.length; g++) {
|
|
154
|
+
const group = groups[g];
|
|
155
|
+
if (group.pixels < bar) continue;
|
|
156
|
+
if (group.pixels <= stormBar) {
|
|
157
|
+
passed.push(group);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
suppressedByCoverage += 1;
|
|
161
|
+
for (const member of membersByGroup.get(order[g]) ?? []) {
|
|
162
|
+
if (member.pixels < bar || member.pixels > stormBar) continue;
|
|
163
|
+
passed.push({
|
|
164
|
+
x: member.x,
|
|
165
|
+
y: member.y,
|
|
166
|
+
w: member.w,
|
|
167
|
+
h: member.h,
|
|
168
|
+
pixels: member.pixels,
|
|
169
|
+
members: 1
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
groups,
|
|
175
|
+
passed,
|
|
176
|
+
suppressedByCoverage
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
5
180
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
6
181
|
/**
|
|
7
182
|
* WasmMotionDetector — loads the AssemblyScript WASM module and provides
|
|
@@ -9,18 +184,33 @@ import { join } from "node:path";
|
|
|
9
184
|
*
|
|
10
185
|
* Pipeline: blur → diff → threshold → dilate → CCL → bounding boxes
|
|
11
186
|
* All in one WASM call, ~3ms for 640×360 grayscale frames.
|
|
187
|
+
*
|
|
188
|
+
* The area filter that follows is NOT applied per connected component — see
|
|
189
|
+
* {@link groupMotionRegions}. Fragments of one subject are merged first and the
|
|
190
|
+
* bar is applied to the merged subject, relative to the motion view. D394.
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* The one place the motion defaults live. The addon's schema defaults read
|
|
194
|
+
* from here — two copies of this object drifted apart once already (3000 here
|
|
195
|
+
* against 1500 in the addon), and the addon's copy is the one that ran.
|
|
12
196
|
*/
|
|
13
|
-
var DEFAULT_CONFIG
|
|
197
|
+
var DEFAULT_CONFIG = {
|
|
14
198
|
threshold: 45,
|
|
15
199
|
blurRadius: 1,
|
|
16
200
|
dilateRadius: 4,
|
|
17
|
-
minArea:
|
|
201
|
+
minArea: DEFAULT_MIN_AREA,
|
|
202
|
+
minAreaFraction: DEFAULT_MIN_AREA_FRACTION,
|
|
203
|
+
mergeRadiusDivisor: 6,
|
|
204
|
+
maxCoverageFraction: DEFAULT_MAX_COVERAGE_FRACTION
|
|
18
205
|
};
|
|
19
206
|
var WasmMotionDetector = class {
|
|
20
207
|
wasm = null;
|
|
21
208
|
prevOffset = 0;
|
|
22
209
|
currOffset = 0;
|
|
23
210
|
regionOffset = 0;
|
|
211
|
+
/** The motion view the regions are expressed in — the relative bar's denominator. */
|
|
212
|
+
width = 0;
|
|
213
|
+
height = 0;
|
|
24
214
|
/** Load the WASM module. Call once before detect(). */
|
|
25
215
|
async load() {
|
|
26
216
|
const wasmBytes = readFileSync(join(__dirname, "..", "..", "wasm", "motion.wasm"));
|
|
@@ -44,10 +234,25 @@ var WasmMotionDetector = class {
|
|
|
44
234
|
}
|
|
45
235
|
this.wasm = exports;
|
|
46
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Initialize for these frame dimensions when they are not already the ones
|
|
239
|
+
* the WASM is sized for. One detector instance serves every camera on the
|
|
240
|
+
* node, so two cameras with different views interleaving would otherwise
|
|
241
|
+
* leave the WASM sized for whichever one called `init` last — and, since
|
|
242
|
+
* D394, would divide the relative area bar by the wrong view. `init` only
|
|
243
|
+
* recomputes offsets and grows memory, so a call that changes nothing is
|
|
244
|
+
* skipped and a call that does is cheap.
|
|
245
|
+
*/
|
|
246
|
+
ensureSize(w, h) {
|
|
247
|
+
if (this.width === w && this.height === h) return;
|
|
248
|
+
this.init(w, h);
|
|
249
|
+
}
|
|
47
250
|
/** Initialize for given frame dimensions. Call when resolution changes. */
|
|
48
251
|
init(w, h) {
|
|
49
252
|
if (!this.wasm) throw new Error("WASM not loaded — call load() first");
|
|
50
253
|
this.wasm.init(w, h);
|
|
254
|
+
this.width = w;
|
|
255
|
+
this.height = h;
|
|
51
256
|
this.prevOffset = this.wasm.getPrevOffset();
|
|
52
257
|
this.currOffset = this.wasm.getCurrOffset();
|
|
53
258
|
this.regionOffset = this.wasm.getRegionOffset();
|
|
@@ -58,12 +263,12 @@ var WasmMotionDetector = class {
|
|
|
58
263
|
* @param prevGray - previous frame (Uint8Array, width×height)
|
|
59
264
|
* @param currGray - current frame (Uint8Array, width×height)
|
|
60
265
|
* @param config - detection parameters (optional, uses defaults)
|
|
61
|
-
* @returns raw
|
|
266
|
+
* @returns raw CCL regions, the subjects they merge into, and the survivors
|
|
62
267
|
*/
|
|
63
268
|
detect(prevGray, currGray, config = {}) {
|
|
64
269
|
if (!this.wasm) throw new Error("WASM not loaded");
|
|
65
270
|
const cfg = {
|
|
66
|
-
...DEFAULT_CONFIG
|
|
271
|
+
...DEFAULT_CONFIG,
|
|
67
272
|
...config
|
|
68
273
|
};
|
|
69
274
|
const mem = new Uint8Array(this.wasm.memory.buffer);
|
|
@@ -72,7 +277,9 @@ var WasmMotionDetector = class {
|
|
|
72
277
|
const numRegions = this.wasm.detectMotion(cfg.threshold, cfg.blurRadius, cfg.dilateRadius, 0);
|
|
73
278
|
if (numRegions === 0) return {
|
|
74
279
|
raw: [],
|
|
75
|
-
filtered: []
|
|
280
|
+
filtered: [],
|
|
281
|
+
groups: [],
|
|
282
|
+
suppressedByCoverage: 0
|
|
76
283
|
};
|
|
77
284
|
const view = new DataView(this.wasm.memory.buffer);
|
|
78
285
|
const raw = [];
|
|
@@ -86,9 +293,19 @@ var WasmMotionDetector = class {
|
|
|
86
293
|
pixels: view.getInt32(base + 16, true)
|
|
87
294
|
});
|
|
88
295
|
}
|
|
296
|
+
const grouped = groupMotionRegions(raw, {
|
|
297
|
+
viewWidth: this.width,
|
|
298
|
+
viewHeight: this.height,
|
|
299
|
+
mergeRadiusDivisor: cfg.mergeRadiusDivisor,
|
|
300
|
+
minAreaFraction: cfg.minAreaFraction,
|
|
301
|
+
minArea: cfg.minArea,
|
|
302
|
+
maxCoverageFraction: cfg.maxCoverageFraction
|
|
303
|
+
});
|
|
89
304
|
return {
|
|
90
305
|
raw,
|
|
91
|
-
filtered:
|
|
306
|
+
filtered: grouped.passed,
|
|
307
|
+
groups: grouped.groups,
|
|
308
|
+
suppressedByCoverage: grouped.suppressedByCoverage
|
|
92
309
|
};
|
|
93
310
|
}
|
|
94
311
|
};
|
|
@@ -117,25 +334,27 @@ function gateMotionRegions(regions, zones, rules, frameWidth, frameHeight) {
|
|
|
117
334
|
*
|
|
118
335
|
* Settings redesign Phase 3: motion-wasm has ONLY device-level settings
|
|
119
336
|
* (all four fields were `scope: 'device'` in the legacy schema — every
|
|
120
|
-
* camera tunes threshold/
|
|
337
|
+
* camera tunes threshold/subject size/blurRadius/dilateRadius independently
|
|
121
338
|
* for its scene). So the addon implements `getDeviceSettings` /
|
|
122
339
|
* `updateDeviceSettings` and nothing else. Fresh cameras with no
|
|
123
340
|
* overrides get the schema defaults via `hydrateSchema()` at read time.
|
|
124
341
|
* There is no cluster-wide "default threshold" knob — if operators want
|
|
125
342
|
* to change the baseline, they edit the schema defaults in code.
|
|
126
343
|
*/
|
|
127
|
-
var DEFAULT_CONFIG = {
|
|
128
|
-
threshold: 45,
|
|
129
|
-
blurRadius: 1,
|
|
130
|
-
dilateRadius: 4,
|
|
131
|
-
minArea: 1500
|
|
132
|
-
};
|
|
133
344
|
var MotionWasmAddon = class MotionWasmAddon extends BaseAddon {
|
|
134
345
|
detector = null;
|
|
135
346
|
cameras = /* @__PURE__ */ new Map();
|
|
136
347
|
deviceConfigCache = /* @__PURE__ */ new Map();
|
|
137
348
|
static DEVICE_CONFIG_TTL_MS = 6e4;
|
|
138
349
|
/**
|
|
350
|
+
* Last logged "which rule is this camera on" signature, per camera. The
|
|
351
|
+
* operator's gesture for D394 is a settings write, and a settings write is
|
|
352
|
+
* invisible: this is how they confirm it took, on the next analysed frame.
|
|
353
|
+
* Keyed and logged only on CHANGE, so it costs one line per camera per
|
|
354
|
+
* change rather than one per TTL expiry.
|
|
355
|
+
*/
|
|
356
|
+
loggedRule = /* @__PURE__ */ new Map();
|
|
357
|
+
/**
|
|
139
358
|
* Per-device {@link DeviceProxy} used for zone gating. Built lazily
|
|
140
359
|
* on first analyze() for a device; the proxy's reactive state
|
|
141
360
|
* handles (`state.zones`, `state.zoneRules`) keep their `.value`
|
|
@@ -252,7 +471,16 @@ var MotionWasmAddon = class MotionWasmAddon extends BaseAddon {
|
|
|
252
471
|
}
|
|
253
472
|
state = this.cameras.get(cameraId);
|
|
254
473
|
const deviceCfg = await this.getDeviceConfig(cameraId);
|
|
255
|
-
|
|
474
|
+
this.detector.ensureSize(width, height);
|
|
475
|
+
const { raw, filtered, suppressedByCoverage } = this.detector.detect(state.prevGray, gray, deviceCfg);
|
|
476
|
+
if (suppressedByCoverage > 0) this.ctx.logger.info("motion storm suppressed", {
|
|
477
|
+
tags: { deviceId: Number(cameraId) },
|
|
478
|
+
meta: {
|
|
479
|
+
groups: suppressedByCoverage,
|
|
480
|
+
maxCoverageFraction: deviceCfg.maxCoverageFraction,
|
|
481
|
+
view: `${width}x${height}`
|
|
482
|
+
}
|
|
483
|
+
});
|
|
256
484
|
state.prevGray = Buffer.from(gray);
|
|
257
485
|
const toRegion = (r) => ({
|
|
258
486
|
bbox: {
|
|
@@ -343,28 +571,48 @@ var MotionWasmAddon = class MotionWasmAddon extends BaseAddon {
|
|
|
343
571
|
threshold: typeof flat["threshold"] === "number" ? flat["threshold"] : DEFAULT_CONFIG.threshold,
|
|
344
572
|
blurRadius: typeof flat["blurRadius"] === "number" ? flat["blurRadius"] : DEFAULT_CONFIG.blurRadius,
|
|
345
573
|
dilateRadius: typeof flat["dilateRadius"] === "number" ? flat["dilateRadius"] : DEFAULT_CONFIG.dilateRadius,
|
|
346
|
-
minArea: typeof flat["minArea"] === "number" ? flat["minArea"] : DEFAULT_CONFIG.minArea
|
|
574
|
+
minArea: typeof flat["minArea"] === "number" ? flat["minArea"] : DEFAULT_CONFIG.minArea,
|
|
575
|
+
minAreaFraction: typeof flat["minAreaFraction"] === "number" ? flat["minAreaFraction"] : DEFAULT_CONFIG.minAreaFraction,
|
|
576
|
+
mergeRadiusDivisor: typeof flat["mergeRadiusDivisor"] === "number" ? flat["mergeRadiusDivisor"] : DEFAULT_CONFIG.mergeRadiusDivisor,
|
|
577
|
+
maxCoverageFraction: typeof flat["maxCoverageFraction"] === "number" ? flat["maxCoverageFraction"] : DEFAULT_CONFIG.maxCoverageFraction
|
|
347
578
|
};
|
|
348
579
|
this.deviceConfigCache.set(cameraId, {
|
|
349
580
|
config: resolved,
|
|
350
581
|
fetchedAt: now
|
|
351
582
|
});
|
|
583
|
+
const rule = resolved.minArea > 0 ? "legacy-per-region" : "merged-relative";
|
|
584
|
+
const signature = rule === "legacy-per-region" ? `${rule}:${resolved.minArea}` : `${rule}:${resolved.minAreaFraction}:${resolved.maxCoverageFraction}`;
|
|
585
|
+
if (this.loggedRule.get(cameraId) !== signature) {
|
|
586
|
+
this.loggedRule.set(cameraId, signature);
|
|
587
|
+
this.ctx.logger.info("motion area rule resolved", {
|
|
588
|
+
tags: { deviceId: Number(cameraId) },
|
|
589
|
+
meta: {
|
|
590
|
+
rule,
|
|
591
|
+
minArea: resolved.minArea,
|
|
592
|
+
minAreaFraction: resolved.minAreaFraction,
|
|
593
|
+
maxCoverageFraction: resolved.maxCoverageFraction
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
352
597
|
return resolved;
|
|
353
598
|
}
|
|
354
599
|
async removeCamera({ deviceId }) {
|
|
355
600
|
const key = String(deviceId);
|
|
356
601
|
this.cameras.delete(key);
|
|
357
602
|
this.deviceConfigCache.delete(key);
|
|
603
|
+
this.loggedRule.delete(key);
|
|
358
604
|
this.releaseProxy(deviceId);
|
|
359
605
|
}
|
|
360
606
|
async reset() {
|
|
361
607
|
this.cameras.clear();
|
|
362
608
|
this.deviceConfigCache.clear();
|
|
609
|
+
this.loggedRule.clear();
|
|
363
610
|
for (const id of this.proxies.keys()) this.releaseProxy(id);
|
|
364
611
|
}
|
|
365
612
|
async onShutdown() {
|
|
366
613
|
this.cameras.clear();
|
|
367
614
|
this.deviceConfigCache.clear();
|
|
615
|
+
this.loggedRule.clear();
|
|
368
616
|
for (const id of this.proxies.keys()) this.releaseProxy(id);
|
|
369
617
|
this.detector = null;
|
|
370
618
|
}
|
|
@@ -391,11 +639,26 @@ var MotionWasmAddon = class MotionWasmAddon extends BaseAddon {
|
|
|
391
639
|
includes: "analyzer"
|
|
392
640
|
}
|
|
393
641
|
},
|
|
642
|
+
{
|
|
643
|
+
type: "slider",
|
|
644
|
+
key: "minAreaFraction",
|
|
645
|
+
label: "Min Subject Size",
|
|
646
|
+
description: "Smallest subject that triggers motion, as a share of the analysed view. Fragments of one subject are merged before this is applied, so a partly hidden animal counts once at its full size. Only in effect when Min Area is 0.",
|
|
647
|
+
min: .001,
|
|
648
|
+
max: .05,
|
|
649
|
+
step: 5e-4,
|
|
650
|
+
default: DEFAULT_CONFIG.minAreaFraction,
|
|
651
|
+
showValue: true,
|
|
652
|
+
showWhen: {
|
|
653
|
+
field: "motionSources",
|
|
654
|
+
includes: "analyzer"
|
|
655
|
+
}
|
|
656
|
+
},
|
|
394
657
|
{
|
|
395
658
|
type: "number",
|
|
396
659
|
key: "minArea",
|
|
397
660
|
label: "Min Area",
|
|
398
|
-
description: "
|
|
661
|
+
description: "Absolute pixel bar, applied to each motion region on its own. This is how motion has always worked, and it stays the default. Set it to 0 to move THIS camera to Min Subject Size, which merges the fragments of one subject before measuring it and keeps its meaning at any resolution — expect more motion, so move one camera at a time.",
|
|
399
662
|
min: 0,
|
|
400
663
|
max: 5e4,
|
|
401
664
|
step: 100,
|
|
@@ -406,6 +669,21 @@ var MotionWasmAddon = class MotionWasmAddon extends BaseAddon {
|
|
|
406
669
|
includes: "analyzer"
|
|
407
670
|
}
|
|
408
671
|
},
|
|
672
|
+
{
|
|
673
|
+
type: "slider",
|
|
674
|
+
key: "maxCoverageFraction",
|
|
675
|
+
label: "Storm Cutoff",
|
|
676
|
+
description: "A merged region whose changed pixels cover more of the view than this is weather, an IR cut or an exposure step, not a subject, and does not fire motion. A real mover inside the storm still fires on its own region. Only in effect when Min Area is 0; 1 disables the cutoff.",
|
|
677
|
+
min: .2,
|
|
678
|
+
max: 1,
|
|
679
|
+
step: .05,
|
|
680
|
+
default: DEFAULT_CONFIG.maxCoverageFraction,
|
|
681
|
+
showValue: true,
|
|
682
|
+
showWhen: {
|
|
683
|
+
field: "motionSources",
|
|
684
|
+
includes: "analyzer"
|
|
685
|
+
}
|
|
686
|
+
},
|
|
409
687
|
{
|
|
410
688
|
type: "slider",
|
|
411
689
|
key: "blurRadius",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { It as errMsg, Rt as Fmp4BoxSplitter, Vt as isSoftwareDecode, zt as buildFfmpegArgs } from "./dist-
|
|
1
|
+
import { It as errMsg, Rt as Fmp4BoxSplitter, Vt as isSoftwareDecode, zt as buildFfmpegArgs } from "./dist-FaCzok3C.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node-
|
|
7
|
-
const require_default_detection_model = require("../default-detection-model-
|
|
8
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
5
|
+
const require_dist = require("../dist-CR3_dWir.js");
|
|
6
|
+
const require_node = require("../node-Re3uLz8K.js");
|
|
7
|
+
const require_default_detection_model = require("../default-detection-model-BGsoboIK.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-lIjbPNbH.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
10
|
+
const require_lazy_sharp = require("../lazy-sharp-CKXcDP-V.js");
|
|
11
11
|
const require_restream_intent = require("../restream-intent-B1Difcnq.js");
|
|
12
12
|
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
|
|
13
13
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $ as failureContributionCapability, It as errMsg, Qt as nodePin, R as customAction, U as deriveDetailCropRect, Ut as BaseAddon, V as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, an as boolean, bt as pipelineRunnerCapability, cn as literal, hn as EventCategory, in as array, it as loadContributionCapability, ln as number, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as string, qt as createEvent, rn as _enum, sn as lazy, u as DEFAULT_DETAIL_CROP_CONVENTION, un as object, vt as pickNativeLeaseOverride } from "../dist-
|
|
2
|
-
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
3
|
-
import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-
|
|
4
|
-
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-
|
|
1
|
+
import { $ as failureContributionCapability, It as errMsg, Qt as nodePin, R as customAction, U as deriveDetailCropRect, Ut as BaseAddon, V as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, an as boolean, bt as pipelineRunnerCapability, cn as literal, hn as EventCategory, in as array, it as loadContributionCapability, ln as number, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as string, qt as createEvent, rn as _enum, sn as lazy, u as DEFAULT_DETAIL_CROP_CONVENTION, un as object, vt as pickNativeLeaseOverride } from "../dist-FaCzok3C.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-f4fUlGHQ.mjs";
|
|
3
|
+
import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-23wBHkOQ.mjs";
|
|
4
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-Dz6_u2y6.mjs";
|
|
5
5
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
6
6
|
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
7
7
|
import { a as preRollRequestOf, o as withDetectionIntent, t as DETECTION_PRE_ROLL_REQUEST_MS } from "../restream-intent-DWe2kInY.mjs";
|
package/dist/recorder/index.js
CHANGED
|
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node-
|
|
5
|
+
const require_dist = require("../dist-CR3_dWir.js");
|
|
6
|
+
const require_node = require("../node-Re3uLz8K.js");
|
|
7
7
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
8
8
|
const require_restream_intent = require("../restream-intent-B1Difcnq.js");
|
|
9
9
|
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
|
|
@@ -11498,11 +11498,13 @@ function weightOfUnmeasured(demands) {
|
|
|
11498
11498
|
return measured.length % 2 === 1 ? measured[mid] : (measured[mid - 1] + measured[mid]) / 2;
|
|
11499
11499
|
}
|
|
11500
11500
|
/** Normalised weight per location: headroom share, or an even split when no
|
|
11501
|
-
* location reports any headroom (nothing is known yet — never divide by 0).
|
|
11501
|
+
* location reports any headroom (nothing is known yet — never divide by 0).
|
|
11502
|
+
* An UNMEASURABLE location (`headroomBytes: null`) contributes nothing and
|
|
11503
|
+
* weighs nothing: it keeps what it already holds and attracts no new work. */
|
|
11502
11504
|
function weightsFor(pool) {
|
|
11503
|
-
const total = pool.reduce((sum, c) => sum + Math.max(0, c.headroomBytes), 0);
|
|
11505
|
+
const total = pool.reduce((sum, c) => sum + Math.max(0, c.headroomBytes ?? 0), 0);
|
|
11504
11506
|
const weights = /* @__PURE__ */ new Map();
|
|
11505
|
-
for (const c of pool) weights.set(c.locationId, total > 0 ? Math.max(0, c.headroomBytes) / total : 1 / pool.length);
|
|
11507
|
+
for (const c of pool) weights.set(c.locationId, total > 0 ? Math.max(0, c.headroomBytes ?? 0) / total : 1 / pool.length);
|
|
11506
11508
|
return weights;
|
|
11507
11509
|
}
|
|
11508
11510
|
/** The location with the largest unmet share of the target, ties broken by the
|
|
@@ -12036,6 +12038,9 @@ var PlacementService = class {
|
|
|
12036
12038
|
verified = /* @__PURE__ */ new Set();
|
|
12037
12039
|
/** Locations the probe declared unusable — re-probed after a change. */
|
|
12038
12040
|
unusable = /* @__PURE__ */ new Set();
|
|
12041
|
+
/** Locations whose capacity could not be read, by reason. One line per
|
|
12042
|
+
* distinct fault (this is on the 30 s tick) and one when it clears. */
|
|
12043
|
+
capacityFaults = /* @__PURE__ */ new Map();
|
|
12039
12044
|
/** Cameras that have already announced "every location is unusable". One line
|
|
12040
12045
|
* per camera per episode: the fault is asked per-camera, and repeating it per
|
|
12041
12046
|
* attach is the noise this state exists to end. */
|
|
@@ -12238,7 +12243,7 @@ var PlacementService = class {
|
|
|
12238
12243
|
const byId = new Map(candidates.map((c) => [c.locationId, c.headroomBytes]));
|
|
12239
12244
|
return pool.map((location) => {
|
|
12240
12245
|
const headroomBytes = byId.get(location.id);
|
|
12241
|
-
return headroomBytes === void 0 ? location : {
|
|
12246
|
+
return headroomBytes === void 0 || headroomBytes === null ? location : {
|
|
12242
12247
|
...location,
|
|
12243
12248
|
headroomBytes
|
|
12244
12249
|
};
|
|
@@ -12281,7 +12286,7 @@ var PlacementService = class {
|
|
|
12281
12286
|
return NO_PLAN;
|
|
12282
12287
|
}
|
|
12283
12288
|
const current = new Map(Object.entries(blob.assignments));
|
|
12284
|
-
const evacuate = new Set(candidates.filter((c) => c.headroomBytes <= 0).map((c) => c.locationId));
|
|
12289
|
+
const evacuate = new Set(candidates.filter((c) => c.headroomBytes !== null && c.headroomBytes <= 0).map((c) => c.locationId));
|
|
12285
12290
|
for (const location of pool) if (location.readOnly) evacuate.add(location.id);
|
|
12286
12291
|
const plan = planPlacement({
|
|
12287
12292
|
candidates,
|
|
@@ -12441,8 +12446,20 @@ var PlacementService = class {
|
|
|
12441
12446
|
* `min(free − minFree floor, maxUsedGb − used)`, clamped at zero: the first
|
|
12442
12447
|
* term is the per-location free-space guard the storage pressure manager also
|
|
12443
12448
|
* enforces, the second an optional operator cap on what recordings may take.
|
|
12444
|
-
*
|
|
12445
|
-
*
|
|
12449
|
+
*
|
|
12450
|
+
* **Nothing measurable is `null`, not zero.** `readLocationCapacity` models
|
|
12451
|
+
* its failure — a NAS that stops answering `statfs` returns nulls AND the
|
|
12452
|
+
* reason. This method used to discard the reason and fold the whole case into
|
|
12453
|
+
* `headroomBytes: 0`, which `recompute` reads as "at its limit" and puts in
|
|
12454
|
+
* `evacuate`: one failed `statfs` moved every camera off a healthy disk, and
|
|
12455
|
+
* not one line was written about it. (The comment that used to sit here
|
|
12456
|
+
* claimed the opposite — "no signal → the planner splits evenly" — which is
|
|
12457
|
+
* true of the PLANNER's weights and was never true of the evacuation set.)
|
|
12458
|
+
*
|
|
12459
|
+
* So an unmeasurable location gets `null`, which is not evacuated and wins no
|
|
12460
|
+
* new work, and the reason is logged against the location — once per distinct
|
|
12461
|
+
* fault, since this runs on the 30 s placement tick, plus one line when it
|
|
12462
|
+
* recovers.
|
|
12446
12463
|
*/
|
|
12447
12464
|
async candidatesFor(pool) {
|
|
12448
12465
|
const out = [];
|
|
@@ -12450,7 +12467,8 @@ var PlacementService = class {
|
|
|
12450
12467
|
if (location.readOnly) continue;
|
|
12451
12468
|
const capacity = await this.deps.capacityFor?.(location) ?? {
|
|
12452
12469
|
availableBytes: null,
|
|
12453
|
-
totalBytes: null
|
|
12470
|
+
totalBytes: null,
|
|
12471
|
+
error: "no capacity source is wired on this node"
|
|
12454
12472
|
};
|
|
12455
12473
|
const limits = [];
|
|
12456
12474
|
if (capacity.availableBytes !== null && capacity.totalBytes !== null) {
|
|
@@ -12461,15 +12479,45 @@ var PlacementService = class {
|
|
|
12461
12479
|
const used = await this.deps.usedOnLocation?.(location.id) ?? 0;
|
|
12462
12480
|
limits.push(require_dist.gbToBytes(location.maxUsedGb) - used);
|
|
12463
12481
|
}
|
|
12482
|
+
const headroomBytes = limits.length === 0 ? null : Math.max(0, Math.min(...limits));
|
|
12483
|
+
this.reportCapacitySignal(location, headroomBytes, capacity.error);
|
|
12464
12484
|
out.push({
|
|
12465
12485
|
locationId: location.id,
|
|
12466
12486
|
type: location.type,
|
|
12467
|
-
headroomBytes
|
|
12487
|
+
headroomBytes
|
|
12468
12488
|
});
|
|
12469
12489
|
}
|
|
12470
12490
|
return out;
|
|
12471
12491
|
}
|
|
12472
12492
|
/**
|
|
12493
|
+
* Say it out loud when a location's capacity cannot be read, and say it again
|
|
12494
|
+
* when it comes back. Deduplicated by reason because `candidatesFor` runs on
|
|
12495
|
+
* every placement tick — a line per tick is a line nobody reads, and no line
|
|
12496
|
+
* at all is what let a dead NAS look like a full one.
|
|
12497
|
+
*/
|
|
12498
|
+
reportCapacitySignal(location, headroomBytes, error) {
|
|
12499
|
+
const previous = this.capacityFaults.get(location.id);
|
|
12500
|
+
if (headroomBytes !== null) {
|
|
12501
|
+
if (previous !== void 0) {
|
|
12502
|
+
this.capacityFaults.delete(location.id);
|
|
12503
|
+
this.deps.logger.info("recorder placement: capacity readable again — this location is a write target once more", { meta: {
|
|
12504
|
+
locationId: location.id,
|
|
12505
|
+
root: location.root,
|
|
12506
|
+
previousError: previous
|
|
12507
|
+
} });
|
|
12508
|
+
}
|
|
12509
|
+
return;
|
|
12510
|
+
}
|
|
12511
|
+
const reason = error ?? "the capacity read returned no bytes and no reason";
|
|
12512
|
+
if (previous === reason) return;
|
|
12513
|
+
this.capacityFaults.set(location.id, reason);
|
|
12514
|
+
this.deps.logger.warn("recorder placement: capacity UNMEASURABLE — headroom is unknown, so this location is neither evacuated nor given new cameras", { meta: {
|
|
12515
|
+
locationId: location.id,
|
|
12516
|
+
root: location.root,
|
|
12517
|
+
error: reason
|
|
12518
|
+
} });
|
|
12519
|
+
}
|
|
12520
|
+
/**
|
|
12473
12521
|
* Volume-check the chosen location, failing the assignment over to a sibling
|
|
12474
12522
|
* when the bytes under its root are not the bytes we wrote. The probe runs
|
|
12475
12523
|
* ONCE per location per location-set generation — it is a claim, not a poll.
|
|
@@ -18304,10 +18352,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
18304
18352
|
const [assignments, candidates] = await Promise.all([placement.currentAssignments(), placement.candidates()]);
|
|
18305
18353
|
return {
|
|
18306
18354
|
assignments,
|
|
18307
|
-
headroom: candidates.
|
|
18355
|
+
headroom: candidates.flatMap((candidate) => candidate.headroomBytes === null ? [] : [{
|
|
18308
18356
|
locationId: candidate.locationId,
|
|
18309
18357
|
headroomBytes: candidate.headroomBytes
|
|
18310
|
-
})
|
|
18358
|
+
}])
|
|
18311
18359
|
};
|
|
18312
18360
|
}
|
|
18313
18361
|
/**
|