@camstack/addon-pipeline 1.1.48 → 1.1.50

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.
@@ -9,6 +9,7 @@ const require_worker_protocol = require("../worker-protocol-BCfO8gUF.js");
9
9
  let _camstack_shm_ring = require("@camstack/shm-ring");
10
10
  let node_child_process = require("node:child_process");
11
11
  let node_url = require("node:url");
12
+ let node_fs = require("node:fs");
12
13
  //#region src/pipeline-runner/bench-actions.ts
13
14
  /**
14
15
  * Synthetic-bench custom actions for pipeline-runner.
@@ -1294,6 +1295,139 @@ function startGuardedSessionDecode(deps) {
1294
1295
  };
1295
1296
  }
1296
1297
  //#endregion
1298
+ //#region src/session-decode/retention-sink.ts
1299
+ /**
1300
+ * Session-decode frame retention ring (Shape-2, Slice 1).
1301
+ *
1302
+ * The default detection path is **session-decode** (`useSessionDecode` true):
1303
+ * a per-session forked decode worker hands the runner decoded rgb pixels over
1304
+ * IPC — the frames never touch shared memory, so the emitted
1305
+ * `PipelineInferenceResult` carried NO `FrameHandle`, and hub-side post-analysis
1306
+ * (which gates ALL event/face/track crop media on a present handle) produced no
1307
+ * media. This ring restores that: every decoded detection frame is copied into
1308
+ * a short shared-memory retention ring on the runner's node, and the resulting
1309
+ * `FrameHandle` rides the inference-result event so post-analysis can resolve
1310
+ * the exact frame zero-copy and cut crops from it.
1311
+ *
1312
+ * ## Why a distinct prefix (`csr.`)
1313
+ *
1314
+ * The decoder addon runs `purgeOrphanSegments('csf.')` at startup and unlinks
1315
+ * EVERY `csf.*` segment on the node. A retention ring under the `csf.` prefix
1316
+ * would be reclaimed out from under a live session on every decoder redeploy.
1317
+ * The retention ring therefore uses its own prefix `csr.` and its own startup
1318
+ * orphan purge (`shm-retention-purge.ts`).
1319
+ *
1320
+ * ## Sizing
1321
+ *
1322
+ * Retention is ring depth: `slots ≈ detectionFps × retainSeconds`. At the
1323
+ * Slice-1 ≤640-wide detection geometry a slot is ~0.69 MB, so the 32 MB default
1324
+ * budget yields ~46 latest-wins slots (~4.6 s @10 fps). Budget is per active
1325
+ * session; the sink is armed lazily on the first rgb frame and destroyed on
1326
+ * session teardown, so idle cameras hold no shared memory.
1327
+ */
1328
+ /** Segment name prefix for session-decode retention rings — isolated from the
1329
+ * decoder's `csf.` orphan purge so a decoder redeploy never reclaims a live
1330
+ * retention ring. */
1331
+ var RETENTION_SEGMENT_PREFIX = "csr.";
1332
+ /** {@link RETAIN_BUDGET_MB} in bytes. */
1333
+ var RETAIN_BUDGET_BYTES = (() => {
1334
+ const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
1335
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 32;
1336
+ })() * 1024 * 1024;
1337
+ /**
1338
+ * Create a session-decode retention ring for one device. The underlying
1339
+ * shared-memory segment is created lazily on the first rgb frame.
1340
+ */
1341
+ function createSessionRetentionSink(options) {
1342
+ const { deviceId, nodeId, logger } = options;
1343
+ const sink = new _camstack_shm_ring.DecoderFrameRingSink({
1344
+ seed: `retain:${deviceId}`,
1345
+ logger,
1346
+ nodeId,
1347
+ segmentPrefix: RETENTION_SEGMENT_PREFIX,
1348
+ budgetBytes: RETAIN_BUDGET_BYTES
1349
+ });
1350
+ return {
1351
+ write(frame) {
1352
+ if (frame.format !== "rgb") return null;
1353
+ return sink.writeFrame(frame.data, {
1354
+ width: frame.width,
1355
+ height: frame.height,
1356
+ format: frame.format,
1357
+ pts: performance.now(),
1358
+ byteLength: frame.data.byteLength
1359
+ });
1360
+ },
1361
+ destroy() {
1362
+ sink.destroy();
1363
+ },
1364
+ get currentSegmentName() {
1365
+ return sink.currentSegmentName;
1366
+ }
1367
+ };
1368
+ }
1369
+ //#endregion
1370
+ //#region src/pipeline-runner/shm-retention-purge.ts
1371
+ /**
1372
+ * Startup reclamation of orphaned session-decode retention segments.
1373
+ *
1374
+ * The retention ring (`retention-sink.ts`) writes decoded detection frames into
1375
+ * named `/dev/shm` segments under the `csr.` prefix and unlinks each on graceful
1376
+ * session teardown. When the runner process dies ungracefully (SIGBUS, OOM-kill,
1377
+ * SIGKILL during redeploy) that teardown never runs and the segment is orphaned.
1378
+ * Across crashes/redeploys these accumulate until `/dev/shm` fills, at which
1379
+ * point the next `mmap` write faults with an uncatchable SIGBUS.
1380
+ *
1381
+ * This is the retention-ring analogue of the decoder addon's
1382
+ * `purgeOrphanSegments('csf.')`: a fresh pipeline-runner owns no live sessions,
1383
+ * so every pre-existing `csr.*` segment is by definition an orphan from a dead
1384
+ * instance. It MUST use the `csr.` prefix (never `csf.`): the decoder purge owns
1385
+ * `csf.` and unlinking a live retention ring would break media mid-session.
1386
+ *
1387
+ * `shm_unlink` only removes the name — a consumer still holding a mapping keeps
1388
+ * reading valid memory until it closes (POSIX deferred reclaim + the ring
1389
+ * seqlock), so unlinking is safe even if a stale reader is momentarily attached.
1390
+ *
1391
+ * POSIX-only: segments surface as files under `/dev/shm` on Linux. On platforms
1392
+ * without that directory (Windows, macOS) the scan finds nothing — no-op.
1393
+ */
1394
+ /** Default tmpfs directory where POSIX shared-memory segments appear on Linux. */
1395
+ var DEFAULT_SHM_DIR = "/dev/shm";
1396
+ /**
1397
+ * Unlink every session-decode retention segment whose name starts with
1398
+ * `prefix`. Intended to run ONCE at pipeline-runner startup, before any session
1399
+ * is created, to reclaim segments orphaned by a previously-crashed instance. A
1400
+ * per-file unlink failure is swallowed so one stuck segment cannot block
1401
+ * reclaiming the rest.
1402
+ */
1403
+ function purgeOrphanSegments(prefix, options = {}) {
1404
+ const dir = options.dir ?? DEFAULT_SHM_DIR;
1405
+ const unlink = options.unlink ?? _camstack_shm_ring.unlinkSegment;
1406
+ let entries;
1407
+ try {
1408
+ entries = (0, node_fs.readdirSync)(dir);
1409
+ } catch {
1410
+ return {
1411
+ scanned: 0,
1412
+ removed: 0,
1413
+ names: []
1414
+ };
1415
+ }
1416
+ const names = [];
1417
+ for (const name of entries) {
1418
+ if (!name.startsWith(prefix)) continue;
1419
+ try {
1420
+ unlink(name);
1421
+ names.push(name);
1422
+ } catch {}
1423
+ }
1424
+ return {
1425
+ scanned: entries.length,
1426
+ removed: names.length,
1427
+ names
1428
+ };
1429
+ }
1430
+ //#endregion
1297
1431
  //#region src/session-decode/session-decode-coordinator.ts
