@camstack/addon-pipeline 1.1.30 → 1.1.32

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.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_dist = require("../dist-Cwc0TUQr.js");
6
- const require_frame_ring_sink = require("../frame-ring-sink-8LLV-cvH.js");
6
+ const require_frame_ring_sink = require("../frame-ring-sink-DvdhCFr3.js");
7
7
  const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
8
8
  const require_frame_dropper = require("../frame-dropper-DKLM6pMz.js");
9
9
  let _camstack_shm_ring = require("@camstack/shm-ring");
@@ -706,6 +706,9 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
706
706
  }] });
707
707
  }
708
708
  async onInitialize() {
709
+ if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
710
+ this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
711
+ });
709
712
  const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
710
713
  if (backend !== "ffmpeg") {
711
714
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
@@ -719,9 +722,6 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
719
722
  filters: [...this.probedGpuScaleFilters],
720
723
  ffmpeg: this.ffmpegPath
721
724
  } });
722
- if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
723
- this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
724
- });
725
725
  return [{
726
726
  capability: require_dist.decoderCapability,
727
727
  provider: this
@@ -1,5 +1,5 @@
1
1
  import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-DjuGmyG9.mjs";
2
- import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, o as DEFAULT_DECODER_BACKEND, t as DecoderFrameRingSink } from "../frame-ring-sink-ClEWjiRU.mjs";
2
+ import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, s as DEFAULT_DECODER_BACKEND, t as DecoderFrameRingSink } from "../frame-ring-sink-sY-8XUsH.mjs";
3
3
  import { a as codecToInputFormat, c as maxDecodeWidth, d as pickDecodeHwAccel, f as rankDecodeHwAccels, i as channelsForPixel, l as parseAvailableGpuScaleFilters, m as resolveGpuScaleFilter, n as DEFAULT_VAAPI_RENDER_DEVICE, o as computeOutputGeometry, p as rawPixelForFormat, r as buildFfmpegDecodeArgs, s as isSoftwareHwAccel, t as DECODE_HWACCEL_RANK, u as parseFfmpegOutputDims } from "../ffmpeg-args-C5GPp8Cw.mjs";
4
4
  import { t as FrameDropper } from "../frame-dropper-AjheBGMG.mjs";
5
5
  import { FrameRingReaderCache } from "@camstack/shm-ring";
@@ -702,6 +702,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
702
702
  }] });
703
703
  }
704
704
  async onInitialize() {
705
+ if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
706
+ this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
707
+ });
705
708
  const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
706
709
  if (backend !== "ffmpeg") {
707
710
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
@@ -715,9 +718,6 @@ var DecoderFfmpegAddon = class extends BaseAddon {
715
718
  filters: [...this.probedGpuScaleFilters],
716
719
  ffmpeg: this.ffmpegPath
717
720
  } });
718
- if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
719
- this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
720
- });
721
721
  return [{
722
722
  capability: decoderCapability,
723
723
  provider: this
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_dist = require("../dist-Cwc0TUQr.js");
6
- const require_frame_ring_sink = require("../frame-ring-sink-8LLV-cvH.js");
6
+ const require_frame_ring_sink = require("../frame-ring-sink-DvdhCFr3.js");
7
7
  const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
8
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
9
9
  let node_crypto = require("node:crypto");
@@ -955,14 +955,16 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
955
955
  }];
956
956
  }
957
957
  /**
958
- * Resolve the effective hwaccel backend for a new session. Reads
959
- * this addon's own `hwaccel` setting first. `'auto'` defers to the
960
- * session's local resolver (`ctx.kernel.hwaccel`) which probes the
961
- * host and picks. No more orchestrator round-trip decoder addon
962
- * is self-sufficient for this setting as of phase 2d.
958
+ * Resolve the effective hwaccel backend for a new session. Reads the SHARED
959
+ * per-node override from the `decoder-ffmpeg` OWNER the single value the
960
+ * Pipeline UI edits — exactly as `resolveDecoderBackend` reads the owner's
961
+ * `backend`. This makes a manual hwaccel override take effect even when
962
+ * node-av is the active backend (its OWN `hwaccel@node` is never surfaced by
963
+ * the UI). `'auto'` defers to the session's local resolver
964
+ * (`ctx.kernel.hwaccel`). Fails safe to `'auto'`.
963
965
  */
