@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
@@ -1,8 +1,9 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as parseProfileBrokerId, B as errMsg, G as EventCategory, H as CAM_PROFILE_ORDER, J as createEvent, K as asJsonObject, L as streamBrokerCapability, O as maskUrlCredentials, U as DeviceFeature, V as BaseAddon, W as DeviceType, X as makeProfileBrokerId, Z as makeSourceBrokerId, at as discriminatedUnion, ct as number, dt as string, ft as union, h as addonWidgetsSourceCapability, it as boolean, j as nodePin, l as EncodeProfileSchema, lt as object, nt as _enum, p as RingBuffer, rt as array, st as literal, ut as record, y as cameraStreamsCapability, z as webrtcSessionCapability } from "../dist-CgEP_0OL.mjs";
3
- import { n as DecoderSessionProxy, t as FrameHandlePlane } from "../frame-handle-plane-Dq20KtKL.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,7 +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;
11400
+ hwaccel;
11364
11401
  cameraStreamLookup;
11365
11402
  rtspEntryLookup;
11366
11403
  resolveProfileSource;
@@ -11371,7 +11408,7 @@ var TranscodePipelineManager = class {
11371
11408
  acquireChannel;
11372
11409
  constructor(opts) {
11373
11410
  this.logger = opts.logger;
11374
- this.api = opts.api;
11411
+ this.hwaccel = opts.hwaccel ?? null;
11375
11412
  this.cameraStreamLookup = opts.cameraStreamLookup;
11376
11413
  this.rtspEntryLookup = opts.rtspEntryLookup;
11377
11414
  this.resolveProfileSource = opts.resolveProfileSource ?? null;
@@ -11572,13 +11609,21 @@ var TranscodePipelineManager = class {
11572
11609
  if (!stream) throw new Error(`getStreamWithCodec: profile "${profile}" source "${camStreamId}" not published for device ${deviceId}`);
11573
11610
  return stream;
11574
11611
  }
11575
- /** Query the typed, cached hardware-encoder probe; null on any failure. */
11612
+ /**
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`.
11620
+ */
11576
11621
  async resolveHardwareEncoders() {
11577
- if (!this.api) return null;
11622
+ if (!this.hwaccel) return null;
11578
11623
  try {
11579
- return await this.api.platformProbe.getHardwareEncoders.query();
11624
+ return localHardwareEncoders(rankDecodeHwAccels((await this.hwaccel.resolve(null)).preferred, [])[0] ?? null);
11580
11625
  } catch (err) {
11581
- 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) } });
11582
11627
  return null;
11583
11628
  }
11584
11629
  }
@@ -11606,35 +11651,20 @@ var TranscodePipelineManager = class {
11606
11651
  if (strategy.kind === "software") return null;
11607
11652
  return this.resolveSpecificDecodeHwAccel(strategy.prefer);
11608
11653
  }
11609
- /** Gate a specific backend against the probed support list; null on miss/error. */
11610
- async resolveSpecificDecodeHwAccel(prefer) {
11611
- if (!this.api) return null;
11612
- try {
11613
- const res = await this.api.platformProbe.resolveHwAccel.query({ prefer });
11614
- const supported = await this.resolveDecodeAccelMethods();
11615
- const picked = pickDecodeHwAccel(res.preferred, supported);
11616
- if (picked === null && res.preferred.length > 0) this.logger.info("decode hw-accel: configured ffmpeg supports none of the preferred backends — software decode", { meta: {
11617
- preferred: res.preferred,
11618
- supported
11619
- } });
11620
- return picked;
11621
- } catch (err) {
11622
- this.logger.warn("platformProbe.resolveHwAccel failed — software decode fallback", { meta: { error: errMsg(err) } });
11623
- return null;
11624
- }
11625
- }
11626
11654
  /**
11627
- * The `-hwaccel` methods the configured ffmpeg supports; `[]` on any failure
11628
- * (treated as "unknown" by {@link pickDecodeHwAccel}, which then keeps the
11629
- * 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.
11630
11660
  */
