@camstack/addon-pipeline 1.1.27 → 1.1.28

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 (43) hide show
  1. package/dist/audio-analyzer/index.js +7 -6
  2. package/dist/audio-analyzer/index.mjs +7 -6
  3. package/dist/audio-codec-ffmpeg/index.js +1 -1
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -1
  5. package/dist/decoder-ffmpeg/index.js +43 -11
  6. package/dist/decoder-ffmpeg/index.mjs +43 -11
  7. package/dist/decoder-nodeav/index.js +13 -8
  8. package/dist/decoder-nodeav/index.mjs +13 -8
  9. package/dist/detection-pipeline/index.js +157 -219
  10. package/dist/detection-pipeline/index.mjs +157 -219
  11. package/dist/{dist-DAIlCdAx.js → dist-Biq62zt4.js} +372 -35
  12. package/dist/{dist-CgEP_0OL.mjs → dist-C6_wgXqF.mjs} +373 -24
  13. package/dist/{frame-handle-plane-Dq20KtKL.mjs → frame-handle-plane-BIoY6nRV.mjs} +13 -5
  14. package/dist/{frame-handle-plane-DtTRX_0n.js → frame-handle-plane-D6BzyEgy.js} +13 -5
  15. package/dist/{frame-ring-sink-BbzrqTWj.js → frame-ring-sink-9J0wCdLF.js} +128 -23
  16. package/dist/{frame-ring-sink-B_NvPTJZ.mjs → frame-ring-sink-Cs9vby6v.mjs} +117 -24
  17. package/dist/motion-wasm/index.js +55 -3
  18. package/dist/motion-wasm/index.mjs +55 -3
  19. package/dist/node-topology-platform-BkR_k6WT.mjs +15 -0
  20. package/dist/node-topology-platform-CFZ7F4xW.js +20 -0
  21. package/dist/pipeline-runner/index.js +50 -16
  22. package/dist/pipeline-runner/index.mjs +50 -16
  23. package/dist/recorder/index.js +36 -40
  24. package/dist/recorder/index.mjs +36 -40
  25. package/dist/stream-broker/_stub.js +2 -2
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DoReAb4y.mjs} +3 -3
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.mjs +26 -0
  28. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BJK0-svt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs} +1 -1
  29. package/dist/stream-broker/{hostInit-DyLqyJaS.mjs → hostInit-C0SuwQhL.mjs} +3 -3
  30. package/dist/stream-broker/index.js +61 -25
  31. package/dist/stream-broker/index.mjs +61 -25
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js → MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js} +1 -1
  34. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CoLjNiUN.js → MotionZonesSettings-NcxxQN8r-BSCcPdPf.js} +1 -1
  35. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js → PrivacyMaskSettings-APgPLF7p-ztxvxSjS.js} +1 -1
  36. package/embed-dist/assets/index-C5UpuPr8.css +2 -0
  37. package/embed-dist/assets/{index-C-pL8ETk.js → index-CeZV1B-2.js} +10 -10
  38. package/embed-dist/index.html +2 -2
  39. package/package.json +1 -1
  40. package/python/inference_pool.py +93 -102
  41. package/python/test_inference_pool_device_selection.py +43 -38
  42. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.mjs +0 -26
  43. package/embed-dist/assets/index-DrJ0ee3f.css +0 -2
@@ -3,7 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
- const require_dist = require("../dist-DAIlCdAx.js");
6
+ const require_dist = require("../dist-Biq62zt4.js");
7
+ const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
7
8
  let node_fs = require("node:fs");
8
9
  node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
9
10
  let node_path = require("node:path");
@@ -672,6 +673,7 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
672
673
  description: "Auto-detected best audio backend on this host. Click the refresh icon to re-run the probe.",
673
674
  readonlyField: true,
674
675
  default: "",
