@camstack/addon-pipeline 1.1.41 → 1.1.42

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 (27) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +1 -1
  4. package/dist/detection-pipeline/index.mjs +1 -1
  5. package/dist/{dist-v6cKLmU3.mjs → dist-GFd8M6KO.mjs} +9 -1
  6. package/dist/{dist-BJTPkJFw.js → dist-NvwN60Fq.js} +9 -1
  7. package/dist/{frame-handle-plane-BP8YV4sF.js → frame-handle-plane-B3Fxww8H.js} +1 -1
  8. package/dist/{frame-handle-plane-DKAXTtfn.mjs → frame-handle-plane-E_AsR77T.mjs} +1 -1
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +47 -9
  12. package/dist/pipeline-runner/index.mjs +47 -9
  13. package/dist/recorder/index.js +1 -1
  14. package/dist/recorder/index.mjs +1 -1
  15. package/dist/session-decode/decode-worker-child.js +312 -21
  16. package/dist/session-decode/decode-worker-child.mjs +312 -21
  17. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2l0kmD4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CeielP5P.mjs} +3 -3
  18. package/dist/stream-broker/{hostInit-DHaCRMjM.mjs → hostInit-DmjqskOR.mjs} +3 -3
  19. package/dist/stream-broker/index.js +2 -2
  20. package/dist/stream-broker/index.mjs +2 -2
  21. package/dist/stream-broker/remoteEntry.js +1 -1
  22. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-B6fza7ic.js → MaskShapeCanvas-DI4BY7W2-CEsPjwzT.js} +1 -1
  23. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Cy-4iTog.js → MotionZonesSettings-NcxxQN8r-D43fIRgB.js} +1 -1
  24. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CYX33lJb.js → PrivacyMaskSettings-APgPLF7p-C3wJxG8V.js} +1 -1
  25. package/embed-dist/assets/{index-BPkayx9u.js → index-CX-rILhw.js} +4 -4
  26. package/embed-dist/index.html +1 -1
  27. package/package.json +1 -1
