@camstack/addon-pipeline 1.1.32 → 1.1.34

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 (41) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/audio-codec-ffmpeg/index.js +118 -3
  4. package/dist/audio-codec-ffmpeg/index.mjs +118 -3
  5. package/dist/decoder-ffmpeg/index.js +75 -10
  6. package/dist/decoder-ffmpeg/index.mjs +75 -10
  7. package/dist/decoder-nodeav/index.js +209 -9
  8. package/dist/decoder-nodeav/index.mjs +209 -9
  9. package/dist/detection-pipeline/index.js +1 -1
  10. package/dist/detection-pipeline/index.mjs +1 -1
  11. package/dist/{dist-Cwc0TUQr.js → dist-BrIE85BG.js} +38 -1
  12. package/dist/{dist-DjuGmyG9.mjs → dist-CfSORkv2.mjs} +38 -1
  13. package/dist/{ffmpeg-args-C5GPp8Cw.mjs → ffmpeg-args-CpeRWwL4.mjs} +7 -4
  14. package/dist/{ffmpeg-args-D6h1edXK.js → ffmpeg-args-Dl9mLcOC.js} +7 -4
  15. package/dist/{frame-handle-plane-Bkxz-TTD.mjs → frame-handle-plane-C05rq9Zs.mjs} +108 -17
  16. package/dist/{frame-handle-plane-DQNCTrpC.js → frame-handle-plane-zMju9vmk.js} +108 -17
  17. package/dist/{frame-ring-sink-sY-8XUsH.mjs → frame-ring-sink-1aLGcNNf.mjs} +81 -31
  18. package/dist/{frame-ring-sink-DvdhCFr3.js → frame-ring-sink-CvlcAUOu.js} +86 -30
  19. package/dist/motion-wasm/index.js +1 -1
  20. package/dist/motion-wasm/index.mjs +1 -1
  21. package/dist/pipeline-runner/index.js +86 -16
  22. package/dist/pipeline-runner/index.mjs +85 -17
  23. package/dist/recorder/index.js +1 -1
  24. package/dist/recorder/index.mjs +1 -1
  25. package/dist/stream-broker/_stub.js +1 -1
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFAbcUo.mjs} +3 -3
  27. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsrrdOBU.mjs} +1 -1
  28. package/dist/stream-broker/{hostInit-B_b3PIZB.mjs → hostInit-C8slrxw1.mjs} +3 -3
  29. package/dist/stream-broker/index.js +165 -27
  30. package/dist/stream-broker/index.mjs +165 -27
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js → MaskShapeCanvas-DI4BY7W2-WUVfFzT-.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DTeuDGCU.js → MotionZonesSettings-NcxxQN8r-CF7FXXLs.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CirtxO3e.js → PrivacyMaskSettings-APgPLF7p-qQCpVob3.js} +1 -1
  35. package/embed-dist/assets/{index-CpUy8OIE.js → index-BNhisVUZ.js} +4 -4
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/dist/constants-CAqYVigN.js +0 -6615
  39. package/dist/constants-oYgW36Ty.mjs +0 -3587
  40. package/dist/dist-D3Ytt-_Y.js +0 -41111
  41. package/dist/dist-DluJuxV5.mjs +0 -37976
@@ -1,8 +1,11 @@
1
- import { d as HWACCEL_OPTIONS, p as RingBuffer } from "./dist-DjuGmyG9.mjs";
1
+ import { d as HWACCEL_OPTIONS, p as RingBuffer } from "./dist-CfSORkv2.mjs";
2
2
  import { FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount } from "@camstack/shm-ring";
3
3
  //#region src/shared/decoder-backend-keys.ts
4
- /** Built-in default when a node has no selection — the subprocess decoder. */
5
- var DEFAULT_DECODER_BACKEND = "ffmpeg";
4
+ /** Built-in default when a node has no selection — the in-process node-av
5
+ * decoder. node-av is the standing default everywhere; the ffmpeg subprocess
6
+ * decoder is an explicit opt-in fallback only. A stale/failed settings read at
7
+ * boot therefore resolves to node-av (never leaves the node with no decoder). */
8
+ var DEFAULT_DECODER_BACKEND = "nodeav";
6
9
  /** Narrow an unknown settings value to a {@link DecoderBackend}, else `null`. */
