@camstack/addon-pipeline 1.1.48 → 1.1.49
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.
- package/dist/pipeline-runner/index.js +159 -5
- package/dist/pipeline-runner/index.mjs +160 -6
- package/dist/recorder/index.js +3 -1
- package/dist/recorder/index.mjs +3 -1
- package/dist/stream-broker/index.js +2 -0
- package/dist/stream-broker/index.mjs +2 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BSSjCL6j.js → MaskShapeCanvas-DI4BY7W2-D8NXKYD0.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BDb2ziPs.js → MotionZonesSettings-NcxxQN8r-DB0CpPOD.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-ARYovxqE.js → PrivacyMaskSettings-APgPLF7p-Bhjiklzh.js} +1 -1
- package/embed-dist/assets/{index-ByiLuUQZ.js → index-Btxl30DR.js} +4 -4
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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],
|
package/dist/recorder/index.js
CHANGED
|
@@ -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,
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -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-
|
|
1
|
+
import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-Btxl30DR.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-
|
|
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-Btxl30DR.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-D8NXKYD0.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-
|
|
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-Btxl30DR.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-D8NXKYD0.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};
|