@camstack/addon-pipeline 1.1.27 → 1.1.29

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 (49) 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 -5
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -5
  5. package/dist/decoder-ffmpeg/index.js +138 -356
  6. package/dist/decoder-ffmpeg/index.mjs +118 -336
  7. package/dist/decoder-nodeav/index.js +17 -10
  8. package/dist/decoder-nodeav/index.mjs +17 -10
  9. package/dist/detection-pipeline/index.js +341 -379
  10. package/dist/detection-pipeline/index.mjs +341 -379
  11. package/dist/{dist-DAIlCdAx.js → dist-Cwc0TUQr.js} +378 -76
  12. package/dist/{dist-CgEP_0OL.mjs → dist-DjuGmyG9.mjs} +379 -65
  13. package/dist/ffmpeg-args-C5GPp8Cw.mjs +323 -0
  14. package/dist/ffmpeg-args-D6h1edXK.js +418 -0
  15. package/dist/frame-dropper-AjheBGMG.mjs +22 -0
  16. package/dist/frame-dropper-DKLM6pMz.js +27 -0
  17. package/dist/{frame-handle-plane-Dq20KtKL.mjs → frame-handle-plane-Bkxz-TTD.mjs} +13 -5
  18. package/dist/{frame-handle-plane-DtTRX_0n.js → frame-handle-plane-DQNCTrpC.js} +13 -5
  19. package/dist/{frame-ring-sink-BbzrqTWj.js → frame-ring-sink-8LLV-cvH.js} +128 -23
  20. package/dist/{frame-ring-sink-B_NvPTJZ.mjs → frame-ring-sink-ClEWjiRU.mjs} +117 -24
  21. package/dist/motion-wasm/index.js +55 -3
  22. package/dist/motion-wasm/index.mjs +55 -3
  23. package/dist/node-topology-platform-BkR_k6WT.mjs +15 -0
  24. package/dist/node-topology-platform-CFZ7F4xW.js +20 -0
  25. package/dist/pipeline-runner/index.js +50 -16
  26. package/dist/pipeline-runner/index.mjs +50 -16
  27. package/dist/recorder/index.js +36 -40
  28. package/dist/recorder/index.mjs +36 -40
  29. package/dist/stream-broker/_stub.js +2 -2
  30. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs} +3 -3
  31. 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
  32. 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
  33. package/dist/stream-broker/{hostInit-DyLqyJaS.mjs → hostInit-B_b3PIZB.mjs} +3 -3
  34. package/dist/stream-broker/index.js +198 -115
  35. package/dist/stream-broker/index.mjs +189 -106
  36. package/dist/stream-broker/remoteEntry.js +1 -1
  37. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js → MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js} +1 -1
  38. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CoLjNiUN.js → MotionZonesSettings-NcxxQN8r-DTeuDGCU.js} +1 -1
  39. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js → PrivacyMaskSettings-APgPLF7p-CirtxO3e.js} +1 -1
  40. package/embed-dist/assets/index-C5UpuPr8.css +2 -0
  41. package/embed-dist/assets/{index-C-pL8ETk.js → index-CpUy8OIE.js} +10 -10
  42. package/embed-dist/index.html +2 -2
  43. package/package.json +1 -1
  44. package/python/inference_pool.py +93 -102
  45. package/python/test_inference_pool_device_selection.py +43 -38
  46. package/dist/frame-dropper-7RTo_YyG.js +0 -68
  47. package/dist/frame-dropper-CwkBTPGV.mjs +0 -51
  48. 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
  49. package/embed-dist/assets/index-DrJ0ee3f.css +0 -2
@@ -3,9 +3,10 @@ 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");
7
- const require_frame_handle_plane = require("../frame-handle-plane-DtTRX_0n.js");
8
- const require_frame_dropper = require("../frame-dropper-7RTo_YyG.js");
6
+ const require_dist = require("../dist-Cwc0TUQr.js");
7
+ const require_frame_handle_plane = require("../frame-handle-plane-DQNCTrpC.js");
8
+ const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
9
+ const require_frame_dropper = require("../frame-dropper-DKLM6pMz.js");
9
10
  let node_crypto = require("node:crypto");
10
11
  node_crypto = require_model_download_service_C_IHWnXx.__toESM(node_crypto);
11
12
  let node_fs = require("node:fs");