@@ -23,6 +23,19 @@ var FrameSlot = class {
23
23
  */
24
24
  reserved = null;
25
25
  pendingPull = false;
26
+ /** Frames dropped by the `minIntervalMs` throttle (never entered the slot). */
27
+ throttledCount = 0;
28
+ /** Frames dropped by latest-wins (an unpulled slot frame superseded before delivery). */
29
+ supersededCount = 0;
30
+ /**
31
+ * Total frames this slot has DROPPED before delivery — the sum of the
32
+ * `minIntervalMs` throttle drops and the latest-wins (drop-older)
33
+ * supersessions. Read by the decode-worker child for its `framesSkipped`
34
+ * metric; purely additive, never affects slot behaviour.
35
+ */
36
+ get droppedCount() {
37
+ return this.throttledCount + this.supersededCount;
38
+ }
26
39
  /**
27
40
  * Publish a newly decoded frame into the latest-wins slot, freeing
28
41
  * whatever the slot held before (the drop-older half of "latest-wins").
@@ -32,6 +45,7 @@ var FrameSlot = class {
32
45
  */
33
46
  publish(frame, timestamp, minIntervalMs) {
34
47
  if (minIntervalMs > 0 && this.slot && timestamp - this.slot.timestamp < minIntervalMs) {
48
+ this.throttledCount++;
35
49
  frame.free();
36
50
  return null;
37
51
  }
@@ -44,6 +58,7 @@ var FrameSlot = class {
44
58
  width: frame.width,
45
59
  height: frame.height
46
60
  };
61
+ if (previous) this.supersededCount++;
47
62
  previous?.frame.free();
48
63
  if (this.pendingPull) {
49
64
  this.pendingPull = false;
@@ -90,6 +105,27 @@ var FrameSlot = class {
90
105
  //#region src/session-decode/decode-worker-child.ts
91
106
  /** Re-dial backoff on a transient dial error/EOF (mirrors the decoder's PULL_REDIAL_MS). */
92
107
  var REDIAL_MS = 3e3;
108
+ /** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
109
+ var METRICS_INTERVAL_MS = 1e4;
110
+ /**
111
+ * The libav GPU scale filter for a hwaccel backend, or `null` when none is
112
+ * known here — those backends fall back to the software crop+scale path.
113
+ * Mirrors `addon-decoder-ffmpeg/src/ffmpeg-args.ts` `gpuScaleFilterForBackend`.
114
+ */
115
+ function gpuScaleFilterForBackend(backend) {
116
+ switch (backend) {
117
+ case "vaapi": return "scale_vaapi";
118
+ case "qsv": return "scale_qsv";
119
+ case "cuda":
120
+ case "nvdec": return "scale_cuda";
121
+ case "videotoolbox": return "scale_vt";
122
+ default: return null;
123
+ }
124
+ }
125
+ /** libav `format` filter target name for the requested detection pixel format. */
126
+ function pixelFilterName(format) {
127
+ return format === "gray" ? "gray" : "rgb24";
128
+ }
93
129
  var _nav = null;
94
130
  var _consts = null;
95
131
  async function getNodeAv() {
@@ -182,6 +218,45 @@ function resolveResizeTarget(cropWidth, cropHeight, resize) {
182
218
  height: Math.max(1, Math.round(resize.height))
183
219
  };
184
220
  }
221
+ /** Whether `region` covers the entire `srcW x srcH` frame (i.e. no real crop). */
222
+ function isFullFrameRegion(region, srcW, srcH) {
223
+ return region.left === 0 && region.top === 0 && region.width === srcW && region.height === srcH;
224
+ }
225
+ /**
226
+ * Build the libav filtergraph description for the GPU crop+scale path.
227
+ *
228
+ * Full-frame — the ONLY shape the session-decode pump ever asks for (it passes
229
+ * `resize`+`format`, never `crop`, see `session-decode-pump.ts`): scale on the
230
+ * GPU, then `hwdownload` pulls only the small detection-sized surface into
231
+ * system memory + a cheap pixel-convert. This is byte-for-byte the chain the
232
+ * ffmpeg decoder already ships (`addon-decoder-ffmpeg/src/ffmpeg-args.ts`
233
+ * `buildVideoFilter`): `scale_<be>=w=W:h=H,hwdownload,format=nv12,format=<pixel>`.
234
+ *
235
+ * Sub-region crop — contract-supported but unused on the hot path: the generic
236
+ * `crop` filter offsets plane POINTERS, which corrupts opaque VAAPI/QSV
237
+ * surfaces, so there is no safe GPU crop for those. A real crop is therefore
238
+ * done on the CPU AFTER `hwdownload` (`hwdownload,format=nv12,crop=…,scale=…`).
239
+ * It is correct and never crashes; it just forgoes the GPU win for that rare
240
+ * call — exactly the safety/behaviour trade-off the task calls for.
241
+ */
242
+ function buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel) {
243
+ if (fullFrame) return `${scaleFilter}=w=${target.width}:h=${target.height},hwdownload,format=nv12,format=${pixel}`;
244
+ return `hwdownload,format=nv12,crop=${region.width}:${region.height}:${region.left}:${region.top},scale=${target.width}:${target.height},format=${pixel}`;
245
+ }
246
+ /**
247
+ * Copy a single-plane packed frame (rgb24 / gray8 out of the filtergraph) into
248
+ * a tightly-packed `Uint8Array` of `width*channels*height`, stripping libav's
249
+ * row padding (`linesize` ≥ `width*channels`). Keeps the `toBuffer` output
250
+ * contract byte-identical to the software `scaleRoiToBuffer`.
251
+ */
252
+ function packSinglePlane(plane, linesize, width, height, channels) {
253
+ const rowBytes = width * channels;
254
+ const stride = linesize > 0 ? linesize : rowBytes;
255
+ const out = Buffer.allocUnsafe(rowBytes * height);
256
+ if (stride === rowBytes) plane.copy(out, 0, 0, rowBytes * height);
257
+ else for (let row = 0; row < height; row++) plane.copy(out, row * rowBytes, row * stride, row * stride + rowBytes);
258
+ return out;
259
+ }
185
260
  /**
186
261
  * The forked child's whole runtime: decode-loop lifecycle, the latest-wins
187
262
  * frame slot, and the ROI scaler for `toBuffer`. One instance per process.
@@ -201,6 +276,42 @@ var DecodeWorkerChild = class {
201
276
  */
202
277
  scaler = null;
203
278
  scalerKey = "";
279
+ /** libav GPU scale filter for the resolved hwaccel backend, or null when GPU filtering isn't available. */
280
+ gpuScaleFilter = null;
281
+ /**
282
+ * GPU crop+scale path decision — picked ONCE per session.
283
+ * - `software`: the decoder downloads every frame to YUV420P system memory and
284
+ * `toBuffer` uses the CPU {@link scaler}. This is the original, leak-safe
285
+ * behaviour; the default until a HW device + `scale_<be>` filter is found.
286
+ * - `undecided`: a HW device + GPU scale filter are available; the decoder
287
+ * emits GPU surfaces and the FIRST HW frame is probed through the GPU
288
+ * filtergraph — success ⇒ `hardware`, any failure ⇒ `software` (re-dial).
289
+ * - `hardware`: the decoder emits GPU surfaces and `toBuffer` runs the GPU
290
+ * crop+scale, downloading only the tiny detection-sized result.
291
+ */
292
+ hwFilterDecision = "software";
293
+ /** Skip the re-dial backoff when the re-dial is specifically to switch to software. */
294
+ redialAsSoftware = false;
295
+ /**
296
+ * Cached GPU crop+scale filtergraph + the geometry/format key it was built
297
+ * for — the HW analogue of {@link scaler}/{@link scalerKey}. Rebuilt (old
298
+ * CLOSED) only when the key changes. Its internal `scale_<be>` OUTPUT hw
299
+ * frames pool is the leak-prone VAAPI surface pool, so it is closed on EVERY
300
+ * re-dial ({@link closeInput}) and on teardown — this discipline is what stops
301
+ * the prior "keeping decoded frames on the GPU leaked the VAAPI surface pool
302
+ * on every re-dial" incident from recurring.
303
+ */
304
+ hwFilter = null;
305
+ hwFilterKey = "";
306
+ /** Scrypted-style throughput counters (plain integers, no per-frame allocation). */
307
+ framesDecoded = 0;
308
+ /** Child-side skips only (HW-guard drops + probe/degrade frees); slot drops add {@link FrameSlot.droppedCount}. */
309
+ framesSkipped = 0;
310
+ /** Frames actually handed to the parent as a `frame` reply. */
311
+ framesDelivered = 0;
312
+ lastMetricsAt = 0;
313
+ lastDeliveredSnapshot = 0;
314
+ metricsTimer = null;
204
315
  demuxer = null;
205
316
  decoder = null;
206
317
  abortController = null;
@@ -253,9 +364,14 @@ var DecodeWorkerChild = class {
253
364
  clearTimeout(this.redialTimer);
254
365
  this.redialTimer = null;
255
366
  }
367
+ if (this.metricsTimer) {
368
+ clearInterval(this.metricsTimer);
369
+ this.metricsTimer = null;
370
+ }
371
+ if (this.started) this.logMetrics(true);
256
372
  this.abortController?.abort();
257
- this.closeInput();
258
373
  this.frames.free();
374
+ this.closeInput();
259
375
  this.scaler?.[Symbol.dispose]?.();
260
376
  this.scaler = null;
261
377
  this.scalerKey = "";
@@ -266,6 +382,17 @@ var DecodeWorkerChild = class {
266
382
  this.send({ kind: "ended" });
267
383
  }
268
384
  }
385
+ /** Emit the compact throughput line (window `deliveredFps`); cheap, integer-only. */
386
+ logMetrics(final) {
387
+ const now = Date.now();
388
+ const windowMs = Math.max(1, now - this.lastMetricsAt);
389
+ const deliveredDelta = this.framesDelivered - this.lastDeliveredSnapshot;
390
+ const deliveredFps = Math.round(deliveredDelta / windowMs * 1e3 * 10) / 10;
391
+ const skipped = this.framesSkipped + this.frames.droppedCount;
392
+ process.stderr.write(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}}${final ? " (final)" : ""}\n`);
393
+ this.lastMetricsAt = now;
394
+ this.lastDeliveredSnapshot = this.framesDelivered;
395
+ }
269
396
  handleStart(source, opts) {
270
397
  if (this.started) {
271
398
  process.stderr.write("decode-worker-child: duplicate start ignored\n");
@@ -274,6 +401,9 @@ var DecodeWorkerChild = class {
274
401
  this.started = true;
275
402
  this.sessionFormat = opts.format ?? "rgb";
276
403
  this.minIntervalMs = opts.fps && opts.fps > 0 ? 1e3 / opts.fps : 0;
404
+ this.lastMetricsAt = Date.now();
405
+ this.metricsTimer = setInterval(() => this.logMetrics(false), METRICS_INTERVAL_MS);
406
+ this.metricsTimer.unref?.();
277
407
  this.runDecodeLoop(source.restreamUrl).catch((err) => {
278
408
  this.sendError(`decode-worker-child: decode loop crashed — ${errMessage(err)}`);
279
409
  });
@@ -285,7 +415,7 @@ var DecodeWorkerChild = class {
285
415
  }
286
416
  const reply = this.frames.pull();
287
417
  if (reply) {
288
- this.send(this.toFrameReply(reply));
418
+ this.deliverFrame(reply);
289
419
  return;
290
420
  }
291
421
  this.pendingPull = true;
@@ -298,7 +428,7 @@ var DecodeWorkerChild = class {
298
428
  return;
299
429
  }
300
430
  try {
301
- const bytes = this.scaleRoiToBuffer(frame, opts);
431
+ const bytes = frame.isHwFrame() ? this.hwScaleToBuffer(frame, opts) : this.scaleRoiToBuffer(frame, opts);
302
432
  this.send({
303
433
  kind: "buffer",
304
434
  frameId,
@@ -330,21 +460,29 @@ var DecodeWorkerChild = class {
330
460
  this.closeInput();
331
461
  }
332
462
  if (this.stopped) break;
333
- await this.sleep(REDIAL_MS);
463
+ const skipBackoff = this.redialAsSoftware;
464
+ this.redialAsSoftware = false;
465
+ if (!skipBackoff) await this.sleep(REDIAL_MS);
334
466
  }
335
467
  }
336
468
  /**
337
- * One dial: open the demuxer, pick the video stream, build a HW/SW decoder
338
- * that downloads + normalises frames to YUV420P system memory (so the ROI
339
- * scaler always reads plain software planes regardless of hwaccel), and
340
- * pump `demuxer.packets → decoder.frames → consumeDecodedFrame` until the
469
+ * One dial: open the demuxer, pick the video stream, build a HW/SW decoder,
470
+ * and pump `demuxer.packets decoder.frames consumeDecodedFrame` until the
341
471
  * stream ends, errors, or teardown aborts it.
342
472
  *
343
- * Copied from `nodeav-decoder-session.ts` `pullDialAndDecode` (lines
344
- * ~916-996): same demuxer options, same `rescale.pixelFormat: YUV420P`
345
- * download-always contract (keeping decoded frames on the GPU leaked the
346
- * VAAPI surface pool on every re-dial see that file's comment), same
347
- * `exitOnError: false` bad-frame tolerance.
473
+ * Two decoder shapes, selected by {@link hwFilterDecision} (picked once per
474
+ * session):
475
+ * - HW path (`undecided`/`hardware`): NO `rescale` the decoder keeps frames
476
+ * as GPU surfaces so `toBuffer` can crop+scale them on the GPU and download
477
+ * only the small detection-sized result.
478
+ * - Software path (`software`): `rescale.pixelFormat: YUV420P` forces a
479
+ * full-res GPU→system download so the CPU ROI scaler reads plain planes.
480
+ * This is the ORIGINAL contract — keeping decoded frames on the GPU leaked
481
+ * the VAAPI surface pool on every re-dial (see `nodeav-decoder-session.ts`);
482
+ * the HW path avoids that leak by closing its filtergraph + freeing every
483
+ * surface on every re-dial/teardown (see {@link closeInput}/{@link teardown}).
484
+ *
485
+ * Same demuxer options + `exitOnError: false` bad-frame tolerance throughout.
348
486
  */
349
487
  async dialAndDecode(nav, C, url) {
350
488
  this.abortController = new AbortController();
@@ -359,9 +497,10 @@ var DecodeWorkerChild = class {
359
497
  this.demuxer = demuxer;
360
498
  const videoStream = demuxer.video();
361
499
  if (!videoStream) throw new Error("decode-worker-child: pull input has no video stream");
500
+ const wantHwFrames = this.hwContext !== null && this.hwFilterDecision !== "software";
362
501
  const decoder = await nav.Decoder.create(videoStream, {
363
502
  ...this.hwContext ? { hardware: this.hwContext } : {},
364
- rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P },
503
+ ...wantHwFrames ? {} : { rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P } },
365
504
  exitOnError: false
366
505
  });
367
506
  if (this.stopped) {
@@ -373,24 +512,103 @@ var DecodeWorkerChild = class {
373
512
  if (this.stopped) break;
374
513
  if (!frame) continue;
375
514
  this.consumeDecodedFrame(frame);
515
+ if (this.redialAsSoftware) break;
376
516
  }
377
517
  }
378
518
  /**
379
- * Publish a decoded frame into the latest-wins slot (mirrors Scrypted's
380
- * `libav.py:32,50-53` single-slot queue) via `FrameSlot`, which handles
381
- * drop-older, the `minIntervalMs` throttle, and resolving a pending `pull`.
519
+ * Route a decoded frame according to the session's {@link hwFilterDecision}.
520
+ * On the software path a frame is published straight to the latest-wins slot;
521
+ * on the HW path the FIRST GPU surface is probed through the GPU filtergraph
522
+ * to commit (or fall back) the path, then GPU surfaces are published for
523
+ * `toBuffer` to crop+scale on the GPU. Publishing itself (drop-older, the
524
+ * `minIntervalMs` throttle, resolving a pending `pull`) is `FrameSlot`'s job.
382
525
  */
383
526
  consumeDecodedFrame(frame) {
384
- if (frame.isHwFrame()) {
527
+ this.framesDecoded++;
528
+ if (this.hwFilterDecision === "software") {
529
+ if (frame.isHwFrame()) {
530
+ this.framesSkipped++;
531
+ frame.free();
532
+ return;
533
+ }
534
+ this.publishFrame(frame);
535
+ return;
536
+ }
537
+ if (!frame.isHwFrame()) {
538
+ this.framesSkipped++;
385
539
  frame.free();
540
+ this.degradeToSoftware();
386
541
  return;
387
542
  }
543
+ if (this.hwFilterDecision === "undecided") {
544
+ if (!this.probeHwFilter(frame)) {
545
+ this.framesSkipped++;
546
+ frame.free();
547
+ this.degradeToSoftware();
548
+ return;
549
+ }
550
+ this.hwFilterDecision = "hardware";
551
+ process.stderr.write(`decode-worker-child: GPU crop+scale path active (${this.gpuScaleFilter})\n`);
552
+ }
553
+ this.publishFrame(frame);
554
+ }
555
+ /** Publish to the latest-wins slot; deliver immediately if a `pull` is pending. */
556
+ publishFrame(frame) {
388
557
  const reply = this.frames.publish(frame, Date.now(), this.minIntervalMs);
389
558
  if (reply) {
390
559
  this.pendingPull = false;
391
- this.send(this.toFrameReply(reply));
560
+ this.deliverFrame(reply);
561
+ }
562
+ }
563
+ /** Send a `frame` reply to the parent and count it toward `deliveredFps`. */
564
+ deliverFrame(reply) {
565
+ this.framesDelivered++;
566
+ this.send(this.toFrameReply(reply));
567
+ }
568
+ /**
569
+ * One-shot GPU-filter capability probe on the FIRST decoded HW frame: build a
570
+ * no-op `scale_<be>=iw:ih,hwdownload,format=nv12` graph and push the frame
571
+ * through it. A frame out ⇒ the driver supports GPU crop+scale for this
572
+ * session; any throw / empty output ⇒ fall back to software. The probe graph
573
+ * and its output frames are freed here regardless of outcome, so no surface
574
+ * leaks out of the probe. The input `frame` keeps its own ref (buffersrc only
575
+ * ref'd it) and is published by the caller on success.
576
+ */
577
+ probeHwFilter(frame) {
578
+ const nav = this.nav;
579
+ const scaleFilter = this.gpuScaleFilter;
580
+ if (!nav || !scaleFilter || !this.hwContext) return false;
581
+ let filter = null;
582
+ try {
583
+ filter = nav.FilterAPI.create(`${scaleFilter}=w=iw:h=ih,hwdownload,format=nv12`, { hardware: this.hwContext });
584
+ const outputs = filter.processAllSync(frame);
585
+ const ok = outputs.length > 0;
586
+ for (const out of outputs) out.free();
587
+ return ok;
588
+ } catch (err) {
589
+ process.stderr.write(`decode-worker-child: GPU filter probe failed — using software decode: ${errMessage(err)}\n`);
590
+ return false;
591
+ } finally {
592
+ filter?.close();
392
593
  }
393
594
  }
595
+ /**
596
+ * Abandon the GPU filter path for the rest of the session: close the
597
+ * filtergraph (freeing its GPU surface pool), mark the decision `software`,
598
+ * and abort the current HW dial so the decode loop re-dials with a YUV420P
599
+ * rescale. Called from the probe path AND from a runtime `toBuffer` GPU-filter
600
+ * failure, so a surprising driver/format error never leaves the session
601
+ * silently producing no usable frames. Idempotent.
602
+ */
603
+ degradeToSoftware() {
604
+ if (this.hwFilterDecision === "software") return;
605
+ this.hwFilterDecision = "software";
606
+ this.redialAsSoftware = true;
607
+ this.hwFilter?.close();
608
+ this.hwFilter = null;
609
+ this.hwFilterKey = "";
610
+ this.abortController?.abort();
611
+ }
394
612
  toFrameReply(reply) {
395
613
  return {
396
614
  kind: "frame",
@@ -473,10 +691,71 @@ var DecodeWorkerChild = class {
473
691
  return scaler;
474
692
  }
475
693
  /**
694
+ * GPU crop+scale `toBuffer`: run the retained GPU surface through the cached
695
+ * GPU filtergraph (`scale_<be>→hwdownload→format` for the full-frame case),
696
+ * pulling ONLY the small detection-sized result into system memory, then pack
697
+ * it into the tightly-packed output buffer. On any GPU-filter failure,
698
+ * {@link degradeToSoftware} flips the whole session to the CPU path (via a
699
+ * re-dial) and this call rethrows so the pump drops just this one frame.
700
+ *
701
+ * Surface lifecycle: the input GPU surface is owned by {@link frames} (freed
702
+ * by the slot lifecycle) — `processAllSync` only ref's it. Every OUTPUT frame
703
+ * the filtergraph returns is freed here in the `finally`.
704
+ */
705
+ hwScaleToBuffer(frame, opts) {
706
+ try {
707
+ const region = resolveCropRegion(frame.width, frame.height, opts?.crop);
708
+ const target = resolveResizeTarget(region.width, region.height, opts?.resize);
709
+ const format = opts?.format ?? this.sessionFormat;
710
+ const channels = format === "gray" ? 1 : 3;
711
+ const outputs = this.ensureHwFilter(region, target, frame.width, frame.height, format).processAllSync(frame);
712
+ const first = outputs[0];
713
+ if (!first) throw new Error("GPU filtergraph produced no frame");
714
+ try {
715
+ const plane = first.data?.[0];
716
+ if (!plane) throw new Error("GPU filtergraph frame missing packed plane data");
717
+ return packSinglePlane(plane, first.linesize[0] ?? 0, target.width, target.height, channels);
718
+ } finally {
719
+ for (const out of outputs) out.free();
720
+ }
721
+ } catch (err) {
722
+ this.degradeToSoftware();
723
+ throw err instanceof Error ? err : new Error(errMessage(err));
724
+ }
725
+ }
726
+ /**
727
+ * Build-once / reuse the GPU crop+scale {@link NavFilterApi} for a given crop
728
+ * region + resize target + pixel format — the HW analogue of
729
+ * {@link ensureScaler}. Rebuilt (old CLOSED, freeing its GPU surface pool)
730
+ * only when the key changes; detection's ROI + resize target are stable, so
731
+ * this is a per-worker singleton in the steady state.
732
+ */
733
+ ensureHwFilter(region, target, srcW, srcH, format) {
734
+ const nav = this.nav;
735
+ const scaleFilter = this.gpuScaleFilter;
736
+ if (!nav || !scaleFilter || !this.hwContext) throw new Error("decode-worker-child: GPU filter requested without a HW context");
737
+ const fullFrame = isFullFrameRegion(region, srcW, srcH);
738
+ const pixel = pixelFilterName(format);
739
+ const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}:${pixel}`;
740
+ const cached = this.hwFilter;
741
+ if (cached && key === this.hwFilterKey) return cached;
742
+ this.hwFilter?.close();
743
+ this.hwFilter = null;
744
+ this.hwFilterKey = "";
745
+ const description = buildGpuFilterDescription(scaleFilter, region, target, fullFrame, pixel);
746
+ const filter = nav.FilterAPI.create(description, { hardware: this.hwContext });
747
+ this.hwFilter = filter;
748
+ this.hwFilterKey = key;
749
+ return filter;
750
+ }
751
+ /**
476
752
  * Resolve the HW context ONCE (reused across every re-dial, freed in
477
753
  * `teardown`). Tries the platform's ordered candidate list; the first
478
754
  * `HardwareContext.create` that succeeds wins; every failure falls through
479
- * to software decode (`hardware` omitted from `Decoder.create`).
755
+ * to software decode (`hardware` omitted from `Decoder.create`). When a GPU
756
+ * device is found, the session becomes a GPU crop+scale CANDIDATE
757
+ * (`hwFilterDecision = 'undecided'`) only if a `scale_<be>` filter exists for
758
+ * that backend — otherwise it stays on the software path.
480
759
  */
481
760
  async ensureHwContext(nav, C) {
482
761
  for (const backend of candidateBackends()) {
@@ -488,6 +767,8 @@ var DecodeWorkerChild = class {
488
767
  continue;
489
768
  }
490
769
  this.hwContext = hw;
770
+ this.gpuScaleFilter = gpuScaleFilterForBackend(backend);
771
+ this.hwFilterDecision = this.gpuScaleFilter !== null ? "undecided" : "software";
491
772
  return;
492
773
  }
493
774
  process.stderr.write("decode-worker-child: no hwaccel backend available — using software decode\n");
@@ -501,8 +782,18 @@ var DecodeWorkerChild = class {
501
782
  }, ms);
502
783
  });