11631
- async resolveDecodeAccelMethods() {
11632
- if (!this.api) return [];
11661
+ async resolveSpecificDecodeHwAccel(prefer) {
11662
+ if (!this.hwaccel) return null;
11633
11663
  try {
11634
- return (await this.api.platformProbe.getHardwareDecodeAccels.query()).methods;
11664
+ return rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
11635
11665
  } catch (err) {
11636
- this.logger.warn("platformProbe.getHardwareDecodeAccels failed — decode gate skipped", { meta: { error: errMsg(err) } });
11637
- return [];
11666
+ this.logger.warn("local decode hw-accel resolve failed — software decode fallback", { meta: { error: errMsg(err) } });
11667
+ return null;
11638
11668
  }
11639
11669
  }
11640
11670
  shareEntry(entry) {
@@ -11952,6 +11982,14 @@ var StreamBrokerManager = class {
11952
11982
  */
11953
11983
  api = null;
11954
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
+ /**
11955
11993
  * This broker node's own Moleculer nodeID (`ctx.kernel.localNodeId`). The
11956
11994
  * shm frame plane's decoder sessions MUST be co-located on this node — an
11957
11995
  * shm frame ring is node-local, so a decoder placed on any OTHER node can
@@ -11993,7 +12031,7 @@ var StreamBrokerManager = class {
11993
12031
  getTranscodeManager() {
11994
12032
  if (!this.transcodeManager) this.transcodeManager = new TranscodePipelineManager({
11995
12033
  logger: this.logger.child("transcode"),
11996
- api: this.api,
12034
+ hwaccel: this.hwaccel ?? void 0,
11997
12035
  cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
11998
12036
  rtspEntryLookup: (brokerId) => {
11999
12037
  const entry = this.rtspProvider.getEntry(brokerId);
@@ -12199,9 +12237,10 @@ var StreamBrokerManager = class {
12199
12237
  * resolve the `decoder` capability via tRPC — see
12200
12238
  * `buildDecoderCapApi` for the full rationale.
12201
12239
  */
12202
- setApiAccess(api, localNodeId) {
12240
+ setApiAccess(api, localNodeId, hwaccel) {
12203
12241
  this.api = api;
12204
12242
  if (localNodeId !== void 0) this.localNodeId = localNodeId;
12243
+ if (hwaccel !== void 0) this.hwaccel = hwaccel;
12205
12244
  }
12206
12245
  /** Update the system `ffmpeg` config snapshot used by the transcode egress. */
12207
12246
  setFfmpegConfig(cfg) {
@@ -12209,11 +12248,11 @@ var StreamBrokerManager = class {
12209
12248
  }
12210
12249
  /**
12211
12250
  * The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
12212
- * settings (`copy`, `auto`, `none`) plus the concrete backends the configured
12213
- * ffmpeg build actually supports (probed via `getHardwareDecodeAccels`). Lets
12214
- * a settings form offer exactly the backends this host can use, instead of the
12215
- * full static list that may include unsupported ones. Best-effort: on a probe
12216
- * 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.
12217
12256
  */
12218
12257
  async getHwAccelOptions() {
12219
12258
  const always = [
@@ -12221,12 +12260,12 @@ var StreamBrokerManager = class {
12221
12260
  "auto",
12222
12261
  "none"
12223
12262
  ];
12224
- if (!this.api) return always;
12263
+ if (!this.hwaccel) return always;
12225
12264
  try {
12226
- const probed = (await this.api.platformProbe.getHardwareDecodeAccels.query()).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));
12227
12266
  return [...always, ...probed];
12228
12267
  } catch (err) {
12229
- 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) } });
12230
12269
  return always;
12231
12270
  }
12232
12271
  }
@@ -17057,8 +17096,24 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
17057
17096
  };
17058
17097
  //#endregion
17059
17098
  //#region src/stream-broker/addon.ts
