@camstack/addon-pipeline 1.1.28 → 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 (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 +204 -180
  10. package/dist/detection-pipeline/index.mjs +204 -180
  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
@@ -1,8 +1,9 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as errMsg, G as EventCategory, H as CAM_PROFILE_ORDER, J as makeProfileBrokerId, K as createEvent, L as streamBrokerCapability, O as maskUrlCredentials, U as DeviceFeature, V as BaseAddon, W as DeviceType, Y as makeSourceBrokerId, Z as parseProfileBrokerId, at as literal, ct as record, et as _enum, h as addonWidgetsSourceCapability, j as nodePin, l as EncodeProfileSchema, lt as string, nt as boolean, ot as number, p as RingBuffer, rt as discriminatedUnion, st as object, tt as array, ut as union, y as cameraStreamsCapability, z as webrtcSessionCapability } from "../dist-C6_wgXqF.mjs";
3
- import { n as DecoderSessionProxy, t as FrameHandlePlane } from "../frame-handle-plane-BIoY6nRV.mjs";
2
+ import { B as errMsg, G as EventCategory, H as CAM_PROFILE_ORDER, J as makeProfileBrokerId, K as createEvent, L as streamBrokerCapability, O as maskUrlCredentials, U as DeviceFeature, V as BaseAddon, W as DeviceType, Y as makeSourceBrokerId, Z as parseProfileBrokerId, at as literal, ct as record, et as _enum, h as addonWidgetsSourceCapability, j as nodePin, l as EncodeProfileSchema, lt as string, nt as boolean, ot as number, p as RingBuffer, rt as discriminatedUnion, st as object, tt as array, ut as union, y as cameraStreamsCapability, z as webrtcSessionCapability } from "../dist-DjuGmyG9.mjs";
3
+ import { n as DecoderSessionProxy, t as FrameHandlePlane } from "../frame-handle-plane-Bkxz-TTD.mjs";
4
4
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
- import { n as audioEncoderArgs, r as logBannerArgs, t as FrameDropper } from "../frame-dropper-CwkBTPGV.mjs";
5
+ import { f as rankDecodeHwAccels, g as logBannerArgs, h as audioEncoderArgs } from "../ffmpeg-args-C5GPp8Cw.mjs";
6
+ import { t as FrameDropper } from "../frame-dropper-AjheBGMG.mjs";
6
7
  import { createRequire } from "node:module";
7
8
  import * as crypto$1 from "node:crypto";
8
9
  import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -5908,24 +5909,6 @@ function toAudioSidecarCodec(audio) {
5908
5909
  return audio === "aac" || audio === "opus" || audio === "pcmu" ? audio : null;
5909
5910
  }
5910
5911
  /**
5911
- * Pick the decode `-hwaccel` backend, gated by what the configured ffmpeg
5912
- * binary actually supports (`platformProbe.getHardwareDecodeAccels`).
5913
- *
5914
- * `preferred` entries are literal `-hwaccel` names (kernel-resolved), so they
5915
- * intersect directly with the probed method list. Conservative by design:
5916
- * - empty `preferred` → `null` (software decode);
5917
- * - empty `supportedMethods` (probe miss / build reports none) → keep the top
5918
- * preference and let the egress's per-stream software fallback cover failures;
5919
- * - otherwise return the first preference the build supports, or `null` when it
5920
- * supports none of them (positive evidence to skip a doomed hw-decode spawn).
5921
- */
5922
- function pickDecodeHwAccel(preferred, supportedMethods) {
5923
- if (preferred.length === 0) return null;
5924
- if (supportedMethods.length === 0) return preferred[0] ?? null;
5925
- const supported = new Set(supportedMethods.map((m) => m.toLowerCase()));
5926
- return preferred.find((name) => supported.has(name.toLowerCase())) ?? null;
5927
- }
5928
- /**
5929
5912
  * Pick the video encoder, honouring the `hwAccel` preference: `'none'`/`'copy'`
5930
5913
  * (software-decode settings) force the software ENCODER too; otherwise the
5931
5914
  * probe's default (hw when available) is used, falling back to software when no
@@ -5940,6 +5923,60 @@ function pickVideoEncoder(target, encoders, hwAccel) {
5940
5923
  if (hwAccel === "none" || hwAccel === "copy" || !encoders) return software;
5941
5924
  return target === "H264" ? encoders.defaultH264 : encoders.defaultH265;
5942
5925
  }
5926
+ /**
5927
+ * The h264/hevc hardware ENCODER ids for a decode-hwaccel backend name — a
5928
+ * static, deterministic map (the same shape Scrypted uses: platform → encoder,
5929
+ * no probe). Consumed by {@link localHardwareEncoders}. `null` for a backend
5930
+ * with no hardware encoder (software / unknown).
5931
+ */
5932
+ var ENCODER_IDS_BY_BACKEND = {
5933
+ videotoolbox: {
5934
+ h264: "h264_videotoolbox",
5935
+ h265: "hevc_videotoolbox"
5936
+ },
5937
+ vaapi: {
5938
+ h264: "h264_vaapi",
5939
+ h265: "hevc_vaapi"
5940
+ },
5941
+ cuda: {
5942
+ h264: "h264_nvenc",
5943
+ h265: "hevc_nvenc"
5944
+ },
5945
+ nvdec: {
5946
+ h264: "h264_nvenc",
5947
+ h265: "hevc_nvenc"
5948
+ },
5949
+ qsv: {
5950
+ h264: "h264_qsv",
5951
+ h265: "hevc_qsv"
5952
+ },
5953
+ amf: {
5954
+ h264: "h264_amf",
5955
+ h265: "hevc_amf"
5956
+ }
5957
+ };
5958
+ /**
5959
+ * Build the {@link HardwareEncoders} default pair for THIS node's decode-hwaccel
5960
+ * backend — resolved LOCALLY from `ctx.kernel.hwaccel` — WITHOUT probing
5961
+ * `ffmpeg -encoders` via the `platform-probe` singleton (whose per-node
5962
+ * `nodePin` does not survive a forked agent child's link chain → it answers with
5963
+ * the HUB's encoders). Mirrors how Scrypted derives encoders: a static
5964
+ * platform→encoder map. Returns `null` for software / an unmapped backend, which
5965
+ * makes {@link pickVideoEncoder} fall back to the software encoder. Only
5966
+ * `defaultH264`/`defaultH265` are consumed downstream, so `encoders` is left
5967
+ * empty (the per-encoder capability list was never read here).
5968
+ */
5969
+ function localHardwareEncoders(backend) {
5970
+ if (backend === null) return null;
5971
+ const ids = ENCODER_IDS_BY_BACKEND[backend.toLowerCase()];
5972
+ if (!ids) return null;
5973
+ return {
5974
+ encoders: [],
5975
+ defaultH264: ids.h264,
5976
+ defaultH265: ids.h265,
5977
+ probedAt: Date.now()
5978
+ };
5979
+ }
5943
5980
  /** Audio encoder args for a transcode target (preset values over the shared low-level builder). */