7
10
  function parseDecoderBackend(value) {
8
11
  return value === "ffmpeg" || value === "nodeav" ? value : null;
@@ -65,9 +68,13 @@ function normalizeDecoderNodeId(rawNodeId) {
65
68
  * original bug), the raw settings-store cap, or any custom side channel.
66
69
  *
67
70
  * Resolution: the node's scoped value → the built-in default
68
- * {@link DEFAULT_DECODER_BACKEND} (`'ffmpeg'`). There is deliberately NO
71
+ * {@link DEFAULT_DECODER_BACKEND} (`'nodeav'` node-av is primary everywhere;
72
+ * ffmpeg is an explicit opt-in fallback only). There is deliberately NO
69
73
  * bare-key fallback (see `decoder-backend-keys.ts`), and any read failure
70
- * resolves to the default so the safe backend still comes up.
74
+ * resolves to the default so a decoder still comes up — and, because the
75
+ * default IS the primary backend, a stale/failed read can never leave the node
76
+ * with NO decoder (the old `'ffmpeg'` default made node-av stand down on a
77
+ * missed read → both addons down).
71
78
  *
72
79
  * ## Switching backends
73
80
  *
@@ -100,44 +107,87 @@ function pickDecoderBackendFromSettings(view) {
100
107
  if (view === null) return DEFAULT_DECODER_BACKEND;
101
108
  for (const section of view.sections) for (const entry of section.fields) {
102
109
  if (!isHydratedField(entry) || entry.key !== "backend") continue;
103
- return parseDecoderBackend(entry.value) ?? "ffmpeg";
110
+ return parseDecoderBackend(entry.value) ?? "nodeav";
104
111
  }
105
112
  return DEFAULT_DECODER_BACKEND;
106
113
  }
107
114
  /**
108
- * Resolve the decoder backend this node should run. Called by BOTH decoder
109
- * addons at the start of `onInitialize`; the addon whose backend is NOT
110
- * selected registers nothing.
115
+ * Resolve the OWNER addon's (`decoder-ffmpeg`) own per-node backend from its
116
+ * ALREADY-PROJECTED global store the store `BaseAddon.resolveGlobalStore`
117
+ * returns, where the `perNode` `backend` field carries THIS node's value on
118
+ * its bare key. The owner MUST use this instead of {@link resolveDecoderBackend}:
119
+ * routing `addonSettings.getGlobalSettings({addonId:'decoder-ffmpeg'})` back to
120
+ * itself during its OWN `onInitialize` DEADLOCKS (the addon is not "loaded" on
121
+ * the node until init completes, but init is blocked on the read) → the read
122
+ * throws `transport-failed (addon not loaded)` → it silently defaults to the
123
+ * built-in default and mis-decides its own registration (e.g. an explicitly
124
+ * selected `ffmpeg` fallback would never come up). Reading its own store is
125
+ * local, race-free, and has no deadlock.
126
+ */
127
+ function resolveOwnDecoderBackend(projectedGlobalStore) {
128
+ return parseDecoderBackend(projectedGlobalStore["backend"]) ?? "nodeav";
129
+ }
130
+ /** Transient transport/settings-store fingerprints worth retrying on. */
131
+ function isTransientSettingsError(message) {
132
+ return /not routable/i.test(message) || /not loaded/i.test(message) || /transport-failed/i.test(message) || /not connected/i.test(message) || /provider not available/i.test(message) || /SqliteSettingsBackend not initialized/i.test(message);
133
+ }
134
+ /**
135
+ * Resolve the decoder backend a NON-OWNER addon (`decoder-nodeav`) should run,
136
+ * by reading the OWNER's (`decoder-ffmpeg`) hub-central per-node `backend`.
111
137
  *
112
- * Fails safe: no api surface, a read error, or a null payload (owner not
113
- * answering yet) resolves to {@link DEFAULT_DECODER_BACKEND} the default
114
- * backend still registers, and the non-default one stands down (never both).
138
+ * The read routes to the owner addon's child runner; during a simultaneous
139
+ * (re)start the owner may not be up yet → a transient `transport-failed (addon
140
+ * not loaded)`. Immediately defaulting here would ignore an explicit `ffmpeg`
141
+ * selection (the node would silently run the node-av default instead). So retry
142
+ * on the transient fingerprints with a bounded budget (mirrors
143
+ * `BaseAddon.readAddonStoreWithRetry`) until the owner answers; only a
144
+ * persistent failure falls back to {@link DEFAULT_DECODER_BACKEND}.
145
+ *
146
+ * The OWNER addon must NOT call this (it would self-route + deadlock) — it uses
147
+ * {@link resolveOwnDecoderBackend} against its own store instead.
115
148
  */
116
149
  async function resolveDecoderBackend(api, nodeId, logger) {
117
150
  if (!api) {
118
151
  logger.warn("decoder-backend: no api surface — using default backend", { meta: { default: DEFAULT_DECODER_BACKEND } });
119
152
  return DEFAULT_DECODER_BACKEND;
120
153
  }
121
- try {
122
- const view = await api.addonSettings.getGlobalSettings.query({
123
- addonId: DECODER_OWNER_ADDON_ID,
124
- nodeId: normalizeDecoderNodeId(nodeId)
125
- });
126
- if (view === null) {
127
- logger.warn("decoder-backend: owner settings unavailable — using default backend", { meta: {
128
- default: DEFAULT_DECODER_BACKEND,
129
- owner: DECODER_OWNER_ADDON_ID
130
- } });
131
- return DEFAULT_DECODER_BACKEND;
154
+ const normalized = normalizeDecoderNodeId(nodeId);
155
+ const delaysMs = [
156
+ 150,
157
+ 350,
158
+ 600,
159
+ 900,
160
+ 1200
161
+ ];
162
+ let lastErr;
163
+ for (let attempt = 0; attempt <= delaysMs.length; attempt++) {
164
+ try {
165
+ const view = await api.addonSettings.getGlobalSettings.query({
166
+ addonId: DECODER_OWNER_ADDON_ID,
167
+ nodeId: normalized
168
+ });
169
+ if (view !== null) return pickDecoderBackendFromSettings(view);
170
+ lastErr = /* @__PURE__ */ new Error("owner settings unavailable (null)");
171
+ } catch (err) {
172
+ lastErr = err;
173
+ const msg = err instanceof Error ? err.message : String(err);
174
+ if (!isTransientSettingsError(msg)) {
175
+ logger.warn("decoder-backend: settings read failed — using default backend", { meta: {
176
+ default: DEFAULT_DECODER_BACKEND,
177
+ error: msg
178
+ } });
179
+ return DEFAULT_DECODER_BACKEND;
180
+ }
132
181
  }
133
- return pickDecoderBackendFromSettings(view);
134
- } catch (err) {
135
- logger.warn("decoder-backend: settings read failed — using default backend", { meta: {
136
- default: DEFAULT_DECODER_BACKEND,
137
- error: err instanceof Error ? err.message : String(err)
138
- } });
139
- return DEFAULT_DECODER_BACKEND;
182
+ if (attempt === delaysMs.length) break;
183
+ await new Promise((resolve) => setTimeout(resolve, delaysMs[attempt]));
140
184
  }
185
+ logger.warn("decoder-backend: owner settings unavailable after retries — using default backend", { meta: {
186
+ default: DEFAULT_DECODER_BACKEND,
187
+ owner: DECODER_OWNER_ADDON_ID,
188
+ error: lastErr instanceof Error ? lastErr.message : String(lastErr)
189
+ } });
190
+ return DEFAULT_DECODER_BACKEND;
141
191
  }
142
192
  /**
143
193
  * Pure selection of the owner's `hwaccel` field from an already-read hydrated
@@ -504,4 +554,4 @@ var DecoderFrameRingSink = class {
504
554
  }
505
555
  };
506
556
  //#endregion
507
- export { resolveDecoderBackend as a, NotifyingRingBuffer as i, RING_BUDGET_MB as n, resolveDecoderHwAccel as o, makeSegmentName as r, DEFAULT_DECODER_BACKEND as s, DecoderFrameRingSink as t };
557
+ export { resolveDecoderBackend as a, DEFAULT_DECODER_BACKEND as c, NotifyingRingBuffer as i, RING_BUDGET_MB as n, resolveDecoderHwAccel as o, makeSegmentName as r, resolveOwnDecoderBackend as s, DecoderFrameRingSink as t };
@@ -1,8 +1,11 @@
1
- const require_dist = require("./dist-Cwc0TUQr.js");
1
+ const require_dist = require("./dist-BrIE85BG.js");
2
2
  let _camstack_shm_ring = require("@camstack/shm-ring");
3
3
  //#region src/shared/decoder-backend-keys.ts
4
- /** Built-in default when a node has no selection — the subprocess decoder. */
5
- var DEFAULT_DECODER_BACKEND = "ffmpeg";
4
+ /** Built-in default when a node has no selection — the in-process node-av
5
+ * decoder. node-av is the standing default everywhere; the ffmpeg subprocess
6
+ * decoder is an explicit opt-in fallback only. A stale/failed settings read at
7
+ * boot therefore resolves to node-av (never leaves the node with no decoder). */
8
+ var DEFAULT_DECODER_BACKEND = "nodeav";
6
9
  /** Narrow an unknown settings value to a {@link DecoderBackend}, else `null`. */
7
10
  function parseDecoderBackend(value) {
8
11
  return value === "ffmpeg" || value === "nodeav" ? value : null;
@@ -65,9 +68,13 @@ function normalizeDecoderNodeId(rawNodeId) {
65
68
  * original bug), the raw settings-store cap, or any custom side channel.
66
69
  *
67
70
  * Resolution: the node's scoped value → the built-in default
68
- * {@link DEFAULT_DECODER_BACKEND} (`'ffmpeg'`). There is deliberately NO
71
+ * {@link DEFAULT_DECODER_BACKEND} (`'nodeav'` node-av is primary everywhere;
72
+ * ffmpeg is an explicit opt-in fallback only). There is deliberately NO
69
73
  * bare-key fallback (see `decoder-backend-keys.ts`), and any read failure
70
- * resolves to the default so the safe backend still comes up.
74
+ * resolves to the default so a decoder still comes up — and, because the
75
+ * default IS the primary backend, a stale/failed read can never leave the node
76
+ * with NO decoder (the old `'ffmpeg'` default made node-av stand down on a
77
+ * missed read → both addons down).
71
78
  *
72
79
  * ## Switching backends
73
80
  *
@@ -100,44 +107,87 @@ function pickDecoderBackendFromSettings(view) {
100
107
  if (view === null) return DEFAULT_DECODER_BACKEND;
101
108
  for (const section of view.sections) for (const entry of section.fields) {
102
109
  if (!isHydratedField(entry) || entry.key !== "backend") continue;
103
- return parseDecoderBackend(entry.value) ?? "ffmpeg";
110
+ return parseDecoderBackend(entry.value) ?? "nodeav";
104
111
  }
105
112
  return DEFAULT_DECODER_BACKEND;
106
113
  }
107
114
  /**
108
- * Resolve the decoder backend this node should run. Called by BOTH decoder
109
- * addons at the start of `onInitialize`; the addon whose backend is NOT
110
- * selected registers nothing.
115
+ * Resolve the OWNER addon's (`decoder-ffmpeg`) own per-node backend from its
116
+ * ALREADY-PROJECTED global store the store `BaseAddon.resolveGlobalStore`
117
+ * returns, where the `perNode` `backend` field carries THIS node's value on
118
+ * its bare key. The owner MUST use this instead of {@link resolveDecoderBackend}:
119
+ * routing `addonSettings.getGlobalSettings({addonId:'decoder-ffmpeg'})` back to
120
+ * itself during its OWN `onInitialize` DEADLOCKS (the addon is not "loaded" on
121
+ * the node until init completes, but init is blocked on the read) → the read
122
+ * throws `transport-failed (addon not loaded)` → it silently defaults to the
123
+ * built-in default and mis-decides its own registration (e.g. an explicitly
124
+ * selected `ffmpeg` fallback would never come up). Reading its own store is
125
+ * local, race-free, and has no deadlock.
126
+ */
127
+ function resolveOwnDecoderBackend(projectedGlobalStore) {
128
+ return parseDecoderBackend(projectedGlobalStore["backend"]) ?? "nodeav";
129
+ }
130
+ /** Transient transport/settings-store fingerprints worth retrying on. */
131
+ function isTransientSettingsError(message) {
132
+ return /not routable/i.test(message) || /not loaded/i.test(message) || /transport-failed/i.test(message) || /not connected/i.test(message) || /provider not available/i.test(message) || /SqliteSettingsBackend not initialized/i.test(message);
133
+ }
134
+ /**
135
+ * Resolve the decoder backend a NON-OWNER addon (`decoder-nodeav`) should run,
136
+ * by reading the OWNER's (`decoder-ffmpeg`) hub-central per-node `backend`.
137
+ *
138
+ * The read routes to the owner addon's child runner; during a simultaneous
139
+ * (re)start the owner may not be up yet → a transient `transport-failed (addon
140
+ * not loaded)`. Immediately defaulting here would ignore an explicit `ffmpeg`
141
+ * selection (the node would silently run the node-av default instead). So retry
142
+ * on the transient fingerprints with a bounded budget (mirrors
143
+ * `BaseAddon.readAddonStoreWithRetry`) until the owner answers; only a
144
+ * persistent failure falls back to {@link DEFAULT_DECODER_BACKEND}.
111
145
  *
112
- * Fails safe: no api surface, a read error, or a null payload (owner not
113
- * answering yet) resolves to {@link DEFAULT_DECODER_BACKEND} the default
114
- * backend still registers, and the non-default one stands down (never both).
146
+ * The OWNER addon must NOT call this (it would self-route + deadlock) it uses
147
+ * {@link resolveOwnDecoderBackend} against its own store instead.
115
148
  */
116
149
  async function resolveDecoderBackend(api, nodeId, logger) {
117
150
  if (!api) {
118
151
  logger.warn("decoder-backend: no api surface — using default backend", { meta: { default: DEFAULT_DECODER_BACKEND } });
119
152
  return DEFAULT_DECODER_BACKEND;
120
153
  }
121
- try {
122
- const view = await api.addonSettings.getGlobalSettings.query({
123
- addonId: DECODER_OWNER_ADDON_ID,
124
- nodeId: normalizeDecoderNodeId(nodeId)
125
- });
126
- if (view === null) {
127
- logger.warn("decoder-backend: owner settings unavailable — using default backend", { meta: {
128
- default: DEFAULT_DECODER_BACKEND,
129
- owner: DECODER_OWNER_ADDON_ID
130
- } });
131
- return DEFAULT_DECODER_BACKEND;
154
+ const normalized = normalizeDecoderNodeId(nodeId);
155
+ const delaysMs = [
156
+ 150,
157
+ 350,
158
+ 600,
159
+ 900,
160
+ 1200
161
+ ];
162
+ let lastErr;
163
+ for (let attempt = 0; attempt <= delaysMs.length; attempt++) {
164
+ try {
165
+ const view = await api.addonSettings.getGlobalSettings.query({
166
+ addonId: DECODER_OWNER_ADDON_ID,
167
+ nodeId: normalized
168
+ });
169
+ if (view !== null) return pickDecoderBackendFromSettings(view);
170
+ lastErr = /* @__PURE__ */ new Error("owner settings unavailable (null)");
171
+ } catch (err) {
172
+ lastErr = err;
173
+ const msg = err instanceof Error ? err.message : String(err);
174
+ if (!isTransientSettingsError(msg)) {
175
+ logger.warn("decoder-backend: settings read failed — using default backend", { meta: {
176
+ default: DEFAULT_DECODER_BACKEND,
177
+ error: msg
178
+ } });
179
+ return DEFAULT_DECODER_BACKEND;
180
+ }
132
181
  }
133
- return pickDecoderBackendFromSettings(view);
134
- } catch (err) {
135
- logger.warn("decoder-backend: settings read failed — using default backend", { meta: {
136
- default: DEFAULT_DECODER_BACKEND,
137
- error: err instanceof Error ? err.message : String(err)
138
- } });
139
- return DEFAULT_DECODER_BACKEND;
182
+ if (attempt === delaysMs.length) break;
183
+ await new Promise((resolve) => setTimeout(resolve, delaysMs[attempt]));
140
184
  }
185
+ logger.warn("decoder-backend: owner settings unavailable after retries — using default backend", { meta: {
186
+ default: DEFAULT_DECODER_BACKEND,
187
+ owner: DECODER_OWNER_ADDON_ID,
188
+ error: lastErr instanceof Error ? lastErr.message : String(lastErr)
189
+ } });
190
+ return DEFAULT_DECODER_BACKEND;
141
191
  }
142
192
  /**
143
193
  * Pure selection of the owner's `hwaccel` field from an already-read hydrated
@@ -546,3 +596,9 @@ Object.defineProperty(exports, "resolveDecoderHwAccel", {
546
596
  return resolveDecoderHwAccel;
547
597
  }
548
598
  });
599
+ Object.defineProperty(exports, "resolveOwnDecoderBackend", {
600
+ enumerable: true,
601
+ get: function() {
602
+ return resolveOwnDecoderBackend;
603
+ }
604
+ });
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Cwc0TUQr.js");
5
+ const require_dist = require("../dist-BrIE85BG.js");
6
6
  let _camstack_shm_ring = require("@camstack/shm-ring");
7
7
  let node_fs = require("node:fs");
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { A as motionDetectionCapability, T as evaluateZoneRules, V as BaseAddon, W as DeviceType, q as hydrateSchema } from "../dist-DjuGmyG9.mjs";
1
+ import { A as motionDetectionCapability, T as evaluateZoneRules, V as BaseAddon, W as DeviceType, q as hydrateSchema } from "../dist-CfSORkv2.mjs";
2
2
  import { FrameRingReaderCache } from "@camstack/shm-ring";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Cwc0TUQr.js");
6
- const require_frame_handle_plane = require("../frame-handle-plane-DQNCTrpC.js");
5
+ const require_dist = require("../dist-BrIE85BG.js");
6
+ const require_frame_handle_plane = require("../frame-handle-plane-zMju9vmk.js");
7
7
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
9
9
  //#region src/pipeline-runner/bench-actions.ts
@@ -1409,6 +1409,55 @@ function toFrameInput(frame) {
1409
1409
  };
1410
1410
  }
1411
1411
  /**
1412
+ * Convert a packed RGB24 `DecodedFrame` into a packed GRAY8 one using the
1413
+ * BT.601 luma weighting (Y = 0.299R + 0.587G + 0.114B, integer-approximated as
1414
+ * 77/150/29 over 256).
1415
+ *
1416
+ * This is what lets **sensor-triggered (onboard) motion share the detection
1417
+ * decode**: the gray the motion analyzer needs is just the luma of the
1418
+ * already-decoded detection `rgb` frame, so no second decoder is spun up. Far
1419
+ * cheaper than a full second decode (a per-pixel weighted sum vs. an entire
1420
+ * H.264/H.265 decode). Persistent (analyzer-only) motion keeps its own gray
1421
+ * decode on the sub stream and never hits this path.
1422
+ */
1423
+ function rgbFrameToGrayFrame(frame) {
1424
+ const rgb = frame.data;
1425
+ const pixels = frame.width * frame.height;
1426
+ const gray = Buffer.allocUnsafe(pixels);
1427
+ for (let i = 0, j = 0; i < pixels; i++, j += 3) gray[i] = rgb[j] * 77 + rgb[j + 1] * 150 + rgb[j + 2] * 29 >> 8;
1428
+ return {
1429
+ data: gray,
1430
+ width: frame.width,
1431
+ height: frame.height,
1432
+ format: "gray",
1433
+ timestamp: frame.timestamp,
1434
+ ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {}
1435
+ };
1436
+ }
1437
+ /**
1438
+ * Decide where the motion analyzer reads frames from.
1439
+ *
1440
+ * - `sharedWithDetection` (onboard/sensor-triggered) AND detection actually
1441
+ * runs (`detectionMode !== 'disabled'`) → ride the detection decode:
1442
+ * `rgb` on the MAIN (`detectionStreamId`) stream, converted to gray. No
1443
+ * second decoder — the `FrameHandlePlane` coalesces it onto detection's session.
1444
+ * - Otherwise (persistent analyzer, or detection disabled) → a dedicated
1445
+ * `gray` decode on the SUB (`motionStreamId`) stream, exactly as before.
1446
+ *
1447
+ * Pure so the decision is unit-testable without the addon/broker.
1448
+ */
1449
+ function resolveMotionFrameSource(config, sharedWithDetection) {
1450
+ return sharedWithDetection && config.detectionMode !== "disabled" ? {
1451
+ streamId: config.detectionStreamId,
1452
+ format: "rgb",
1453
+ shared: true
1454
+ } : {
1455
+ streamId: config.motionStreamId,
1456
+ format: "gray",
1457
+ shared: false
1458
+ };
1459
+ }
1460
+ /**
1412
1461
  * Interval for periodic metric snapshot emission. ~1 Hz strikes the
1413
1462
  * balance between UI freshness (overlay phase / fps numbers feel live)
1414
1463
  * and bus traffic (one snapshot per attached camera + one per node
@@ -1911,6 +1960,12 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1911
1960
  }
1912
1961
  });
1913
1962
  if (this.attached.has(config.deviceId)) this.detachInternal(config.deviceId);
1963
+ const attachment = {
1964
+ config,
1965
+ motionUnsubscribe: null,
1966
+ detectionUnsubscribe: null
1967
+ };
1968
+ this.attached.set(config.deviceId, attachment);
1914
1969
  runner.registerCamera(config.deviceId, {
1915
1970
  detectionMode: config.detectionMode,
1916
1971
  fps: config.detectionFps,
@@ -1918,12 +1973,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1918
1973
  occupancyRecheckSec: config.occupancyRecheckSec,
1919
1974
  occupancyRecheckFrames: config.occupancyRecheckFrames
1920
1975
  });
1921
- const attachment = {
1922
- config,
1923
- motionUnsubscribe: null,
1924
- detectionUnsubscribe: null
1925
- };
1926
- this.attached.set(config.deviceId, attachment);
1927
1976
  if (config.motionSources.includes("analyzer")) attachment.motionUnsubscribe = await this.subscribeMotionFrames(config);
1928
1977
  const stepsCount = config.steps?.length ?? 0;
1929
1978
  const dispatch = stepsCount > 0 ? `runPipeline(${stepsCount}step${stepsCount === 1 ? "" : "s"})` : config.steps !== void 0 ? "skip(0steps)" : "runFrame(legacy)";
@@ -2013,10 +2062,10 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2013
2062
  if (!shouldStartOnboardAnalyzer(config)) return;
2014
2063
  const log = this.ctx.logger.withTags({ deviceId });
2015
2064
  if (detected && !this.onboardAnalyzerSubs.has(deviceId)) {
2016
- const unsub = await this.subscribeMotionFrames(config);
2065
+ const unsub = await this.subscribeMotionFrames(config, { sharedWithDetection: true });
2017
2066
  if (unsub) {
2018
2067
  this.onboardAnalyzerSubs.set(deviceId, unsub);
2019
- log.debug("onboard-analyzer: opened motion-frame subscription");
2068
+ log.debug("onboard-analyzer: opened motion-frame subscription (shared detection decode)");
2020
2069
  }
2021
2070
  }
2022
2071
  const existing = this.onboardAnalyzerTeardownTimers.get(deviceId);
@@ -2140,7 +2189,21 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2140
2189
  this.remotePlanes.set(config.deviceId, plane);
2141
2190
  return plane;
2142
2191
  }
2143
- async subscribeMotionFrames(config) {
2192
+ /**
2193
+ * Subscribe the motion analyzer to decoded frames.
2194
+ *
2195
+ * Two modes:
2196
+ * - **Persistent (default)** — a dedicated `gray` decode of the camera's
2197
+ * `motionStreamId` (the sub stream). Cheap, always-on; used when the SW
2198
+ * analyzer is the motion source (`motionSources` includes `'analyzer'`).
2199
+ * - **Shared with detection** (`opts.sharedWithDetection`) — subscribes
2200
+ * `rgb` on `detectionStreamId` (the MAIN stream) so it rides the SAME
2201
+ * decoder session detection already uses (the `FrameHandlePlane` coalesces
2202
+ * same-format subscribers on a stream into ONE decode), then derives the
2203
+ * analyzer's `gray` from that rgb frame's luma. No second decoder. Used for
2204
+ * onboard/sensor-triggered motion, where a decode is already active.
2205
+ */
2206
+ async subscribeMotionFrames(config, opts) {
2144
2207
  const ctx = this.ctx;
2145
2208
  const runner = this.runner;
2146
2209
  if (!ctx || !runner) return null;
@@ -2150,18 +2213,23 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2150
2213
  log.warn("subscribeMotionFrames: this.ctx.api not available");
2151
2214
  return null;
2152
2215
  }
2216
+ const src = resolveMotionFrameSource(config, opts?.sharedWithDetection === true);
2217
+ const shared = src.shared;
2218
+ const streamId = src.streamId;
2153
2219
  const remote = this.remoteSourceFor(config);
2154
2220
  return startFrameHandlePoller({
2155
2221
  api,
2156
- brokerId: require_dist.makeSourceBrokerId(config.deviceId, config.motionStreamId),
2157
- format: "gray",
2222
+ brokerId: require_dist.makeSourceBrokerId(config.deviceId, streamId),
2223
+ format: src.format,
2158
2224
  maxFps: config.motionFps,
2159
- tag: "motion",
2225
+ tag: shared ? "motion-shared" : "motion",
2160
2226
  logger: log,
2161
2227
  ownNodeId: this.nodeId,
2162
2228
  getRemoteFrame: (handle) => this.getRemoteFrame(handle),
2163
- ...remote ? { source: remote.makeSource(config.motionStreamId) } : {},
2164
- onFrame: (frame, handle) => {
2229
+ ...remote ? { source: remote.makeSource(streamId) } : {},
2230
+ onFrame: shared ? (frame) => {
2231
+ runner.enqueueMotionFrame(config.deviceId, rgbFrameToGrayFrame(frame), void 0);
2232
+ } : (frame, handle) => {
2165
2233
  runner.enqueueMotionFrame(config.deviceId, frame, handle);
2166
2234
  }
2167
2235
  });
@@ -2588,4 +2656,6 @@ exports.PipelineTimingSampler = PipelineTimingSampler;
2588
2656
  exports.Semaphore = Semaphore;
2589
2657
  exports.customActions = pipelineRunnerBenchActions;
2590
2658
  exports.default = PipelineRunnerAddon;
2659
+ exports.resolveMotionFrameSource = resolveMotionFrameSource;
2660
+ exports.rgbFrameToGrayFrame = rgbFrameToGrayFrame;
2591
2661
  exports.shouldStartOnboardAnalyzer = shouldStartOnboardAnalyzer;
@@ -1,5 +1,5 @@
1
- import { B as errMsg, C as defineCustomActions, G as EventCategory, K as createEvent, N as pipelineRunnerCapability, V as BaseAddon, Y as makeSourceBrokerId, b as customAction, et as _enum, it as lazy, j as nodePin, lt as string, nt as boolean, ot as number, st as object, tt as array } from "../dist-DjuGmyG9.mjs";
2
- import { t as FrameHandlePlane } from "../frame-handle-plane-Bkxz-TTD.mjs";
1
+ import { B as errMsg, C as defineCustomActions, G as EventCategory, K as createEvent, N as pipelineRunnerCapability, V as BaseAddon, Y as makeSourceBrokerId, b as customAction, et as _enum, it as lazy, j as nodePin, lt as string, nt as boolean, ot as number, st as object, tt as array } from "../dist-CfSORkv2.mjs";
2
+ import { t as FrameHandlePlane } from "../frame-handle-plane-C05rq9Zs.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { FrameRingReaderCache } from "@camstack/shm-ring";
5
5
  //#region src/pipeline-runner/bench-actions.ts
@@ -1405,6 +1405,55 @@ function toFrameInput(frame) {
1405
1405
  };
1406
1406
  }
1407
1407
  /**
1408
+ * Convert a packed RGB24 `DecodedFrame` into a packed GRAY8 one using the
1409
+ * BT.601 luma weighting (Y = 0.299R + 0.587G + 0.114B, integer-approximated as
1410
+ * 77/150/29 over 256).
1411
+ *
1412
+ * This is what lets **sensor-triggered (onboard) motion share the detection
1413
+ * decode**: the gray the motion analyzer needs is just the luma of the
1414
+ * already-decoded detection `rgb` frame, so no second decoder is spun up. Far
1415
+ * cheaper than a full second decode (a per-pixel weighted sum vs. an entire
1416
+ * H.264/H.265 decode). Persistent (analyzer-only) motion keeps its own gray
1417
+ * decode on the sub stream and never hits this path.
1418
+ */
1419
+ function rgbFrameToGrayFrame(frame) {
1420
+ const rgb = frame.data;
1421
+ const pixels = frame.width * frame.height;
1422
+ const gray = Buffer.allocUnsafe(pixels);
1423
+ for (let i = 0, j = 0; i < pixels; i++, j += 3) gray[i] = rgb[j] * 77 + rgb[j + 1] * 150 + rgb[j + 2] * 29 >> 8;
1424
+ return {
1425
+ data: gray,
1426
+ width: frame.width,
1427
+ height: frame.height,
1428
+ format: "gray",
1429
+ timestamp: frame.timestamp,
1430
+ ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {}
1431
+ };
1432
+ }
1433
+ /**
1434
+ * Decide where the motion analyzer reads frames from.
1435
+ *
1436
+ * - `sharedWithDetection` (onboard/sensor-triggered) AND detection actually
1437
+ * runs (`detectionMode !== 'disabled'`) → ride the detection decode:
1438
+ * `rgb` on the MAIN (`detectionStreamId`) stream, converted to gray. No
1439
+ * second decoder — the `FrameHandlePlane` coalesces it onto detection's session.
1440
+ * - Otherwise (persistent analyzer, or detection disabled) → a dedicated
1441
+ * `gray` decode on the SUB (`motionStreamId`) stream, exactly as before.
1442
+ *
1443
+ * Pure so the decision is unit-testable without the addon/broker.
1444
+ */
1445
+ function resolveMotionFrameSource(config, sharedWithDetection) {
1446
+ return sharedWithDetection && config.detectionMode !== "disabled" ? {
1447
+ streamId: config.detectionStreamId,
1448
+ format: "rgb",
1449
+ shared: true
1450
+ } : {
1451
+ streamId: config.motionStreamId,
1452
+ format: "gray",
1453
+ shared: false
1454
+ };
1455
+ }
1456
+ /**
1408
1457
  * Interval for periodic metric snapshot emission. ~1 Hz strikes the
1409
1458
  * balance between UI freshness (overlay phase / fps numbers feel live)
1410
1459
  * and bus traffic (one snapshot per attached camera + one per node
@@ -1907,6 +1956,12 @@ var PipelineRunnerAddon = class extends BaseAddon {
1907
1956
  }
1908
1957
  });
1909
1958
  if (this.attached.has(config.deviceId)) this.detachInternal(config.deviceId);
1959
+ const attachment = {
1960
+ config,
1961
+ motionUnsubscribe: null,
1962
+ detectionUnsubscribe: null
1963
+ };
1964
+ this.attached.set(config.deviceId, attachment);
1910
1965
  runner.registerCamera(config.deviceId, {
1911
1966
  detectionMode: config.detectionMode,
1912
1967
  fps: config.detectionFps,
@@ -1914,12 +1969,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
1914
1969
  occupancyRecheckSec: config.occupancyRecheckSec,
1915
1970
  occupancyRecheckFrames: config.occupancyRecheckFrames
1916
1971
  });
1917
- const attachment = {
1918
- config,
1919
- motionUnsubscribe: null,
1920
- detectionUnsubscribe: null
1921
- };
1922
- this.attached.set(config.deviceId, attachment);
1923
1972
  if (config.motionSources.includes("analyzer")) attachment.motionUnsubscribe = await this.subscribeMotionFrames(config);
1924
1973
  const stepsCount = config.steps?.length ?? 0;
1925
1974
  const dispatch = stepsCount > 0 ? `runPipeline(${stepsCount}step${stepsCount === 1 ? "" : "s"})` : config.steps !== void 0 ? "skip(0steps)" : "runFrame(legacy)";
@@ -2009,10 +2058,10 @@ var PipelineRunnerAddon = class extends BaseAddon {
2009
2058
  if (!shouldStartOnboardAnalyzer(config)) return;
2010
2059
  const log = this.ctx.logger.withTags({ deviceId });
2011
2060
  if (detected && !this.onboardAnalyzerSubs.has(deviceId)) {
2012
- const unsub = await this.subscribeMotionFrames(config);
2061
+ const unsub = await this.subscribeMotionFrames(config, { sharedWithDetection: true });
2013
2062
  if (unsub) {
2014
2063
  this.onboardAnalyzerSubs.set(deviceId, unsub);
2015
- log.debug("onboard-analyzer: opened motion-frame subscription");
2064
+ log.debug("onboard-analyzer: opened motion-frame subscription (shared detection decode)");
2016
2065
  }
2017
2066
  }
2018
2067
  const existing = this.onboardAnalyzerTeardownTimers.get(deviceId);
@@ -2136,7 +2185,21 @@ var PipelineRunnerAddon = class extends BaseAddon {
2136
2185
  this.remotePlanes.set(config.deviceId, plane);
2137
2186
  return plane;
2138
2187
  }
2139
- async subscribeMotionFrames(config) {
2188
+ /**
2189
+ * Subscribe the motion analyzer to decoded frames.
2190
+ *
2191
+ * Two modes:
2192
+ * - **Persistent (default)** — a dedicated `gray` decode of the camera's
2193
+ * `motionStreamId` (the sub stream). Cheap, always-on; used when the SW
2194
+ * analyzer is the motion source (`motionSources` includes `'analyzer'`).
2195
+ * - **Shared with detection** (`opts.sharedWithDetection`) — subscribes
2196
+ * `rgb` on `detectionStreamId` (the MAIN stream) so it rides the SAME
2197
+ * decoder session detection already uses (the `FrameHandlePlane` coalesces
2198
+ * same-format subscribers on a stream into ONE decode), then derives the
2199
+ * analyzer's `gray` from that rgb frame's luma. No second decoder. Used for
2200
+ * onboard/sensor-triggered motion, where a decode is already active.
2201
+ */
2202
+ async subscribeMotionFrames(config, opts) {
2140
2203
  const ctx = this.ctx;
2141
2204
  const runner = this.runner;
2142
2205
  if (!ctx || !runner) return null;
@@ -2146,18 +2209,23 @@ var PipelineRunnerAddon = class extends BaseAddon {
2146
2209
  log.warn("subscribeMotionFrames: this.ctx.api not available");
2147
2210
  return null;
2148
2211
  }
2212
+ const src = resolveMotionFrameSource(config, opts?.sharedWithDetection === true);
2213
+ const shared = src.shared;
2214
+ const streamId = src.streamId;
2149
2215
  const remote = this.remoteSourceFor(config);
2150
2216
  return startFrameHandlePoller({
2151
2217
  api,
2152
- brokerId: makeSourceBrokerId(config.deviceId, config.motionStreamId),
2153
- format: "gray",
2218
+ brokerId: makeSourceBrokerId(config.deviceId, streamId),
2219
+ format: src.format,
2154
2220
  maxFps: config.motionFps,
2155
- tag: "motion",
2221
+ tag: shared ? "motion-shared" : "motion",
2156
2222
  logger: log,
2157
2223
  ownNodeId: this.nodeId,
2158
2224
  getRemoteFrame: (handle) => this.getRemoteFrame(handle),
2159
- ...remote ? { source: remote.makeSource(config.motionStreamId) } : {},
2160
- onFrame: (frame, handle) => {
2225
+ ...remote ? { source: remote.makeSource(streamId) } : {},
2226
+ onFrame: shared ? (frame) => {
2227
+ runner.enqueueMotionFrame(config.deviceId, rgbFrameToGrayFrame(frame), void 0);
2228
+ } : (frame, handle) => {
2161
2229
  runner.enqueueMotionFrame(config.deviceId, frame, handle);
2162
2230
  }
2163
2231
  });
@@ -2578,4 +2646,4 @@ var PipelineRunnerAddon = class extends BaseAddon {
2578
2646
  }
2579
2647
  };
2580
2648
  //#endregion
2581
- export { FrameQueue, PipelineRunner, PipelineTimingSampler, Semaphore, pipelineRunnerBenchActions as customActions, PipelineRunnerAddon as default, shouldStartOnboardAnalyzer };
2649
+ export { FrameQueue, PipelineRunner, PipelineTimingSampler, Semaphore, pipelineRunnerBenchActions as customActions, PipelineRunnerAddon as default, resolveMotionFrameSource, rgbFrameToGrayFrame, shouldStartOnboardAnalyzer };
@@ -1,5 +1,5 @@
1
1
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
2
- const require_dist = require("../dist-Cwc0TUQr.js");
2
+ const require_dist = require("../dist-BrIE85BG.js");
3
3
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
4
4
  let node_fs = require("node:fs");
5
5
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { B as errMsg, G as EventCategory, I as storageEvictableCapability, P as recordingCapability, Q as selectAssignedProfileSlots, V as BaseAddon, W as DeviceType, c as EVENT_PAD_MS, ct as record, f as RecordingConfigSchema, k as migrateConfigToBands, lt as string, q as hydrateSchema } from "../dist-DjuGmyG9.mjs";
1
+ import { B as errMsg, G as EventCategory, I as storageEvictableCapability, P as recordingCapability, Q as selectAssignedProfileSlots, V as BaseAddon, W as DeviceType, c as EVENT_PAD_MS, ct as record, f as RecordingConfigSchema, k as migrateConfigToBands, lt as string, q as hydrateSchema } from "../dist-CfSORkv2.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
4
  import { promises } from "node:fs";