17060
- function isFfmpegHwAccel(v) {
17061
- return typeof v === "string" && FFMPEG_HW_ACCEL_SETTINGS.includes(v);
17099
+ /**
17100
+ * Hydrate the persisted RTSP restream state (`rtspTokens` / `rtspEnabled`)
17101
+ * through the durable `state()` handles — the sanctioned settings accessor —
17102
+ * instead of a raw `readAddonStore()`. Same store keys, same persisted
17103
+ * shapes; a corrupt blob falls back to empty (durable-state semantics) and
17104
+ * absent keys keep first-boot silence (no loader call, no log).
17105
+ */
17106
+ async function hydratePersistedRtspState(deps) {
17107
+ const tokens = new Map(Object.entries(await deps.tokensState.get()));
17108
+ if (tokens.size > 0) {
17109
+ deps.loadTokens(tokens);
17110
+ deps.logger.info("Loaded persisted RTSP tokens", { meta: { tokenCount: tokens.size } });
17111
+ }
17112
+ const states = new Map(Object.entries(await deps.enabledState.get()));
17113
+ if (states.size > 0) {
17114
+ deps.loadEnabled(states);
17115
+ deps.logger.info("Loaded persisted RTSP enabled states", { meta: { stateCount: states.size } });
17116
+ }
17062
17117
  }
17063
17118
  var PROFILE_KEYS = [
17064
17119
  "high",
@@ -17149,39 +17204,54 @@ var StreamBrokerAddon = class extends BaseAddon {
17149
17204
  maxDecodeFps: 5,
17150
17205
  initialReconnectDelayMs: 1e3,
17151
17206
  maxReconnectDelayMs: 3e4,
17152
- catalogReconcileIntervalSec: 30
17207
+ catalogReconcileIntervalSec: 30,
17208
+ binaryPath: "",
17209
+ hwaccel: "copy"
17153
17210
  });
17154
17211
  }
17155
17212
  /**
17156
- * Read the cluster `ffmpeg` config section (binaryPath / hwAccel /
17157
- * threadCount) and push it into the broker. Defensive: any missing or
17158
- * malformed field falls back to the safe default (PATH `ffmpeg`, `'copy'`
17159
- * software decode, `0` threads), and a read failure leaves the defaults in
17160
- * place. `'copy'` is the safe default it decodes every HEVC source rather
17161
- * 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).
17162
17237
  */
17163
17238
  async loadFfmpegConfig() {
17164
17239
  if (!this.brokerManager) return;
17165
17240
  try {
17166
- const section = await this.ctx.settings?.getSection("ffmpeg") ?? {};
17167
- const binaryPathRaw = section["binaryPath"];
17168
- const binaryPath = typeof binaryPathRaw === "string" && binaryPathRaw.trim().length > 0 ? binaryPathRaw : "ffmpeg";
17169
- const hwAccel = isFfmpegHwAccel(section["hwAccel"]) ? section["hwAccel"] : "copy";
17170
- const threadCountRaw = section["threadCount"];
17171
- 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;
17172
17243
  this.brokerManager.setFfmpegConfig({
17173
17244
  binaryPath,
17174
17245
  hwAccel,
17175
- threadCount
17246
+ threadCount: 0
17176
17247
  });
17177
17248
  this.ffmpegBinaryPath = binaryPath;
17178
17249
  this.ctx.logger.info("Loaded ffmpeg config", { meta: {
17179
17250
  binaryPath,
17180
- hwAccel,
17181
- threadCount
17251
+ hwAccel
17182
17252
  } });
17183
17253
  } catch (err) {
17184
- 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) } });
17185
17255
  }
17186
17256
  }
17187
17257
  /**
@@ -17189,20 +17259,19 @@ var StreamBrokerAddon = class extends BaseAddon {
17189
17259
  * configured `ffmpeg.hwAccel` preference (default `'copy'` = software):
17190
17260
  * - `'auto'` → the literal `'auto'` (ffmpeg self-selects + software-fallbacks).
17191
17261
  * - `'copy'`/`'none'` → `null` (software decode).
17192
- * - a specific backend → gated against what the configured ffmpeg can decode
17193
- * (`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
17194
17264
  * per-stream miss.
17195
17265
  * Mirrors the transcode egress's resolution.
17196
17266
  */