676
+ perNode: true,
675
677
  actions: [{
676
678
  action: "reprobe-audio-engine",
677
679
  icon: "refresh-cw",
@@ -720,8 +722,7 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
720
722
  * semantics detection-pipeline relies on.
721
723
  */
722
724
  async getGlobalSettings(overlay, _cap, nodeId) {
723
- const ctx = this.ctxIfReady;
724
- const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
725
+ const stored = await this.resolveGlobalStore(nodeId);
725
726
  const merged = overlay ? {
726
727
  ...stored,
727
728
  ...overlay
@@ -777,8 +778,8 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
777
778
  const localNode = this.ctxIfReady?.kernel?.localNodeId;
778
779
  if (!nodeId || nodeId === localNode) return process.platform;
779
780
  try {
780
- const entry = (await this.ctxIfReady?.api?.nodes?.topology.query())?.find((n) => n.id === nodeId);
781
- if (entry?.platform) return entry.platform;
781
+ const { platform } = require_node_topology_platform.pickNodePlatformArch(await this.ctxIfReady?.api?.nodes?.topology.query(), nodeId);
782
+ if (platform) return platform;
782
783
  } catch {}
783
784
  return process.platform;
784
785
  }
@@ -789,7 +790,7 @@ var AudioAnalyzerAddon = class extends require_dist.BaseAddon {
789
790
  */
790
791
  async reprobeAudioEngine() {
791
792
  const backend = process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
792
- await this.ctx.settings?.writeAddonStore({ probedBestAudioBackend: backend });
793
+ await this.updateGlobalSettings({ probedBestAudioBackend: backend });
793
794
  this.ctx.logger.info("reprobeAudioEngine: wrote probedBestAudioBackend", { meta: { backend } });
794
795
  return { backend };
795
796
  }
@@ -1,5 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as errMsg, D as mapAudioLabelToMacro, V as BaseAddon, Y as hydrateSchema, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-CgEP_0OL.mjs";
2
+ import { B as errMsg, D as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, q as hydrateSchema, u as HF_BASE_URL } from "../dist-C6_wgXqF.mjs";
3
+ import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
3
4
  import { r as downloadFile } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
5
  import * as fs from "node:fs";
5
6
  import * as path$1 from "node:path";
@@ -667,6 +668,7 @@ var AudioAnalyzerAddon = class extends BaseAddon {
667
668
  description: "Auto-detected best audio backend on this host. Click the refresh icon to re-run the probe.",
668
669
  readonlyField: true,
669
670
  default: "",
671
+ perNode: true,
670
672
  actions: [{
671
673
  action: "reprobe-audio-engine",
672
674
  icon: "refresh-cw",
@@ -715,8 +717,7 @@ var AudioAnalyzerAddon = class extends BaseAddon {
715
717
  * semantics detection-pipeline relies on.
716
718
  */
717
719
  async getGlobalSettings(overlay, _cap, nodeId) {
718
- const ctx = this.ctxIfReady;
719
- const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
720
+ const stored = await this.resolveGlobalStore(nodeId);
720
721
  const merged = overlay ? {
721
722
  ...stored,
722
723
  ...overlay
@@ -772,8 +773,8 @@ var AudioAnalyzerAddon = class extends BaseAddon {
772
773
  const localNode = this.ctxIfReady?.kernel?.localNodeId;
773
774
  if (!nodeId || nodeId === localNode) return process.platform;
774
775
  try {
775
- const entry = (await this.ctxIfReady?.api?.nodes?.topology.query())?.find((n) => n.id === nodeId);
776
- if (entry?.platform) return entry.platform;
776
+ const { platform } = pickNodePlatformArch(await this.ctxIfReady?.api?.nodes?.topology.query(), nodeId);
777
+ if (platform) return platform;
777
778
  } catch {}
778
779
  return process.platform;
779
780
  }
@@ -784,7 +785,7 @@ var AudioAnalyzerAddon = class extends BaseAddon {
784
785
  */
785
786
  async reprobeAudioEngine() {
786
787
  const backend = process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
787
- await this.ctx.settings?.writeAddonStore({ probedBestAudioBackend: backend });
788
+ await this.updateGlobalSettings({ probedBestAudioBackend: backend });
788
789
  this.ctx.logger.info("reprobeAudioEngine: wrote probedBestAudioBackend", { meta: { backend } });
789
790
  return { backend };
790
791
  }
@@ -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-DAIlCdAx.js");
5
+ const require_dist = require("../dist-Biq62zt4.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/audio-codec-ffmpeg/ffmpeg-audio-process.ts
@@ -1,4 +1,4 @@
1
- import { B as errMsg, V as BaseAddon, v as audioCodecCapability } from "../dist-CgEP_0OL.mjs";
1
+ import { B as errMsg, V as BaseAddon, v as audioCodecCapability } from "../dist-C6_wgXqF.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src/audio-codec-ffmpeg/ffmpeg-audio-process.ts
@@ -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-DAIlCdAx.js");
6
- const require_frame_ring_sink = require("../frame-ring-sink-BbzrqTWj.js");
5
+ const require_dist = require("../dist-Biq62zt4.js");
6
+ const require_frame_ring_sink = require("../frame-ring-sink-9J0wCdLF.js");
7
7
  const require_frame_dropper = require("../frame-dropper-7RTo_YyG.js");
8
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
9
9
  let node_crypto = require("node:crypto");
@@ -896,6 +896,18 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
896
896
  //#endregion
897
897
  //#region src/decoder-ffmpeg/addon/index.ts
898
898
  var FRAME_BUFFER_CAPACITY = 32;
899
+ var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = {
900
+ ...require_dist.DEFAULT_DECODER_HWACCEL_CONFIG,
901
+ backend: require_frame_ring_sink.DEFAULT_DECODER_BACKEND
902
+ };
903
+ /** The two selectable decoder backends, in default-first order. */
904
+ var DECODER_BACKEND_OPTIONS = [{
905
+ value: "ffmpeg",
906
+ label: "ffmpeg (subprocess — crash-isolated, default)"
907
+ }, {
908
+ value: "nodeav",
909
+ label: "node-av (in-process FFmpeg bindings)"
910
+ }];
899
911
  var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
900
912
  sessions = /* @__PURE__ */ new Map();
901
913
  frameBuffers = /* @__PURE__ */ new Map();
@@ -926,10 +938,24 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
926
938
  */
927
939
  unsupportedGpuScaleFilters = /* @__PURE__ */ new Set();
928
940
  constructor() {
929
- super(require_dist.DEFAULT_DECODER_HWACCEL_CONFIG);
941
+ super(DEFAULT_DECODER_FFMPEG_ADDON_CONFIG);
930
942
  }
931
943
  globalSettingsSchema() {
932
944
  return this.schema({ sections: [{
945
+ id: "backend",
946
+ title: "Decoder backend",
947
+ tab: "decoder",
948
+ description: "Which decoder addon provides the decoder capability on this node — per-node selection, stored centrally on the hub. Selection is decided once at addon init (registration-time, race-free), so a change applies at the next restart of the decoder addons on the node.",
949
+ fields: [this.field({
950
+ type: "select",
951
+ key: "backend",
952
+ label: "Backend",
953
+ options: [...DECODER_BACKEND_OPTIONS],
954
+ default: require_frame_ring_sink.DEFAULT_DECODER_BACKEND,
955
+ immediate: true,
956
+ perNode: true
957
+ })]
958
+ }, {
933
959
  id: "hwaccel",
934
960
  title: "Hardware acceleration",
935
961
  tab: "decoder",
@@ -948,6 +974,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
948
974
  description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
949
975
  readonlyField: true,
950
976
  default: "",
977
+ perNode: true,
951
978
  actions: [{
952
979
  action: "reprobe-hwaccel",
953
980
  icon: "refresh-cw",
@@ -957,7 +984,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
957
984
  }] });
958
985
  }
959
986
  async onInitialize() {
960
- const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.settings, this.resolveLocalNodeId(), this.ctx.logger);
987
+ const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
961
988
  if (backend !== "ffmpeg") {
962
989
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
963
990
  return [];
@@ -1025,8 +1052,9 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1025
1052
  if (pref === "none") return [];
1026
1053
  if (pref !== "auto") return [pref];
1027
1054
  try {
1028
- const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: null });
1029
- const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query();
1055
+ const pin = require_dist.nodePin(this.resolveLocalNodeId());
1056
+ const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: null }, pin);
1057
+ const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query(void 0, pin);
1030
1058
  return rankDecodeHwAccels(res.preferred, supported.methods);
1031
1059
  } catch (err) {
1032
1060
  this.ctx.logger.warn("ffmpeg: decode-hwaccel probe failed — software decode", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -1037,13 +1065,13 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1037
1065
  const resolver = this.ctx.kernel.hwaccel;
1038
1066
  if (!resolver) {
1039
1067
  this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
1040
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
1068
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
1041
1069
  return { backend: "none" };
1042
1070
  }
1043
1071
  try {
1044
1072
  const res = await resolver.resolve();
1045
1073
  const backend = res.preferred[0] ?? "none";
1046
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend });
1074
+ await this.updateGlobalSettings({ probedBestHwaccel: backend });
1047
1075
  this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
1048
1076
  backend,
1049
1077
  rationale: res.rationale,
@@ -1052,7 +1080,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1052
1080
  return { backend };
1053
1081
  } catch (err) {
1054
1082
  this.ctx.logger.warn("reprobeHwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1055
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
1083
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
1056
1084
  return { backend: "none" };
1057
1085
  }
1058
1086
  }
@@ -1119,7 +1147,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1119
1147
  };
1120
1148
  }
1121
1149
  wireCallbackSink(sessionId, session) {
1122
- const ringBuffer = new require_dist.RingBuffer(FRAME_BUFFER_CAPACITY);
1150
+ const ringBuffer = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1123
1151
  this.frameBuffers.set(sessionId, ringBuffer);
1124
1152
  return session.onFrame((frame) => {
1125
1153
  const { format } = frame;
@@ -1137,7 +1165,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1137
1165
  });
1138
1166
  }
1139
1167
  wireShmSink(sessionId, session) {
1140
- const handleRing = new require_dist.RingBuffer(FRAME_BUFFER_CAPACITY);
1168
+ const handleRing = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1141
1169
  this.handleBuffers.set(sessionId, handleRing);
1142
1170
  return session.onFrameHandle((frame) => {
1143
1171
  handleRing.push(frame.handle);
@@ -1149,6 +1177,8 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1149
1177
  if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`);
1150
1178
  const unsub = this.unsubscribers.get(sessionId);
1151
1179
  if (unsub) unsub();
1180
+ this.frameBuffers.get(sessionId)?.cancel();
1181
+ this.handleBuffers.get(sessionId)?.cancel();
1152
1182
  await session.destroy();
1153
1183
  this.sessions.delete(sessionId);
1154
1184
  this.frameBuffers.delete(sessionId);
@@ -1186,12 +1216,14 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1186
1216
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1187
1217
  const ringBuffer = this.frameBuffers.get(input.sessionId);
1188
1218
  if (!ringBuffer) return [];
1219
+ if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
1189
1220
  return ringBuffer.drain(input.maxCount);
1190
1221
  }
1191
1222
  async pullHandles(input) {
1192
1223
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1193
1224
  const handleRing = this.handleBuffers.get(input.sessionId);
1194
1225
  if (!handleRing) return [];
1226
+ if (handleRing.size === 0 && input.waitMs) await handleRing.waitForItem(input.waitMs);
1195
1227
  return handleRing.drain(input.maxCount);
1196
1228
  }
1197
1229
  async updateConfig(input) {
@@ -1,5 +1,5 @@
1
- import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, p as RingBuffer, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-CgEP_0OL.mjs";
2
- import { i as resolveDecoderBackend, n as RING_BUDGET_MB, t as DecoderFrameRingSink } from "../frame-ring-sink-B_NvPTJZ.mjs";
1
+ import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, j as nodePin, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-C6_wgXqF.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-Cs9vby6v.mjs";
3
3
  import { r as logBannerArgs, t as FrameDropper } from "../frame-dropper-CwkBTPGV.mjs";
4
4
  import { FrameRingReaderCache } from "@camstack/shm-ring";
5
5
  import { randomUUID } from "node:crypto";
@@ -892,6 +892,18 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
892
892
  //#endregion
893
893
  //#region src/decoder-ffmpeg/addon/index.ts
894
894
  var FRAME_BUFFER_CAPACITY = 32;
895
+ var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = {
896
+ ...DEFAULT_DECODER_HWACCEL_CONFIG,
897
+ backend: DEFAULT_DECODER_BACKEND
898
+ };
899
+ /** The two selectable decoder backends, in default-first order. */
900
+ var DECODER_BACKEND_OPTIONS = [{
901
+ value: "ffmpeg",
902
+ label: "ffmpeg (subprocess — crash-isolated, default)"
903
+ }, {
904
+ value: "nodeav",
905
+ label: "node-av (in-process FFmpeg bindings)"
906
+ }];
895
907
  var DecoderFfmpegAddon = class extends BaseAddon {
896
908
  sessions = /* @__PURE__ */ new Map();
897
909
  frameBuffers = /* @__PURE__ */ new Map();
@@ -922,10 +934,24 @@ var DecoderFfmpegAddon = class extends BaseAddon {
922
934
  */
923
935
  unsupportedGpuScaleFilters = /* @__PURE__ */ new Set();
924
936
  constructor() {
925
- super(DEFAULT_DECODER_HWACCEL_CONFIG);
937
+ super(DEFAULT_DECODER_FFMPEG_ADDON_CONFIG);
926
938
  }
927
939
  globalSettingsSchema() {
928
940
  return this.schema({ sections: [{
941
+ id: "backend",
942
+ title: "Decoder backend",
943
+ tab: "decoder",
944
+ description: "Which decoder addon provides the decoder capability on this node — per-node selection, stored centrally on the hub. Selection is decided once at addon init (registration-time, race-free), so a change applies at the next restart of the decoder addons on the node.",
945
+ fields: [this.field({
946
+ type: "select",
947
+ key: "backend",
948
+ label: "Backend",
949
+ options: [...DECODER_BACKEND_OPTIONS],
950
+ default: DEFAULT_DECODER_BACKEND,
951
+ immediate: true,
952
+ perNode: true
953
+ })]
954
+ }, {
929
955
  id: "hwaccel",
930
956
  title: "Hardware acceleration",
931
957
  tab: "decoder",
@@ -944,6 +970,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
944
970
  description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
945
971
  readonlyField: true,
946
972
  default: "",
973
+ perNode: true,
947
974
  actions: [{
948
975
  action: "reprobe-hwaccel",
949
976
  icon: "refresh-cw",
@@ -953,7 +980,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
953
980
  }] });
954
981
  }
955
982
  async onInitialize() {
956
- const backend = await resolveDecoderBackend(this.ctx.settings, this.resolveLocalNodeId(), this.ctx.logger);
983
+ const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
957
984
  if (backend !== "ffmpeg") {
958
985
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
959
986
  return [];
@@ -1021,8 +1048,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1021
1048
  if (pref === "none") return [];
1022
1049
  if (pref !== "auto") return [pref];
1023
1050
  try {
1024
- const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: null });
1025
- const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query();
1051
+ const pin = nodePin(this.resolveLocalNodeId());
1052
+ const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: null }, pin);
1053
+ const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query(void 0, pin);
1026
1054
  return rankDecodeHwAccels(res.preferred, supported.methods);
1027
1055
  } catch (err) {
1028
1056
  this.ctx.logger.warn("ffmpeg: decode-hwaccel probe failed — software decode", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -1033,13 +1061,13 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1033
1061
  const resolver = this.ctx.kernel.hwaccel;
1034
1062
  if (!resolver) {
1035
1063
  this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
1036
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
1064
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
1037
1065
  return { backend: "none" };
1038
1066
  }
1039
1067
  try {
1040
1068
  const res = await resolver.resolve();
1041
1069
  const backend = res.preferred[0] ?? "none";
1042
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend });
1070
+ await this.updateGlobalSettings({ probedBestHwaccel: backend });
1043
1071
  this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
1044
1072
  backend,
1045
1073
  rationale: res.rationale,
@@ -1048,7 +1076,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1048
1076
  return { backend };
1049
1077
  } catch (err) {
1050
1078
  this.ctx.logger.warn("reprobeHwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1051
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
1079
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
1052
1080
  return { backend: "none" };
1053
1081
  }
1054
1082
  }
@@ -1115,7 +1143,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1115
1143
  };
1116
1144
  }
1117
1145
  wireCallbackSink(sessionId, session) {
1118
- const ringBuffer = new RingBuffer(FRAME_BUFFER_CAPACITY);
1146
+ const ringBuffer = new NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1119
1147
  this.frameBuffers.set(sessionId, ringBuffer);
1120
1148
  return session.onFrame((frame) => {
1121
1149
  const { format } = frame;
@@ -1133,7 +1161,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1133
1161
  });
1134
1162
  }
1135
1163
  wireShmSink(sessionId, session) {
1136
- const handleRing = new RingBuffer(FRAME_BUFFER_CAPACITY);
1164
+ const handleRing = new NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1137
1165
  this.handleBuffers.set(sessionId, handleRing);
1138
1166
  return session.onFrameHandle((frame) => {
1139
1167
  handleRing.push(frame.handle);
@@ -1145,6 +1173,8 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1145
1173
  if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`);
1146
1174
  const unsub = this.unsubscribers.get(sessionId);
1147
1175
  if (unsub) unsub();
1176
+ this.frameBuffers.get(sessionId)?.cancel();
1177
+ this.handleBuffers.get(sessionId)?.cancel();
1148
1178
  await session.destroy();
1149
1179
  this.sessions.delete(sessionId);
1150
1180
  this.frameBuffers.delete(sessionId);
@@ -1182,12 +1212,14 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1182
1212
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1183
1213
  const ringBuffer = this.frameBuffers.get(input.sessionId);
1184
1214
  if (!ringBuffer) return [];
1215
+ if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
1185
1216
  return ringBuffer.drain(input.maxCount);
1186
1217
  }
1187
1218
  async pullHandles(input) {
1188
1219
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1189
1220
  const handleRing = this.handleBuffers.get(input.sessionId);
1190
1221
  if (!handleRing) return [];
1222
+ if (handleRing.size === 0 && input.waitMs) await handleRing.waitForItem(input.waitMs);
1191
1223
  return handleRing.drain(input.maxCount);
1192
1224
  }
1193
1225
  async updateConfig(input) {
@@ -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-DAIlCdAx.js");
6
- const require_frame_ring_sink = require("../frame-ring-sink-BbzrqTWj.js");
5
+ const require_dist = require("../dist-Biq62zt4.js");
6
+ const require_frame_ring_sink = require("../frame-ring-sink-9J0wCdLF.js");
7
7
  let _camstack_shm_ring = require("@camstack/shm-ring");
8
8
  let node_crypto = require("node:crypto");
9
9
  //#region src/decoder-nodeav/scaler-geometry.ts
@@ -927,6 +927,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
927
927
  description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
928
928
  readonlyField: true,
929
929
  default: "",
930
+ perNode: true,
930
931
  actions: [{
931
932
  action: "reprobe-hwaccel",
932
933
  icon: "refresh-cw",
@@ -936,7 +937,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
936
937
  }] });
937
938
  }
938
939
  async onInitialize() {
939
- const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.settings, this.resolveLocalNodeId(), this.ctx.logger);
940
+ const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
940
941
  if (backend !== "nodeav") {
941
942
  this.ctx.logger.info("node-av decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
942
943
  return [];
@@ -970,13 +971,13 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
970
971
  const resolver = this.ctx.kernel.hwaccel;
971
972
  if (!resolver) {
972
973
  this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
973
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
974
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
974
975
  return { backend: "none" };
975
976
  }
976
977
  try {
977
978
  const res = await resolver.resolve();
978
979
  const backend = res.preferred[0] ?? "none";
979
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend });
980
+ await this.updateGlobalSettings({ probedBestHwaccel: backend });
980
981
  this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
981
982
  backend,
982
983
  rationale: res.rationale,
@@ -985,7 +986,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
985
986
  return { backend };
986
987
  } catch (err) {
987
988
  this.ctx.logger.warn("reprobeHwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
988
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
989
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
989
990
  return { backend: "none" };
990
991
  }
991
992
  }
@@ -1052,7 +1053,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1052
1053
  * ring buffer drained by `pullFrames`.
1053
1054
  */
1054
1055
  wireCallbackSink(sessionId, session) {
1055
- const ringBuffer = new require_dist.RingBuffer(FRAME_BUFFER_CAPACITY);
1056
+ const ringBuffer = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1056
1057
  this.frameBuffers.set(sessionId, ringBuffer);
1057
1058
  return session.onFrame((frame) => {
1058
1059
  const { format } = frame;
@@ -1075,7 +1076,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1075
1076
  * — the broker opens the named segment and reads the pixels zero-copy.
1076
1077
  */
1077
1078
  wireShmSink(sessionId, session) {
1078
- const handleRing = new require_dist.RingBuffer(FRAME_BUFFER_CAPACITY);
1079
+ const handleRing = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1079
1080
  this.handleBuffers.set(sessionId, handleRing);
1080
1081
  return session.onFrameHandle((frame) => {
1081
1082
  handleRing.push(frame.handle);
@@ -1087,6 +1088,8 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1087
1088
  if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${sessionId}`);
1088
1089
  const unsub = this.unsubscribers.get(sessionId);
1089
1090
  if (unsub) unsub();
1091
+ this.frameBuffers.get(sessionId)?.cancel();
1092
+ this.handleBuffers.get(sessionId)?.cancel();
1090
1093
  await session.destroy();
1091
1094
  this.sessions.delete(sessionId);
1092
1095
  this.frameBuffers.delete(sessionId);
@@ -1123,12 +1126,14 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1123
1126
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1124
1127
  const ringBuffer = this.frameBuffers.get(input.sessionId);
1125
1128
  if (!ringBuffer) return [];
1129
+ if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
1126
1130
  return ringBuffer.drain(input.maxCount);
1127
1131
  }
1128
1132
  async pullHandles(input) {
1129
1133
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1130
1134
  const handleRing = this.handleBuffers.get(input.sessionId);
1131
1135
  if (!handleRing) return [];
1136
+ if (handleRing.size === 0 && input.waitMs) await handleRing.waitForItem(input.waitMs);
1132
1137
  return handleRing.drain(input.maxCount);
1133
1138
  }
1134
1139
  async updateConfig(input) {
@@ -1,5 +1,5 @@
1
- import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, p as RingBuffer, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-CgEP_0OL.mjs";
2
- import { i as resolveDecoderBackend, n as RING_BUDGET_MB, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-B_NvPTJZ.mjs";
1
+ import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-C6_wgXqF.mjs";
2
+ import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-Cs9vby6v.mjs";
3
3
  import { FrameRingReaderCache } from "@camstack/shm-ring";
4
4
  import { randomUUID } from "node:crypto";
5
5
  //#region src/decoder-nodeav/scaler-geometry.ts
@@ -923,6 +923,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
923
923
  description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
924
924
  readonlyField: true,
925
925
  default: "",
926
+ perNode: true,
926
927
  actions: [{
927
928
  action: "reprobe-hwaccel",
928
929
  icon: "refresh-cw",
@@ -932,7 +933,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
932
933
  }] });
933
934
  }
934
935
  async onInitialize() {
935
- const backend = await resolveDecoderBackend(this.ctx.settings, this.resolveLocalNodeId(), this.ctx.logger);
936
+ const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
936
937
  if (backend !== "nodeav") {
937
938
  this.ctx.logger.info("node-av decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
938
939
  return [];
@@ -966,13 +967,13 @@ var DecoderNodeAvAddon = class extends BaseAddon {
966
967
  const resolver = this.ctx.kernel.hwaccel;
967
968
  if (!resolver) {
968
969
  this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
969
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
970
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
970
971
  return { backend: "none" };
971
972
  }
972
973
  try {
973
974
  const res = await resolver.resolve();
974
975
  const backend = res.preferred[0] ?? "none";
975
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend });
976
+ await this.updateGlobalSettings({ probedBestHwaccel: backend });
976
977
  this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
977
978
  backend,
978
979
  rationale: res.rationale,
@@ -981,7 +982,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
981
982
  return { backend };
982
983
  } catch (err) {
983
984
  this.ctx.logger.warn("reprobeHwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
984
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
985
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
985
986
  return { backend: "none" };
986
987
  }
987
988
  }
@@ -1048,7 +1049,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1048
1049
  * ring buffer drained by `pullFrames`.
1049
1050
  */
1050
1051
  wireCallbackSink(sessionId, session) {
1051
- const ringBuffer = new RingBuffer(FRAME_BUFFER_CAPACITY);
1052
+ const ringBuffer = new NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1052
1053
  this.frameBuffers.set(sessionId, ringBuffer);
1053
1054
  return session.onFrame((frame) => {
1054
1055
  const { format } = frame;
@@ -1071,7 +1072,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1071
1072
  * — the broker opens the named segment and reads the pixels zero-copy.
1072
1073
  */
1073
1074
  wireShmSink(sessionId, session) {
1074
- const handleRing = new RingBuffer(FRAME_BUFFER_CAPACITY);
1075
+ const handleRing = new NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1075
1076
  this.handleBuffers.set(sessionId, handleRing);
1076
1077
  return session.onFrameHandle((frame) => {
1077
1078
  handleRing.push(frame.handle);
@@ -1083,6 +1084,8 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1083
1084
  if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${sessionId}`);
1084
1085
  const unsub = this.unsubscribers.get(sessionId);
1085
1086
  if (unsub) unsub();
1087
+ this.frameBuffers.get(sessionId)?.cancel();
1088
+ this.handleBuffers.get(sessionId)?.cancel();
1086
1089
  await session.destroy();
1087
1090
  this.sessions.delete(sessionId);
1088
1091
  this.frameBuffers.delete(sessionId);
@@ -1119,12 +1122,14 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1119
1122
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1120
1123
  const ringBuffer = this.frameBuffers.get(input.sessionId);
1121
1124
  if (!ringBuffer) return [];
1125
+ if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
1122
1126
  return ringBuffer.drain(input.maxCount);
1123
1127
  }
1124
1128
  async pullHandles(input) {
1125
1129
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1126
1130
  const handleRing = this.handleBuffers.get(input.sessionId);
1127
1131
  if (!handleRing) return [];
1132
+ if (handleRing.size === 0 && input.waitMs) await handleRing.waitForItem(input.waitMs);
1128
1133
  return handleRing.drain(input.maxCount);
1129
1134
  }
1130
1135
  async updateConfig(input) {