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