@camstack/addon-pipeline 1.1.28 → 1.1.30

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 (39) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  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 +102 -352
  6. package/dist/decoder-ffmpeg/index.mjs +82 -332
  7. package/dist/decoder-nodeav/index.js +6 -4
  8. package/dist/decoder-nodeav/index.mjs +6 -4
  9. package/dist/detection-pipeline/index.js +254 -186
  10. package/dist/detection-pipeline/index.mjs +254 -186
  11. package/dist/{dist-Biq62zt4.js → dist-Cwc0TUQr.js} +7 -42
  12. package/dist/{dist-C6_wgXqF.mjs → dist-DjuGmyG9.mjs} +7 -42
  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-BIoY6nRV.mjs → frame-handle-plane-Bkxz-TTD.mjs} +1 -1
  18. package/dist/{frame-handle-plane-D6BzyEgy.js → frame-handle-plane-DQNCTrpC.js} +1 -1
  19. package/dist/{frame-ring-sink-9J0wCdLF.js → frame-ring-sink-8LLV-cvH.js} +1 -1
  20. package/dist/{frame-ring-sink-Cs9vby6v.mjs → frame-ring-sink-ClEWjiRU.mjs} +1 -1
  21. package/dist/motion-wasm/index.js +1 -1
  22. package/dist/motion-wasm/index.mjs +1 -1
  23. package/dist/pipeline-runner/index.js +2 -2
  24. package/dist/pipeline-runner/index.mjs +2 -2
  25. package/dist/recorder/index.js +1 -1
  26. package/dist/recorder/index.mjs +1 -1
  27. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DoReAb4y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs} +3 -3
  28. package/dist/stream-broker/{hostInit-C0SuwQhL.mjs → hostInit-B_b3PIZB.mjs} +3 -3
  29. package/dist/stream-broker/index.js +168 -121
  30. package/dist/stream-broker/index.mjs +159 -112
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js → MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BSCcPdPf.js → MotionZonesSettings-NcxxQN8r-DTeuDGCU.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-ztxvxSjS.js → PrivacyMaskSettings-APgPLF7p-CirtxO3e.js} +1 -1
  35. package/embed-dist/assets/{index-CeZV1B-2.js → index-CpUy8OIE.js} +9 -9
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/dist/frame-dropper-7RTo_YyG.js +0 -68
  39. package/dist/frame-dropper-CwkBTPGV.mjs +0 -51
@@ -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-Biq62zt4.js");
7
- const require_frame_handle_plane = require("../frame-handle-plane-D6BzyEgy.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,8 +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;
11368
- localNodeId;
11404
+ hwaccel;
11369
11405
  cameraStreamLookup;
11370
11406
  rtspEntryLookup;
11371
11407
  resolveProfileSource;
@@ -11376,8 +11412,7 @@ var TranscodePipelineManager = class {
11376
11412
  acquireChannel;
11377
11413
  constructor(opts) {
11378
11414
  this.logger = opts.logger;
11379
- this.api = opts.api;
11380
- this.localNodeId = opts.localNodeId ?? null;
11415
+ this.hwaccel = opts.hwaccel ?? null;
11381
11416
  this.cameraStreamLookup = opts.cameraStreamLookup;
11382
11417
  this.rtspEntryLookup = opts.rtspEntryLookup;
11383
11418
  this.resolveProfileSource = opts.resolveProfileSource ?? null;
@@ -11579,21 +11614,20 @@ var TranscodePipelineManager = class {
11579
11614
  return stream;
11580
11615
  }
11581
11616
  /**
11582
- * Per-call pin routing every `platform-probe` read to THIS node's probe.
11583
- * The cap is a `singleton` unpinned, an agent child gets the HUB's
11584
- * hardware answer. `undefined` (legacy unpinned) when the node id is
11585
- * unknown.
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`.
11586
11624
  */
11587
- probePin() {
11588
- return this.localNodeId ? require_dist.nodePin(this.localNodeId) : void 0;
11589
- }
11590
- /** Query the typed, cached hardware-encoder probe; null on any failure. */
11591
11625
  async resolveHardwareEncoders() {
11592
- if (!this.api) return null;
11626
+ if (!this.hwaccel) return null;
11593
11627
  try {
11594
- return await this.api.platformProbe.getHardwareEncoders.query(void 0, this.probePin());
11628
+ return localHardwareEncoders(require_ffmpeg_args.rankDecodeHwAccels((await this.hwaccel.resolve(null)).preferred, [])[0] ?? null);
11595
11629
  } catch (err) {
11596
- 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) } });
11597
11631
  return null;
11598
11632
  }
