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