964
966
  resolveHwAccelPref() {
965
- return this.config.hwaccel;
967
+ return require_frame_ring_sink.resolveDecoderHwAccel(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
966
968
  }
967
969
  /**
968
970
  * Re-run the platform probe on this host and persist the detected
@@ -1021,7 +1023,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1021
1023
  }
1022
1024
  async createSession(config) {
1023
1025
  const sessionId = (0, node_crypto.randomUUID)();
1024
- const hwaccel = this.resolveHwAccelPref();
1026
+ const hwaccel = await this.resolveHwAccelPref();
1025
1027
  const { frameSink } = config;
1026
1028
  const nodeId = this.resolveLocalNodeId();
1027
1029
  const session = new NodeAvDecoderSession(config, this.ctx.logger, {
@@ -1,5 +1,5 @@
1
1
  import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-DjuGmyG9.mjs";
2
- import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-ClEWjiRU.mjs";
2
+ import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, o as resolveDecoderHwAccel, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-sY-8XUsH.mjs";
3
3
  import { f as rankDecodeHwAccels } from "../ffmpeg-args-C5GPp8Cw.mjs";
4
4
  import { FrameRingReaderCache } from "@camstack/shm-ring";
5
5
  import { randomUUID } from "node:crypto";
@@ -951,14 +951,16 @@ var DecoderNodeAvAddon = class extends BaseAddon {
951
951
  }];
952
952
  }
953
953
  /**
954
- * Resolve the effective hwaccel backend for a new session. Reads
955
- * this addon's own `hwaccel` setting first. `'auto'` defers to the
956
- * session's local resolver (`ctx.kernel.hwaccel`) which probes the
957
- * host and picks. No more orchestrator round-trip decoder addon
958
- * is self-sufficient for this setting as of phase 2d.
954
+ * Resolve the effective hwaccel backend for a new session. Reads the SHARED
955
+ * per-node override from the `decoder-ffmpeg` OWNER the single value the
956
+ * Pipeline UI edits — exactly as `resolveDecoderBackend` reads the owner's
957
+ * `backend`. This makes a manual hwaccel override take effect even when
958
+ * node-av is the active backend (its OWN `hwaccel@node` is never surfaced by
959
+ * the UI). `'auto'` defers to the session's local resolver
960
+ * (`ctx.kernel.hwaccel`). Fails safe to `'auto'`.
959
961
  */
960
962
  resolveHwAccelPref() {
961
- return this.config.hwaccel;
963
+ return resolveDecoderHwAccel(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
962
964
  }
963
965
  /**
964
966
  * Re-run the platform probe on this host and persist the detected
@@ -1017,7 +1019,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1017
1019
  }
1018
1020
  async createSession(config) {
1019
1021
  const sessionId = randomUUID();
1020
- const hwaccel = this.resolveHwAccelPref();
1022
+ const hwaccel = await this.resolveHwAccelPref();
1021
1023
  const { frameSink } = config;
1022
1024
  const nodeId = this.resolveLocalNodeId();
1023
1025
  const session = new NodeAvDecoderSession(config, this.ctx.logger, {
@@ -185,6 +185,35 @@ function pickBestRuntime(env, bestBackendHint) {
185
185
  device: defaultDeviceFor(chosen)
186
186
  };
187
187
  }
188
+ /**
189
+ * The ordered device candidates a runtime will try for an `'auto'`/`'all'`
190
+ * device pick — mirrors what the python inference pool actually resolves
191
+ * (openvino `_resolve_ov_devices`: NPU>GPU>CPU; coreml compute-units `all`:
192
+ * ANE>GPU>CPU). Surfaced in `probedBestEngine` so the UI shows the real
193
+ * accelerator order (`openvino/auto [NPU, GPU, CPU]`) instead of a bare `auto`
194
+ * tag. Returns `[]` for runtimes with a single fixed device (onnx → CPU) — the
195
+ * caller then keeps the plain `backend/device` label.
196
+ */
197
+ function engineDeviceSequence(runtimeId, env) {
198
+ const hw = env.hardware;
199
+ switch (runtimeId) {
200
+ case "openvino": {
201
+ const seq = [];
202
+ if (hw?.npu?.type === "intel-npu") seq.push("NPU");
203
+ if (hw?.gpu?.type === "intel") seq.push("GPU");
204
+ seq.push("CPU");
205
+ return seq;
206
+ }
207
+ case "coreml": {
208
+ const seq = [];
209
+ if (hw?.npu?.type === "apple-ane") seq.push("ANE");
210
+ if (hw?.gpu != null) seq.push("GPU");
211
+ seq.push("CPU");
212
+ return seq;
213
+ }
214
+ default: return [];
215
+ }
216
+ }
188
217
  //#endregion
189
218
  //#region src/detection-pipeline/engine-store-keys.ts
190
219
  /**
@@ -6264,6 +6293,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6264
6293
  const storedRuntime = store["engineRuntime"];
6265
6294
  const node = this.localProbeNodeId();
6266
6295
  const storedBackend = readNodeEngineValue(store, "engineBackend", node);
6296
+ if (storedBackend === "auto") return null;
6267
6297
  if (typeof storedBackend === "string" && storedBackend.length > 0 && storedBackend !== "auto") {
6268
6298
  const backend = storedBackend;
6269
6299
  const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
@@ -6422,14 +6452,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6422
6452
  try {
6423
6453
  hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
6424
6454
  } catch {}
6425
- const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
6455
+ const env = runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null);
6456
+ const pick = pickBestRuntime(env, null);
6426
6457
  const best = {
6427
6458
  runtime: "python",
6428
6459
  backend: pick.runtimeId,
6429
6460
  format: modelFormatFor(pick.runtimeId),
6430
6461
  device: pick.device
6431
6462
  };
6432
- const probedLabel = `${best.backend}/${best.device ?? "default"}`;
6463
+ const deviceSeq = engineDeviceSequence(pick.runtimeId, env);
6464
+ const probedLabel = deviceSeq.length > 1 ? `${best.backend}/${best.device ?? "default"} [${deviceSeq.join(", ")}]` : `${best.backend}/${best.device ?? "default"}`;
6433
6465
  const rpNode = this.localProbeNodeId();
6434
6466
  if (pick.runtimeId !== "onnx" || hardware !== null) {
6435
6467
  await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
@@ -178,6 +178,35 @@ function pickBestRuntime(env, bestBackendHint) {
178
178
  device: defaultDeviceFor(chosen)
179
179
  };
180
180
  }
181
+ /**
182
+ * The ordered device candidates a runtime will try for an `'auto'`/`'all'`
183
+ * device pick — mirrors what the python inference pool actually resolves
184
+ * (openvino `_resolve_ov_devices`: NPU>GPU>CPU; coreml compute-units `all`:
185
+ * ANE>GPU>CPU). Surfaced in `probedBestEngine` so the UI shows the real
186
+ * accelerator order (`openvino/auto [NPU, GPU, CPU]`) instead of a bare `auto`
187
+ * tag. Returns `[]` for runtimes with a single fixed device (onnx → CPU) — the
188
+ * caller then keeps the plain `backend/device` label.
189
+ */
190
+ function engineDeviceSequence(runtimeId, env) {
191
+ const hw = env.hardware;
192
+ switch (runtimeId) {
193
+ case "openvino": {
194
+ const seq = [];
195
+ if (hw?.npu?.type === "intel-npu") seq.push("NPU");
196
+ if (hw?.gpu?.type === "intel") seq.push("GPU");
197
+ seq.push("CPU");
198
+ return seq;
199
+ }
200
+ case "coreml": {
201
+ const seq = [];
202
+ if (hw?.npu?.type === "apple-ane") seq.push("ANE");
203
+ if (hw?.gpu != null) seq.push("GPU");
204
+ seq.push("CPU");
205
+ return seq;
206
+ }
207
+ default: return [];
208
+ }
209
+ }
181
210
  //#endregion
182
211
  //#region src/detection-pipeline/engine-store-keys.ts
183
212
  /**
@@ -6257,6 +6286,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6257
6286
  const storedRuntime = store["engineRuntime"];
6258
6287
  const node = this.localProbeNodeId();
6259
6288
  const storedBackend = readNodeEngineValue(store, "engineBackend", node);
6289
+ if (storedBackend === "auto") return null;
6260
6290
  if (typeof storedBackend === "string" && storedBackend.length > 0 && storedBackend !== "auto") {
6261
6291
  const backend = storedBackend;
6262
6292
  const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
@@ -6415,14 +6445,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6415
6445
  try {
6416
6446
  hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
6417
6447
  } catch {}
6418
- const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
6448
+ const env = runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null);
6449
+ const pick = pickBestRuntime(env, null);
6419
6450
  const best = {
6420
6451
  runtime: "python",
6421
6452
  backend: pick.runtimeId,
6422
6453
  format: modelFormatFor(pick.runtimeId),
6423
6454
  device: pick.device
6424
6455
  };
6425
- const probedLabel = `${best.backend}/${best.device ?? "default"}`;
6456
+ const deviceSeq = engineDeviceSequence(pick.runtimeId, env);
6457
+ const probedLabel = deviceSeq.length > 1 ? `${best.backend}/${best.device ?? "default"} [${deviceSeq.join(", ")}]` : `${best.backend}/${best.device ?? "default"}`;
6426
6458
  const rpNode = this.localProbeNodeId();
6427
6459
  if (pick.runtimeId !== "onnx" || hardware !== null) {
6428
6460
  await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
@@ -18,8 +18,75 @@ function normalizeDecoderNodeId(rawNodeId) {
18
18
  }
19
19
  //#endregion
20
20
  //#region src/shared/decoder-backend.ts
21
+ /**
22
+ * Decoder backend selection — decides which decoder addon registers the
23
+ * `decoder` cap on a node.
24
+ *
25
+ * Two decoder addons ship in this bundle: `decoder-ffmpeg` (subprocess
26
+ * decode; a decode crash is isolated to the child) and `decoder-nodeav`
27
+ * (in-process decode via FFmpeg native bindings; no subprocess lifecycle).
28
+ * Both are installed on every node, but only ONE may provide the `decoder`
29
+ * cap at a time.
30
+ *
31
+ * ## Why selection happens at REGISTRATION time (race-free by construction)
32
+ *
33
+ * The `decoder` cap is a singleton. Historically both decoder addons
34
+ * registered a provider and the active slot was resolved AFTER the fact —
35
+ * which exposed a boot-order race: whichever addon's runner initialised
36
+ * first briefly held the active slot, and any consumer that resolved the
37
+ * cap inside that window was dispatched to the wrong backend (the
38
+ * cap-declared `preferredProvider` override only corrects the slot when
39
+ * the preferred addon's registration eventually lands).
40
+ *
41
+ * This module removes the race by construction instead of arbitrating it:
42
+ * each decoder addon calls {@link resolveDecoderBackend} at the START of
43
+ * its `onInitialize` and registers its provider ONLY when it is the
44
+ * selected backend — the other addon returns no registrations at all. At
45
+ * most one `decoder` provider ever exists per node, so no resolution layer
46
+ * (kernel `CapabilityRegistry`, route resolver, UDS child preference) can
47
+ * pick a wrong one, regardless of boot order or timing.
48
+ *
49
+ * ## The setting
50
+ *
51
+ * The per-node selection lives in the `decoder-ffmpeg` OWNER addon's global
52
+ * settings — the standard `addon-settings` surface, visible in the admin UI
53
+ * as the owner's `backend` select field. The store is hub-central and the
54
+ * value is persisted node-scoped as `backend@<nodeId>`
55
+ * (`decoder-backend-keys.ts`); the owner's `getGlobalSettings` projects the
56
+ * requested node's value onto the bare `backend` field.
57
+ *
58
+ * Both decoder addons read it the SAME way, placement-agnostically: via
59
+ * `ctx.api.addonSettings.getGlobalSettings({ addonId: 'decoder-ffmpeg',
60
+ * nodeId })`. The `addon-settings` cap is hub-routed at the system level
61
+ * (`AddonCallGateway.classify` sends settings calls to the addon's base
62
+ * node — the hub instance answers for every node), so an agent addon
63
+ * transparently reads the hub-central store with no placement branching.
64
+ * NOT used: `ctx.settings.getSection` (node-local — empty on an agent, the
65
+ * original bug), the raw settings-store cap, or any custom side channel.
66
+ *
67
+ * Resolution: the node's scoped value → the built-in default
68
+ * {@link DEFAULT_DECODER_BACKEND} (`'ffmpeg'`). There is deliberately NO
69
+ * bare-key fallback (see `decoder-backend-keys.ts`), and any read failure
70
+ * resolves to the default so the safe backend still comes up.
71
+ *
72
+ * ## Switching backends
73
+ *
74
+ * Because registration is decided once at addon init, changing the setting
75
+ * takes effect on the next restart of the two decoder addons on the
76
+ * affected node (addon restart / redeploy / node reboot). This is the
77
+ * price of race-freedom: there is deliberately NO live re-arbitration
78
+ * path — a live hand-off would reintroduce a window with two providers.
79
+ */
21
80
  /** The addon whose global settings OWN the per-node `backend` field. */
22
81
  var DECODER_OWNER_ADDON_ID = "decoder-ffmpeg";
82
+ /** Fail-safe when the owner's hwaccel can't be read: defer to the local probe. */
83
+ var DEFAULT_DECODER_HWACCEL = "auto";
84
+ /** Narrow an arbitrary stored value to a known {@link HwAccelChoice}, else null. */
85
+ function parseDecoderHwAccel(raw) {
86
+ if (typeof raw !== "string") return null;
87
+ const match = require_dist.HWACCEL_OPTIONS.find((o) => o.value === raw);
88
+ return match ? match.value : null;
89
+ }
23
90
  function isHydratedField(entry) {
24
91
  return typeof entry === "object" && entry !== null && "key" in entry;
25
92
  }
@@ -72,6 +139,37 @@ async function resolveDecoderBackend(api, nodeId, logger) {
72
139
  return DEFAULT_DECODER_BACKEND;
73
140
  }
74
141
  }
142
+ /**
143
+ * Pure selection of the owner's `hwaccel` field from an already-read hydrated
144
+ * settings payload. Missing/invalid/null → {@link DEFAULT_DECODER_HWACCEL}.
145
+ */
146
+ function pickDecoderHwAccelFromSettings(view) {
147
+ if (view === null) return DEFAULT_DECODER_HWACCEL;
148
+ for (const section of view.sections) for (const entry of section.fields) {
149
+ if (!isHydratedField(entry) || entry.key !== "hwaccel") continue;
150
+ return parseDecoderHwAccel(entry.value) ?? "auto";
151
+ }
152
+ return DEFAULT_DECODER_HWACCEL;
153
+ }
154
+ /**
155
+ * Resolve the effective decoder hwaccel override for a node by reading the
156
+ * `decoder-ffmpeg` OWNER's `hwaccel@<node>` setting — the exact same
157
+ * hub-routed, placement-agnostic read as {@link resolveDecoderBackend}, so
158
+ * node-av and ffmpeg honour ONE per-node value (the one the UI edits). Fails
159
+ * safe to {@link DEFAULT_DECODER_HWACCEL} (`'auto'` → the session's local probe).
160
+ */
161
+ async function resolveDecoderHwAccel(api, nodeId, logger) {
162
+ if (!api) return DEFAULT_DECODER_HWACCEL;
163
+ try {
164
+ return pickDecoderHwAccelFromSettings(await api.addonSettings.getGlobalSettings.query({
165
+ addonId: DECODER_OWNER_ADDON_ID,
166
+ nodeId: normalizeDecoderNodeId(nodeId)
167
+ }));
168
+ } catch (err) {
169
+ logger.warn("decoder-hwaccel: owner settings read failed — deferring to local probe", { meta: { error: err instanceof Error ? err.message : String(err) } });
170
+ return DEFAULT_DECODER_HWACCEL;
171
+ }
172
+ }
75
173
  //#endregion
76
174
  //#region src/shared/notifying-ring-buffer.ts
77
175
  /**
@@ -442,3 +540,9 @@ Object.defineProperty(exports, "resolveDecoderBackend", {
442
540
  return resolveDecoderBackend;
443
541
  }
444
542
  });
543
+ Object.defineProperty(exports, "resolveDecoderHwAccel", {
544
+ enumerable: true,
545
+ get: function() {
546
+ return resolveDecoderHwAccel;
547
+ }
548
+ });
@@ -1,4 +1,4 @@
1
- import { p as RingBuffer } from "./dist-DjuGmyG9.mjs";
1
+ import { d as HWACCEL_OPTIONS, p as RingBuffer } from "./dist-DjuGmyG9.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
4
  /** Built-in default when a node has no selection — the subprocess decoder. */
@@ -18,8 +18,75 @@ function normalizeDecoderNodeId(rawNodeId) {
18
18
  }
19
19
  //#endregion
20
20
  //#region src/shared/decoder-backend.ts
21
+ /**
22
+ * Decoder backend selection — decides which decoder addon registers the
23
+ * `decoder` cap on a node.
24
+ *
25
+ * Two decoder addons ship in this bundle: `decoder-ffmpeg` (subprocess
26
+ * decode; a decode crash is isolated to the child) and `decoder-nodeav`
27
+ * (in-process decode via FFmpeg native bindings; no subprocess lifecycle).
28
+ * Both are installed on every node, but only ONE may provide the `decoder`
29
+ * cap at a time.
30
+ *
31
+ * ## Why selection happens at REGISTRATION time (race-free by construction)
32
+ *
33
+ * The `decoder` cap is a singleton. Historically both decoder addons
34
+ * registered a provider and the active slot was resolved AFTER the fact —
35
+ * which exposed a boot-order race: whichever addon's runner initialised
36
+ * first briefly held the active slot, and any consumer that resolved the
37
+ * cap inside that window was dispatched to the wrong backend (the
38
+ * cap-declared `preferredProvider` override only corrects the slot when
39
+ * the preferred addon's registration eventually lands).
40
+ *
41
+ * This module removes the race by construction instead of arbitrating it:
42
+ * each decoder addon calls {@link resolveDecoderBackend} at the START of
43
+ * its `onInitialize` and registers its provider ONLY when it is the
44
+ * selected backend — the other addon returns no registrations at all. At
45
+ * most one `decoder` provider ever exists per node, so no resolution layer
46
+ * (kernel `CapabilityRegistry`, route resolver, UDS child preference) can
47
+ * pick a wrong one, regardless of boot order or timing.
48
+ *
49
+ * ## The setting
50
+ *
51
+ * The per-node selection lives in the `decoder-ffmpeg` OWNER addon's global
52
+ * settings — the standard `addon-settings` surface, visible in the admin UI
53
+ * as the owner's `backend` select field. The store is hub-central and the
54
+ * value is persisted node-scoped as `backend@<nodeId>`
55
+ * (`decoder-backend-keys.ts`); the owner's `getGlobalSettings` projects the
56
+ * requested node's value onto the bare `backend` field.
57
+ *
58
+ * Both decoder addons read it the SAME way, placement-agnostically: via
59
+ * `ctx.api.addonSettings.getGlobalSettings({ addonId: 'decoder-ffmpeg',
60
+ * nodeId })`. The `addon-settings` cap is hub-routed at the system level
61
+ * (`AddonCallGateway.classify` sends settings calls to the addon's base
62
+ * node — the hub instance answers for every node), so an agent addon
63
+ * transparently reads the hub-central store with no placement branching.
64
+ * NOT used: `ctx.settings.getSection` (node-local — empty on an agent, the
65
+ * original bug), the raw settings-store cap, or any custom side channel.
66
+ *
67
+ * Resolution: the node's scoped value → the built-in default
68
+ * {@link DEFAULT_DECODER_BACKEND} (`'ffmpeg'`). There is deliberately NO
69
+ * bare-key fallback (see `decoder-backend-keys.ts`), and any read failure
70
+ * resolves to the default so the safe backend still comes up.
71
+ *
72
+ * ## Switching backends
73
+ *
74
+ * Because registration is decided once at addon init, changing the setting
75
+ * takes effect on the next restart of the two decoder addons on the
76
+ * affected node (addon restart / redeploy / node reboot). This is the
77
+ * price of race-freedom: there is deliberately NO live re-arbitration
78
+ * path — a live hand-off would reintroduce a window with two providers.
79
+ */
21
80
  /** The addon whose global settings OWN the per-node `backend` field. */
22
81
  var DECODER_OWNER_ADDON_ID = "decoder-ffmpeg";
82
+ /** Fail-safe when the owner's hwaccel can't be read: defer to the local probe. */
83
+ var DEFAULT_DECODER_HWACCEL = "auto";
84
+ /** Narrow an arbitrary stored value to a known {@link HwAccelChoice}, else null. */
85
+ function parseDecoderHwAccel(raw) {
86
+ if (typeof raw !== "string") return null;
87
+ const match = HWACCEL_OPTIONS.find((o) => o.value === raw);
88
+ return match ? match.value : null;
89
+ }
23
90
  function isHydratedField(entry) {
24
91
  return typeof entry === "object" && entry !== null && "key" in entry;
25
92
  }
@@ -72,6 +139,37 @@ async function resolveDecoderBackend(api, nodeId, logger) {
72
139
  return DEFAULT_DECODER_BACKEND;
73
140
  }
74
141
  }