@@ -2527,7 +2528,7 @@ function planAudioSilencePadPackets(input) {
2527
2528
  * audio plane honours the profile literally.
2528
2529
  */
2529
2530
  function buildTranscodeFfmpegArgs(inputFormat, profile, decodeHwAccel, sourceUrl) {
2530
- const args = [...require_frame_dropper.logBannerArgs("error")];
2531
+ const args = [...require_ffmpeg_args.logBannerArgs("error")];
2531
2532
  if (profile.inputArgs?.length) args.push(...profile.inputArgs);
2532
2533
  if (decodeHwAccel && decodeHwAccel !== "none") args.push("-hwaccel", decodeHwAccel);
2533
2534
  args.push("-fflags", "+discardcorrupt", "-rtsp_transport", "tcp", "-i", sourceUrl);
@@ -2549,7 +2550,7 @@ function buildTranscodeFfmpegArgs(inputFormat, profile, decodeHwAccel, sourceUrl
2549
2550
  if (profile.audio === "passthrough") args.push("-an");
2550
2551
  else {
2551
2552
  const a = profile.audio;
2552
- args.push(...require_frame_dropper.audioEncoderArgs(a.codec, {
2553
+ args.push(...require_ffmpeg_args.audioEncoderArgs(a.codec, {
2553
2554
  bitrateKbps: a.bitrateKbps,
2554
2555
  sampleRateHz: a.sampleRateHz,
2555
2556
  channels: a.channels
@@ -5912,24 +5913,6 @@ function toAudioSidecarCodec(audio) {
5912
5913
  return audio === "aac" || audio === "opus" || audio === "pcmu" ? audio : null;
5913
5914
  }
5914
5915
  /**
5915
- * Pick the decode `-hwaccel` backend, gated by what the configured ffmpeg
5916
- * binary actually supports (`platformProbe.getHardwareDecodeAccels`).
5917
- *
5918
- * `preferred` entries are literal `-hwaccel` names (kernel-resolved), so they
5919
- * intersect directly with the probed method list. Conservative by design:
5920
- * - empty `preferred` → `null` (software decode);
5921
- * - empty `supportedMethods` (probe miss / build reports none) → keep the top
5922
- * preference and let the egress's per-stream software fallback cover failures;
5923
- * - otherwise return the first preference the build supports, or `null` when it
5924
- * supports none of them (positive evidence to skip a doomed hw-decode spawn).
5925
- */
5926
- function pickDecodeHwAccel(preferred, supportedMethods) {
5927
- if (preferred.length === 0) return null;
5928
- if (supportedMethods.length === 0) return preferred[0] ?? null;
5929
- const supported = new Set(supportedMethods.map((m) => m.toLowerCase()));
5930
- return preferred.find((name) => supported.has(name.toLowerCase())) ?? null;
5931
- }
5932
- /**
5933
5916
  * Pick the video encoder, honouring the `hwAccel` preference: `'none'`/`'copy'`
5934
5917
  * (software-decode settings) force the software ENCODER too; otherwise the
5935
5918
  * probe's default (hw when available) is used, falling back to software when no
@@ -5944,26 +5927,80 @@ function pickVideoEncoder(target, encoders, hwAccel) {
5944
5927
  if (hwAccel === "none" || hwAccel === "copy" || !encoders) return software;
5945
5928
  return target === "H264" ? encoders.defaultH264 : encoders.defaultH265;
5946
5929
  }
5930
+ /**
5931
+ * The h264/hevc hardware ENCODER ids for a decode-hwaccel backend name — a
5932
+ * static, deterministic map (the same shape Scrypted uses: platform → encoder,
5933
+ * no probe). Consumed by {@link localHardwareEncoders}. `null` for a backend
5934
+ * with no hardware encoder (software / unknown).
5935
+ */
5936
+ var ENCODER_IDS_BY_BACKEND = {
5937
+ videotoolbox: {
5938
+ h264: "h264_videotoolbox",
5939
+ h265: "hevc_videotoolbox"
5940
+ },
5941
+ vaapi: {
5942
+ h264: "h264_vaapi",
5943
+ h265: "hevc_vaapi"
5944
+ },
5945
+ cuda: {
5946
+ h264: "h264_nvenc",
5947
+ h265: "hevc_nvenc"
5948
+ },
5949
+ nvdec: {
5950
+ h264: "h264_nvenc",
5951
+ h265: "hevc_nvenc"
5952
+ },
5953
+ qsv: {
5954
+ h264: "h264_qsv",
5955
+ h265: "hevc_qsv"
5956
+ },
5957
+ amf: {
5958
+ h264: "h264_amf",
5959
+ h265: "hevc_amf"
5960
+ }
5961
+ };
5962
+ /**
5963
+ * Build the {@link HardwareEncoders} default pair for THIS node's decode-hwaccel
5964
+ * backend — resolved LOCALLY from `ctx.kernel.hwaccel` — WITHOUT probing
5965
+ * `ffmpeg -encoders` via the `platform-probe` singleton (whose per-node
5966
+ * `nodePin` does not survive a forked agent child's link chain → it answers with
5967
+ * the HUB's encoders). Mirrors how Scrypted derives encoders: a static
5968
+ * platform→encoder map. Returns `null` for software / an unmapped backend, which
5969
+ * makes {@link pickVideoEncoder} fall back to the software encoder. Only
5970
+ * `defaultH264`/`defaultH265` are consumed downstream, so `encoders` is left
5971
+ * empty (the per-encoder capability list was never read here).
5972
+ */
5973
+ function localHardwareEncoders(backend) {
5974
+ if (backend === null) return null;
5975
+ const ids = ENCODER_IDS_BY_BACKEND[backend.toLowerCase()];
5976
+ if (!ids) return null;
5977
+ return {
5978
+ encoders: [],
5979
+ defaultH264: ids.h264,
5980
+ defaultH265: ids.h265,
5981
+ probedAt: Date.now()
5982
+ };
5983
+ }
5947
5984
  /** Audio encoder args for a transcode target (preset values over the shared low-level builder). */
5948
5985
  function pickAudioEncoderArgs(audio) {
5949
5986
  switch (audio) {
5950
- case "aac": return require_frame_dropper.audioEncoderArgs("aac", {
5987
+ case "aac": return require_ffmpeg_args.audioEncoderArgs("aac", {
5951
5988
  bitrateKbps: 128,
5952
5989
  sampleRateHz: 48e3,
5953
5990
  channels: 2
5954
5991
  });
5955
- case "opus": return require_frame_dropper.audioEncoderArgs("opus", {
5992
+ case "opus": return require_ffmpeg_args.audioEncoderArgs("opus", {
5956
5993
  bitrateKbps: 64,
5957
5994
  sampleRateHz: 48e3,
5958
5995
  channels: 2
5959
5996
  });
5960
- case "pcmu": return require_frame_dropper.audioEncoderArgs("pcmu", {
5997
+ case "pcmu": return require_ffmpeg_args.audioEncoderArgs("pcmu", {
5961
5998
  sampleRateHz: 8e3,
5962
5999
  channels: 1
5963
6000
  });
5964
- case "copy": return require_frame_dropper.audioEncoderArgs("copy");
6001
+ case "copy": return require_ffmpeg_args.audioEncoderArgs("copy");
5965
6002
  case "none": return ["-an"];
5966
- default: return require_frame_dropper.audioEncoderArgs("aac", { bitrateKbps: 128 });
6003
+ default: return require_ffmpeg_args.audioEncoderArgs("aac", { bitrateKbps: 128 });
5967
6004
  }
5968
6005
  }
5969
6006
  /**
@@ -6028,7 +6065,7 @@ function buildFfmpegArgs$1(inv) {
6028
6065
  const threadArgs = inv.threadCount > 0 ? ["-threads", String(inv.threadCount)] : [];
6029
6066
  const audioArgs = inv.sink.kind === "stdout" && (inv.sink.container === "h264" || inv.sink.container === "hevc") ? ["-an"] : pickAudioEncoderArgs(inv.audio);
6030
6067
  return [
6031
- ...require_frame_dropper.logBannerArgs("warning"),
6068
+ ...require_ffmpeg_args.logBannerArgs("warning"),
6032
6069
  ...decodeArgs,
6033
6070
  "-rtsp_transport",
6034
6071
  "tcp",
@@ -11364,7 +11401,7 @@ var TranscodePipelineManager = class {
11364
11401
  * async egress start so concurrent acquires share one egress. */
11365
11402
  pending = /* @__PURE__ */ new Map();
11366
11403
  logger;
11367
- api;
11404
+ hwaccel;
11368
11405
  cameraStreamLookup;
11369
11406
  rtspEntryLookup;
11370
11407
  resolveProfileSource;
@@ -11375,7 +11412,7 @@ var TranscodePipelineManager = class {
11375
11412
  acquireChannel;
11376
11413
  constructor(opts) {
11377
11414
  this.logger = opts.logger;
11378
- this.api = opts.api;
11415
+ this.hwaccel = opts.hwaccel ?? null;
11379
11416
  this.cameraStreamLookup = opts.cameraStreamLookup;
11380
11417
  this.rtspEntryLookup = opts.rtspEntryLookup;
11381
11418
  this.resolveProfileSource = opts.resolveProfileSource ?? null;
@@ -11576,13 +11613,21 @@ var TranscodePipelineManager = class {
11576
11613
  if (!stream) throw new Error(`getStreamWithCodec: profile "${profile}" source "${camStreamId}" not published for device ${deviceId}`);
11577
11614
  return stream;
11578
11615
  }
11579
- /** Query the typed, cached hardware-encoder probe; null on any failure. */
11616
+ /**
11617
+ * The h264/hevc hardware ENCODER defaults for THIS node — derived locally
11618
+ * from `ctx.kernel.hwaccel` (os.platform + GPU-vendor probe) via a static
11619
+ * backend→encoder map, NOT the `platform-probe` singleton's `ffmpeg -encoders`
11620
+ * probe (whose per-node `nodePin` answers with the HUB's encoders). `null` →
11621
+ * software encode (also the outcome when no local resolver is wired). vaapi is
11622
+ * ranked above qsv (qsv is broken on the Intel stack) so an Intel node
11623
+ * transcodes with `h264_vaapi`.
11624
+ */
11580
11625
  async resolveHardwareEncoders() {
11581
- if (!this.api) return null;
11626
+ if (!this.hwaccel) return null;
11582
11627
  try {
11583
- return await this.api.platformProbe.getHardwareEncoders.query();
11628
+ return localHardwareEncoders(require_ffmpeg_args.rankDecodeHwAccels((await this.hwaccel.resolve(null)).preferred, [])[0] ?? null);
11584
11629
  } catch (err) {
11585
- this.logger.warn("platformProbe.getHardwareEncoders failed — software encode fallback", { meta: { error: require_dist.errMsg(err) } });
11630
+ this.logger.warn("local hardware-encoder resolve failed — software encode fallback", { meta: { error: require_dist.errMsg(err) } });
11586
11631
  return null;
11587
11632
  }
11588
11633
  }
@@ -11610,35 +11655,20 @@ var TranscodePipelineManager = class {
11610
11655
  if (strategy.kind === "software") return null;
11611
11656
  return this.resolveSpecificDecodeHwAccel(strategy.prefer);
11612
11657
  }
11613
- /** Gate a specific backend against the probed support list; null on miss/error. */
11614
- async resolveSpecificDecodeHwAccel(prefer) {
11615
- if (!this.api) return null;
11616
- try {
11617
- const res = await this.api.platformProbe.resolveHwAccel.query({ prefer });
11618
- const supported = await this.resolveDecodeAccelMethods();
11619
- const picked = pickDecodeHwAccel(res.preferred, supported);
11620
- if (picked === null && res.preferred.length > 0) this.logger.info("decode hw-accel: configured ffmpeg supports none of the preferred backends — software decode", { meta: {
11621
- preferred: res.preferred,
11622
- supported
11623
- } });
11624
- return picked;
11625
- } catch (err) {
11626
- this.logger.warn("platformProbe.resolveHwAccel failed — software decode fallback", { meta: { error: require_dist.errMsg(err) } });
11627
- return null;
11628
- }
11629
- }
11630
11658
  /**
11631
- * The `-hwaccel` methods the configured ffmpeg supports; `[]` on any failure
11632
- * (treated as "unknown" by {@link pickDecodeHwAccel}, which then keeps the
11633
- * top preference rather than filtering it out).
11659
+ * Resolve a specific operator-forced decode backend against THIS node's local
11660
+ * hwaccel resolver. `ctx.kernel.hwaccel.resolve(prefer)` echoes the forced
11661
+ * backend (re-ranked so vaapi precedes qsv); the egress's per-stream software
11662
+ * fallback still catches a backend the actual ffmpeg build can't drive. Null
11663
+ * when no local resolver is wired.
11634
11664
  */
11635
- async resolveDecodeAccelMethods() {
11636
- if (!this.api) return [];
11665
+ async resolveSpecificDecodeHwAccel(prefer) {
11666
+ if (!this.hwaccel) return null;
11637
11667
  try {
11638
- return (await this.api.platformProbe.getHardwareDecodeAccels.query()).methods;
11668
+ return require_ffmpeg_args.rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
11639
11669
  } catch (err) {
11640
- this.logger.warn("platformProbe.getHardwareDecodeAccels failed — decode gate skipped", { meta: { error: require_dist.errMsg(err) } });
11641
- return [];
11670
+ this.logger.warn("local decode hw-accel resolve failed — software decode fallback", { meta: { error: require_dist.errMsg(err) } });
11671
+ return null;
11642
11672
  }
11643
11673
  }
11644
11674
  shareEntry(entry) {
@@ -11956,6 +11986,14 @@ var StreamBrokerManager = class {
11956
11986
  */
11957
11987
  api = null;
11958
11988
  /**
11989
+ * This node's LOCAL hwaccel resolver (`ctx.kernel.hwaccel`), wired by
11990
+ * `setApiAccess`. Drives the transcode manager's decode/encode backend picks
11991
+ * and `getHwAccelOptions` from THIS node's hardware in-process — never the
11992
+ * `platform-probe` singleton (whose per-node `nodePin` answers with the hub's
11993
+ * hardware from a forked agent child).
11994
+ */
11995
+ hwaccel = null;
11996
+ /**
11959
11997
  * This broker node's own Moleculer nodeID (`ctx.kernel.localNodeId`). The
11960
11998
  * shm frame plane's decoder sessions MUST be co-located on this node — an
11961
11999
  * shm frame ring is node-local, so a decoder placed on any OTHER node can
@@ -11997,7 +12035,7 @@ var StreamBrokerManager = class {
11997
12035
  getTranscodeManager() {
11998
12036
  if (!this.transcodeManager) this.transcodeManager = new TranscodePipelineManager({
11999
12037
  logger: this.logger.child("transcode"),
12000
- api: this.api,
12038
+ hwaccel: this.hwaccel ?? void 0,
12001
12039
  cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
12002
12040
  rtspEntryLookup: (brokerId) => {
12003
12041
  const entry = this.rtspProvider.getEntry(brokerId);
@@ -12203,9 +12241,10 @@ var StreamBrokerManager = class {
12203
12241
  * resolve the `decoder` capability via tRPC — see
12204
12242
  * `buildDecoderCapApi` for the full rationale.
12205
12243
  */
12206
- setApiAccess(api, localNodeId) {
12244
+ setApiAccess(api, localNodeId, hwaccel) {
12207
12245
  this.api = api;
12208
12246
  if (localNodeId !== void 0) this.localNodeId = localNodeId;
12247
+ if (hwaccel !== void 0) this.hwaccel = hwaccel;
12209
12248
  }
12210
12249
  /** Update the system `ffmpeg` config snapshot used by the transcode egress. */
12211
12250
  setFfmpegConfig(cfg) {
@@ -12213,11 +12252,11 @@ var StreamBrokerManager = class {
12213
12252
  }
12214
12253
  /**
12215
12254
  * The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
12216
- * settings (`copy`, `auto`, `none`) plus the concrete backends the configured
12217
- * ffmpeg build actually supports (probed via `getHardwareDecodeAccels`). Lets
12218
- * a settings form offer exactly the backends this host can use, instead of the
12219
- * full static list that may include unsupported ones. Best-effort: on a probe
12220
- * miss it returns the always-valid settings only.
12255
+ * settings (`copy`, `auto`, `none`) plus the concrete hardware backends THIS
12256
+ * node can use, resolved from the LOCAL `ctx.kernel.hwaccel` (os.platform +
12257
+ * GPU-vendor probe), ranked so vaapi precedes qsv. Node-local never the
12258
+ * `platform-probe` singleton (which answers with the hub's methods from a
12259
+ * forked agent broker). Best-effort: no resolver / error → always-valid only.
12221
12260
  */
12222
12261
  async getHwAccelOptions() {
12223
12262
  const always = [
@@ -12225,12 +12264,12 @@ var StreamBrokerManager = class {
12225
12264
  "auto",
12226
12265
  "none"
12227
12266
  ];
12228
- if (!this.api) return always;
12267
+ if (!this.hwaccel) return always;
12229
12268
  try {
12230
- const probed = (await this.api.platformProbe.getHardwareDecodeAccels.query()).methods.filter((m) => FFMPEG_HW_ACCEL_SETTINGS.includes(m));
12269
+ const probed = require_ffmpeg_args.rankDecodeHwAccels((await this.hwaccel.resolve(null)).preferred, []).filter((m) => FFMPEG_HW_ACCEL_SETTINGS.includes(m));
12231
12270
  return [...always, ...probed];
12232
12271
  } catch (err) {
12233
- this.logger.warn("getHwAccelOptions: getHardwareDecodeAccels failed — static options only", { meta: { error: require_dist.errMsg(err) } });
12272
+ this.logger.warn("getHwAccelOptions: local hwaccel resolve failed — static options only", { meta: { error: require_dist.errMsg(err) } });
12234
12273
  return always;
12235
12274
  }
12236
12275
  }
@@ -17061,8 +17100,24 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
17061
17100
  };
17062
17101
  //#endregion
17063
17102
  //#region src/stream-broker/addon.ts
17064
- function isFfmpegHwAccel(v) {
17065
- return typeof v === "string" && FFMPEG_HW_ACCEL_SETTINGS.includes(v);
17103
+ /**
17104
+ * Hydrate the persisted RTSP restream state (`rtspTokens` / `rtspEnabled`)
17105
+ * through the durable `state()` handles — the sanctioned settings accessor —
17106
+ * instead of a raw `readAddonStore()`. Same store keys, same persisted
17107
+ * shapes; a corrupt blob falls back to empty (durable-state semantics) and
17108
+ * absent keys keep first-boot silence (no loader call, no log).
17109
+ */
17110
+ async function hydratePersistedRtspState(deps) {
17111
+ const tokens = new Map(Object.entries(await deps.tokensState.get()));
17112
+ if (tokens.size > 0) {
17113
+ deps.loadTokens(tokens);
17114
+ deps.logger.info("Loaded persisted RTSP tokens", { meta: { tokenCount: tokens.size } });
17115
+ }
17116
+ const states = new Map(Object.entries(await deps.enabledState.get()));
17117
+ if (states.size > 0) {
17118
+ deps.loadEnabled(states);
17119
+ deps.logger.info("Loaded persisted RTSP enabled states", { meta: { stateCount: states.size } });
17120
+ }
17066
17121
  }
17067
17122
  var PROFILE_KEYS = [
17068
17123
  "high",
@@ -17153,39 +17208,54 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17153
17208
  maxDecodeFps: 5,
17154
17209
  initialReconnectDelayMs: 1e3,
17155
17210
  maxReconnectDelayMs: 3e4,
17156
- catalogReconcileIntervalSec: 30
17211
+ catalogReconcileIntervalSec: 30,
17212
+ binaryPath: "",
17213
+ hwaccel: "copy"
17157
17214
  });
17158
17215
  }
17159
17216
  /**
17160
- * Read the cluster `ffmpeg` config section (binaryPath / hwAccel /
17161
- * threadCount) and push it into the broker. Defensive: any missing or
17162
- * malformed field falls back to the safe default (PATH `ffmpeg`, `'copy'`
17163
- * software decode, `0` threads), and a read failure leaves the defaults in
17164
- * place. `'copy'` is the safe default it decodes every HEVC source rather
17165
- * than hard-failing on a fragile one (the forced-backend trap).
17217
+ * Resolve the ffmpeg binary for THIS node: a per-node `binaryPath` override
17218
+ * (`binaryPath@<node>`) else `ensureFfmpeg()` which provisions a guaranteed
17219
+ * portable static binary locally (hub / agent / Mac). Replaces the retired
17220
+ * global `ffmpeg` system section (node-local/empty on agents a cross-node
17221
+ * footgun). Falls back to PATH `ffmpeg` only if the provision genuinely fails.
17222
+ */
17223
+ async resolveFfmpegBinaryPath() {
17224
+ const override = this.config.binaryPath;
17225
+ if (typeof override === "string" && override.trim().length > 0) return override;
17226
+ try {
17227
+ return await this.ctx.deps.ensureFfmpeg();
17228
+ } catch (err) {
17229
+ this.ctx.logger.error("stream-broker: ensureFfmpeg() failed to provision ffmpeg — falling back to PATH \"ffmpeg\"", { meta: { error: require_dist.errMsg(err) } });
17230
+ return "ffmpeg";
17231
+ }
17232
+ }
17233
+ /**
17234
+ * Push THIS node's ffmpeg config into the broker — all node-local now, NOT the
17235
+ * retired global `ffmpeg` section. `binaryPath` via {@link resolveFfmpegBinaryPath}
17236
+ * (per-node override → ensureFfmpeg); `hwAccel` = the per-node `hwaccel`
17237
+ * preference (default `'copy'` = software, the safe HEVC-compatible choice; the
17238
+ * concrete backend is resolved node-locally in `resolveTranscodeDecodeHwAccel`
17239
+ * / the transcode manager). `threadCount` is always `0` (auto — ffmpeg uses the
17240
+ * node's own cores; the manual knob was removed).
17166
17241
  */
17167
17242
  async loadFfmpegConfig() {
17168
17243
  if (!this.brokerManager) return;
17169
17244
  try {
17170
- const section = await this.ctx.settings?.getSection("ffmpeg") ?? {};
17171
- const binaryPathRaw = section["binaryPath"];
17172
- const binaryPath = typeof binaryPathRaw === "string" && binaryPathRaw.trim().length > 0 ? binaryPathRaw : "ffmpeg";
17173
- const hwAccel = isFfmpegHwAccel(section["hwAccel"]) ? section["hwAccel"] : "copy";
17174
- const threadCountRaw = section["threadCount"];
17175
- const threadCount = typeof threadCountRaw === "number" && Number.isFinite(threadCountRaw) && threadCountRaw >= 0 ? Math.floor(threadCountRaw) : 0;
17245
+ const binaryPath = await this.resolveFfmpegBinaryPath();
17246
+ const hwAccel = this.config.hwaccel;
17176
17247
  this.brokerManager.setFfmpegConfig({
17177
17248
  binaryPath,
17178
17249
  hwAccel,
17179
- threadCount
17250
+ threadCount: 0
17180
17251
  });
17181
17252
  this.ffmpegBinaryPath = binaryPath;
17182
17253
  this.ctx.logger.info("Loaded ffmpeg config", { meta: {
17183
17254
  binaryPath,
17184
- hwAccel,
17185
- threadCount
17255
+ hwAccel
17186
17256
  } });
17187
17257
  } catch (err) {
17188
- this.ctx.logger.warn("Failed to read ffmpeg config section — using defaults", { meta: { error: require_dist.errMsg(err) } });
17258
+ this.ctx.logger.warn("Failed to load ffmpeg config — using defaults", { meta: { error: require_dist.errMsg(err) } });
17189
17259
  }
17190
17260
  }
17191
17261
  /**
@@ -17193,20 +17263,19 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17193
17263
  * configured `ffmpeg.hwAccel` preference (default `'copy'` = software):
17194
17264
  * - `'auto'` → the literal `'auto'` (ffmpeg self-selects + software-fallbacks).
17195
17265
  * - `'copy'`/`'none'` → `null` (software decode).
17196
- * - a specific backend → gated against what the configured ffmpeg can decode
17197
- * (`getHardwareDecodeAccels`); the feed still falls back to software on a
17266
+ * - a specific backend → resolved from THIS node's LOCAL `ctx.kernel.hwaccel`
17267
+ * (ranked so vaapi precedes qsv); the feed still falls back to software on a
17198
17268
  * per-stream miss.
17199
17269
  * Mirrors the transcode egress's resolution.
17200
17270
  */
17201
17271
  async resolveTranscodeDecodeHwAccel() {
17202
17272
  try {
17203
- const section = await this.ctx.settings?.getSection("ffmpeg") ?? {};
17204
- const strategy = classifyDecodeHwAccel(isFfmpegHwAccel(section["hwAccel"]) ? section["hwAccel"] : "copy");
17273
+ const strategy = classifyDecodeHwAccel(this.config.hwaccel);
17205
17274
  if (strategy.kind === "auto") return "auto";
17206
17275
  if (strategy.kind === "software") return null;
17207
- const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: strategy.prefer });
17208
- const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query();
17209
- return pickDecodeHwAccel(res.preferred, supported.methods);
17276
+ const resolver = this.ctx.kernel.hwaccel;
17277
+ if (!resolver) return null;
17278
+ return require_ffmpeg_args.rankDecodeHwAccels((await resolver.resolve(strategy.prefer)).preferred, [])[0] ?? null;
17210
17279
  } catch (err) {
17211
17280
  this.ctx.logger.warn("resolveTranscodeDecodeHwAccel failed — software decode", { meta: { error: require_dist.errMsg(err) } });
17212
17281
  return null;
@@ -17218,28 +17287,22 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17218
17287
  this.brokerManager.setEventBus(this.ctx.eventBus);
17219
17288
  const localNode = this.ctx.kernel.localNodeId;
17220
17289
  const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
17221
- this.brokerManager.setApiAccess(this.ctx.api, localNodeId);
17290
+ this.brokerManager.setApiAccess(this.ctx.api, localNodeId, this.ctx.kernel.hwaccel);
17222
17291
  if (this.capabilities) this.brokerManager.setCapabilitiesAccess(this.capabilities);
17223
17292
  await this.loadFfmpegConfig();
17224
17293
  const rtspProvider = this.brokerManager.getRtspRestreamProvider();
17294
+ const brokerManager = this.brokerManager;
17295
+ const rtspTokensState = this.state("rtspTokens", RtspTokensSchema, {});
17296
+ const rtspEnabledState = this.state("rtspEnabled", RtspEnabledSchema, {});
17225
17297
  try {
17226
- const addonStore = await this.ctx.settings?.readAddonStore() ?? {};
17227
- const tokenData = require_dist.asJsonObject(addonStore["rtspTokens"]);
17228
- if (tokenData) {
17229
- const tokens = /* @__PURE__ */ new Map();
17230
- for (const [k, v] of Object.entries(tokenData)) if (typeof v === "string") tokens.set(k, v);
17231
- this.brokerManager.loadPersistedTokens(tokens);
17232
- this.ctx.logger.info("Loaded persisted RTSP tokens", { meta: { tokenCount: tokens.size } });
17233
- }
17234
- const enabledData = require_dist.asJsonObject(addonStore["rtspEnabled"]);
17235
- if (enabledData) {
17236
- const states = /* @__PURE__ */ new Map();
17237
- for (const [k, v] of Object.entries(enabledData)) states.set(k, Boolean(v));
17238
- rtspProvider.loadPersistedEnabled(states);
17239
- this.ctx.logger.info("Loaded persisted RTSP enabled states", { meta: { stateCount: states.size } });
17240
- }
17298
+ await hydratePersistedRtspState({
17299
+ tokensState: rtspTokensState,
17300
+ enabledState: rtspEnabledState,
17301
+ loadTokens: (tokens) => brokerManager.loadPersistedTokens(tokens),
17302
+ loadEnabled: (states) => rtspProvider.loadPersistedEnabled(states),
17303
+ logger: this.ctx.logger
17304
+ });
17241
17305
  } catch {}
17242
- const rtspTokensState = this.state("rtspTokens", RtspTokensSchema, {});
17243
17306
  rtspProvider.setTokenPersister(async (tokens) => {
17244
17307
  try {
17245
17308
  await rtspTokensState.update((prev) => {
@@ -17252,7 +17315,6 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17252
17315
  this.ctx.logger.warn("Failed to persist RTSP tokens", { meta: { error: require_dist.errMsg(err) } });
17253
17316
  }
17254
17317
  });
17255
- const rtspEnabledState = this.state("rtspEnabled", RtspEnabledSchema, {});
17256
17318
  rtspProvider.setEnabledPersister((states) => {
17257
17319
  const obj = {};
17258
17320
  for (const [k, v] of states) obj[k] = v;
@@ -17684,6 +17746,27 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17684
17746
  step: 1,
17685
17747
  default: 5
17686
17748
  },
17749
+ {
17750
+ type: "select",
17751
+ key: "hwaccel",
17752
+ label: "Transcode hardware acceleration",
17753
+ description: "Per-node decode/encode hwaccel for the WebRTC/restream transcode path. \"Software (copy)\" is the safe default (decodes every HEVC source); a concrete backend is resolved from this node’s own hardware. Per-node because hardware differs per node.",
17754
+ options: [...FFMPEG_HW_ACCEL_SETTINGS].map((s) => ({
17755
+ value: s,
17756
+ label: hwAccelOptionLabel(s)
17757
+ })),
17758
+ default: "copy",
17759
+ perNode: true
17760
+ },
17761
+ {
17762
+ type: "text",
17763
+ key: "binaryPath",
17764
+ label: "ffmpeg binary path (override)",
17765
+ description: "Optional per-node override for the ffmpeg binary. Leave empty to auto-provision a portable static ffmpeg on this node (recommended).",
17766
+ placeholder: "ffmpeg",
17767
+ default: "",
17768
+ perNode: true
17769
+ },
17687
17770
  {
17688
17771
  type: "number",
17689
17772
  key: "catalogReconcileIntervalSec",
@@ -17753,7 +17836,7 @@ function codecToInputFormat(codec) {
17753
17836
  function buildFfmpegArgs(config) {
17754
17837
  const inputFormat = codecToInputFormat(config.codec);
17755
17838
  const args = [
17756
- ...require_frame_dropper.logBannerArgs("error"),
17839
+ ...require_ffmpeg_args.logBannerArgs("error"),
17757
17840
  "-fflags",
17758
17841
  "+nobuffer+flush_packets",
17759
17842
  "-flags",