11599
11633
  }
@@ -11621,35 +11655,20 @@ var TranscodePipelineManager = class {
11621
11655
  if (strategy.kind === "software") return null;
11622
11656
  return this.resolveSpecificDecodeHwAccel(strategy.prefer);
11623
11657
  }
11624
- /** Gate a specific backend against the probed support list; null on miss/error. */
11625
- async resolveSpecificDecodeHwAccel(prefer) {
11626
- if (!this.api) return null;
11627
- try {
11628
- const res = await this.api.platformProbe.resolveHwAccel.query({ prefer }, this.probePin());
11629
- const supported = await this.resolveDecodeAccelMethods();
11630
- const picked = pickDecodeHwAccel(res.preferred, supported);
11631
- if (picked === null && res.preferred.length > 0) this.logger.info("decode hw-accel: configured ffmpeg supports none of the preferred backends — software decode", { meta: {
11632
- preferred: res.preferred,
11633
- supported
11634
- } });
11635
- return picked;
11636
- } catch (err) {
11637
- this.logger.warn("platformProbe.resolveHwAccel failed — software decode fallback", { meta: { error: require_dist.errMsg(err) } });
11638
- return null;
11639
- }
11640
- }
11641
11658
  /**
11642
- * The `-hwaccel` methods the configured ffmpeg supports; `[]` on any failure
11643
- * (treated as "unknown" by {@link pickDecodeHwAccel}, which then keeps the
11644
- * 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.
11645
11664
  */
11646
- async resolveDecodeAccelMethods() {
11647
- if (!this.api) return [];
11665
+ async resolveSpecificDecodeHwAccel(prefer) {
11666
+ if (!this.hwaccel) return null;
11648
11667
  try {
11649
- return (await this.api.platformProbe.getHardwareDecodeAccels.query(void 0, this.probePin())).methods;
11668
+ return require_ffmpeg_args.rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
11650
11669
  } catch (err) {
11651
- this.logger.warn("platformProbe.getHardwareDecodeAccels failed — decode gate skipped", { meta: { error: require_dist.errMsg(err) } });
11652
- return [];
11670
+ this.logger.warn("local decode hw-accel resolve failed — software decode fallback", { meta: { error: require_dist.errMsg(err) } });
11671
+ return null;
11653
11672
  }
11654
11673
  }
11655
11674
  shareEntry(entry) {
@@ -11967,6 +11986,14 @@ var StreamBrokerManager = class {
11967
11986
  */
11968
11987
  api = null;
11969
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
+ /**
11970
11997
  * This broker node's own Moleculer nodeID (`ctx.kernel.localNodeId`). The
11971
11998
  * shm frame plane's decoder sessions MUST be co-located on this node — an
11972
11999
  * shm frame ring is node-local, so a decoder placed on any OTHER node can
@@ -12008,8 +12035,7 @@ var StreamBrokerManager = class {
12008
12035
  getTranscodeManager() {
12009
12036
  if (!this.transcodeManager) this.transcodeManager = new TranscodePipelineManager({
12010
12037
  logger: this.logger.child("transcode"),
12011
- api: this.api,
12012
- localNodeId: this.localNodeId ?? void 0,
12038
+ hwaccel: this.hwaccel ?? void 0,
12013
12039
  cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
12014
12040
  rtspEntryLookup: (brokerId) => {
12015
12041
  const entry = this.rtspProvider.getEntry(brokerId);
@@ -12215,9 +12241,10 @@ var StreamBrokerManager = class {
12215
12241
  * resolve the `decoder` capability via tRPC — see
12216
12242
  * `buildDecoderCapApi` for the full rationale.
12217
12243
  */
12218
- setApiAccess(api, localNodeId) {
12244
+ setApiAccess(api, localNodeId, hwaccel) {
12219
12245
  this.api = api;
12220
12246
  if (localNodeId !== void 0) this.localNodeId = localNodeId;
12247
+ if (hwaccel !== void 0) this.hwaccel = hwaccel;
12221
12248
  }
12222
12249
  /** Update the system `ffmpeg` config snapshot used by the transcode egress. */
12223
12250
  setFfmpegConfig(cfg) {
@@ -12225,11 +12252,11 @@ var StreamBrokerManager = class {
12225
12252
  }
12226
12253
  /**
12227
12254
  * The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
12228
- * settings (`copy`, `auto`, `none`) plus the concrete backends the configured
12229
- * ffmpeg build actually supports (probed via `getHardwareDecodeAccels`). Lets
12230
- * a settings form offer exactly the backends this host can use, instead of the
12231
- * full static list that may include unsupported ones. Best-effort: on a probe
12232
- * 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.
12233
12260
  */
12234
12261
  async getHwAccelOptions() {
12235
12262
  const always = [
@@ -12237,13 +12264,12 @@ var StreamBrokerManager = class {
12237
12264
  "auto",
12238
12265
  "none"
12239
12266
  ];
12240
- if (!this.api) return always;
12267
+ if (!this.hwaccel) return always;
12241
12268
  try {
12242
- const pin = this.localNodeId ? require_dist.nodePin(this.localNodeId) : void 0;
12243
- const probed = (await this.api.platformProbe.getHardwareDecodeAccels.query(void 0, pin)).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));
12244
12270
  return [...always, ...probed];
12245
12271
  } catch (err) {
12246
- 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) } });
12247
12273
  return always;
12248
12274
  }
12249
12275
  }