1298
1432
  /** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
1299
1433
  var SIGKILL_FALLBACK_MS = 1e3;
@@ -2061,6 +2195,16 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2061
2195
  async onInitialize() {
2062
2196
  const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
2063
2197
  this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
2198
+ try {
2199
+ const purged = purgeOrphanSegments(RETENTION_SEGMENT_PREFIX);
2200
+ if (purged.removed > 0) this.ctx.logger.info("session-decode retention: reclaimed orphaned shm segments", { meta: {
2201
+ removed: purged.removed,
2202
+ scanned: purged.scanned,
2203
+ prefix: RETENTION_SEGMENT_PREFIX
2204
+ } });
2205
+ } catch (err) {
2206
+ this.ctx.logger.warn("session-decode retention: orphan purge failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
2207
+ }
2064
2208
  this.runner = new PipelineRunner({
2065
2209
  maxQueueDepth: this.config.maxQueueDepth,
2066
2210
  maxConcurrentInferences: this.config.maxConcurrentInferences,
@@ -2767,10 +2911,10 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2767
2911
  * no second dial, no second decode, no GPU). Mirrors the shm shared-motion
2768
2912
  * seam (`subscribeMotionFrames` `shared` branch).
2769
2913
  */
2770
- enqueueSharedDetectionFrame(deviceId, frame) {
2914
+ enqueueSharedDetectionFrame(deviceId, frame, handle) {
2771
2915
  const runner = this.runner;
2772
2916
  if (!runner) return;
2773
- runner.enqueueDetectionFrame(deviceId, frame, void 0);
2917
+ runner.enqueueDetectionFrame(deviceId, frame, handle);
2774
2918
  const sink = this.sharedMotionSinks.get(deviceId);
2775
2919
  if (sink) {
2776
2920
  const now = this.sharedSinkNow();
@@ -2824,7 +2968,12 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2824
2968
  const runner = this.runner;
2825
2969
  const log = this.ctx.logger.withTags({ deviceId: config.deviceId });
2826
2970
  if (!runner) return () => {};
2827
- return startGuardedSessionDecode({
2971
+ const retentionSink = createSessionRetentionSink({
2972
+ deviceId: config.deviceId,
2973
+ nodeId: this.nodeId,
2974
+ logger: log
2975
+ });
2976
+ const teardown = startGuardedSessionDecode({
2828
2977
  logger: log,
2829
2978
  acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
2830
2979
  startPump: (source) => {
@@ -2842,11 +2991,16 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2842
2991
  },
2843
2992
  acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
2844
2993
  onDecodedFrame: (frame) => {
2845
- this.enqueueSharedDetectionFrame(config.deviceId, frame);
2994
+ const handle = retentionSink.write(frame) ?? void 0;
2995
+ this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
2846
2996
  }
2847
2997
  });
2848
2998
  }
2849
2999
  });
3000
+ return () => {
3001
+ teardown();
3002
+ retentionSink.destroy();
3003
+ };
2850
3004
  }
2851
3005
  /**
2852
3006
  * Epic C P1 motion path for a flagged camera: a dedicated `gray` decode of
@@ -3053,7 +3207,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3053
3207
  return null;
3054
3208
  }
3055
3209
  if (steps.length === 0) return null;
3056
- const useHandle = handle !== void 0 && handle.nodeId === this.nodeId;
3210
+ const useHandle = handle !== void 0 && handle.nodeId === this.nodeId && handle.width === frame.width && handle.height === frame.height;
3057
3211
  try {
3058
3212
  return await api.pipelineExecutor.runPipeline.mutate({
3059
3213
  steps: [...steps],
@@ -2,9 +2,10 @@ import { $ as boolean, D as nodePin, H as createEvent, I as errMsg, K as makeSou
2
2
  import { n as isRemoteRestream, r as profileForStreamId, t as RemoteSourcePlane } from "../remote-source-plane-D9mX0HiW.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { t as isWorkerReply } from "../worker-protocol-pk7qdYXt.mjs";
5
- import { FrameRingReaderCache } from "@camstack/shm-ring";
5
+ import { DecoderFrameRingSink, FrameRingReaderCache, unlinkSegment } from "@camstack/shm-ring";
6
6
  import { fork } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { readdirSync } from "node:fs";
8
9
  //#region src/pipeline-runner/bench-actions.ts
9
10
  /**
10
11
  * Synthetic-bench custom actions for pipeline-runner.
@@ -1290,6 +1291,139 @@ function startGuardedSessionDecode(deps) {
1290
1291
  };
1291
1292
  }
1292
1293
  //#endregion
1294
+ //#region src/session-decode/retention-sink.ts
1295
+ /**
1296
+ * Session-decode frame retention ring (Shape-2, Slice 1).
1297
+ *
1298
+ * The default detection path is **session-decode** (`useSessionDecode` true):
1299
+ * a per-session forked decode worker hands the runner decoded rgb pixels over
1300
+ * IPC — the frames never touch shared memory, so the emitted
1301
+ * `PipelineInferenceResult` carried NO `FrameHandle`, and hub-side post-analysis
1302
+ * (which gates ALL event/face/track crop media on a present handle) produced no
1303
+ * media. This ring restores that: every decoded detection frame is copied into
1304
+ * a short shared-memory retention ring on the runner's node, and the resulting
1305
+ * `FrameHandle` rides the inference-result event so post-analysis can resolve
1306
+ * the exact frame zero-copy and cut crops from it.
1307
+ *
1308
+ * ## Why a distinct prefix (`csr.`)
1309
+ *
1310
+ * The decoder addon runs `purgeOrphanSegments('csf.')` at startup and unlinks
1311
+ * EVERY `csf.*` segment on the node. A retention ring under the `csf.` prefix
1312
+ * would be reclaimed out from under a live session on every decoder redeploy.
1313
+ * The retention ring therefore uses its own prefix `csr.` and its own startup
1314
+ * orphan purge (`shm-retention-purge.ts`).
1315
+ *
1316
+ * ## Sizing
1317
+ *
1318
+ * Retention is ring depth: `slots ≈ detectionFps × retainSeconds`. At the
1319
+ * Slice-1 ≤640-wide detection geometry a slot is ~0.69 MB, so the 32 MB default
1320
+ * budget yields ~46 latest-wins slots (~4.6 s @10 fps). Budget is per active
1321
+ * session; the sink is armed lazily on the first rgb frame and destroyed on
1322
+ * session teardown, so idle cameras hold no shared memory.
1323
+ */
1324
+ /** Segment name prefix for session-decode retention rings — isolated from the
1325
+ * decoder's `csf.` orphan purge so a decoder redeploy never reclaims a live
1326
+ * retention ring. */
1327
+ var RETENTION_SEGMENT_PREFIX = "csr.";
1328
+ /** {@link RETAIN_BUDGET_MB} in bytes. */
1329
+ var RETAIN_BUDGET_BYTES = (() => {
1330
+ const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
1331
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 32;
1332
+ })() * 1024 * 1024;
1333
+ /**
1334
+ * Create a session-decode retention ring for one device. The underlying
1335
+ * shared-memory segment is created lazily on the first rgb frame.
1336
+ */
1337
+ function createSessionRetentionSink(options) {
1338
+ const { deviceId, nodeId, logger } = options;
1339
+ const sink = new DecoderFrameRingSink({
1340
+ seed: `retain:${deviceId}`,
1341
+ logger,
1342
+ nodeId,
1343
+ segmentPrefix: RETENTION_SEGMENT_PREFIX,
1344
+ budgetBytes: RETAIN_BUDGET_BYTES
1345
+ });
1346
+ return {
1347
+ write(frame) {
1348
+ if (frame.format !== "rgb") return null;
1349
+ return sink.writeFrame(frame.data, {
1350
+ width: frame.width,
1351
+ height: frame.height,
1352
+ format: frame.format,
1353
+ pts: performance.now(),
1354
+ byteLength: frame.data.byteLength
1355
+ });
1356
+ },
1357
+ destroy() {
1358
+ sink.destroy();
1359
+ },
1360
+ get currentSegmentName() {
1361
+ return sink.currentSegmentName;
1362
+ }
1363
+ };
1364
+ }
1365
+ //#endregion
1366
+ //#region src/pipeline-runner/shm-retention-purge.ts
1367
+ /**
1368
+ * Startup reclamation of orphaned session-decode retention segments.
1369
+ *
1370
+ * The retention ring (`retention-sink.ts`) writes decoded detection frames into
1371
+ * named `/dev/shm` segments under the `csr.` prefix and unlinks each on graceful
1372
+ * session teardown. When the runner process dies ungracefully (SIGBUS, OOM-kill,
1373
+ * SIGKILL during redeploy) that teardown never runs and the segment is orphaned.
1374
+ * Across crashes/redeploys these accumulate until `/dev/shm` fills, at which
1375
+ * point the next `mmap` write faults with an uncatchable SIGBUS.
1376
+ *
1377
+ * This is the retention-ring analogue of the decoder addon's
1378
+ * `purgeOrphanSegments('csf.')`: a fresh pipeline-runner owns no live sessions,
1379
+ * so every pre-existing `csr.*` segment is by definition an orphan from a dead
1380
+ * instance. It MUST use the `csr.` prefix (never `csf.`): the decoder purge owns
1381
+ * `csf.` and unlinking a live retention ring would break media mid-session.
1382
+ *
1383
+ * `shm_unlink` only removes the name — a consumer still holding a mapping keeps
1384
+ * reading valid memory until it closes (POSIX deferred reclaim + the ring
1385
+ * seqlock), so unlinking is safe even if a stale reader is momentarily attached.
1386
+ *
1387
+ * POSIX-only: segments surface as files under `/dev/shm` on Linux. On platforms
1388
+ * without that directory (Windows, macOS) the scan finds nothing — no-op.
1389
+ */
1390
+ /** Default tmpfs directory where POSIX shared-memory segments appear on Linux. */
1391
+ var DEFAULT_SHM_DIR = "/dev/shm";
1392
+ /**
1393
+ * Unlink every session-decode retention segment whose name starts with
1394
+ * `prefix`. Intended to run ONCE at pipeline-runner startup, before any session
1395
+ * is created, to reclaim segments orphaned by a previously-crashed instance. A
1396
+ * per-file unlink failure is swallowed so one stuck segment cannot block
1397
+ * reclaiming the rest.
1398
+ */
1399
+ function purgeOrphanSegments(prefix, options = {}) {
1400
+ const dir = options.dir ?? DEFAULT_SHM_DIR;
1401
+ const unlink = options.unlink ?? unlinkSegment;
1402
+ let entries;
1403
+ try {
1404
+ entries = readdirSync(dir);
1405
+ } catch {
1406
+ return {
1407
+ scanned: 0,
1408
+ removed: 0,
1409
+ names: []
1410
+ };
1411
+ }
1412
+ const names = [];
1413
+ for (const name of entries) {
1414
+ if (!name.startsWith(prefix)) continue;
1415
+ try {
1416
+ unlink(name);
1417
+ names.push(name);
1418
+ } catch {}
1419
+ }
1420
+ return {
1421
+ scanned: entries.length,
1422
+ removed: names.length,
1423
+ names
1424
+ };
1425
+ }
1426
+ //#endregion
1293
1427
  //#region src/session-decode/session-decode-coordinator.ts
1294
1428
  /** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
1295
1429
  var SIGKILL_FALLBACK_MS = 1e3;
@@ -2057,6 +2191,16 @@ var PipelineRunnerAddon = class extends BaseAddon {
2057
2191
  async onInitialize() {
2058
2192
  const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
2059
2193
  this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
2194
+ try {
2195
+ const purged = purgeOrphanSegments(RETENTION_SEGMENT_PREFIX);
2196
+ if (purged.removed > 0) this.ctx.logger.info("session-decode retention: reclaimed orphaned shm segments", { meta: {
2197
+ removed: purged.removed,
2198
+ scanned: purged.scanned,
2199
+ prefix: RETENTION_SEGMENT_PREFIX
2200
+ } });
2201
+ } catch (err) {
2202
+ this.ctx.logger.warn("session-decode retention: orphan purge failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
2203
+ }
2060
2204
  this.runner = new PipelineRunner({
2061
2205
  maxQueueDepth: this.config.maxQueueDepth,
2062
2206
  maxConcurrentInferences: this.config.maxConcurrentInferences,
@@ -2763,10 +2907,10 @@ var PipelineRunnerAddon = class extends BaseAddon {
2763
2907
  * no second dial, no second decode, no GPU). Mirrors the shm shared-motion
2764
2908
  * seam (`subscribeMotionFrames` `shared` branch).
2765
2909
  */
2766
- enqueueSharedDetectionFrame(deviceId, frame) {
2910
+ enqueueSharedDetectionFrame(deviceId, frame, handle) {
2767
2911
  const runner = this.runner;
2768
2912
  if (!runner) return;
2769
- runner.enqueueDetectionFrame(deviceId, frame, void 0);
2913
+ runner.enqueueDetectionFrame(deviceId, frame, handle);
2770
2914
  const sink = this.sharedMotionSinks.get(deviceId);
2771
2915
  if (sink) {
2772
2916
  const now = this.sharedSinkNow();
@@ -2820,7 +2964,12 @@ var PipelineRunnerAddon = class extends BaseAddon {
2820
2964
  const runner = this.runner;
2821
2965
  const log = this.ctx.logger.withTags({ deviceId: config.deviceId });
2822
2966
  if (!runner) return () => {};
2823
- return startGuardedSessionDecode({
2967
+ const retentionSink = createSessionRetentionSink({
2968
+ deviceId: config.deviceId,
2969
+ nodeId: this.nodeId,
2970
+ logger: log
2971
+ });
2972
+ const teardown = startGuardedSessionDecode({
2824
2973
  logger: log,
2825
2974
  acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
2826
2975
  startPump: (source) => {
@@ -2838,11 +2987,16 @@ var PipelineRunnerAddon = class extends BaseAddon {
2838
2987
  },
2839
2988
  acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
2840
2989
  onDecodedFrame: (frame) => {
2841
- this.enqueueSharedDetectionFrame(config.deviceId, frame);
2990
+ const handle = retentionSink.write(frame) ?? void 0;
2991
+ this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
2842
2992
  }
2843
2993
  });
2844
2994
  }
2845
2995
  });
2996
+ return () => {
2997
+ teardown();
2998
+ retentionSink.destroy();
2999
+ };
2846
3000
  }
2847
3001
  /**
2848
3002
  * Epic C P1 motion path for a flagged camera: a dedicated `gray` decode of
@@ -3049,7 +3203,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
3049
3203
  return null;
3050
3204
  }
3051
3205
  if (steps.length === 0) return null;
3052
- const useHandle = handle !== void 0 && handle.nodeId === this.nodeId;
3206
+ const useHandle = handle !== void 0 && handle.nodeId === this.nodeId && handle.width === frame.width && handle.height === frame.height;
3053
3207
  try {
3054
3208
  return await api.pipelineExecutor.runPipeline.mutate({
3055
3209
  steps: [...steps],
@@ -1532,6 +1532,7 @@ function buildPassthroughArgs(a) {
1532
1532
  "0",
1533
1533
  "-c:v",
1534
1534
  "copy",
1535
+ ...a.videoCodec === "H265" ? ["-tag:v", "hvc1"] : [],
1535
1536
  "-c:a",
1536
1537
  "aac",
1537
1538
  "-f",
@@ -1903,7 +1904,8 @@ var RecordingController = class {
1903
1904
  const writer = new SegmentWriter({
1904
1905
  rtspUrl: source.url,
1905
1906
  outDir,
1906
- segmentSeconds
1907
+ segmentSeconds,
1908
+ videoCodec: source.videoCodec
1907
1909
  }, {
1908
1910
  spawn: this.deps.spawn,
1909
1911
  logger: deviceLog,
@@ -1530,6 +1530,7 @@ function buildPassthroughArgs(a) {
1530
1530
  "0",
1531
1531
  "-c:v",
1532
1532
  "copy",
1533
+ ...a.videoCodec === "H265" ? ["-tag:v", "hvc1"] : [],
1533
1534
  "-c:a",
1534
1535
  "aac",
1535
1536
  "-f",
@@ -1901,7 +1902,8 @@ var RecordingController = class {
1901
1902
  const writer = new SegmentWriter({
1902
1903
  rtspUrl: source.url,
1903
1904
  outDir,
1904
- segmentSeconds
1905
+ segmentSeconds,
1906
+ videoCodec: source.videoCodec
1905
1907
  }, {
1906
1908
  spawn: this.deps.spawn,
1907
1909
  logger: deviceLog,
@@ -18691,6 +18691,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
18691
18691
  const widgetsProvider = { listWidgets: async () => [{
18692
18692
  tab: "device-tab",
18693
18693
  label: "Stream Brokers",
18694
+ preAuth: false,
18694
18695
  kind: "remote",
18695
18696
  remote: {
18696
18697
  remoteName: "addon_stream_broker_widgets",
@@ -18717,6 +18718,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
18717
18718
  }, {
18718
18719
  tab: "device-tab",
18719
18720
  label: "FFmpeg Parameters",
18721
+ preAuth: false,
18720
18722
  kind: "remote",
18721
18723
  remote: {
18722
18724
  remoteName: "addon_stream_broker_widgets",
@@ -18686,6 +18686,7 @@ var StreamBrokerAddon = class extends BaseAddon {
18686
18686
  const widgetsProvider = { listWidgets: async () => [{
18687
18687
  tab: "device-tab",
18688
18688
  label: "Stream Brokers",
18689
+ preAuth: false,
18689
18690
  kind: "remote",
18690
18691
  remote: {
18691
18692
  remoteName: "addon_stream_broker_widgets",
@@ -18712,6 +18713,7 @@ var StreamBrokerAddon = class extends BaseAddon {
18712
18713
  }, {
18713
18714
  tab: "device-tab",
18714
18715
  label: "FFmpeg Parameters",
18716
+ preAuth: false,
18715
18717
  kind: "remote",
18716
18718
  remote: {
18717
18719
  remoteName: "addon_stream_broker_widgets",
@@ -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-ByiLuUQZ.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-CceKv__y.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-ByiLuUQZ.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-BSSjCL6j.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-CceKv__y.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-9CvcTvAN.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-ByiLuUQZ.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-BSSjCL6j.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-CceKv__y.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-9CvcTvAN.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};