17197
17267
  async resolveTranscodeDecodeHwAccel() {
17198
17268
  try {
17199
- const section = await this.ctx.settings?.getSection("ffmpeg") ?? {};
17200
- const strategy = classifyDecodeHwAccel(isFfmpegHwAccel(section["hwAccel"]) ? section["hwAccel"] : "copy");
17269
+ const strategy = classifyDecodeHwAccel(this.config.hwaccel);
17201
17270
  if (strategy.kind === "auto") return "auto";
17202
17271
  if (strategy.kind === "software") return null;
17203
- const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: strategy.prefer });
17204
- const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query();
17205
- 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;
17206
17275
  } catch (err) {
17207
17276
  this.ctx.logger.warn("resolveTranscodeDecodeHwAccel failed — software decode", { meta: { error: errMsg(err) } });
17208
17277
  return null;
@@ -17214,28 +17283,22 @@ var StreamBrokerAddon = class extends BaseAddon {
17214
17283
  this.brokerManager.setEventBus(this.ctx.eventBus);
17215
17284
  const localNode = this.ctx.kernel.localNodeId;
17216
17285
  const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
17217
- this.brokerManager.setApiAccess(this.ctx.api, localNodeId);
17286
+ this.brokerManager.setApiAccess(this.ctx.api, localNodeId, this.ctx.kernel.hwaccel);
17218
17287
  if (this.capabilities) this.brokerManager.setCapabilitiesAccess(this.capabilities);
17219
17288
  await this.loadFfmpegConfig();
17220
17289
  const rtspProvider = this.brokerManager.getRtspRestreamProvider();
17290
+ const brokerManager = this.brokerManager;
17291
+ const rtspTokensState = this.state("rtspTokens", RtspTokensSchema, {});
17292
+ const rtspEnabledState = this.state("rtspEnabled", RtspEnabledSchema, {});
17221
17293
  try {
17222
- const addonStore = await this.ctx.settings?.readAddonStore() ?? {};
17223
- const tokenData = asJsonObject(addonStore["rtspTokens"]);
17224
- if (tokenData) {
17225
- const tokens = /* @__PURE__ */ new Map();
17226
- for (const [k, v] of Object.entries(tokenData)) if (typeof v === "string") tokens.set(k, v);
17227
- this.brokerManager.loadPersistedTokens(tokens);
17228
- this.ctx.logger.info("Loaded persisted RTSP tokens", { meta: { tokenCount: tokens.size } });
17229
- }
17230
- const enabledData = asJsonObject(addonStore["rtspEnabled"]);
17231
- if (enabledData) {
17232
- const states = /* @__PURE__ */ new Map();
17233
- for (const [k, v] of Object.entries(enabledData)) states.set(k, Boolean(v));
17234
- rtspProvider.loadPersistedEnabled(states);
17235
- this.ctx.logger.info("Loaded persisted RTSP enabled states", { meta: { stateCount: states.size } });
17236
- }
17294
+ await hydratePersistedRtspState({
17295
+ tokensState: rtspTokensState,
17296
+ enabledState: rtspEnabledState,
17297
+ loadTokens: (tokens) => brokerManager.loadPersistedTokens(tokens),
17298
+ loadEnabled: (states) => rtspProvider.loadPersistedEnabled(states),
17299
+ logger: this.ctx.logger
17300
+ });
17237
17301
  } catch {}
17238
- const rtspTokensState = this.state("rtspTokens", RtspTokensSchema, {});
17239
17302
  rtspProvider.setTokenPersister(async (tokens) => {
17240
17303
  try {
17241
17304
  await rtspTokensState.update((prev) => {
@@ -17248,7 +17311,6 @@ var StreamBrokerAddon = class extends BaseAddon {
17248
17311
  this.ctx.logger.warn("Failed to persist RTSP tokens", { meta: { error: errMsg(err) } });
17249
17312
  }
17250
17313
  });
17251
- const rtspEnabledState = this.state("rtspEnabled", RtspEnabledSchema, {});
17252
17314
  rtspProvider.setEnabledPersister((states) => {
17253
17315
  const obj = {};
17254
17316
  for (const [k, v] of states) obj[k] = v;
@@ -17680,6 +17742,27 @@ var StreamBrokerAddon = class extends BaseAddon {
17680
17742
  step: 1,
17681
17743
  default: 5
17682
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
+ },
17683
17766
  {
17684
17767
  type: "number",
17685
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-5tQlh9h4.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-C-pL8ETk.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-C-pL8ETk.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.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-C-pL8ETk.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.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};