142
+ /**
143
+ * Pure selection of the owner's `hwaccel` field from an already-read hydrated
144
+ * settings payload. Missing/invalid/null → {@link DEFAULT_DECODER_HWACCEL}.
145
+ */
146
+ function pickDecoderHwAccelFromSettings(view) {
147
+ if (view === null) return DEFAULT_DECODER_HWACCEL;
148
+ for (const section of view.sections) for (const entry of section.fields) {
149
+ if (!isHydratedField(entry) || entry.key !== "hwaccel") continue;
150
+ return parseDecoderHwAccel(entry.value) ?? "auto";
151
+ }
152
+ return DEFAULT_DECODER_HWACCEL;
153
+ }
154
+ /**
155
+ * Resolve the effective decoder hwaccel override for a node by reading the
156
+ * `decoder-ffmpeg` OWNER's `hwaccel@<node>` setting — the exact same
157
+ * hub-routed, placement-agnostic read as {@link resolveDecoderBackend}, so
158
+ * node-av and ffmpeg honour ONE per-node value (the one the UI edits). Fails
159
+ * safe to {@link DEFAULT_DECODER_HWACCEL} (`'auto'` → the session's local probe).
160
+ */
161
+ async function resolveDecoderHwAccel(api, nodeId, logger) {
162
+ if (!api) return DEFAULT_DECODER_HWACCEL;
163
+ try {
164
+ return pickDecoderHwAccelFromSettings(await api.addonSettings.getGlobalSettings.query({
165
+ addonId: DECODER_OWNER_ADDON_ID,
166
+ nodeId: normalizeDecoderNodeId(nodeId)
167
+ }));
168
+ } catch (err) {
169
+ logger.warn("decoder-hwaccel: owner settings read failed — deferring to local probe", { meta: { error: err instanceof Error ? err.message : String(err) } });
170
+ return DEFAULT_DECODER_HWACCEL;
171
+ }
172
+ }
75
173
  //#endregion
76
174
  //#region src/shared/notifying-ring-buffer.ts
77
175
  /**
@@ -406,4 +504,4 @@ var DecoderFrameRingSink = class {
406
504
  }
407
505
  };
408
506
  //#endregion
409
- export { resolveDecoderBackend as a, NotifyingRingBuffer as i, RING_BUDGET_MB as n, DEFAULT_DECODER_BACKEND as o, makeSegmentName as r, DecoderFrameRingSink as t };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.1.30",
3
+ "version": "1.1.32",
4
4
  "description": "CamStack Pipeline bundle — runner, detection, motion, decoders, audio + stream broker. Multi-entry npm package shipping 7 addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",