@@ -17074,9 +17100,6 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
17074
17100
  };
17075
17101
  //#endregion
17076
17102
  //#region src/stream-broker/addon.ts
17077
- function isFfmpegHwAccel(v) {
17078
- return typeof v === "string" && FFMPEG_HW_ACCEL_SETTINGS.includes(v);
17079
- }
17080
17103
  /**
17081
17104
  * Hydrate the persisted RTSP restream state (`rtspTokens` / `rtspEnabled`)
17082
17105
  * through the durable `state()` handles — the sanctioned settings accessor —
@@ -17160,15 +17183,6 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17160
17183
  /** Handle for the embed SPA data-plane listener (disposed on shutdown). */
17161
17184
  embedDataPlane = null;
17162
17185
  /**
17163
- * This broker's bare cluster node id (`ctx.kernel.localNodeId` with any
17164
- * `node/addon` fork suffix stripped), computed once in onInitialize.
17165
- * `platform-probe` is a `singleton` cap, so every probe read (transcode
17166
- * decode hw-accel) MUST be pinned here — an unpinned call from an agent
17167
- * child resolves to the HUB's probe (the wrong hardware). `undefined` when
17168
- * the kernel id is unknown → legacy unpinned behaviour.
17169
- */
17170
- localNodeId;
17171
- /**
17172
17186
  * Snapshot-equality cache for the broker-metrics emit. Each
17173
17187
  * broker emits a stats snapshot every BROKER_METRICS_SNAPSHOT_-
17174
17188
  * INTERVAL_MS; for an idle broker (status=streaming, fps=0,
@@ -17194,39 +17208,54 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17194
17208
  maxDecodeFps: 5,
17195
17209
  initialReconnectDelayMs: 1e3,
17196
17210
  maxReconnectDelayMs: 3e4,
17197
- catalogReconcileIntervalSec: 30
17211
+ catalogReconcileIntervalSec: 30,
17212
+ binaryPath: "",
17213
+ hwaccel: "copy"
17198
17214
  });
17199
17215
  }
17200
17216
  /**
17201
- * Read the cluster `ffmpeg` config section (binaryPath / hwAccel /
17202
- * threadCount) and push it into the broker. Defensive: any missing or
17203
- * malformed field falls back to the safe default (PATH `ffmpeg`, `'copy'`
17204
- * software decode, `0` threads), and a read failure leaves the defaults in
17205
- * place. `'copy'` is the safe default it decodes every HEVC source rather
17206
- * 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).
17207
17241
  */