503
784
  }
504
- /** Dispose the CURRENT dial's demuxer + decoder (the HW context is reused across dials). */
785
+ /**
786
+ * Dispose the CURRENT dial's demuxer + decoder (the HW *device* context is
787
+ * reused across dials). The GPU filtergraph is closed FIRST: it references the
788
+ * decoder's per-dial `hw_frames_ctx` AND owns the `scale_<be>` OUTPUT surface
789
+ * pool, so freeing it before the decoder is what stops re-dials from
790
+ * accumulating VAAPI surfaces (the exact prior leak). It is rebuilt lazily on
791
+ * the next dial's first `toBuffer`.
792
+ */
505
793
  closeInput() {
794
+ this.hwFilter?.close();
795
+ this.hwFilter = null;
796
+ this.hwFilterKey = "";
506
797
  this.decoder?.[Symbol.dispose]?.();
507
798
  this.decoder = null;
508
799
  this.demuxer?.[Symbol.dispose]?.();
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.1.18",
6
+ version: "1.1.19",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_stream_broker_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.1.31",
21
+ version: "1.1.32",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.1.26",
36
+ version: "1.1.27",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.1.31",
39
+ version: "1.1.32",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.1.18",
48
+ version: "1.1.19",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.1.26",
84
+ version: "1.1.27",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
- const require_dist = require("../dist-BJTPkJFw.js");
7
- const require_frame_handle_plane = require("../frame-handle-plane-BP8YV4sF.js");
6
+ const require_dist = require("../dist-NvwN60Fq.js");
7
+ const require_frame_handle_plane = require("../frame-handle-plane-B3Fxww8H.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  node_crypto = require_model_download_service_C_IHWnXx.__toESM(node_crypto);
10
10
  let node_child_process = require("node:child_process");
@@ -1,6 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as discriminatedUnion, B as DeviceType, D as nodePin, F as webrtcSessionCapability, G as makeSourceBrokerId, H as createEvent, I as errMsg, L as BaseAddon, N as streamBrokerCapability, Q as boolean, R as CAM_PROFILE_ORDER, V as EventCategory, W as makeProfileBrokerId, X as _enum, Z as array, at as string, c as EncodeProfileSchema, d as RingBuffer, g as cameraStreamsCapability, it as record, nt as number, ot as union, p as addonWidgetsSourceCapability, q as parseProfileBrokerId, rt as object, tt as literal, w as maskUrlCredentials, z as DeviceFeature } from "../dist-v6cKLmU3.mjs";
3
- import { n as DecoderSessionProxy, t as FrameHandlePlane } from "../frame-handle-plane-DKAXTtfn.mjs";
2
+ import { $ as discriminatedUnion, B as DeviceType, D as nodePin, F as webrtcSessionCapability, G as makeSourceBrokerId, H as createEvent, I as errMsg, L as BaseAddon, N as streamBrokerCapability, Q as boolean, R as CAM_PROFILE_ORDER, V as EventCategory, W as makeProfileBrokerId, X as _enum, Z as array, at as string, c as EncodeProfileSchema, d as RingBuffer, g as cameraStreamsCapability, it as record, nt as number, ot as union, p as addonWidgetsSourceCapability, q as parseProfileBrokerId, rt as object, tt as literal, w as maskUrlCredentials, z as DeviceFeature } from "../dist-GFd8M6KO.mjs";
3
+ import { n as DecoderSessionProxy, t as FrameHandlePlane } from "../frame-handle-plane-E_AsR77T.mjs";
4
4
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import * as crypto$1 from "node:crypto";
@@ -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-C2l0kmD4.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CeielP5P.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-BPkayx9u.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-CX-rILhw.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-BPkayx9u.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-B6fza7ic.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-CX-rILhw.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-CEsPjwzT.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-BPkayx9u.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-B6fza7ic.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-CX-rILhw.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-CEsPjwzT.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};