@camstack/addon-pipeline 0.1.19 → 0.2.0
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 +736 -716
- package/dist/audio-analyzer/index.mjs +726 -676
- package/dist/audio-codec-nodeav/index.js +304 -461
- package/dist/audio-codec-nodeav/index.mjs +300 -462
- package/dist/chunk-BdkLduGY.mjs +5 -0
- package/dist/chunk-D6vf50IK.js +28 -0
- package/dist/codec-runtime-BOk-13PN.js +202 -0
- package/dist/codec-runtime-BsqlEjPi.mjs +197 -0
- package/dist/constants-B_b0a-6h.mjs +3119 -0
- package/dist/{index-D_cl0Qqb.js → constants-D65v6yp6.js} +3107 -2935
- package/dist/decoder-nodeav/index.js +1374 -1444
- package/dist/decoder-nodeav/index.mjs +1369 -1425
- package/dist/detection-pipeline/index.js +6462 -5613
- package/dist/detection-pipeline/index.mjs +6451 -5574
- package/dist/dist-7ewQjTle.js +22454 -0
- package/dist/dist-C5jnNl0n.mjs +22089 -0
- package/dist/motion-wasm/index.js +469 -467
- package/dist/motion-wasm/index.mjs +464 -446
- package/dist/pipeline-runner/index.js +2035 -1836
- package/dist/pipeline-runner/index.mjs +2031 -1820
- package/dist/recorder/index.js +2097 -0
- package/dist/recorder/index.mjs +2095 -0
- package/dist/stream-broker/_stub.js +1818 -734
- package/dist/stream-broker/_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D4-DHanK.mjs +156 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Tf-HACFd.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C9WX5HNw.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs +26 -0
- package/dist/stream-broker/dist-CYZr2fwk.mjs +2726 -0
- package/dist/stream-broker/hostInit-Di6vceAU.mjs +129 -0
- package/dist/stream-broker/index.js +17837 -12904
- package/dist/stream-broker/index.mjs +17826 -12896
- package/dist/stream-broker/remoteEntry.js +134 -2973
- package/dist/stream-broker/remoteEntry.ssr.js +33 -0
- package/dist/stream-broker/virtualExposes-dYNvIwoR.mjs +27 -0
- package/dist/stream-broker/virtual_mf-exposes-ssr___mfe_internal__addon_stream_broker_widgets__remoteEntry_js-Cmqfp4i_.mjs +10 -0
- package/embed-dist/assets/index-B8VlSD0-.js +150 -0
- package/embed-dist/assets/index-ZhDdp1Nd.css +2 -0
- package/embed-dist/index.html +13 -0
- package/package.json +75 -9
- package/wasm/assembly/index.ts +41 -16
- package/dist/audio-analyzer/index.js.map +0 -1
- package/dist/audio-analyzer/index.mjs.map +0 -1
- package/dist/audio-codec-nodeav/index.js.map +0 -1
- package/dist/audio-codec-nodeav/index.mjs.map +0 -1
- package/dist/decoder-nodeav/index.js.map +0 -1
- package/dist/decoder-nodeav/index.mjs.map +0 -1
- package/dist/detection-pipeline/index.js.map +0 -1
- package/dist/detection-pipeline/index.mjs.map +0 -1
- package/dist/index-BbPPvoCx.js +0 -14682
- package/dist/index-BbPPvoCx.js.map +0 -1
- package/dist/index-Bmlkm0Fd.mjs +0 -14683
- package/dist/index-Bmlkm0Fd.mjs.map +0 -1
- package/dist/index-D_cl0Qqb.js.map +0 -1
- package/dist/index-UbcdLS7a.mjs +0 -5790
- package/dist/index-UbcdLS7a.mjs.map +0 -1
- package/dist/motion-wasm/index.js.map +0 -1
- package/dist/motion-wasm/index.mjs.map +0 -1
- package/dist/pipeline-runner/index.js.map +0 -1
- package/dist/pipeline-runner/index.mjs.map +0 -1
- package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/StreamBrokerPanel.d.ts +0 -21
- package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/index.d.ts +0 -13
- package/dist/stream-broker/@mf-types/widgets.d.ts +0 -2
- package/dist/stream-broker/@mf-types.d.ts +0 -3
- package/dist/stream-broker/@mf-types.zip +0 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-h5aXOPSA.mjs +0 -12
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-NjF4kxzW.mjs +0 -19
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BAv_5ISf.mjs +0 -20
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-U1EUeEPs.mjs +0 -104
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-DeouEaSs.mjs +0 -85
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-DHUwjbb9.mjs +0 -62
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-BsB2G7oY.mjs +0 -88
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-xrRiPUpA.mjs +0 -29
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-gBEZsQrp.mjs +0 -36
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-DYEKzzY-.mjs +0 -45
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-C0E2yCzO.mjs +0 -6
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-DICOtMTl.mjs +0 -34
- package/dist/stream-broker/_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CupRlwqG.mjs +0 -156
- package/dist/stream-broker/client-NPZqorv9.mjs +0 -9836
- package/dist/stream-broker/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +0 -211
- package/dist/stream-broker/hostInit-Bh4w7o5_.mjs +0 -168
- package/dist/stream-broker/index-2Qp8vT3w.mjs +0 -185
- package/dist/stream-broker/index-BBcZvb5t.mjs +0 -435
- package/dist/stream-broker/index-CIJue-4t.mjs +0 -37880
- package/dist/stream-broker/index-CWkKuNLr.mjs +0 -232
- package/dist/stream-broker/index-Cc6QBqMk.mjs +0 -1655
- package/dist/stream-broker/index-D_1p2K9B.mjs +0 -2603
- package/dist/stream-broker/index-Dy2V7VOm.mjs +0 -14379
- package/dist/stream-broker/index-mX3Kgiv1.mjs +0 -725
- package/dist/stream-broker/index-xncRG7-x.mjs +0 -2713
- package/dist/stream-broker/index.js.map +0 -1
- package/dist/stream-broker/index.mjs.map +0 -1
- package/dist/stream-broker/jsx-runtime-lb0mH5st.mjs +0 -55
- package/dist/stream-broker/schemas-ClCuS4qa.mjs +0 -3594
- package/dist/stream-broker/virtualExposes-pCd777Rp.mjs +0 -42
|
@@ -1,452 +1,470 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as motionDetectionCapability, M as evaluateZoneRules, P as hydrateSchema, d as DeviceType, i as BaseAddon } from "../dist-C5jnNl0n.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
5
|
+
/**
|
|
6
|
+
* WasmMotionDetector — loads the AssemblyScript WASM module and provides
|
|
7
|
+
* a typed interface for motion detection.
|
|
8
|
+
*
|
|
9
|
+
* Pipeline: blur → diff → threshold → dilate → CCL → bounding boxes
|
|
10
|
+
* All in one WASM call, ~3ms for 640×360 grayscale frames.
|
|
11
|
+
*/
|
|
12
|
+
var DEFAULT_CONFIG$1 = {
|
|
13
|
+
threshold: 45,
|
|
14
|
+
blurRadius: 1,
|
|
15
|
+
dilateRadius: 4,
|
|
16
|
+
minArea: 3e3
|
|
9
17
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
const filtered = raw.filter((r) => r.pixels >= cfg.minArea);
|
|
90
|
-
return { raw, filtered };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
18
|
+
var WasmMotionDetector = class {
|
|
19
|
+
wasm = null;
|
|
20
|
+
prevOffset = 0;
|
|
21
|
+
currOffset = 0;
|
|
22
|
+
regionOffset = 0;
|
|
23
|
+
/** Load the WASM module. Call once before detect(). */
|
|
24
|
+
async load() {
|
|
25
|
+
const wasmBytes = readFileSync(join(__dirname, "..", "..", "wasm", "motion.wasm"));
|
|
26
|
+
const { instance } = await WebAssembly.instantiate(wasmBytes, { env: { abort: () => {
|
|
27
|
+
throw new Error("WASM abort");
|
|
28
|
+
} } });
|
|
29
|
+
const exports = instance.exports;
|
|
30
|
+
for (const name of [
|
|
31
|
+
"memory",
|
|
32
|
+
"init",
|
|
33
|
+
"getPrevOffset",
|
|
34
|
+
"getCurrOffset",
|
|
35
|
+
"getRegionOffset",
|
|
36
|
+
"detectMotion"
|
|
37
|
+
]) {
|
|
38
|
+
const v = exports[name];
|
|
39
|
+
if (v === void 0) throw new Error(`motion.wasm contract violation: missing export "${String(name)}"`);
|
|
40
|
+
if (name === "memory") {
|
|
41
|
+
if (!(v instanceof WebAssembly.Memory)) throw new Error("motion.wasm contract violation: \"memory\" is not a WebAssembly.Memory");
|
|
42
|
+
} else if (typeof v !== "function") throw new Error(`motion.wasm contract violation: "${String(name)}" is not callable`);
|
|
43
|
+
}
|
|
44
|
+
this.wasm = exports;
|
|
45
|
+
}
|
|
46
|
+
/** Initialize for given frame dimensions. Call when resolution changes. */
|
|
47
|
+
init(w, h) {
|
|
48
|
+
if (!this.wasm) throw new Error("WASM not loaded — call load() first");
|
|
49
|
+
this.wasm.init(w, h);
|
|
50
|
+
this.prevOffset = this.wasm.getPrevOffset();
|
|
51
|
+
this.currOffset = this.wasm.getCurrOffset();
|
|
52
|
+
this.regionOffset = this.wasm.getRegionOffset();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Detect motion between previous and current grayscale frames.
|
|
56
|
+
*
|
|
57
|
+
* @param prevGray - previous frame (Uint8Array, width×height)
|
|
58
|
+
* @param currGray - current frame (Uint8Array, width×height)
|
|
59
|
+
* @param config - detection parameters (optional, uses defaults)
|
|
60
|
+
* @returns raw (all CCL regions) + filtered (passing minArea) regions
|
|
61
|
+
*/
|
|
62
|
+
detect(prevGray, currGray, config = {}) {
|
|
63
|
+
if (!this.wasm) throw new Error("WASM not loaded");
|
|
64
|
+
const cfg = {
|
|
65
|
+
...DEFAULT_CONFIG$1,
|
|
66
|
+
...config
|
|
67
|
+
};
|
|
68
|
+
const mem = new Uint8Array(this.wasm.memory.buffer);
|
|
69
|
+
mem.set(prevGray, this.prevOffset);
|
|
70
|
+
mem.set(currGray, this.currOffset);
|
|
71
|
+
const numRegions = this.wasm.detectMotion(cfg.threshold, cfg.blurRadius, cfg.dilateRadius, 0);
|
|
72
|
+
if (numRegions === 0) return {
|
|
73
|
+
raw: [],
|
|
74
|
+
filtered: []
|
|
75
|
+
};
|
|
76
|
+
const view = new DataView(this.wasm.memory.buffer);
|
|
77
|
+
const raw = [];
|
|
78
|
+
for (let i = 0; i < numRegions; i++) {
|
|
79
|
+
const base = this.regionOffset + i * 20;
|
|
80
|
+
raw.push({
|
|
81
|
+
x: view.getInt32(base, true),
|
|
82
|
+
y: view.getInt32(base + 4, true),
|
|
83
|
+
w: view.getInt32(base + 8, true),
|
|
84
|
+
h: view.getInt32(base + 12, true),
|
|
85
|
+
pixels: view.getInt32(base + 16, true)
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
raw,
|
|
90
|
+
filtered: raw.filter((r) => r.pixels >= cfg.minArea)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/motion-wasm/zone-gate.ts
|
|
93
96
|
function gateMotionRegions(regions, zones, rules, frameWidth, frameHeight) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
if (frameWidth === 0 || frameHeight === 0) return {
|
|
98
|
+
passed: regions,
|
|
99
|
+
excluded: []
|
|
100
|
+
};
|
|
101
|
+
return evaluateZoneRules(regions, zones, rules, (region) => ({
|
|
102
|
+
x: (region.bbox.x + region.bbox.w / 2) / frameWidth,
|
|
103
|
+
y: (region.bbox.y + region.bbox.h / 2) / frameHeight
|
|
104
|
+
}));
|
|
101
105
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/motion-wasm/addon/index.ts
|
|
108
|
+
/**
|
|
109
|
+
* Motion detection addon using WebAssembly.
|
|
110
|
+
*
|
|
111
|
+
* Drop-in replacement for addon-motion-detection. Same IMotionDetector interface,
|
|
112
|
+
* but uses WASM blur+diff+dilate+CCL instead of JS pixel loop.
|
|
113
|
+
*
|
|
114
|
+
* Produces bounding box regions (not just changed pixel count).
|
|
115
|
+
* ~3ms/frame at 640×360 with full pipeline.
|
|
116
|
+
*
|
|
117
|
+
* Settings redesign Phase 3: motion-wasm has ONLY device-level settings
|
|
118
|
+
* (all four fields were `scope: 'device'` in the legacy schema — every
|
|
119
|
+
* camera tunes threshold/minArea/blurRadius/dilateRadius independently
|
|
120
|
+
* for its scene). So the addon implements `getDeviceSettings` /
|
|
121
|
+
* `updateDeviceSettings` and nothing else. Fresh cameras with no
|
|
122
|
+
* overrides get the schema defaults via `hydrateSchema()` at read time.
|
|
123
|
+
* There is no cluster-wide "default threshold" knob — if operators want
|
|
124
|
+
* to change the baseline, they edit the schema defaults in code.
|
|
125
|
+
*/
|
|
126
|
+
var DEFAULT_CONFIG = {
|
|
127
|
+
threshold: 45,
|
|
128
|
+
blurRadius: 1,
|
|
129
|
+
dilateRadius: 4,
|
|
130
|
+
minArea: 1500
|
|
131
|
+
};
|
|
132
|
+
var MotionWasmAddon = class MotionWasmAddon extends BaseAddon {
|
|
133
|
+
detector = null;
|
|
134
|
+
cameras = /* @__PURE__ */ new Map();
|
|
135
|
+
deviceConfigCache = /* @__PURE__ */ new Map();
|
|
136
|
+
static DEVICE_CONFIG_TTL_MS = 6e4;
|
|
137
|
+
/**
|
|
138
|
+
* Per-device {@link DeviceProxy} used for zone gating. Built lazily
|
|
139
|
+
* on first analyze() for a device; the proxy's reactive state
|
|
140
|
+
* handles (`state.zones`, `state.zoneRules`) keep their `.value`
|
|
141
|
+
* fresh via the kernel's runtime-state mirror, so the gate reads
|
|
142
|
+
* are sync and free of bus plumbing inside this addon.
|
|
143
|
+
*/
|
|
144
|
+
proxies = /* @__PURE__ */ new Map();
|
|
145
|
+
/** Unsubscribe pins kept so the slice handles stay subscribed for
|
|
146
|
+
* the camera's lifetime — `.value` only flows through the cache
|
|
147
|
+
* when at least one watcher is active. */
|
|
148
|
+
proxyUnsubs = /* @__PURE__ */ new Map();
|
|
149
|
+
constructor() {
|
|
150
|
+
super({});
|
|
151
|
+
}
|
|
152
|
+
async onInitialize() {
|
|
153
|
+
this.detector = new WasmMotionDetector();
|
|
154
|
+
await this.detector.load();
|
|
155
|
+
this.ctx.logger.info("WASM motion detector loaded");
|
|
156
|
+
return [{
|
|
157
|
+
capability: motionDetectionCapability,
|
|
158
|
+
provider: this
|
|
159
|
+
}];
|
|
160
|
+
}
|
|
161
|
+
static PIPELINE_STEP_KEY = "__pipeline__";
|
|
162
|
+
/**
|
|
163
|
+
* Pipeline step interface — called by PipelineRunner.
|
|
164
|
+
* Uses single-camera state (one instance per camera via AddonEngineManager).
|
|
165
|
+
*/
|
|
166
|
+
async process(input) {
|
|
167
|
+
const start = performance.now();
|
|
168
|
+
const result = await this.analyzeInternal(MotionWasmAddon.PIPELINE_STEP_KEY, input);
|
|
169
|
+
const toSpatial = (r) => ({
|
|
170
|
+
class: "motion",
|
|
171
|
+
originalClass: "motion",
|
|
172
|
+
score: Math.min(1, r.intensity / 128),
|
|
173
|
+
bbox: r.bbox
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
detections: result.regions.map(toSpatial),
|
|
177
|
+
rawDetections: result.rawRegions.map(toSpatial),
|
|
178
|
+
inferenceMs: performance.now() - start,
|
|
179
|
+
modelId: "wasm-motion"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* motion-detection cap: analyze a frame, return regions + stats.
|
|
184
|
+
* Per-device state keyed by the numeric deviceId (stringified internally
|
|
185
|
+
* so the pipeline-step sentinel shares the same Map).
|
|
186
|
+
*/
|
|
187
|
+
async analyze({ deviceId, frame }) {
|
|
188
|
+
return this.analyzeInternal(String(deviceId), frame);
|
|
189
|
+
}
|
|
190
|
+
async analyzeInternal(cameraId, frame) {
|
|
191
|
+
if (!this.detector) return {
|
|
192
|
+
detected: false,
|
|
193
|
+
regionCount: 0,
|
|
194
|
+
regions: [],
|
|
195
|
+
rawRegions: [],
|
|
196
|
+
frameWidth: 0,
|
|
197
|
+
frameHeight: 0,
|
|
198
|
+
analysisMs: 0
|
|
199
|
+
};
|
|
200
|
+
const start = performance.now();
|
|
201
|
+
let gray;
|
|
202
|
+
let width;
|
|
203
|
+
let height;
|
|
204
|
+
if (frame.format === "jpeg") {
|
|
205
|
+
const sharp = (await import("sharp")).default;
|
|
206
|
+
const { data, info } = await sharp(Buffer.from(frame.data)).grayscale().raw().toBuffer({ resolveWithObject: true });
|
|
207
|
+
gray = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
208
|
+
width = info.width;
|
|
209
|
+
height = info.height;
|
|
210
|
+
} else {
|
|
211
|
+
gray = frame.data instanceof Uint8Array ? frame.data : new Uint8Array(frame.data);
|
|
212
|
+
width = frame.width;
|
|
213
|
+
height = frame.height;
|
|
214
|
+
}
|
|
215
|
+
let state = this.cameras.get(cameraId);
|
|
216
|
+
if (!state || state.width !== width || state.height !== height) {
|
|
217
|
+
this.detector.init(width, height);
|
|
218
|
+
this.cameras.set(cameraId, {
|
|
219
|
+
prevGray: gray,
|
|
220
|
+
width,
|
|
221
|
+
height
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
detected: false,
|
|
225
|
+
regionCount: 0,
|
|
226
|
+
regions: [],
|
|
227
|
+
rawRegions: [],
|
|
228
|
+
frameWidth: width,
|
|
229
|
+
frameHeight: height,
|
|
230
|
+
analysisMs: performance.now() - start
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
state = this.cameras.get(cameraId);
|
|
234
|
+
const deviceCfg = await this.getDeviceConfig(cameraId);
|
|
235
|
+
const { raw, filtered } = this.detector.detect(state.prevGray, gray, deviceCfg);
|
|
236
|
+
state.prevGray = Buffer.from(gray);
|
|
237
|
+
const toRegion = (r) => ({
|
|
238
|
+
bbox: {
|
|
239
|
+
x: r.x,
|
|
240
|
+
y: r.y,
|
|
241
|
+
w: r.w,
|
|
242
|
+
h: r.h
|
|
243
|
+
},
|
|
244
|
+
pixelCount: r.pixels,
|
|
245
|
+
intensity: Math.min(255, Math.round(r.pixels / (r.w * r.h) * 255))
|
|
246
|
+
});
|
|
247
|
+
let regions = filtered.map(toRegion);
|
|
248
|
+
const rawRegions = raw.map(toRegion);
|
|
249
|
+
const numericDeviceId = Number(cameraId);
|
|
250
|
+
if (Number.isFinite(numericDeviceId)) {
|
|
251
|
+
const proxy = await this.ensureProxy(numericDeviceId);
|
|
252
|
+
if (proxy) {
|
|
253
|
+
const zones = proxy.state.zones.value?.zones ?? [];
|
|
254
|
+
const rules = proxy.state.zoneRules.value?.motion ?? [];
|
|
255
|
+
if (rules.length > 0 && zones.length > 0) regions = [...gateMotionRegions(regions, zones, rules, width, height).passed];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
detected: regions.length > 0,
|
|
260
|
+
regionCount: regions.length,
|
|
261
|
+
regions,
|
|
262
|
+
rawRegions,
|
|
263
|
+
frameWidth: width,
|
|
264
|
+
frameHeight: height,
|
|
265
|
+
analysisMs: performance.now() - start
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Resolve (and cache) a {@link DeviceProxy} for the given device.
|
|
270
|
+
* Pins the `state.zones` + `state.zoneRules` slice handles so the
|
|
271
|
+
* kernel runtime-state mirror keeps `.value` warm — subsequent
|
|
272
|
+
* analyze() calls read both slices synchronously without any bus
|
|
273
|
+
* plumbing or per-frame cap query. Returns null on first-call
|
|
274
|
+
* failure (logged) so motion analysis never blocks on zone
|
|
275
|
+
* resolution.
|
|
276
|
+
*/
|
|
277
|
+
async ensureProxy(deviceId) {
|
|
278
|
+
const cached = this.proxies.get(deviceId);
|
|
279
|
+
if (cached) return cached;
|
|
280
|
+
try {
|
|
281
|
+
const proxy = await this.ctx.fetchDevice(deviceId);
|
|
282
|
+
this.proxies.set(deviceId, proxy);
|
|
283
|
+
const unsubs = [proxy.state.zones.subscribe(() => {}), proxy.state.zoneRules.subscribe(() => {})];
|
|
284
|
+
this.proxyUnsubs.set(deviceId, unsubs);
|
|
285
|
+
return proxy;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
this.ctx.logger.debug("ensureProxy failed — gating skipped", {
|
|
288
|
+
tags: { deviceId },
|
|
289
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
290
|
+
});
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Drop the proxy + slice subscriptions for a device. */
|
|
295
|
+
releaseProxy(deviceId) {
|
|
296
|
+
const unsubs = this.proxyUnsubs.get(deviceId);
|
|
297
|
+
if (unsubs) for (const u of unsubs) try {
|
|
298
|
+
u();
|
|
299
|
+
} catch {}
|
|
300
|
+
this.proxyUnsubs.delete(deviceId);
|
|
301
|
+
this.proxies.delete(deviceId);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Resolve the effective per-device config for a camera. Reads the raw
|
|
305
|
+
* device store and overlays it on top of the schema defaults via
|
|
306
|
+
* `hydrateSchema()`, then narrows to the typed `WasmMotionConfig` shape.
|
|
307
|
+
* Cached with a TTL to avoid hammering the settings store on every frame.
|
|
308
|
+
*/
|
|
309
|
+
async getDeviceConfig(cameraId) {
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
const cached = this.deviceConfigCache.get(cameraId);
|
|
312
|
+
if (cached && now - cached.fetchedAt < MotionWasmAddon.DEVICE_CONFIG_TTL_MS) return cached.config;
|
|
313
|
+
if (!this.ctx?.settings) return DEFAULT_CONFIG;
|
|
314
|
+
const raw = await this.ctx.settings.readDeviceStore(Number(cameraId));
|
|
315
|
+
const hydrated = hydrateSchema(this.deviceSettingsSchema(), raw);
|
|
316
|
+
const flat = {};
|
|
317
|
+
for (const section of hydrated.sections) for (const field of section.fields) {
|
|
318
|
+
if (field.type === "separator" || field.type === "info" || field.type === "button") continue;
|
|
319
|
+
if (field.type === "group") continue;
|
|
320
|
+
flat[field.key] = field.value;
|
|
321
|
+
}
|
|
322
|
+
const resolved = {
|
|
323
|
+
threshold: typeof flat["threshold"] === "number" ? flat["threshold"] : DEFAULT_CONFIG.threshold,
|
|
324
|
+
blurRadius: typeof flat["blurRadius"] === "number" ? flat["blurRadius"] : DEFAULT_CONFIG.blurRadius,
|
|
325
|
+
dilateRadius: typeof flat["dilateRadius"] === "number" ? flat["dilateRadius"] : DEFAULT_CONFIG.dilateRadius,
|
|
326
|
+
minArea: typeof flat["minArea"] === "number" ? flat["minArea"] : DEFAULT_CONFIG.minArea
|
|
327
|
+
};
|
|
328
|
+
this.deviceConfigCache.set(cameraId, {
|
|
329
|
+
config: resolved,
|
|
330
|
+
fetchedAt: now
|
|
331
|
+
});
|
|
332
|
+
return resolved;
|
|
333
|
+
}
|
|
334
|
+
async removeCamera({ deviceId }) {
|
|
335
|
+
const key = String(deviceId);
|
|
336
|
+
this.cameras.delete(key);
|
|
337
|
+
this.deviceConfigCache.delete(key);
|
|
338
|
+
this.releaseProxy(deviceId);
|
|
339
|
+
}
|
|
340
|
+
async reset() {
|
|
341
|
+
this.cameras.clear();
|
|
342
|
+
this.deviceConfigCache.clear();
|
|
343
|
+
for (const id of this.proxies.keys()) this.releaseProxy(id);
|
|
344
|
+
}
|
|
345
|
+
async onShutdown() {
|
|
346
|
+
this.cameras.clear();
|
|
347
|
+
this.deviceConfigCache.clear();
|
|
348
|
+
for (const id of this.proxies.keys()) this.releaseProxy(id);
|
|
349
|
+
this.detector = null;
|
|
350
|
+
}
|
|
351
|
+
deviceSettingsSchema() {
|
|
352
|
+
return this.schema({ sections: [{
|
|
353
|
+
id: "motion-wasm-settings",
|
|
354
|
+
title: "",
|
|
355
|
+
tab: "motion",
|
|
356
|
+
order: 10,
|
|
357
|
+
columns: 2,
|
|
358
|
+
fields: [
|
|
359
|
+
{
|
|
360
|
+
type: "slider",
|
|
361
|
+
key: "threshold",
|
|
362
|
+
label: "Threshold",
|
|
363
|
+
description: "Pixel intensity difference to count as changed (higher = less sensitive)",
|
|
364
|
+
min: 1,
|
|
365
|
+
max: 255,
|
|
366
|
+
step: 1,
|
|
367
|
+
default: DEFAULT_CONFIG.threshold,
|
|
368
|
+
showValue: true,
|
|
369
|
+
showWhen: {
|
|
370
|
+
field: "motionSources",
|
|
371
|
+
includes: "analyzer"
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
type: "number",
|
|
376
|
+
key: "minArea",
|
|
377
|
+
label: "Min Area",
|
|
378
|
+
description: "Minimum pixel area of a motion region to trigger detection",
|
|
379
|
+
min: 0,
|
|
380
|
+
max: 5e4,
|
|
381
|
+
step: 100,
|
|
382
|
+
default: DEFAULT_CONFIG.minArea,
|
|
383
|
+
unit: "px",
|
|
384
|
+
showWhen: {
|
|
385
|
+
field: "motionSources",
|
|
386
|
+
includes: "analyzer"
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
type: "slider",
|
|
391
|
+
key: "blurRadius",
|
|
392
|
+
label: "Blur Radius",
|
|
393
|
+
description: "Gaussian blur before diff (reduces noise)",
|
|
394
|
+
min: 0,
|
|
395
|
+
max: 10,
|
|
396
|
+
step: 1,
|
|
397
|
+
default: DEFAULT_CONFIG.blurRadius,
|
|
398
|
+
showValue: true,
|
|
399
|
+
showWhen: {
|
|
400
|
+
field: "motionSources",
|
|
401
|
+
includes: "analyzer"
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
type: "slider",
|
|
406
|
+
key: "dilateRadius",
|
|
407
|
+
label: "Dilation Radius",
|
|
408
|
+
description: "Expand motion regions to merge nearby changes",
|
|
409
|
+
min: 0,
|
|
410
|
+
max: 10,
|
|
411
|
+
step: 1,
|
|
412
|
+
default: DEFAULT_CONFIG.dilateRadius,
|
|
413
|
+
showValue: true,
|
|
414
|
+
showWhen: {
|
|
415
|
+
field: "motionSources",
|
|
416
|
+
includes: "analyzer"
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
]
|
|
420
|
+
}] });
|
|
421
|
+
}
|
|
422
|
+
async getDeviceSettings(deviceId) {
|
|
423
|
+
const raw = await this.ctxIfReady?.settings?.readDeviceStore(deviceId) ?? {};
|
|
424
|
+
return hydrateSchema(this.deviceSettingsSchema(), raw);
|
|
425
|
+
}
|
|
426
|
+
async updateDeviceSettings(deviceId, patch) {
|
|
427
|
+
await this.ctx.settings?.writeDeviceStore(deviceId, patch);
|
|
428
|
+
this.deviceConfigCache.delete(String(deviceId));
|
|
429
|
+
}
|
|
430
|
+
async getDeviceSettingsContribution(input) {
|
|
431
|
+
if (!await this.isCameraDevice(input.deviceId)) return null;
|
|
432
|
+
const schema = this.deviceSettingsSchema();
|
|
433
|
+
if (!schema) return null;
|
|
434
|
+
const raw = await this.ctx?.settings?.readDeviceStore(input.deviceId) ?? {};
|
|
435
|
+
return hydrateSchema({
|
|
436
|
+
...schema,
|
|
437
|
+
sections: schema.sections.map((s) => ({
|
|
438
|
+
...s,
|
|
439
|
+
tab: s.tab ?? "detection"
|
|
440
|
+
}))
|
|
441
|
+
}, raw);
|
|
442
|
+
}
|
|
443
|
+
async getDeviceLiveContribution(_input) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
async applyDeviceSettingsPatch(input) {
|
|
447
|
+
await this.updateDeviceSettings(input.deviceId, input.patch);
|
|
448
|
+
return { success: true };
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Best-effort camera-type check. Used by the settings/live contribution
|
|
452
|
+
* methods to short-circuit on non-camera devices (Lights, Switches,
|
|
453
|
+
* Sensors, Buttons). Returns `true` on lookup failure so a transient
|
|
454
|
+
* device-manager hiccup never silently hides legitimate camera
|
|
455
|
+
* sections.
|
|
456
|
+
*/
|
|
457
|
+
async isCameraDevice(deviceId) {
|
|
458
|
+
const api = this.ctx?.api;
|
|
459
|
+
if (!api) return true;
|
|
460
|
+
try {
|
|
461
|
+
const dev = await api.deviceManager.getDevice.query({ deviceId });
|
|
462
|
+
if (!dev) return true;
|
|
463
|
+
return dev.type === DeviceType.Camera;
|
|
464
|
+
} catch {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
451
468
|
};
|
|
452
|
-
//#
|
|
469
|
+
//#endregion
|
|
470
|
+
export { MotionWasmAddon, MotionWasmAddon as default, WasmMotionDetector };
|