17208
17242
  async loadFfmpegConfig() {
17209
17243
  if (!this.brokerManager) return;
17210
17244
  try {
17211
- const section = await this.ctx.settings?.getSection("ffmpeg") ?? {};
17212
- const binaryPathRaw = section["binaryPath"];
17213
- const binaryPath = typeof binaryPathRaw === "string" && binaryPathRaw.trim().length > 0 ? binaryPathRaw : "ffmpeg";
17214
- const hwAccel = isFfmpegHwAccel(section["hwAccel"]) ? section["hwAccel"] : "copy";
17215
- const threadCountRaw = section["threadCount"];
17216
- 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;
17217
17247
  this.brokerManager.setFfmpegConfig({
17218
17248
  binaryPath,
17219
17249
  hwAccel,
17220
- threadCount
17250
+ threadCount: 0
17221
17251
  });
17222
17252
  this.ffmpegBinaryPath = binaryPath;
17223
17253
  this.ctx.logger.info("Loaded ffmpeg config", { meta: {
17224
17254
  binaryPath,
17225
- hwAccel,
17226
- threadCount
17255
+ hwAccel
17227
17256
  } });
17228
17257
  } catch (err) {
17229
- 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) } });
17230
17259
  }
17231
17260
  }
17232
17261
  /**
@@ -17234,21 +17263,19 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17234
17263
  * configured `ffmpeg.hwAccel` preference (default `'copy'` = software):
17235
17264
  * - `'auto'` → the literal `'auto'` (ffmpeg self-selects + software-fallbacks).
17236
17265
  * - `'copy'`/`'none'` → `null` (software decode).
17237
- * - a specific backend → gated against what the configured ffmpeg can decode
17238
- * (`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
17239
17268
  * per-stream miss.
17240
17269
  * Mirrors the transcode egress's resolution.
17241
17270
  */
17242
17271
  async resolveTranscodeDecodeHwAccel() {
17243
17272
  try {
17244
- const section = await this.ctx.settings?.getSection("ffmpeg") ?? {};
17245
- const strategy = classifyDecodeHwAccel(isFfmpegHwAccel(section["hwAccel"]) ? section["hwAccel"] : "copy");
17273
+ const strategy = classifyDecodeHwAccel(this.config.hwaccel);
17246
17274
  if (strategy.kind === "auto") return "auto";
17247
17275
  if (strategy.kind === "software") return null;
17248
- const pin = this.localNodeId ? require_dist.nodePin(this.localNodeId) : void 0;
17249
- const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: strategy.prefer }, pin);
17250
- const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query(void 0, pin);
17251
- 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;
17252
17279
  } catch (err) {
17253
17280
  this.ctx.logger.warn("resolveTranscodeDecodeHwAccel failed — software decode", { meta: { error: require_dist.errMsg(err) } });
17254
17281
  return null;
@@ -17260,8 +17287,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17260
17287
  this.brokerManager.setEventBus(this.ctx.eventBus);
17261
17288
  const localNode = this.ctx.kernel.localNodeId;
17262
17289
  const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
17263
- this.localNodeId = localNodeId;
17264
- this.brokerManager.setApiAccess(this.ctx.api, localNodeId);
17290
+ this.brokerManager.setApiAccess(this.ctx.api, localNodeId, this.ctx.kernel.hwaccel);
17265
17291
  if (this.capabilities) this.brokerManager.setCapabilitiesAccess(this.capabilities);
17266
17292
  await this.loadFfmpegConfig();
17267
17293
  const rtspProvider = this.brokerManager.getRtspRestreamProvider();
@@ -17720,6 +17746,27 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17720
17746
  step: 1,
17721
17747
  default: 5
17722
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
+ },
17723
17770
  {
17724
17771
  type: "number",
17725
17772
  key: "catalogReconcileIntervalSec",
@@ -17789,7 +17836,7 @@ function codecToInputFormat(codec) {
17789
17836
  function buildFfmpegArgs(config) {
17790
17837
  const inputFormat = codecToInputFormat(config.codec);
17791
17838
  const args = [
17792
- ...require_frame_dropper.logBannerArgs("error"),
17839
+ ...require_ffmpeg_args.logBannerArgs("error"),
17793
17840
  "-fflags",
17794
17841
  "+nobuffer+flush_packets",
17795
17842
  "-flags",