@camstack/addon-model-studio 1.1.20 → 1.1.21

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.
@@ -23758,13 +23758,24 @@ method(object({
23758
23758
  /** Playback-speed multiplier for the render (1 = realtime). */
23759
23759
  var ExportSpeedSchema = number().min(.25).max(32);
23760
23760
  /**
23761
- * One dense interval, in SECONDS FROM THE EXPORT'S OWN `fromMs`.
23761
+ * One dense interval, in WALL-CLOCK SECONDS FROM THE EXPORT'S OWN `fromMs`.
23762
23762
  *
23763
- * Relative and not absolute epoch on purpose: the renderer's frame-select
23764
- * expression sees ffmpeg's `t`, which starts at 0 for the export's source
23765
- * playlist. Handing it absolute epochs would make every call site responsible
23766
- * for the same subtraction, and the one that forgot would emit a filter that
23767
- * selects nothing silently, as a uniform timelapse.
23763
+ * **Wall clock, not ffmpeg's `t`** and the recorder translates. A caller
23764
+ * derives these bounds from things that happened at a TIME (a track's
23765
+ * `firstSeen`), while `t` runs over the source playlist: the concatenation of
23766
+ * every segment present for the range, with each recording GAP removed. The
23767
+ * two agree only on a window that recorded without one interruption, and only
23768
+ * the render side knows the segments, so the translation lives there
23769
+ * (`export-dense-map.ts`, addon-pipeline).
23770
+ *
23771
+ * It was not always so. These seconds were fed to `between(t,…)` verbatim, and
23772
+ * on a 10 h window holding 29,393 s of footage every range landed late by the
23773
+ * gap accumulated before it — up to 6,607 s, well past EOF. Nothing matched,
23774
+ * the video was a uniform timelapse, and the log line reported the five ranges
23775
+ * that had been ASKED for (2026-08-13, export `57d14363`, camera 615).
23776
+ *
23777
+ * Relative and not absolute epoch, because an absolute epoch would make every
23778
+ * call site responsible for the same subtraction.
23768
23779
  */
23769
23780
  var ExportDenseRangeSchema = object({
23770
23781
  fromSec: number().nonnegative(),
@@ -30894,25 +30905,32 @@ object({
30894
30905
  var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
30895
30906
  object({
30896
30907
  /**
30897
- * How long a retained native frame is served before it counts as a miss.
30908
+ * How many delivered frames the worker HOLDS at once, waiting for each one's
30909
+ * detection result.
30898
30910
  *
30899
- * Must cover the FULL late-crop horizon: detection inference + the
30900
- * cross-process inference-result hop to hub post-analysis + tracking + the
30901
- * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
30902
- * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
30903
- * RAM per busy camera grows linearly with no measured hit-rate gain.
30911
+ * This replaced a TTL on 2026-08-13, and the replacement is the whole point:
30912
+ * a time window was never related to the event the pixels were waiting for.
30913
+ * A held frame now lives from delivery until the runner has its `FrameResult`
30914
+ * at which moment the runner cuts the subject tiles it actually wanted and
30915
+ * releases the frame. The bound exists only so a runner that stops answering
30916
+ * cannot pin RAM: above it the OLDEST held frame is dropped and counted.
30917
+ *
30918
+ * Sizing: the steady state is `inferenceLatency × deliveredFps`, measured at
30919
+ * 40-160 ms × ≤25 fps = 1-4 frames. The default leaves headroom for a hiccup
30920
+ * without ever approaching the old resident set (43 frames × 24.9 MB at 4K).
30921
+ * Raising it does not buy hit rate — it buys tolerance for a slow runner, and
30922
+ * `holdOverflow` on the metrics line is what says you need it.
30904
30923
  */
30905
- ttlMs: number().int().min(250).max(1e4),
30924
+ holdFrames: number().int().min(1).max(64),
30906
30925
  /**
30907
30926
  * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
30908
30927
  *
30909
- * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
30910
- * which one is actually binding before reasoning from that. At the shipped
30911
- * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
30912
- * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
30913
- * `leaseFrames` on the metrics line say which. When the ceiling binds, a
30914
- * change that admits fewer frames buys retention WINDOW at constant RAM
30915
- * rather than giving RAM back — lower this knob if RAM is what you wanted.
30928
+ * Since 2026-08-13 this is a SAFETY ceiling and nothing else: `holdFrames`
30929
+ * is what decides how much is held, and the ceiling is the number above which
30930
+ * something is wrong. Before that it was the effective cap at 1024 MB with
30931
+ * a 2 800 ms TTL a 4K camera sat pinned at `leaseMb:1020, leaseFrames:43`
30932
+ * with the TTL expiring nothing, which is exactly the confusion the hold
30933
+ * removes. `leaseMb` / `leaseFrames` still say what is resident.
30916
30934
  * `0` DISABLES the lease entirely and falls the worker back to the tiny
30917
30935
  * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
30918
30936
  * to replace).
@@ -30938,22 +30956,45 @@ object({
30938
30956
  * there is the signal that some caller names frames outside the inference set
30939
30957
  * and that this must go back to `all`.
30940
30958
  */
30941
- admission: NativeLeaseAdmissionSchema
30959
+ admission: NativeLeaseAdmissionSchema,
30960
+ /**
30961
+ * RAM ceiling per decode worker, in MB, for the SUBJECT TILES — the
30962
+ * compressed native crops the worker cuts at the moment a frame's detection
30963
+ * result arrives, and keeps long after the frame itself is freed.
30964
+ *
30965
+ * This is the knob that replaced the old retention window, and it buys about
30966
+ * three orders of magnitude more of it: a tile is one subject at native
30967
+ * resolution, JPEG-encoded (~60-120 KB on a 4K person), against ~24.9 MB for
30968
+ * the frame it was cut from. A frame on which nothing was detected costs
30969
+ * nothing at all, which is the real change — the old lease paid per FRAME and
30970
+ * was interrogated per SUBJECT.
30971
+ *
30972
+ * `0` DISABLES tiles, leaving only the hold window and the ≤640 RAM
30973
+ * fallback — i.e. the pre-2026-08-13 miss profile. Set it there only to
30974
+ * reproduce that.
30975
+ */
30976
+ tileBudgetMb: number().int().min(0).max(1024)
30942
30977
  });
30943
30978
  /**
30944
- * The values in force when the operator has set nothing — byte-for-byte the
30945
- * constants the decode worker shipped with as env-var defaults, so making these
30946
- * settings changed no behaviour on the day it landed.
30979
+ * The values in force when the operator has set nothing.
30980
+ *
30981
+ * `budgetMb` stays at 1024 on the day the hold landed, deliberately: it stopped
30982
+ * being the retention window and became the OOM ceiling, and lowering a ceiling
30983
+ * in the same change that redefines it would make a regression and a retune
30984
+ * indistinguishable. Cut it once `tileHits` / `holdOverflow` have been read on
30985
+ * live traffic.
30947
30986
  */
30948
30987
  var DEFAULT_NATIVE_LEASE_SETTINGS = {
30949
- ttlMs: 1200,
30988
+ holdFrames: 8,
30950
30989
  budgetMb: 1024,
30951
30990
  activityMs: 15e3,
30991
+ tileBudgetMb: 64,
30952
30992
  admission: "inferred"
30953
30993
  };
30954
- DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
30994
+ DEFAULT_NATIVE_LEASE_SETTINGS.holdFrames;
30955
30995
  DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
30956
30996
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
30997
+ DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
30957
30998
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
30958
30999
  //#endregion
30959
31000
  //#region src/convert-orchestrator.ts
@@ -23735,13 +23735,24 @@ method(object({
23735
23735
  /** Playback-speed multiplier for the render (1 = realtime). */
23736
23736
  var ExportSpeedSchema = number().min(.25).max(32);
23737
23737
  /**
23738
- * One dense interval, in SECONDS FROM THE EXPORT'S OWN `fromMs`.
23738
+ * One dense interval, in WALL-CLOCK SECONDS FROM THE EXPORT'S OWN `fromMs`.
23739
23739
  *
23740
- * Relative and not absolute epoch on purpose: the renderer's frame-select
23741
- * expression sees ffmpeg's `t`, which starts at 0 for the export's source
23742
- * playlist. Handing it absolute epochs would make every call site responsible
23743
- * for the same subtraction, and the one that forgot would emit a filter that
23744
- * selects nothing silently, as a uniform timelapse.
23740
+ * **Wall clock, not ffmpeg's `t`** and the recorder translates. A caller
23741
+ * derives these bounds from things that happened at a TIME (a track's
23742
+ * `firstSeen`), while `t` runs over the source playlist: the concatenation of
23743
+ * every segment present for the range, with each recording GAP removed. The
23744
+ * two agree only on a window that recorded without one interruption, and only
23745
+ * the render side knows the segments, so the translation lives there
23746
+ * (`export-dense-map.ts`, addon-pipeline).
23747
+ *
23748
+ * It was not always so. These seconds were fed to `between(t,…)` verbatim, and
23749
+ * on a 10 h window holding 29,393 s of footage every range landed late by the
23750
+ * gap accumulated before it — up to 6,607 s, well past EOF. Nothing matched,
23751
+ * the video was a uniform timelapse, and the log line reported the five ranges
23752
+ * that had been ASKED for (2026-08-13, export `57d14363`, camera 615).
23753
+ *
23754
+ * Relative and not absolute epoch, because an absolute epoch would make every
23755
+ * call site responsible for the same subtraction.
23745
23756
  */
23746
23757
  var ExportDenseRangeSchema = object({
23747
23758
  fromSec: number().nonnegative(),
@@ -30871,25 +30882,32 @@ object({
30871
30882
  var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
30872
30883
  object({
30873
30884
  /**
30874
- * How long a retained native frame is served before it counts as a miss.
30885
+ * How many delivered frames the worker HOLDS at once, waiting for each one's
30886
+ * detection result.
30875
30887
  *
30876
- * Must cover the FULL late-crop horizon: detection inference + the
30877
- * cross-process inference-result hop to hub post-analysis + tracking + the
30878
- * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
30879
- * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
30880
- * RAM per busy camera grows linearly with no measured hit-rate gain.
30888
+ * This replaced a TTL on 2026-08-13, and the replacement is the whole point:
30889
+ * a time window was never related to the event the pixels were waiting for.
30890
+ * A held frame now lives from delivery until the runner has its `FrameResult`
30891
+ * at which moment the runner cuts the subject tiles it actually wanted and
30892
+ * releases the frame. The bound exists only so a runner that stops answering
30893
+ * cannot pin RAM: above it the OLDEST held frame is dropped and counted.
30894
+ *
30895
+ * Sizing: the steady state is `inferenceLatency × deliveredFps`, measured at
30896
+ * 40-160 ms × ≤25 fps = 1-4 frames. The default leaves headroom for a hiccup
30897
+ * without ever approaching the old resident set (43 frames × 24.9 MB at 4K).
30898
+ * Raising it does not buy hit rate — it buys tolerance for a slow runner, and
30899
+ * `holdOverflow` on the metrics line is what says you need it.
30881
30900
  */
30882
- ttlMs: number().int().min(250).max(1e4),
30901
+ holdFrames: number().int().min(1).max(64),
30883
30902
  /**
30884
30903
  * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
30885
30904
  *
30886
- * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
30887
- * which one is actually binding before reasoning from that. At the shipped
30888
- * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
30889
- * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
30890
- * `leaseFrames` on the metrics line say which. When the ceiling binds, a
30891
- * change that admits fewer frames buys retention WINDOW at constant RAM
30892
- * rather than giving RAM back — lower this knob if RAM is what you wanted.
30905
+ * Since 2026-08-13 this is a SAFETY ceiling and nothing else: `holdFrames`
30906
+ * is what decides how much is held, and the ceiling is the number above which
30907
+ * something is wrong. Before that it was the effective cap at 1024 MB with
30908
+ * a 2 800 ms TTL a 4K camera sat pinned at `leaseMb:1020, leaseFrames:43`
30909
+ * with the TTL expiring nothing, which is exactly the confusion the hold
30910
+ * removes. `leaseMb` / `leaseFrames` still say what is resident.
30893
30911
  * `0` DISABLES the lease entirely and falls the worker back to the tiny
30894
30912
  * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
30895
30913
  * to replace).
@@ -30915,22 +30933,45 @@ object({
30915
30933
  * there is the signal that some caller names frames outside the inference set
30916
30934
  * and that this must go back to `all`.
30917
30935
  */
30918
- admission: NativeLeaseAdmissionSchema
30936
+ admission: NativeLeaseAdmissionSchema,
30937
+ /**
30938
+ * RAM ceiling per decode worker, in MB, for the SUBJECT TILES — the
30939
+ * compressed native crops the worker cuts at the moment a frame's detection
30940
+ * result arrives, and keeps long after the frame itself is freed.
30941
+ *
30942
+ * This is the knob that replaced the old retention window, and it buys about
30943
+ * three orders of magnitude more of it: a tile is one subject at native
30944
+ * resolution, JPEG-encoded (~60-120 KB on a 4K person), against ~24.9 MB for
30945
+ * the frame it was cut from. A frame on which nothing was detected costs
30946
+ * nothing at all, which is the real change — the old lease paid per FRAME and
30947
+ * was interrogated per SUBJECT.
30948
+ *
30949
+ * `0` DISABLES tiles, leaving only the hold window and the ≤640 RAM
30950
+ * fallback — i.e. the pre-2026-08-13 miss profile. Set it there only to
30951
+ * reproduce that.
30952
+ */
30953
+ tileBudgetMb: number().int().min(0).max(1024)
30919
30954
  });
30920
30955
  /**
30921
- * The values in force when the operator has set nothing — byte-for-byte the
30922
- * constants the decode worker shipped with as env-var defaults, so making these
30923
- * settings changed no behaviour on the day it landed.
30956
+ * The values in force when the operator has set nothing.
30957
+ *
30958
+ * `budgetMb` stays at 1024 on the day the hold landed, deliberately: it stopped
30959
+ * being the retention window and became the OOM ceiling, and lowering a ceiling
30960
+ * in the same change that redefines it would make a regression and a retune
30961
+ * indistinguishable. Cut it once `tileHits` / `holdOverflow` have been read on
30962
+ * live traffic.
30924
30963
  */
30925
30964
  var DEFAULT_NATIVE_LEASE_SETTINGS = {
30926
- ttlMs: 1200,
30965
+ holdFrames: 8,
30927
30966
  budgetMb: 1024,
30928
30967
  activityMs: 15e3,
30968
+ tileBudgetMb: 64,
30929
30969
  admission: "inferred"
30930
30970
  };
30931
- DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
30971
+ DEFAULT_NATIVE_LEASE_SETTINGS.holdFrames;
30932
30972
  DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
30933
30973
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
30974
+ DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
30934
30975
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
30935
30976
  //#endregion
30936
30977
  //#region src/convert-orchestrator.ts
@@ -1,6 +1,6 @@
1
1
  import { c as e, g as t, h as n, l as r, n as i, p as a, r as o, t as s, u as c, y as l } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react__loadShare__.js-DJDHChgO.mjs";
2
2
  import { n as u, r as d, t as f } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-ds_Ehzaa.mjs";
3
- import { m as p } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DpvRwqsx.mjs";
3
+ import { m as p } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-COqVsV64.mjs";
4
4
  //#region ../ui-library/src/lib/cap-error.ts
5
5
  function m(e) {
6
6
  if (typeof e != "object" || !e) return null;
@@ -1,2 +1,2 @@
1
- import { n as e, t } from "./virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-CNbwY0Zt.mjs";
1
+ import { n as e, t } from "./virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-BKbceXPH.mjs";
2
2
  export { t as get, e as init };
@@ -1,4 +1,4 @@
1
- import { s as e } from "./player-overlays-9g9Wl5lu.mjs";
1
+ import { s as e } from "./player-overlays-BHBGqRYR.mjs";
2
2
  var t = e("eye-off", [
3
3
  ["path", {
4
4
  d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",
@@ -16,14 +16,31 @@ var t = e("eye-off", [
16
16
  d: "m2 2 20 20",
17
17
  key: "1ooewy"
18
18
  }]
19
- ]), n = e("square", [["rect", {
19
+ ]), n = e("mic", [
20
+ ["path", {
21
+ d: "M12 19v3",
22
+ key: "npa21l"
23
+ }],
24
+ ["path", {
25
+ d: "M19 10v2a7 7 0 0 1-14 0v-2",
26
+ key: "1vc78b"
27
+ }],
28
+ ["rect", {
29
+ x: "9",
30
+ y: "2",
31
+ width: "6",
32
+ height: "13",
33
+ rx: "3",
34
+ key: "s6n7sd"
35
+ }]
36
+ ]), r = e("square", [["rect", {
20
37
  width: "18",
21
38
  height: "18",
22
39
  x: "3",
23
40
  y: "3",
24
41
  rx: "2",
25
42
  key: "afitv7"
26
- }]]), r = e("trash-2", [
43
+ }]]), i = e("trash-2", [
27
44
  ["path", {
28
45
  d: "M10 11v6",
29
46
  key: "nco0om"
@@ -46,4 +63,4 @@ var t = e("eye-off", [
46
63
  }]
47
64
  ]);
48
65
  //#endregion
49
- export { n, t as r, r as t };
66
+ export { t as i, r as n, n as r, i as t };
@@ -2753,7 +2753,7 @@ async function rr(e) {
2753
2753
  }
2754
2754
  }
2755
2755
  async function ir() {
2756
- return tr ||= rr(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-ChXR-SrT.mjs")).catch((e) => {
2756
+ return tr ||= rr(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BD7O4C0O.mjs")).catch((e) => {
2757
2757
  throw tr = void 0, e;
2758
2758
  }), tr;
2759
2759
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-model-studio",
3
- "version": "1.1.20",
3
+ "version": "1.1.21",
4
4
  "description": "Custom detection model registry, conversion & distribution for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -1,290 +0,0 @@
1
- import { c as e, h as t, m as n, p as r, u as i, y as a } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react__loadShare__.js-DJDHChgO.mjs";
2
- import { n as o, r as s, t as c } from "./_virtual_mf___mfe_internal__addon_model_studio_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-ds_Ehzaa.mjs";
3
- import { a as l, l as u, n as ee, r as te, s as d, t as ne, u as re } from "./player-overlays-9g9Wl5lu.mjs";
4
- import { n as f, r as p, t as m } from "./trash-2-CuBBH1-r.mjs";
5
- import { MaskShapeCanvas as h } from "./MaskShapeCanvas-C4g96eMz.mjs";
6
- var g = d("hexagon", [["path", {
7
- 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",
8
- key: "yt0hxn"
9
- }]]);
10
- //#endregion
11
- //#region ../ui-library/src/composites/cap-settings/PrivacyMaskSettings.tsx
12
- a();
13
- var _ = 120, v = "privacy-mask", 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";
14
- function x(e) {
15
- return e.kind === "rect" || e.kind === "polygon" ? e : null;
16
- }
17
- function S(e) {
18
- let t = new Set(e.map((e) => e.id)), n = 0;
19
- for (; t.has(n);) n += 1;
20
- return n;
21
- }
22
- function C(e, t) {
23
- if (e.length !== t.length) return !1;
24
- for (let n = 0; n < e.length; n += 1) if (JSON.stringify(e[n]) !== JSON.stringify(t[n])) return !1;
25
- return !0;
26
- }
27
- function w({ deviceId: a }) {
28
- let d = te(l().trpcClient, a), [w, T] = t(null), [E, D] = t(!1), [O, k] = t(null), [A, j] = t(null), [M, N] = t(!1), [P, F] = t(!1), [I, L] = t(null), [R, z] = t(null), B = n(!1);
29
- i(() => {
30
- if (!d) return;
31
- let e = !1;
32
- return B.current = !1, T(null), D(!1), k(null), j(null), F(!1), L(null), z(null), (async () => {
33
- try {
34
- let t = await d.privacyMask?.getOptions({});
35
- if (e) return;
36
- if (!t) throw Error("device proxy not ready");
37
- if (T(t), B.current) return;
38
- let n = await d.privacyMask?.getStatus({});
39
- if (e) return;
40
- if (!n) throw Error("device proxy not ready");
41
- B.current = !0;
42
- let r = {
43
- enabled: n.enabled,
44
- regions: n.regions
45
- };
46
- j(r), k(r);
47
- } catch (t) {
48
- if (e) return;
49
- re(t) ? D(!0) : console.error("Privacy Mask load failed", t);
50
- }
51
- })(), () => {
52
- e = !0;
53
- };
54
- }, [d]);
55
- let V = r(() => O !== null && A !== null && (O.enabled !== A.enabled || !C(O.regions, A.regions)), [O, A]), H = O ? O.regions.length : 0, U = w ? w.maxRegions : 0, W = n(0);
56
- i(() => {
57
- W.current = U;
58
- }, [U]);
59
- let G = U > 0 && H >= U, K = e(() => {
60
- k((e) => e && {
61
- ...e,
62
- enabled: !e.enabled
63
- });
64
- }, []), q = e((e) => {
65
- L(typeof e == "number" ? e : null);
66
- }, []), J = r(() => O ? O.regions.map((e) => ({
67
- id: e.id,
68
- shape: e.shape,
69
- enabled: e.enabled,
70
- label: `Zone ${String(e.id)}`
71
- })) : [], [O]), Y = e((e, t) => {
72
- let n = x(t);
73
- n && k((t) => t && {
74
- ...t,
75
- regions: t.regions.map((t) => t.id === e ? {
76
- ...t,
77
- shape: n
78
- } : t)
79
- });
80
- }, []), X = e((e) => {
81
- let t = x(e);
82
- t && (z(null), k((e) => {
83
- if (!e) return e;
84
- let n = W.current;
85
- if (n > 0 && e.regions.length >= n) return e;
86
- let r = S(e.regions), i = {
87
- id: r,
88
- enabled: !0,
89
- shape: t
90
- };
91
- return L(r), {
92
- ...e,
93
- regions: [...e.regions, i]
94
- };
95
- }));
96
- }, []), Z = e((e) => {
97
- F(!0), L(null), z(e);
98
- }, []), ie = e((e) => {
99
- F(!0), z(null), L(e);
100
- }, []), ae = e((e) => {
101
- L((t) => t === e ? null : t), k((t) => t && {
102
- ...t,
103
- regions: t.regions.filter((t) => t.id !== e)
104
- });
105
- }, []), oe = e(() => {
106
- z(null), L(null), A && k({
107
- enabled: A.enabled,
108
- regions: A.regions
109
- });
110
- }, [A]), se = e(async () => {
111
- if (!(!d || !O)) {
112
- N(!0);
113
- try {
114
- await d.privacyMask?.setMask({ patch: {
115
- enabled: O.enabled,
116
- regions: [...O.regions]
117
- } });
118
- let e = await d.privacyMask?.getStatus({});
119
- if (e) {
120
- let t = {
121
- enabled: e.enabled,
122
- regions: e.regions
123
- };
124
- j(t), k(t);
125
- }
126
- } catch (e) {
127
- console.error("Privacy Mask save failed", e);
128
- } finally {
129
- N(!1);
130
- }
131
- }
132
- }, [d, O]), ce = e(() => {
133
- F((e) => (e && (z(null), L(null)), !e));
134
- }, []), Q = w?.supportedShapes ?? [];
135
- ne(r(() => P && !E && w && O ? {
136
- id: v,
137
- order: _,
138
- node: /* @__PURE__ */ o(h, {
139
- transparent: !0,
140
- items: J,
141
- supportedShapes: Q,
142
- polygonVertices: w.polygonVertices,
143
- selectedId: I,
144
- onSelect: q,
145
- onShapeChange: Y,
146
- onDrawComplete: X,
147
- drawingKind: R
148
- })
149
- } : null, [
150
- P,
151
- E,
152
- w,
153
- O,
154
- J,
155
- Q,
156
- I,
157
- q,
158
- Y,
159
- X,
160
- R
161
- ]));
162
- let le = w?.supportedShapes.includes("rect") ?? !1, ue = w?.supportedShapes.includes("polygon") ?? !1, $ = w !== null && (w.maxRegions <= 0 || w.supportedShapes.length === 0);
163
- return d ? /* @__PURE__ */ o(ee, {
164
- title: "Privacy Mask",
165
- icon: /* @__PURE__ */ o(p, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
166
- children: /* @__PURE__ */ o("div", {
167
- className: "flex flex-col gap-3",
168
- children: E || $ ? /* @__PURE__ */ o("p", {
169
- className: `${u} leading-relaxed`,
170
- children: "This camera doesn't support an on-board privacy mask."
171
- }) : !E && !$ && w !== null && O !== null ? /* @__PURE__ */ s(c, { children: [
172
- /* @__PURE__ */ s("p", {
173
- className: `${u} leading-relaxed`,
174
- children: [
175
- "Toggle ",
176
- /* @__PURE__ */ o("strong", {
177
- className: "text-foreground",
178
- children: "Edit mask"
179
- }),
180
- " to draw blanked-out zones on the live frame, then ",
181
- /* @__PURE__ */ o("strong", {
182
- className: "text-foreground",
183
- children: "Save"
184
- }),
185
- " 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."
186
- ]
187
- }),
188
- /* @__PURE__ */ s("div", {
189
- className: "flex items-center gap-2 flex-wrap",
190
- children: [
191
- /* @__PURE__ */ o("button", {
192
- type: "button",
193
- onClick: ce,
194
- disabled: M,
195
- "aria-pressed": P,
196
- className: P ? b : y,
197
- children: P ? "Done editing" : "Edit mask"
198
- }),
199
- /* @__PURE__ */ o("button", {
200
- type: "button",
201
- onClick: K,
202
- disabled: M,
203
- "aria-pressed": O.enabled,
204
- className: O.enabled ? b : y,
205
- children: O.enabled ? "Mask on" : "Mask off"
206
- }),
207
- le && /* @__PURE__ */ o("button", {
208
- type: "button",
209
- onClick: () => Z("rect"),
210
- disabled: M || G,
211
- "aria-pressed": R === "rect",
212
- className: R === "rect" ? b : y,
213
- title: G ? "Maximum zones reached" : "Add a rectangle zone",
214
- children: "+ Rect"
215
- }),
216
- ue && /* @__PURE__ */ o("button", {
217
- type: "button",
218
- onClick: () => Z("polygon"),
219
- disabled: M || G,
220
- "aria-pressed": R === "polygon",
221
- className: R === "polygon" ? b : y,
222
- title: G ? "Maximum zones reached" : "Add a polygon zone",
223
- children: "+ Polygon"
224
- }),
225
- /* @__PURE__ */ s("span", {
226
- className: `${u} ml-1 tabular-nums`,
227
- children: [
228
- H,
229
- " / ",
230
- U,
231
- " zones"
232
- ]
233
- }),
234
- /* @__PURE__ */ o("span", { className: "flex-1" }),
235
- /* @__PURE__ */ o("button", {
236
- type: "button",
237
- onClick: oe,
238
- disabled: M || !V,
239
- 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",
240
- children: "Revert"
241
- }),
242
- /* @__PURE__ */ o("button", {
243
- type: "button",
244
- onClick: () => void se(),
245
- disabled: M || !V,
246
- className: b,
247
- children: M ? "Saving…" : "Save"
248
- })
249
- ]
250
- }),
251
- H > 0 ? /* @__PURE__ */ o("div", {
252
- className: "flex flex-col gap-1",
253
- children: O.regions.map((e) => {
254
- let t = I === e.id, n = e.shape.kind === "polygon" ? g : f;
255
- return /* @__PURE__ */ s("div", {
256
- 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"}`,
257
- children: [/* @__PURE__ */ s("button", {
258
- type: "button",
259
- onClick: () => ie(e.id),
260
- disabled: M,
261
- 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",
262
- children: [
263
- /* @__PURE__ */ o(n, { className: "h-3.5 w-3.5 shrink-0" }),
264
- /* @__PURE__ */ s("span", { children: ["Zone ", e.id] }),
265
- /* @__PURE__ */ o("span", {
266
- className: "text-foreground-faint capitalize",
267
- children: e.shape.kind
268
- })
269
- ]
270
- }), /* @__PURE__ */ o("button", {
271
- type: "button",
272
- onClick: () => ae(e.id),
273
- disabled: M,
274
- "aria-label": `Delete zone ${String(e.id)}`,
275
- title: "Delete zone",
276
- 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",
277
- children: /* @__PURE__ */ o(m, { className: "h-3.5 w-3.5" })
278
- })]
279
- }, e.id);
280
- })
281
- }) : null
282
- ] }) : /* @__PURE__ */ o("p", {
283
- className: `${u} leading-relaxed`,
284
- children: "Loading the camera's privacy mask…"
285
- })
286
- })
287
- }) : null;
288
- }
289
- //#endregion
290
- export { w as PrivacyMaskSettings };