5944
5981
  function pickAudioEncoderArgs(audio) {
5945
5982
  switch (audio) {
@@ -11360,8 +11397,7 @@ var TranscodePipelineManager = class {
11360
11397
  * async egress start so concurrent acquires share one egress. */
11361
11398
  pending = /* @__PURE__ */ new Map();
11362
11399
  logger;
11363
- api;
11364
- localNodeId;
11400
+ hwaccel;
11365
11401
  cameraStreamLookup;
11366
11402
  rtspEntryLookup;
11367
11403
  resolveProfileSource;
@@ -11372,8 +11408,7 @@ var TranscodePipelineManager = class {
11372
11408
  acquireChannel;
11373
11409
  constructor(opts) {
11374
11410
  this.logger = opts.logger;
11375
- this.api = opts.api;
11376
- this.localNodeId = opts.localNodeId ?? null;
11411
+ this.hwaccel = opts.hwaccel ?? null;
11377
11412
  this.cameraStreamLookup = opts.cameraStreamLookup;
11378
11413
  this.rtspEntryLookup = opts.rtspEntryLookup;
11379
11414
  this.resolveProfileSource = opts.resolveProfileSource ?? null;
@@ -11575,21 +11610,20 @@ var TranscodePipelineManager = class {
11575
11610
  return stream;
11576
11611
  }
11577
11612
  /**
11578
- * Per-call pin routing every `platform-probe` read to THIS node's probe.
11579
- * The cap is a `singleton` unpinned, an agent child gets the HUB's
11580
- * hardware answer. `undefined` (legacy unpinned) when the node id is
11581
- * unknown.
11613
+ * The h264/hevc hardware ENCODER defaults for THIS node — derived locally
11614
+ * from `ctx.kernel.hwaccel` (os.platform + GPU-vendor probe) via a static
11615
+ * backend→encoder map, NOT the `platform-probe` singleton's `ffmpeg -encoders`
11616
+ * probe (whose per-node `nodePin` answers with the HUB's encoders). `null` →
11617
+ * software encode (also the outcome when no local resolver is wired). vaapi is
11618
+ * ranked above qsv (qsv is broken on the Intel stack) so an Intel node
11619
+ * transcodes with `h264_vaapi`.
11582
11620
  */
11583
- probePin() {
11584
- return this.localNodeId ? nodePin(this.localNodeId) : void 0;
11585
- }
11586
- /** Query the typed, cached hardware-encoder probe; null on any failure. */
11587
11621
  async resolveHardwareEncoders() {
11588
- if (!this.api) return null;
11622
+ if (!this.hwaccel) return null;
11589
11623
  try {
11590
- return await this.api.platformProbe.getHardwareEncoders.query(void 0, this.probePin());
11624
+ return localHardwareEncoders(rankDecodeHwAccels((await this.hwaccel.resolve(null)).preferred, [])[0] ?? null);
11591
11625
  } catch (err) {
11592
- this.logger.warn("platformProbe.getHardwareEncoders failed — software encode fallback", { meta: { error: errMsg(err) } });
11626
+ this.logger.warn("local hardware-encoder resolve failed — software encode fallback", { meta: { error: errMsg(err) } });
11593
11627
  return null;
11594
11628
  }
11595
11629
  }
@@ -11617,35 +11651,20 @@ var TranscodePipelineManager = class {
11617
11651
  if (strategy.kind === "software") return null;
11618
11652
  return this.resolveSpecificDecodeHwAccel(strategy.prefer);
11619
11653
  }
11620
- /** Gate a specific backend against the probed support list; null on miss/error. */
11621
- async resolveSpecificDecodeHwAccel(prefer) {
11622
- if (!this.api) return null;
11623
- try {
11624
- const res = await this.api.platformProbe.resolveHwAccel.query({ prefer }, this.probePin());
11625
- const supported = await this.resolveDecodeAccelMethods();
11626
- const picked = pickDecodeHwAccel(res.preferred, supported);
11627
- if (picked === null && res.preferred.length > 0) this.logger.info("decode hw-accel: configured ffmpeg supports none of the preferred backends — software decode", { meta: {
11628
- preferred: res.preferred,
11629
- supported
11630
- } });
11631
- return picked;
11632
- } catch (err) {
11633
- this.logger.warn("platformProbe.resolveHwAccel failed — software decode fallback", { meta: { error: errMsg(err) } });
11634
- return null;
11635
- }
11636
- }
11637
11654
  /**
11638
- * The `-hwaccel` methods the configured ffmpeg supports; `[]` on any failure
11639
- * (treated as "unknown" by {@link pickDecodeHwAccel}, which then keeps the
11640
- * top preference rather than filtering it out).
11655
+ * Resolve a specific operator-forced decode backend against THIS node's local
11656
+ * hwaccel resolver. `ctx.kernel.hwaccel.resolve(prefer)` echoes the forced
11657
+ * backend (re-ranked so vaapi precedes qsv); the egress's per-stream software
11658
+ * fallback still catches a backend the actual ffmpeg build can't drive. Null
11659
+ * when no local resolver is wired.
11641
11660
  */
11642
- async resolveDecodeAccelMethods() {
11643
- if (!this.api) return [];
11661
+ async resolveSpecificDecodeHwAccel(prefer) {
11662
+ if (!this.hwaccel) return null;
11644
11663
  try {
11645
- return (await this.api.platformProbe.getHardwareDecodeAccels.query(void 0, this.probePin())).methods;
11664
+ return rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
11646
11665
  } catch (err) {
11647
- this.logger.warn("platformProbe.getHardwareDecodeAccels failed — decode gate skipped", { meta: { error: errMsg(err) } });
11648
- return [];
11666
+ this.logger.warn("local decode hw-accel resolve failed — software decode fallback", { meta: { error: errMsg(err) } });
11667
+ return null;
11649
11668
  }
11650
11669
  }
11651
11670
  shareEntry(entry) {
@@ -11963,6 +11982,14 @@ var StreamBrokerManager = class {
11963
11982
  */
11964
11983
  api = null;
11965
11984
  /**
11985
+ * This node's LOCAL hwaccel resolver (`ctx.kernel.hwaccel`), wired by
11986
+ * `setApiAccess`. Drives the transcode manager's decode/encode backend picks
11987
+ * and `getHwAccelOptions` from THIS node's hardware in-process — never the
11988
+ * `platform-probe` singleton (whose per-node `nodePin` answers with the hub's
11989
+ * hardware from a forked agent child).
11990
+ */
11991
+ hwaccel = null;
11992
+ /**
11966
11993
  * This broker node's own Moleculer nodeID (`ctx.kernel.localNodeId`). The
11967
11994
  * shm frame plane's decoder sessions MUST be co-located on this node — an
11968
11995
  * shm frame ring is node-local, so a decoder placed on any OTHER node can
@@ -12004,8 +12031,7 @@ var StreamBrokerManager = class {
12004
12031
  getTranscodeManager() {
12005
12032
  if (!this.transcodeManager) this.transcodeManager = new TranscodePipelineManager({
12006
12033
  logger: this.logger.child("transcode"),
12007
- api: this.api,
12008
- localNodeId: this.localNodeId ?? void 0,
12034
+ hwaccel: this.hwaccel ?? void 0,
12009
12035
  cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
12010
12036
  rtspEntryLookup: (brokerId) => {
12011
12037
  const entry = this.rtspProvider.getEntry(brokerId);
@@ -12211,9 +12237,10 @@ var StreamBrokerManager = class {
12211
12237
  * resolve the `decoder` capability via tRPC — see
12212
12238
  * `buildDecoderCapApi` for the full rationale.
12213
12239
  */
12214
- setApiAccess(api, localNodeId) {
12240
+ setApiAccess(api, localNodeId, hwaccel) {
12215
12241
  this.api = api;
12216
12242
  if (localNodeId !== void 0) this.localNodeId = localNodeId;
12243
+ if (hwaccel !== void 0) this.hwaccel = hwaccel;
12217
12244
  }
12218
12245
  /** Update the system `ffmpeg` config snapshot used by the transcode egress. */
12219
12246
  setFfmpegConfig(cfg) {
@@ -12221,11 +12248,11 @@ var StreamBrokerManager = class {
12221
12248
  }
12222
12249
  /**
12223
12250
  * The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
12224
- * settings (`copy`, `auto`, `none`) plus the concrete backends the configured
12225
- * ffmpeg build actually supports (probed via `getHardwareDecodeAccels`). Lets
12226
- * a settings form offer exactly the backends this host can use, instead of the
12227
- * full static list that may include unsupported ones. Best-effort: on a probe
12228
- * miss it returns the always-valid settings only.
12251
+ * settings (`copy`, `auto`, `none`) plus the concrete hardware backends THIS
12252
+ * node can use, resolved from the LOCAL `ctx.kernel.hwaccel` (os.platform +
12253
+ * GPU-vendor probe), ranked so vaapi precedes qsv. Node-local never the
12254
+ * `platform-probe` singleton (which answers with the hub's methods from a
12255
+ * forked agent broker). Best-effort: no resolver / error → always-valid only.
12229
12256
  */
12230
12257
  async getHwAccelOptions() {
12231
12258
  const always = [
@@ -12233,13 +12260,12 @@ var StreamBrokerManager = class {
12233
12260
  "auto",
12234
12261
  "none"
12235
12262
  ];
12236
- if (!this.api) return always;
12263
+ if (!this.hwaccel) return always;
12237
12264
  try {
12238
- const pin = this.localNodeId ? nodePin(this.localNodeId) : void 0;
12239
- const probed = (await this.api.platformProbe.getHardwareDecodeAccels.query(void 0, pin)).methods.filter((m) => FFMPEG_HW_ACCEL_SETTINGS.includes(m));
12265
+ const probed = rankDecodeHwAccels((await this.hwaccel.resolve(null)).preferred, []).filter((m) => FFMPEG_HW_ACCEL_SETTINGS.includes(m));
12240
12266
  return [...always, ...probed];
12241
12267
  } catch (err) {
12242
- this.logger.warn("getHwAccelOptions: getHardwareDecodeAccels failed — static options only", { meta: { error: errMsg(err) } });
12268
+ this.logger.warn("getHwAccelOptions: local hwaccel resolve failed — static options only", { meta: { error: errMsg(err) } });
12243
12269
  return always;
12244
12270
  }
12245
12271
  }
@@ -17070,9 +17096,6 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
17070
17096
  };
17071
17097
  //#endregion
17072
17098
  //#region src/stream-broker/addon.ts
17073
- function isFfmpegHwAccel(v) {
17074
- return typeof v === "string" && FFMPEG_HW_ACCEL_SETTINGS.includes(v);
17075
- }
17076
17099
  /**
17077
17100
  * Hydrate the persisted RTSP restream state (`rtspTokens` / `rtspEnabled`)
17078
17101
  * through the durable `state()` handles — the sanctioned settings accessor —
@@ -17156,15 +17179,6 @@ var StreamBrokerAddon = class extends BaseAddon {
17156
17179
  /** Handle for the embed SPA data-plane listener (disposed on shutdown). */
17157
17180
  embedDataPlane = null;
17158
17181
  /**
17159
- * This broker's bare cluster node id (`ctx.kernel.localNodeId` with any
17160
- * `node/addon` fork suffix stripped), computed once in onInitialize.
17161
- * `platform-probe` is a `singleton` cap, so every probe read (transcode
17162
- * decode hw-accel) MUST be pinned here — an unpinned call from an agent
17163
- * child resolves to the HUB's probe (the wrong hardware). `undefined` when
17164
- * the kernel id is unknown → legacy unpinned behaviour.
17165
- */
17166
- localNodeId;
17167
- /**
17168
17182
  * Snapshot-equality cache for the broker-metrics emit. Each
17169
17183
  * broker emits a stats snapshot every BROKER_METRICS_SNAPSHOT_-
17170
17184
  * INTERVAL_MS; for an idle broker (status=streaming, fps=0,
@@ -17190,39 +17204,54 @@ var StreamBrokerAddon = class extends BaseAddon {
17190
17204
  maxDecodeFps: 5,
17191
17205
  initialReconnectDelayMs: 1e3,
17192
17206
  maxReconnectDelayMs: 3e4,
17193
- catalogReconcileIntervalSec: 30
17207
+ catalogReconcileIntervalSec: 30,
17208
+ binaryPath: "",
17209
+ hwaccel: "copy"
17194
17210
  });
17195
17211
  }
17196
17212
  /**
17197
- * Read the cluster `ffmpeg` config section (binaryPath / hwAccel /
17198
- * threadCount) and push it into the broker. Defensive: any missing or
17199
- * malformed field falls back to the safe default (PATH `ffmpeg`, `'copy'`
17200
- * software decode, `0` threads), and a read failure leaves the defaults in
17201
- * place. `'copy'` is the safe default it decodes every HEVC source rather
17202
- * than hard-failing on a fragile one (the forced-backend trap).
17213
+ * Resolve the ffmpeg binary for THIS node: a per-node `binaryPath` override
17214
+ * (`binaryPath@<node>`) else `ensureFfmpeg()` which provisions a guaranteed
17215
+ * portable static binary locally (hub / agent / Mac). Replaces the retired
17216
+ * global `ffmpeg` system section (node-local/empty on agents a cross-node
17217
+ * footgun). Falls back to PATH `ffmpeg` only if the provision genuinely fails.
17218
+ */
17219
+ async resolveFfmpegBinaryPath() {
17220
+ const override = this.config.binaryPath;
17221
+ if (typeof override === "string" && override.trim().length > 0) return override;
17222
+ try {
17223
+ return await this.ctx.deps.ensureFfmpeg();
17224
+ } catch (err) {
17225
+ this.ctx.logger.error("stream-broker: ensureFfmpeg() failed to provision ffmpeg — falling back to PATH \"ffmpeg\"", { meta: { error: errMsg(err) } });
17226
+ return "ffmpeg";
17227
+ }
17228
+ }
17229
+ /**
17230
+ * Push THIS node's ffmpeg config into the broker — all node-local now, NOT the
17231
+ * retired global `ffmpeg` section. `binaryPath` via {@link resolveFfmpegBinaryPath}
17232
+ * (per-node override → ensureFfmpeg); `hwAccel` = the per-node `hwaccel`
17233
+ * preference (default `'copy'` = software, the safe HEVC-compatible choice; the
17234
+ * concrete backend is resolved node-locally in `resolveTranscodeDecodeHwAccel`
17235
+ * / the transcode manager). `threadCount` is always `0` (auto — ffmpeg uses the
17236
+ * node's own cores; the manual knob was removed).
17203
17237
  */
17204
17238
  async loadFfmpegConfig() {
17205
17239
  if (!this.brokerManager) return;
17206
17240
  try {
17207
- const section = await this.ctx.settings?.getSection("ffmpeg") ?? {};
17208
- const binaryPathRaw = section["binaryPath"];
17209
- const binaryPath = typeof binaryPathRaw === "string" && binaryPathRaw.trim().length > 0 ? binaryPathRaw : "ffmpeg";
17210
- const hwAccel = isFfmpegHwAccel(section["hwAccel"]) ? section["hwAccel"] : "copy";
17211
- const threadCountRaw = section["threadCount"];
17212
- const threadCount = typeof threadCountRaw === "number" && Number.isFinite(threadCountRaw) && threadCountRaw >= 0 ? Math.floor(threadCountRaw) : 0;
17241
+ const binaryPath = await this.resolveFfmpegBinaryPath();
17242
+ const hwAccel = this.config.hwaccel;
17213
17243
  this.brokerManager.setFfmpegConfig({
17214
17244
  binaryPath,
17215
17245
  hwAccel,
17216
- threadCount
17246
+ threadCount: 0
17217
17247
  });
17218
17248
  this.ffmpegBinaryPath = binaryPath;
17219
17249
  this.ctx.logger.info("Loaded ffmpeg config", { meta: {
17220
17250
  binaryPath,
17221
- hwAccel,
17222
- threadCount
17251
+ hwAccel
17223
17252
  } });
17224
17253
  } catch (err) {
17225
- this.ctx.logger.warn("Failed to read ffmpeg config section — using defaults", { meta: { error: errMsg(err) } });
17254
+ this.ctx.logger.warn("Failed to load ffmpeg config — using defaults", { meta: { error: errMsg(err) } });
17226
17255
  }
17227
17256
  }
17228
17257
  /**
@@ -17230,21 +17259,19 @@ var StreamBrokerAddon = class extends BaseAddon {
17230
17259
  * configured `ffmpeg.hwAccel` preference (default `'copy'` = software):
17231
17260
  * - `'auto'` → the literal `'auto'` (ffmpeg self-selects + software-fallbacks).
17232
17261
  * - `'copy'`/`'none'` → `null` (software decode).
17233
- * - a specific backend → gated against what the configured ffmpeg can decode
17234
- * (`getHardwareDecodeAccels`); the feed still falls back to software on a
17262
+ * - a specific backend → resolved from THIS node's LOCAL `ctx.kernel.hwaccel`
17263
+ * (ranked so vaapi precedes qsv); the feed still falls back to software on a
17235
17264
  * per-stream miss.
17236
17265
  * Mirrors the transcode egress's resolution.
17237
17266
  */
17238
17267
  async resolveTranscodeDecodeHwAccel() {
17239
17268
  try {
17240
- const section = await this.ctx.settings?.getSection("ffmpeg") ?? {};
17241
- const strategy = classifyDecodeHwAccel(isFfmpegHwAccel(section["hwAccel"]) ? section["hwAccel"] : "copy");
17269
+ const strategy = classifyDecodeHwAccel(this.config.hwaccel);
17242
17270
  if (strategy.kind === "auto") return "auto";
17243
17271
  if (strategy.kind === "software") return null;
17244
- const pin = this.localNodeId ? nodePin(this.localNodeId) : void 0;
17245
- const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: strategy.prefer }, pin);
17246
- const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query(void 0, pin);
17247
- return pickDecodeHwAccel(res.preferred, supported.methods);
17272
+ const resolver = this.ctx.kernel.hwaccel;
17273
+ if (!resolver) return null;
17274
+ return rankDecodeHwAccels((await resolver.resolve(strategy.prefer)).preferred, [])[0] ?? null;
17248
17275
  } catch (err) {
17249
17276
  this.ctx.logger.warn("resolveTranscodeDecodeHwAccel failed — software decode", { meta: { error: errMsg(err) } });
17250
17277
  return null;
@@ -17256,8 +17283,7 @@ var StreamBrokerAddon = class extends BaseAddon {
17256
17283
  this.brokerManager.setEventBus(this.ctx.eventBus);
17257
17284
  const localNode = this.ctx.kernel.localNodeId;
17258
17285
  const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
17259
- this.localNodeId = localNodeId;
17260
- this.brokerManager.setApiAccess(this.ctx.api, localNodeId);
17286
+ this.brokerManager.setApiAccess(this.ctx.api, localNodeId, this.ctx.kernel.hwaccel);
17261
17287
  if (this.capabilities) this.brokerManager.setCapabilitiesAccess(this.capabilities);
17262
17288
  await this.loadFfmpegConfig();
17263
17289
  const rtspProvider = this.brokerManager.getRtspRestreamProvider();
@@ -17716,6 +17742,27 @@ var StreamBrokerAddon = class extends BaseAddon {
17716
17742
  step: 1,
17717
17743
  default: 5
17718
17744
  },
17745
+ {
17746
+ type: "select",
17747
+ key: "hwaccel",
17748
+ label: "Transcode hardware acceleration",
17749
+ 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.",
17750
+ options: [...FFMPEG_HW_ACCEL_SETTINGS].map((s) => ({
17751
+ value: s,
17752
+ label: hwAccelOptionLabel(s)
17753
+ })),
17754
+ default: "copy",
17755
+ perNode: true
17756
+ },
17757
+ {
17758
+ type: "text",
17759
+ key: "binaryPath",
17760
+ label: "ffmpeg binary path (override)",
17761
+ description: "Optional per-node override for the ffmpeg binary. Leave empty to auto-provision a portable static ffmpeg on this node (recommended).",
17762
+ placeholder: "ffmpeg",
17763
+ default: "",
17764
+ perNode: true
17765
+ },
17719
17766
  {
17720
17767
  type: "number",
17721
17768
  key: "catalogReconcileIntervalSec",
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DoReAb4y.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
@@ -1,4 +1,4 @@
1
- import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-CeZV1B-2.js";var o=n(t()),s=n(e(),1),c=Math.PI/180;function l(){return typeof window<`u`&&({}.toString.call(window)===`[object Window]`||{}.toString.call(window)===`[object global]`)}var u=typeof global<`u`?global:typeof window<`u`?window:typeof WorkerGlobalScope<`u`?self:{},d={_global:u,version:`10.3.0`,isBrowser:l(),isUnminified:/param/.test(function(e){}.toString()),dblClickWindow:400,getAngle(e){return d.angleDeg?e*c:e},enableTrace:!1,pointerEventsEnabled:!0,autoDrawEnabled:!0,hitOnDragEnabled:!1,capturePointerEventsEnabled:!1,_mouseListenClick:!1,_touchListenClick:!1,_pointerListenClick:!1,_mouseInDblClickWindow:!1,_touchInDblClickWindow:!1,_pointerInDblClickWindow:!1,_mouseDblClickPointerId:null,_touchDblClickPointerId:null,_pointerDblClickPointerId:null,_renderBackend:`web`,legacyTextRendering:!1,pixelRatio:typeof window<`u`&&window.devicePixelRatio||1,dragDistance:3,angleDeg:!0,showWarnings:!0,dragButtons:[0,1],isDragging(){return d.DD.isDragging},isTransforming(){return d.Transformer?.isTransforming()??!1},isDragReady(){return!!d.DD.node},releaseCanvasOnDestroy:!0,document:u.document,_injectGlobal(e){u.Konva!==void 0&&console.error(`Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.`),u.Konva=e}},f=e=>{d[e.prototype.getClassName()]=e};d._injectGlobal(d);var p=`Konva.js unsupported environment.
1
+ import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-CpUy8OIE.js";var o=n(t()),s=n(e(),1),c=Math.PI/180;function l(){return typeof window<`u`&&({}.toString.call(window)===`[object Window]`||{}.toString.call(window)===`[object global]`)}var u=typeof global<`u`?global:typeof window<`u`?window:typeof WorkerGlobalScope<`u`?self:{},d={_global:u,version:`10.3.0`,isBrowser:l(),isUnminified:/param/.test(function(e){}.toString()),dblClickWindow:400,getAngle(e){return d.angleDeg?e*c:e},enableTrace:!1,pointerEventsEnabled:!0,autoDrawEnabled:!0,hitOnDragEnabled:!1,capturePointerEventsEnabled:!1,_mouseListenClick:!1,_touchListenClick:!1,_pointerListenClick:!1,_mouseInDblClickWindow:!1,_touchInDblClickWindow:!1,_pointerInDblClickWindow:!1,_mouseDblClickPointerId:null,_touchDblClickPointerId:null,_pointerDblClickPointerId:null,_renderBackend:`web`,legacyTextRendering:!1,pixelRatio:typeof window<`u`&&window.devicePixelRatio||1,dragDistance:3,angleDeg:!0,showWarnings:!0,dragButtons:[0,1],isDragging(){return d.DD.isDragging},isTransforming(){return d.Transformer?.isTransforming()??!1},isDragReady(){return!!d.DD.node},releaseCanvasOnDestroy:!0,document:u.document,_injectGlobal(e){u.Konva!==void 0&&console.error(`Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.`),u.Konva=e}},f=e=>{d[e.prototype.getClassName()]=e};d._injectGlobal(d);var p=`Konva.js unsupported environment.
2
2
 
3
3
  Looks like you are trying to use Konva.js in Node.js environment. because "document" object is undefined.
4
4
 
@@ -1 +1 @@
1
- import{a as e,c as t,d as n,f as r,g as i,l as a,o,r as s,s as c,u as l}from"./index-CeZV1B-2.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js";var d=i(r(),1),f=i(n(),1),p=o(`grid-2x2`,[[`path`,{d:`M12 3v18`,key:`108xh3`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`rect`,{x:`3`,y:`3`,width:`18`,height:`18`,rx:`2`,key:`h1oib`}]]),m=110,h=`motion-zones`,g=0,_=[1,2,3],v=1,y=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,b=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function x(e,t,n){let r=t*n,i=Array.from({length:r});for(let t=0;t<r;t+=1)i[t]=e[t]===!0;return i}function S(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(e[n]!==t[n])return!1;return!0}function C(e,t){return Math.ceil(e/t)}function w(e,t,n){return Math.min(n-1,Math.floor((e+.5)/t*n))}function T(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:i*a},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)e[r*t+n]===!0&&(o[s*i+w(n,t,i)]=!0)}return o}function E(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:t*n},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)o[r*t+n]=e[s*i+w(n,t,i)]===!0}return o}function D({deviceId:n}){let r=t(l().trpcClient,n),[i,o]=(0,d.useState)(null),[w,D]=(0,d.useState)(!1),[O,k]=(0,d.useState)(null),[A,j]=(0,d.useState)(null),[M,N]=(0,d.useState)(v),[P,F]=(0,d.useState)(!1),[I,L]=(0,d.useState)(!1),R=(0,d.useRef)(!1);(0,d.useEffect)(()=>{if(!r)return;let e=!1;return R.current=!1,o(null),D(!1),k(null),j(null),N(v),L(!1),(async()=>{try{let t=await r.motionZones?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(o(t),R.current)return;let n=await r.motionZones?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);R.current=!0;let i=n.regions.find(e=>e.shape.kind===`grid`),a=x(i?i.shape.cells:[],t.grid.width,t.grid.height);j(a),k(T(a,t.grid.width,t.grid.height,v))}catch(t){if(e)return;c(t)?D(!0):console.error(`Motion Zones load failed`,t)}})(),()=>{e=!0}},[r]);let z=i?i.grid.width:0,B=i?i.grid.height:0,V=C(z,M),H=C(B,M),U=(0,d.useMemo)(()=>O&&i?E(O,z,B,M):null,[O,i,z,B,M]),W=(0,d.useMemo)(()=>U!==null&&A!==null&&!S(U,A),[U,A]),G=(0,d.useMemo)(()=>U?U.reduce((e,t)=>t?e+1:e,0):0,[U]),K=z*B,q=V*H,J=(0,d.useCallback)(e=>{q>0&&k(Array.from({length:q},()=>e))},[q]),Y=(0,d.useCallback)(()=>{k(e=>e&&e.map(e=>!e))},[]),X=(0,d.useCallback)(()=>{A&&i&&k(T(A,z,B,M))},[A,i,z,B,M]),Z=(0,d.useCallback)(e=>{e===M||!i||k(t=>{if(!t)return N(e),t;let n=T(E(t,z,B,M),z,B,e);return N(e),n})},[M,i,z,B]),Q=(0,d.useMemo)(()=>i&&O?[{id:g,shape:{kind:`grid`,gridWidth:V,gridHeight:H,cells:[...O]}}]:[],[i,O,V,H]),$=(0,d.useCallback)((e,t)=>{t.kind===`grid`&&k(t.cells)},[]),ee=(0,d.useCallback)(async()=>{if(!(!r||!O||!i)){F(!0);try{let e=E(O,i.grid.width,i.grid.height,M),t={kind:`grid`,gridWidth:i.grid.width,gridHeight:i.grid.height,cells:e};await r.motionZones?.setZone({patch:{regions:[{id:g,enabled:!0,shape:t}]}});let n=await r.motionZones?.getStatus({});if(n){let e=n.regions.find(e=>e.shape.kind===`grid`),t=x(e?e.shape.cells:[],i.grid.width,i.grid.height);j(t),k(T(t,i.grid.width,i.grid.height,M))}}catch(e){console.error(`Motion Zones save failed`,e)}finally{F(!1)}}},[r,O,i,M]);a((0,d.useMemo)(()=>I&&!w&&i&&O?{id:h,order:m,node:(0,f.jsx)(u,{transparent:!0,items:Q,supportedShapes:[`grid`],grid:{width:V,height:H},selectedId:g,onSelect:()=>{},onShapeChange:$,onDrawComplete:()=>{},drawingKind:null})}:null,[I,w,i,O,Q,$,V,H]));let te=!w&&i!==null&&O!==null;return r?(0,f.jsx)(e,{title:`Motion Zones`,icon:(0,f.jsx)(p,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,f.jsx)(`div`,{className:`flex flex-col gap-3`,children:w?(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`This camera doesn't expose an on-board motion zones grid.`}):te?(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`p`,{className:`${s} leading-relaxed`,children:[`Toggle `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Edit grid`}),` to paint the region directly on the live frame, then `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push the mask to the camera. Pick a bigger`,` `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Cell size`}),` for quicker, broad-stroke painting — it's resampled to the camera's native `,i.grid.width,`×`,i.grid.height,` grid on save (×1 is the finest).`]}),(0,f.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>L(e=>!e),disabled:P,"aria-pressed":I,className:I?b:y,children:I?`Done editing`:`Edit grid`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!0),disabled:P,className:y,children:`All on`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!1),disabled:P,className:y,children:`All off`}),(0,f.jsx)(`button`,{type:`button`,onClick:Y,disabled:P,className:y,children:`Invert`}),(0,f.jsxs)(`div`,{className:`flex items-center gap-1 ml-1`,role:`group`,"aria-label":`Cell size`,children:[(0,f.jsx)(`span`,{className:`${s} mr-0.5`,children:`Cell size`}),_.map(e=>(0,f.jsxs)(`button`,{type:`button`,onClick:()=>Z(e),disabled:P,"aria-pressed":M===e,title:e===1?`Camera grid ${z}×${B} (finest)`:`${C(z,e)}×${C(B,e)} painting grid · cells ×${e} bigger`,className:M===e?b:y,children:[`×`,e]},e))]}),(0,f.jsxs)(`span`,{className:`${s} ml-1 tabular-nums`,children:[G,` / `,K,` cells · `,i.grid.width,`×`,i.grid.height,M===1?``:` · paint ${V}×${H}`]}),(0,f.jsx)(`span`,{className:`flex-1`}),(0,f.jsx)(`button`,{type:`button`,onClick:X,disabled:P||!W,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>void ee(),disabled:P||!W,className:b,children:P?`Saving…`:`Save`})]})]}):(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`Loading the camera's grid…`})})}):null}export{D as MotionZonesSettings};
1
+ import{a as e,c as t,d as n,f as r,g as i,l as a,o,r as s,s as c,u as l}from"./index-CpUy8OIE.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js";var d=i(r(),1),f=i(n(),1),p=o(`grid-2x2`,[[`path`,{d:`M12 3v18`,key:`108xh3`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`rect`,{x:`3`,y:`3`,width:`18`,height:`18`,rx:`2`,key:`h1oib`}]]),m=110,h=`motion-zones`,g=0,_=[1,2,3],v=1,y=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,b=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function x(e,t,n){let r=t*n,i=Array.from({length:r});for(let t=0;t<r;t+=1)i[t]=e[t]===!0;return i}function S(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(e[n]!==t[n])return!1;return!0}function C(e,t){return Math.ceil(e/t)}function w(e,t,n){return Math.min(n-1,Math.floor((e+.5)/t*n))}function T(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:i*a},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)e[r*t+n]===!0&&(o[s*i+w(n,t,i)]=!0)}return o}function E(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:t*n},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)o[r*t+n]=e[s*i+w(n,t,i)]===!0}return o}function D({deviceId:n}){let r=t(l().trpcClient,n),[i,o]=(0,d.useState)(null),[w,D]=(0,d.useState)(!1),[O,k]=(0,d.useState)(null),[A,j]=(0,d.useState)(null),[M,N]=(0,d.useState)(v),[P,F]=(0,d.useState)(!1),[I,L]=(0,d.useState)(!1),R=(0,d.useRef)(!1);(0,d.useEffect)(()=>{if(!r)return;let e=!1;return R.current=!1,o(null),D(!1),k(null),j(null),N(v),L(!1),(async()=>{try{let t=await r.motionZones?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(o(t),R.current)return;let n=await r.motionZones?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);R.current=!0;let i=n.regions.find(e=>e.shape.kind===`grid`),a=x(i?i.shape.cells:[],t.grid.width,t.grid.height);j(a),k(T(a,t.grid.width,t.grid.height,v))}catch(t){if(e)return;c(t)?D(!0):console.error(`Motion Zones load failed`,t)}})(),()=>{e=!0}},[r]);let z=i?i.grid.width:0,B=i?i.grid.height:0,V=C(z,M),H=C(B,M),U=(0,d.useMemo)(()=>O&&i?E(O,z,B,M):null,[O,i,z,B,M]),W=(0,d.useMemo)(()=>U!==null&&A!==null&&!S(U,A),[U,A]),G=(0,d.useMemo)(()=>U?U.reduce((e,t)=>t?e+1:e,0):0,[U]),K=z*B,q=V*H,J=(0,d.useCallback)(e=>{q>0&&k(Array.from({length:q},()=>e))},[q]),Y=(0,d.useCallback)(()=>{k(e=>e&&e.map(e=>!e))},[]),X=(0,d.useCallback)(()=>{A&&i&&k(T(A,z,B,M))},[A,i,z,B,M]),Z=(0,d.useCallback)(e=>{e===M||!i||k(t=>{if(!t)return N(e),t;let n=T(E(t,z,B,M),z,B,e);return N(e),n})},[M,i,z,B]),Q=(0,d.useMemo)(()=>i&&O?[{id:g,shape:{kind:`grid`,gridWidth:V,gridHeight:H,cells:[...O]}}]:[],[i,O,V,H]),$=(0,d.useCallback)((e,t)=>{t.kind===`grid`&&k(t.cells)},[]),ee=(0,d.useCallback)(async()=>{if(!(!r||!O||!i)){F(!0);try{let e=E(O,i.grid.width,i.grid.height,M),t={kind:`grid`,gridWidth:i.grid.width,gridHeight:i.grid.height,cells:e};await r.motionZones?.setZone({patch:{regions:[{id:g,enabled:!0,shape:t}]}});let n=await r.motionZones?.getStatus({});if(n){let e=n.regions.find(e=>e.shape.kind===`grid`),t=x(e?e.shape.cells:[],i.grid.width,i.grid.height);j(t),k(T(t,i.grid.width,i.grid.height,M))}}catch(e){console.error(`Motion Zones save failed`,e)}finally{F(!1)}}},[r,O,i,M]);a((0,d.useMemo)(()=>I&&!w&&i&&O?{id:h,order:m,node:(0,f.jsx)(u,{transparent:!0,items:Q,supportedShapes:[`grid`],grid:{width:V,height:H},selectedId:g,onSelect:()=>{},onShapeChange:$,onDrawComplete:()=>{},drawingKind:null})}:null,[I,w,i,O,Q,$,V,H]));let te=!w&&i!==null&&O!==null;return r?(0,f.jsx)(e,{title:`Motion Zones`,icon:(0,f.jsx)(p,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,f.jsx)(`div`,{className:`flex flex-col gap-3`,children:w?(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`This camera doesn't expose an on-board motion zones grid.`}):te?(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`p`,{className:`${s} leading-relaxed`,children:[`Toggle `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Edit grid`}),` to paint the region directly on the live frame, then `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push the mask to the camera. Pick a bigger`,` `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Cell size`}),` for quicker, broad-stroke painting — it's resampled to the camera's native `,i.grid.width,`×`,i.grid.height,` grid on save (×1 is the finest).`]}),(0,f.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>L(e=>!e),disabled:P,"aria-pressed":I,className:I?b:y,children:I?`Done editing`:`Edit grid`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!0),disabled:P,className:y,children:`All on`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!1),disabled:P,className:y,children:`All off`}),(0,f.jsx)(`button`,{type:`button`,onClick:Y,disabled:P,className:y,children:`Invert`}),(0,f.jsxs)(`div`,{className:`flex items-center gap-1 ml-1`,role:`group`,"aria-label":`Cell size`,children:[(0,f.jsx)(`span`,{className:`${s} mr-0.5`,children:`Cell size`}),_.map(e=>(0,f.jsxs)(`button`,{type:`button`,onClick:()=>Z(e),disabled:P,"aria-pressed":M===e,title:e===1?`Camera grid ${z}×${B} (finest)`:`${C(z,e)}×${C(B,e)} painting grid · cells ×${e} bigger`,className:M===e?b:y,children:[`×`,e]},e))]}),(0,f.jsxs)(`span`,{className:`${s} ml-1 tabular-nums`,children:[G,` / `,K,` cells · `,i.grid.width,`×`,i.grid.height,M===1?``:` · paint ${V}×${H}`]}),(0,f.jsx)(`span`,{className:`flex-1`}),(0,f.jsx)(`button`,{type:`button`,onClick:X,disabled:P||!W,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>void ee(),disabled:P||!W,className:b,children:P?`Saving…`:`Save`})]})]}):(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`Loading the camera's grid…`})})}):null}export{D as MotionZonesSettings};
@@ -1 +1 @@
1
- import{a as e,c as t,d as n,f as r,g as i,i as a,l as o,n as s,o as c,r as l,s as u,t as d,u as ee}from"./index-CeZV1B-2.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js";var f=i(r(),1),p=i(n(),1),m=c(`hexagon`,[[`path`,{d:`M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z`,key:`yt0hxn`}]]),h=120,g=`privacy-mask`,_=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,v=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function y(e){return e.kind===`rect`||e.kind===`polygon`?e:null}function b(e){let t=new Set(e.map(e=>e.id)),n=0;for(;t.has(n);)n+=1;return n}function x(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(JSON.stringify(e[n])!==JSON.stringify(t[n]))return!1;return!0}function S({deviceId:n}){let r=t(ee().trpcClient,n),[i,c]=(0,f.useState)(null),[S,C]=(0,f.useState)(!1),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(null),[O,k]=(0,f.useState)(!1),[A,j]=(0,f.useState)(!1),[M,N]=(0,f.useState)(null),[P,F]=(0,f.useState)(null),I=(0,f.useRef)(!1);(0,f.useEffect)(()=>{if(!r)return;let e=!1;return I.current=!1,c(null),C(!1),T(null),D(null),j(!1),N(null),F(null),(async()=>{try{let t=await r.privacyMask?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(c(t),I.current)return;let n=await r.privacyMask?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);I.current=!0;let i={enabled:n.enabled,regions:n.regions};D(i),T(i)}catch(t){if(e)return;u(t)?C(!0):console.error(`Privacy Mask load failed`,t)}})(),()=>{e=!0}},[r]);let L=(0,f.useMemo)(()=>w!==null&&E!==null&&(w.enabled!==E.enabled||!x(w.regions,E.regions)),[w,E]),R=w?w.regions.length:0,z=i?i.maxRegions:0,B=(0,f.useRef)(0);(0,f.useEffect)(()=>{B.current=z},[z]);let V=z>0&&R>=z,H=(0,f.useCallback)(()=>{T(e=>e&&{...e,enabled:!e.enabled})},[]),U=(0,f.useCallback)(e=>{N(typeof e==`number`?e:null)},[]),W=(0,f.useMemo)(()=>w?w.regions.map(e=>({id:e.id,shape:e.shape,enabled:e.enabled,label:`Zone ${String(e.id)}`})):[],[w]),G=(0,f.useCallback)((e,t)=>{let n=y(t);n&&T(t=>t&&{...t,regions:t.regions.map(t=>t.id===e?{...t,shape:n}:t)})},[]),K=(0,f.useCallback)(e=>{let t=y(e);t&&(F(null),T(e=>{if(!e)return e;let n=B.current;if(n>0&&e.regions.length>=n)return e;let r=b(e.regions),i={id:r,enabled:!0,shape:t};return N(r),{...e,regions:[...e.regions,i]}}))},[]),q=(0,f.useCallback)(e=>{j(!0),N(null),F(e)},[]),J=(0,f.useCallback)(e=>{j(!0),F(null),N(e)},[]),Y=(0,f.useCallback)(e=>{N(t=>t===e?null:t),T(t=>t&&{...t,regions:t.regions.filter(t=>t.id!==e)})},[]),X=(0,f.useCallback)(()=>{F(null),N(null),E&&T({enabled:E.enabled,regions:E.regions})},[E]),Z=(0,f.useCallback)(async()=>{if(!(!r||!w)){k(!0);try{await r.privacyMask?.setMask({patch:{enabled:w.enabled,regions:[...w.regions]}});let e=await r.privacyMask?.getStatus({});if(e){let t={enabled:e.enabled,regions:e.regions};D(t),T(t)}}catch(e){console.error(`Privacy Mask save failed`,e)}finally{k(!1)}}},[r,w]),ne=(0,f.useCallback)(()=>{j(e=>(e&&(F(null),N(null)),!e))},[]),Q=i?.supportedShapes??[];o((0,f.useMemo)(()=>A&&!S&&i&&w?{id:g,order:h,node:(0,p.jsx)(te,{transparent:!0,items:W,supportedShapes:Q,polygonVertices:i.polygonVertices,selectedId:M,onSelect:U,onShapeChange:G,onDrawComplete:K,drawingKind:P})}:null,[A,S,i,w,W,Q,M,U,G,K,P]));let re=i?.supportedShapes.includes(`rect`)??!1,ie=i?.supportedShapes.includes(`polygon`)??!1,$=i!==null&&(i.maxRegions<=0||i.supportedShapes.length===0),ae=!S&&!$&&i!==null&&w!==null;return r?(0,p.jsx)(e,{title:`Privacy Mask`,icon:(0,p.jsx)(d,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,p.jsx)(`div`,{className:`flex flex-col gap-3`,children:S||$?(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`This camera doesn't support an on-board privacy mask.`}):ae?(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(`p`,{className:`${l} leading-relaxed`,children:[`Toggle `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Edit mask`}),` to draw blanked-out zones on the live frame, then `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push them to the camera. Drag a rectangle to move, its corner to resize; drag polygon vertices, click an edge midpoint to add one, or right-click a vertex to remove it.`]}),(0,p.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,p.jsx)(`button`,{type:`button`,onClick:ne,disabled:O,"aria-pressed":A,className:A?v:_,children:A?`Done editing`:`Edit mask`}),(0,p.jsx)(`button`,{type:`button`,onClick:H,disabled:O,"aria-pressed":w.enabled,className:w.enabled?v:_,children:w.enabled?`Mask on`:`Mask off`}),re&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`rect`),disabled:O||V,"aria-pressed":P===`rect`,className:P===`rect`?v:_,title:V?`Maximum zones reached`:`Add a rectangle zone`,children:`+ Rect`}),ie&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`polygon`),disabled:O||V,"aria-pressed":P===`polygon`,className:P===`polygon`?v:_,title:V?`Maximum zones reached`:`Add a polygon zone`,children:`+ Polygon`}),(0,p.jsxs)(`span`,{className:`${l} ml-1 tabular-nums`,children:[R,` / `,z,` zones`]}),(0,p.jsx)(`span`,{className:`flex-1`}),(0,p.jsx)(`button`,{type:`button`,onClick:X,disabled:O||!L,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>void Z(),disabled:O||!L,className:v,children:O?`Saving…`:`Save`})]}),R>0?(0,p.jsx)(`div`,{className:`flex flex-col gap-1`,children:w.regions.map(e=>{let t=M===e.id,n=e.shape.kind===`polygon`?m:s;return(0,p.jsxs)(`div`,{className:`flex items-center gap-2 rounded-md border px-2 py-1 transition-colors ${t?`border-primary/50 bg-primary/10`:`border-border bg-surface`}`,children:[(0,p.jsxs)(`button`,{type:`button`,onClick:()=>J(e.id),disabled:O,className:`flex flex-1 items-center gap-2 text-left text-[11px] font-medium text-foreground-subtle hover:text-foreground disabled:opacity-40 transition-colors`,children:[(0,p.jsx)(n,{className:`h-3.5 w-3.5 shrink-0`}),(0,p.jsxs)(`span`,{children:[`Zone `,e.id]}),(0,p.jsx)(`span`,{className:`text-foreground-faint capitalize`,children:e.shape.kind})]}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>Y(e.id),disabled:O,"aria-label":`Delete zone ${String(e.id)}`,title:`Delete zone`,className:`inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-surface text-foreground-subtle hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-400 disabled:opacity-40 transition-colors`,children:(0,p.jsx)(a,{className:`h-3.5 w-3.5`})})]},e.id)})}):null]}):(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`Loading the camera's privacy mask…`})})}):null}export{S as PrivacyMaskSettings};
1
+ import{a as e,c as t,d as n,f as r,g as i,i as a,l as o,n as s,o as c,r as l,s as u,t as d,u as ee}from"./index-CpUy8OIE.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js";var f=i(r(),1),p=i(n(),1),m=c(`hexagon`,[[`path`,{d:`M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z`,key:`yt0hxn`}]]),h=120,g=`privacy-mask`,_=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,v=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function y(e){return e.kind===`rect`||e.kind===`polygon`?e:null}function b(e){let t=new Set(e.map(e=>e.id)),n=0;for(;t.has(n);)n+=1;return n}function x(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(JSON.stringify(e[n])!==JSON.stringify(t[n]))return!1;return!0}function S({deviceId:n}){let r=t(ee().trpcClient,n),[i,c]=(0,f.useState)(null),[S,C]=(0,f.useState)(!1),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(null),[O,k]=(0,f.useState)(!1),[A,j]=(0,f.useState)(!1),[M,N]=(0,f.useState)(null),[P,F]=(0,f.useState)(null),I=(0,f.useRef)(!1);(0,f.useEffect)(()=>{if(!r)return;let e=!1;return I.current=!1,c(null),C(!1),T(null),D(null),j(!1),N(null),F(null),(async()=>{try{let t=await r.privacyMask?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(c(t),I.current)return;let n=await r.privacyMask?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);I.current=!0;let i={enabled:n.enabled,regions:n.regions};D(i),T(i)}catch(t){if(e)return;u(t)?C(!0):console.error(`Privacy Mask load failed`,t)}})(),()=>{e=!0}},[r]);let L=(0,f.useMemo)(()=>w!==null&&E!==null&&(w.enabled!==E.enabled||!x(w.regions,E.regions)),[w,E]),R=w?w.regions.length:0,z=i?i.maxRegions:0,B=(0,f.useRef)(0);(0,f.useEffect)(()=>{B.current=z},[z]);let V=z>0&&R>=z,H=(0,f.useCallback)(()=>{T(e=>e&&{...e,enabled:!e.enabled})},[]),U=(0,f.useCallback)(e=>{N(typeof e==`number`?e:null)},[]),W=(0,f.useMemo)(()=>w?w.regions.map(e=>({id:e.id,shape:e.shape,enabled:e.enabled,label:`Zone ${String(e.id)}`})):[],[w]),G=(0,f.useCallback)((e,t)=>{let n=y(t);n&&T(t=>t&&{...t,regions:t.regions.map(t=>t.id===e?{...t,shape:n}:t)})},[]),K=(0,f.useCallback)(e=>{let t=y(e);t&&(F(null),T(e=>{if(!e)return e;let n=B.current;if(n>0&&e.regions.length>=n)return e;let r=b(e.regions),i={id:r,enabled:!0,shape:t};return N(r),{...e,regions:[...e.regions,i]}}))},[]),q=(0,f.useCallback)(e=>{j(!0),N(null),F(e)},[]),J=(0,f.useCallback)(e=>{j(!0),F(null),N(e)},[]),Y=(0,f.useCallback)(e=>{N(t=>t===e?null:t),T(t=>t&&{...t,regions:t.regions.filter(t=>t.id!==e)})},[]),X=(0,f.useCallback)(()=>{F(null),N(null),E&&T({enabled:E.enabled,regions:E.regions})},[E]),Z=(0,f.useCallback)(async()=>{if(!(!r||!w)){k(!0);try{await r.privacyMask?.setMask({patch:{enabled:w.enabled,regions:[...w.regions]}});let e=await r.privacyMask?.getStatus({});if(e){let t={enabled:e.enabled,regions:e.regions};D(t),T(t)}}catch(e){console.error(`Privacy Mask save failed`,e)}finally{k(!1)}}},[r,w]),ne=(0,f.useCallback)(()=>{j(e=>(e&&(F(null),N(null)),!e))},[]),Q=i?.supportedShapes??[];o((0,f.useMemo)(()=>A&&!S&&i&&w?{id:g,order:h,node:(0,p.jsx)(te,{transparent:!0,items:W,supportedShapes:Q,polygonVertices:i.polygonVertices,selectedId:M,onSelect:U,onShapeChange:G,onDrawComplete:K,drawingKind:P})}:null,[A,S,i,w,W,Q,M,U,G,K,P]));let re=i?.supportedShapes.includes(`rect`)??!1,ie=i?.supportedShapes.includes(`polygon`)??!1,$=i!==null&&(i.maxRegions<=0||i.supportedShapes.length===0),ae=!S&&!$&&i!==null&&w!==null;return r?(0,p.jsx)(e,{title:`Privacy Mask`,icon:(0,p.jsx)(d,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,p.jsx)(`div`,{className:`flex flex-col gap-3`,children:S||$?(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`This camera doesn't support an on-board privacy mask.`}):ae?(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(`p`,{className:`${l} leading-relaxed`,children:[`Toggle `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Edit mask`}),` to draw blanked-out zones on the live frame, then `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push them to the camera. Drag a rectangle to move, its corner to resize; drag polygon vertices, click an edge midpoint to add one, or right-click a vertex to remove it.`]}),(0,p.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,p.jsx)(`button`,{type:`button`,onClick:ne,disabled:O,"aria-pressed":A,className:A?v:_,children:A?`Done editing`:`Edit mask`}),(0,p.jsx)(`button`,{type:`button`,onClick:H,disabled:O,"aria-pressed":w.enabled,className:w.enabled?v:_,children:w.enabled?`Mask on`:`Mask off`}),re&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`rect`),disabled:O||V,"aria-pressed":P===`rect`,className:P===`rect`?v:_,title:V?`Maximum zones reached`:`Add a rectangle zone`,children:`+ Rect`}),ie&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`polygon`),disabled:O||V,"aria-pressed":P===`polygon`,className:P===`polygon`?v:_,title:V?`Maximum zones reached`:`Add a polygon zone`,children:`+ Polygon`}),(0,p.jsxs)(`span`,{className:`${l} ml-1 tabular-nums`,children:[R,` / `,z,` zones`]}),(0,p.jsx)(`span`,{className:`flex-1`}),(0,p.jsx)(`button`,{type:`button`,onClick:X,disabled:O||!L,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>void Z(),disabled:O||!L,className:v,children:O?`Saving…`:`Save`})]}),R>0?(0,p.jsx)(`div`,{className:`flex flex-col gap-1`,children:w.regions.map(e=>{let t=M===e.id,n=e.shape.kind===`polygon`?m:s;return(0,p.jsxs)(`div`,{className:`flex items-center gap-2 rounded-md border px-2 py-1 transition-colors ${t?`border-primary/50 bg-primary/10`:`border-border bg-surface`}`,children:[(0,p.jsxs)(`button`,{type:`button`,onClick:()=>J(e.id),disabled:O,className:`flex flex-1 items-center gap-2 text-left text-[11px] font-medium text-foreground-subtle hover:text-foreground disabled:opacity-40 transition-colors`,children:[(0,p.jsx)(n,{className:`h-3.5 w-3.5 shrink-0`}),(0,p.jsxs)(`span`,{children:[`Zone `,e.id]}),(0,p.jsx)(`span`,{className:`text-foreground-faint capitalize`,children:e.shape.kind})]}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>Y(e.id),disabled:O,"aria-label":`Delete zone ${String(e.id)}`,title:`Delete zone`,className:`inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-surface text-foreground-subtle hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-400 disabled:opacity-40 transition-colors`,children:(0,p.jsx)(a,{className:`h-3.5 w-3.5`})})]},e.id)})}):null]}):(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`Loading the camera's privacy mask…`})})}):null}export{S as PrivacyMaskSettings};