@camstack/addon-pipeline 1.2.156 → 1.2.160
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/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-C499qEUf.js → dist-5VmTxa8x.js} +183 -76
- package/dist/{dist-C9ohtgTQ.mjs → dist-C8zVKhzA.mjs} +183 -76
- package/dist/{lazy-sharp-D5uApbbe.js → lazy-sharp-C89jIlwj.js} +1 -1
- package/dist/{local-frame-registry-VkzIezFI.js → local-frame-registry-BNMgZ5uA.js} +1 -1
- package/dist/{local-frame-registry-DAVug-jA.mjs → local-frame-registry-Bh-egEcl.mjs} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-DEq8mutP.js → node-FsbZDqyh.js} +1 -1
- package/dist/{node-CwIlVFyz.mjs → node-G9AiO_JJ.mjs} +1 -1
- package/dist/pipeline-runner/index.js +5 -5
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-DWw7FrV5.mjs → process-memory-Bbvc_vQS.mjs} +1 -1
- package/dist/{process-memory-DzycAIEm.js → process-memory-CMC_42XT.js} +1 -1
- package/dist/recorder/index.js +535 -28
- package/dist/recorder/index.mjs +532 -30
- package/dist/{segment-demux-js-D2InpBpa.mjs → segment-demux-js-Cw--C40J.mjs} +1 -1
- package/dist/{segment-demux-js-Df1rAqNh.js → segment-demux-js-iEt5OWTd.js} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BJlcLzCs.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-l7NtBWCj.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Blnh70uP.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CWtPG7OH.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-QpJx-TgK.mjs → hostInit-C2zs9368.mjs} +3 -3
- package/dist/stream-broker/index.js +20 -14
- package/dist/stream-broker/index.mjs +20 -14
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CFHcqXoS.mjs → worker-protocol-CA_SVsYj.mjs} +1 -1
- package/dist/{worker-protocol-ge8Ce0Bo.js → worker-protocol-CB8pNFkV.js} +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-lMPlG5w5.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-yWrVN7u8.mjs +0 -26
package/dist/recorder/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
const require_dist = require("../dist-5VmTxa8x.js");
|
|
6
|
+
const require_node = require("../node-FsbZDqyh.js");
|
|
3
7
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
4
8
|
const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
|
|
5
9
|
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
|
|
@@ -4211,6 +4215,377 @@ function sendDirectory(res, payload) {
|
|
|
4211
4215
|
res.end(Buffer.from(body));
|
|
4212
4216
|
}
|
|
4213
4217
|
//#endregion
|
|
4218
|
+
//#region src/recorder/addon/location-discard-actions.ts
|
|
4219
|
+
var DiscardStartInput = require_dist.object({ locationId: require_dist.string().min(1) });
|
|
4220
|
+
var DiscardStarted = require_dist.object({
|
|
4221
|
+
started: require_dist.boolean(),
|
|
4222
|
+
alreadyRunning: require_dist.boolean(),
|
|
4223
|
+
jobId: require_dist.string().nullable()
|
|
4224
|
+
});
|
|
4225
|
+
var DiscardStatusSchema = require_dist.object({
|
|
4226
|
+
running: require_dist.boolean(),
|
|
4227
|
+
locationId: require_dist.string().nullable(),
|
|
4228
|
+
jobId: require_dist.string().nullable(),
|
|
4229
|
+
phase: require_dist._enum([
|
|
4230
|
+
"idle",
|
|
4231
|
+
"scanning",
|
|
4232
|
+
"wiping-fs",
|
|
4233
|
+
"wiping-db",
|
|
4234
|
+
"cascading",
|
|
4235
|
+
"complete",
|
|
4236
|
+
"cancelled",
|
|
4237
|
+
"error"
|
|
4238
|
+
]),
|
|
4239
|
+
camerasTotal: require_dist.number(),
|
|
4240
|
+
camerasDone: require_dist.number(),
|
|
4241
|
+
foldersRemoved: require_dist.number(),
|
|
4242
|
+
dbRowsDeleted: require_dist.number(),
|
|
4243
|
+
ownersCascaded: require_dist.number(),
|
|
4244
|
+
detail: require_dist.string(),
|
|
4245
|
+
startedAtMs: require_dist.number().nullable(),
|
|
4246
|
+
finishedAtMs: require_dist.number().nullable(),
|
|
4247
|
+
error: require_dist.string().nullable()
|
|
4248
|
+
});
|
|
4249
|
+
var DiscardCancel = require_dist.object({ requested: require_dist.boolean() });
|
|
4250
|
+
var locationDiscardActions = require_dist.defineCustomActions({
|
|
4251
|
+
"storage.discardLocation": require_dist.customAction(DiscardStartInput, DiscardStarted, { auth: "admin" }),
|
|
4252
|
+
"storage.discardLocationStatus": require_dist.customAction(require_dist.object({}), DiscardStatusSchema, { auth: "admin" }),
|
|
4253
|
+
"storage.discardLocationCancel": require_dist.customAction(require_dist.object({}), DiscardCancel, { auth: "admin" })
|
|
4254
|
+
});
|
|
4255
|
+
//#endregion
|
|
4256
|
+
//#region src/recorder/addon/location-class-wipe.ts
|
|
4257
|
+
/**
|
|
4258
|
+
* Class-subtree wipe — the filesystem half of discarding a frozen location
|
|
4259
|
+
* that SHARES its root with another class (today: Toshiba `/recordings`
|
|
4260
|
+
* holds events + low + high).
|
|
4261
|
+
*
|
|
4262
|
+
* The only directories this module will `rm` are
|
|
4263
|
+
* `<root>/<camera>/<subtree>` for `subtree` in the class map. It refuses
|
|
4264
|
+
* a path that is not exactly two levels under the root, a symlink, or a
|
|
4265
|
+
* subtree the class did not name. `rm` of the location root is not a
|
|
4266
|
+
* code path.
|
|
4267
|
+
*/
|
|
4268
|
+
var DISCARD_SUBTREES$1 = {
|
|
4269
|
+
eventMedia: ["events"],
|
|
4270
|
+
recordingsLow: ["low"],
|
|
4271
|
+
recordings: ["high", "mid"]
|
|
4272
|
+
};
|
|
4273
|
+
function discardSubtreesForType(type) {
|
|
4274
|
+
return DISCARD_SUBTREES$1[type] ?? null;
|
|
4275
|
+
}
|
|
4276
|
+
var DEFAULT_FS = {
|
|
4277
|
+
readdir: (dir) => node_fs.promises.readdir(dir, { withFileTypes: true }),
|
|
4278
|
+
lstat: (target) => node_fs.promises.lstat(target),
|
|
4279
|
+
rm: (target) => node_fs.promises.rm(target, {
|
|
4280
|
+
recursive: true,
|
|
4281
|
+
force: true
|
|
4282
|
+
})
|
|
4283
|
+
};
|
|
4284
|
+
/**
|
|
4285
|
+
* Resolve `<root>/<camera>/<subtree>` and refuse anything that is not
|
|
4286
|
+
* exactly that shape under `root`. The check is lexical (after
|
|
4287
|
+
* `path.resolve`) so `../high` in a camera name cannot escape.
|
|
4288
|
+
*/
|
|
4289
|
+
function resolveClassDir(root, cameraId, subtree) {
|
|
4290
|
+
if (cameraId.length === 0 || cameraId === "." || cameraId === "..") throw new Error(`discard: illegal camera id ${cameraId}`);
|
|
4291
|
+
if (subtree.includes("/") || subtree.includes("\\") || subtree === ".." || subtree === ".") throw new Error(`discard: illegal subtree ${subtree}`);
|
|
4292
|
+
const rootResolved = node_path.default.resolve(root);
|
|
4293
|
+
const target = node_path.default.resolve(rootResolved, cameraId, subtree);
|
|
4294
|
+
const rel = node_path.default.relative(rootResolved, target);
|
|
4295
|
+
if (rel.startsWith("..") || node_path.default.isAbsolute(rel)) throw new Error(`discard: path escaped root: ${target}`);
|
|
4296
|
+
const parts = rel.split(node_path.default.sep).filter((p) => p.length > 0);
|
|
4297
|
+
if (parts.length !== 2 || parts[0] !== cameraId || parts[1] !== subtree) throw new Error(`discard: path is not a class subtree: ${target}`);
|
|
4298
|
+
return target;
|
|
4299
|
+
}
|
|
4300
|
+
async function wipeClassSubtrees(input) {
|
|
4301
|
+
const subtrees = discardSubtreesForType(input.type);
|
|
4302
|
+
if (subtrees === null) throw new Error(`discard: unknown location type ${input.type}`);
|
|
4303
|
+
const io = input.io ?? DEFAULT_FS;
|
|
4304
|
+
const rootResolved = node_path.default.resolve(input.root);
|
|
4305
|
+
let cameras;
|
|
4306
|
+
try {
|
|
4307
|
+
cameras = await io.readdir(rootResolved);
|
|
4308
|
+
} catch (err) {
|
|
4309
|
+
if (err.code === "ENOENT") return {
|
|
4310
|
+
camerasDone: 0,
|
|
4311
|
+
foldersRemoved: 0,
|
|
4312
|
+
cancelled: false
|
|
4313
|
+
};
|
|
4314
|
+
throw err;
|
|
4315
|
+
}
|
|
4316
|
+
const dirs = cameras.filter((e) => e.isDirectory() && e.name !== "." && e.name !== "..");
|
|
4317
|
+
let camerasDone = 0;
|
|
4318
|
+
let foldersRemoved = 0;
|
|
4319
|
+
for (const dir of dirs) {
|
|
4320
|
+
if (input.stop()) return {
|
|
4321
|
+
camerasDone,
|
|
4322
|
+
foldersRemoved,
|
|
4323
|
+
cancelled: true
|
|
4324
|
+
};
|
|
4325
|
+
for (const subtree of subtrees) {
|
|
4326
|
+
const target = resolveClassDir(rootResolved, dir.name, subtree);
|
|
4327
|
+
let st;
|
|
4328
|
+
try {
|
|
4329
|
+
st = await io.lstat(target);
|
|
4330
|
+
} catch (err) {
|
|
4331
|
+
if (err.code === "ENOENT") continue;
|
|
4332
|
+
throw err;
|
|
4333
|
+
}
|
|
4334
|
+
if (st.isSymbolicLink()) throw new Error(`discard: refusing to wipe symlink ${target}`);
|
|
4335
|
+
if (!st.isDirectory()) continue;
|
|
4336
|
+
await io.rm(target);
|
|
4337
|
+
foldersRemoved += 1;
|
|
4338
|
+
}
|
|
4339
|
+
camerasDone += 1;
|
|
4340
|
+
input.onProgress({
|
|
4341
|
+
camerasTotal: dirs.length,
|
|
4342
|
+
camerasDone,
|
|
4343
|
+
cameraId: dir.name,
|
|
4344
|
+
foldersRemoved
|
|
4345
|
+
});
|
|
4346
|
+
if (input.pace) await input.pace();
|
|
4347
|
+
}
|
|
4348
|
+
return {
|
|
4349
|
+
camerasDone,
|
|
4350
|
+
foldersRemoved,
|
|
4351
|
+
cancelled: false
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
//#endregion
|
|
4355
|
+
//#region src/recorder/addon/location-discard-policy.ts
|
|
4356
|
+
/**
|
|
4357
|
+
* Server-side eligibility for discarding a frozen location's leftover
|
|
4358
|
+
* class subtree. Same rules as the admin-ui card: frozen, known class,
|
|
4359
|
+
* writable sibling of the same type. The last writable of a type is
|
|
4360
|
+
* refused here even if the UI is bypassed.
|
|
4361
|
+
*/
|
|
4362
|
+
var DISCARD_SUBTREES = {
|
|
4363
|
+
eventMedia: ["events"],
|
|
4364
|
+
recordingsLow: ["low"],
|
|
4365
|
+
recordings: ["high", "mid"]
|
|
4366
|
+
};
|
|
4367
|
+
function locationIsFrozen(location) {
|
|
4368
|
+
const enabled = location.enabled !== false;
|
|
4369
|
+
const readOnly = location.config["readOnly"] === true;
|
|
4370
|
+
return !enabled || readOnly;
|
|
4371
|
+
}
|
|
4372
|
+
function discardEligible(location, siblings) {
|
|
4373
|
+
if (!(location.type in DISCARD_SUBTREES)) return false;
|
|
4374
|
+
if (!locationIsFrozen(location)) return false;
|
|
4375
|
+
return siblings.some((s) => {
|
|
4376
|
+
if (s.id === location.id) return false;
|
|
4377
|
+
if (s.type !== location.type) return false;
|
|
4378
|
+
const enabled = s.enabled !== false;
|
|
4379
|
+
const readOnly = s.config["readOnly"] === true;
|
|
4380
|
+
return enabled && !readOnly;
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
function assertDiscardAllowed(location, all, localNodeId) {
|
|
4384
|
+
if (!discardEligible(location, all.filter((s) => s.type === location.type))) throw new Error(`discard refused: ${location.id} is not a frozen location with a writable sibling of type ${location.type}`);
|
|
4385
|
+
const nodeId = location.nodeId ?? "hub";
|
|
4386
|
+
if (nodeId !== localNodeId) throw new Error(`discard refused: ${location.id} lives on node ${nodeId}, this process is ${localNodeId}`);
|
|
4387
|
+
}
|
|
4388
|
+
function locationBasePath(location) {
|
|
4389
|
+
const basePath = location.config["basePath"];
|
|
4390
|
+
if (typeof basePath !== "string" || basePath.length === 0) throw new Error(`discard refused: ${location.id} has no basePath`);
|
|
4391
|
+
return basePath;
|
|
4392
|
+
}
|
|
4393
|
+
//#endregion
|
|
4394
|
+
//#region src/recorder/addon/location-discard-job.ts
|
|
4395
|
+
/**
|
|
4396
|
+
* Footage discard job: wipe `<camera>/low/` (or `high/`+`mid/` for the
|
|
4397
|
+
* recordings class) on a frozen location, then forget the hour rows
|
|
4398
|
+
* stamped with that `locationId` and drop them from the RAM index.
|
|
4399
|
+
*
|
|
4400
|
+
* The class-subtree wipe is the load-bearing safety: `recordingsLow:default`
|
|
4401
|
+
* shares `/recordings` with `recordings:default`. This job never `rm`s the
|
|
4402
|
+
* location root. Profiles passed to the ledger forget are the class's own
|
|
4403
|
+
* (`low` vs `high`/`mid`) so a mis-stamped hour of the other class is not
|
|
4404
|
+
* dropped.
|
|
4405
|
+
*
|
|
4406
|
+
* Start returns immediately. Progress is the status snapshot.
|
|
4407
|
+
*/
|
|
4408
|
+
var IDLE_DISCARD = {
|
|
4409
|
+
running: false,
|
|
4410
|
+
locationId: null,
|
|
4411
|
+
jobId: null,
|
|
4412
|
+
phase: "idle",
|
|
4413
|
+
camerasTotal: 0,
|
|
4414
|
+
camerasDone: 0,
|
|
4415
|
+
foldersRemoved: 0,
|
|
4416
|
+
dbRowsDeleted: 0,
|
|
4417
|
+
ownersCascaded: 0,
|
|
4418
|
+
detail: "",
|
|
4419
|
+
startedAtMs: null,
|
|
4420
|
+
finishedAtMs: null,
|
|
4421
|
+
error: null
|
|
4422
|
+
};
|
|
4423
|
+
var USB_PACE_MS = 80;
|
|
4424
|
+
var FOOTAGE_TYPES = new Set(["recordings", "recordingsLow"]);
|
|
4425
|
+
function sleep(ms) {
|
|
4426
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4427
|
+
}
|
|
4428
|
+
function profilesForType(type) {
|
|
4429
|
+
return type === "recordingsLow" ? ["low"] : ["high", "mid"];
|
|
4430
|
+
}
|
|
4431
|
+
function createFootageDiscardJob(deps) {
|
|
4432
|
+
let state = { ...IDLE_DISCARD };
|
|
4433
|
+
let cancelRequested = false;
|
|
4434
|
+
let inflight = null;
|
|
4435
|
+
const report = (patch) => {
|
|
4436
|
+
state = {
|
|
4437
|
+
...state,
|
|
4438
|
+
...patch
|
|
4439
|
+
};
|
|
4440
|
+
};
|
|
4441
|
+
const status = () => ({ ...state });
|
|
4442
|
+
const cancel = () => {
|
|
4443
|
+
if (!state.running) return { requested: false };
|
|
4444
|
+
cancelRequested = true;
|
|
4445
|
+
report({ detail: "cancel requested — finishing current camera" });
|
|
4446
|
+
return { requested: true };
|
|
4447
|
+
};
|
|
4448
|
+
const start = async (input) => {
|
|
4449
|
+
if (state.running) return {
|
|
4450
|
+
started: false,
|
|
4451
|
+
alreadyRunning: true,
|
|
4452
|
+
jobId: state.jobId
|
|
4453
|
+
};
|
|
4454
|
+
const jobId = (0, node_crypto.randomUUID)();
|
|
4455
|
+
cancelRequested = false;
|
|
4456
|
+
report({
|
|
4457
|
+
...IDLE_DISCARD,
|
|
4458
|
+
running: true,
|
|
4459
|
+
locationId: input.locationId,
|
|
4460
|
+
jobId,
|
|
4461
|
+
phase: "scanning",
|
|
4462
|
+
startedAtMs: deps.now(),
|
|
4463
|
+
detail: "starting"
|
|
4464
|
+
});
|
|
4465
|
+
try {
|
|
4466
|
+
const all = await deps.listLocations();
|
|
4467
|
+
const location = all.find((row) => row.id === input.locationId);
|
|
4468
|
+
if (!location) throw new Error(`discard refused: unknown location ${input.locationId}`);
|
|
4469
|
+
if (!FOOTAGE_TYPES.has(location.type)) throw new Error(`discard refused: recorder handles footage, not ${location.type}`);
|
|
4470
|
+
assertDiscardAllowed(location, all, deps.localNodeId);
|
|
4471
|
+
const root = locationBasePath(location);
|
|
4472
|
+
report({ detail: `scanning ${root}` });
|
|
4473
|
+
inflight = run(location, root).finally(() => {
|
|
4474
|
+
inflight = null;
|
|
4475
|
+
});
|
|
4476
|
+
return {
|
|
4477
|
+
started: true,
|
|
4478
|
+
alreadyRunning: false,
|
|
4479
|
+
jobId
|
|
4480
|
+
};
|
|
4481
|
+
} catch (err) {
|
|
4482
|
+
report({
|
|
4483
|
+
...IDLE_DISCARD,
|
|
4484
|
+
phase: "error",
|
|
4485
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4486
|
+
finishedAtMs: deps.now()
|
|
4487
|
+
});
|
|
4488
|
+
throw err;
|
|
4489
|
+
}
|
|
4490
|
+
};
|
|
4491
|
+
const run = async (location, root) => {
|
|
4492
|
+
const wipe = deps.wipe ?? wipeClassSubtrees;
|
|
4493
|
+
const paceMs = deps.paceMs ?? USB_PACE_MS;
|
|
4494
|
+
try {
|
|
4495
|
+
report({
|
|
4496
|
+
phase: "wiping-fs",
|
|
4497
|
+
detail: `wiping ${profilesForType(location.type).join("/")} under ${root}`
|
|
4498
|
+
});
|
|
4499
|
+
const fsResult = await wipe({
|
|
4500
|
+
root,
|
|
4501
|
+
type: location.type,
|
|
4502
|
+
stop: () => cancelRequested,
|
|
4503
|
+
onProgress: (p) => {
|
|
4504
|
+
report({
|
|
4505
|
+
phase: "wiping-fs",
|
|
4506
|
+
camerasTotal: p.camerasTotal,
|
|
4507
|
+
camerasDone: p.camerasDone,
|
|
4508
|
+
foldersRemoved: p.foldersRemoved,
|
|
4509
|
+
detail: `camera ${p.cameraId} (${p.camerasDone}/${p.camerasTotal})`
|
|
4510
|
+
});
|
|
4511
|
+
},
|
|
4512
|
+
pace: () => sleep(paceMs)
|
|
4513
|
+
});
|
|
4514
|
+
if (fsResult.cancelled || cancelRequested) {
|
|
4515
|
+
report({
|
|
4516
|
+
running: false,
|
|
4517
|
+
phase: "cancelled",
|
|
4518
|
+
camerasDone: fsResult.camerasDone,
|
|
4519
|
+
foldersRemoved: fsResult.foldersRemoved,
|
|
4520
|
+
finishedAtMs: deps.now(),
|
|
4521
|
+
detail: "cancelled"
|
|
4522
|
+
});
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
report({
|
|
4526
|
+
phase: "wiping-db",
|
|
4527
|
+
camerasDone: fsResult.camerasDone,
|
|
4528
|
+
foldersRemoved: fsResult.foldersRemoved,
|
|
4529
|
+
detail: "forgetting ledger hours"
|
|
4530
|
+
});
|
|
4531
|
+
const forgotten = await forgetLedger(location);
|
|
4532
|
+
if (cancelRequested) {
|
|
4533
|
+
report({
|
|
4534
|
+
running: false,
|
|
4535
|
+
phase: "cancelled",
|
|
4536
|
+
dbRowsDeleted: forgotten,
|
|
4537
|
+
finishedAtMs: deps.now(),
|
|
4538
|
+
detail: "cancelled"
|
|
4539
|
+
});
|
|
4540
|
+
return;
|
|
4541
|
+
}
|
|
4542
|
+
report({
|
|
4543
|
+
running: false,
|
|
4544
|
+
phase: "complete",
|
|
4545
|
+
dbRowsDeleted: forgotten,
|
|
4546
|
+
finishedAtMs: deps.now(),
|
|
4547
|
+
detail: `done — ${fsResult.foldersRemoved} folders, ${forgotten} ledger segments`,
|
|
4548
|
+
error: null
|
|
4549
|
+
});
|
|
4550
|
+
} catch (err) {
|
|
4551
|
+
report({
|
|
4552
|
+
running: false,
|
|
4553
|
+
phase: "error",
|
|
4554
|
+
finishedAtMs: deps.now(),
|
|
4555
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4556
|
+
detail: "failed"
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
4559
|
+
};
|
|
4560
|
+
const forgetLedger = async (location) => {
|
|
4561
|
+
const ledger = deps.ledger;
|
|
4562
|
+
if (ledger === null) return 0;
|
|
4563
|
+
let forgotten = 0;
|
|
4564
|
+
const profiles = profilesForType(location.type);
|
|
4565
|
+
for await (const page of ledger.streamSegments({
|
|
4566
|
+
locationId: location.id,
|
|
4567
|
+
profiles
|
|
4568
|
+
})) {
|
|
4569
|
+
if (cancelRequested) return forgotten;
|
|
4570
|
+
deps.index.removeSegments(page.map((row) => row.path));
|
|
4571
|
+
await ledger.dropSegments(page);
|
|
4572
|
+
forgotten += page.length;
|
|
4573
|
+
report({
|
|
4574
|
+
dbRowsDeleted: forgotten,
|
|
4575
|
+
detail: `forgot ${forgotten} ledger segments`
|
|
4576
|
+
});
|
|
4577
|
+
await sleep(0);
|
|
4578
|
+
}
|
|
4579
|
+
return forgotten;
|
|
4580
|
+
};
|
|
4581
|
+
return {
|
|
4582
|
+
start,
|
|
4583
|
+
status,
|
|
4584
|
+
cancel,
|
|
4585
|
+
waitForIdle: () => inflight
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
//#endregion
|
|
4214
4589
|
//#region src/recorder/addon/export-dense-map.ts
|
|
4215
4590
|
/**
|
|
4216
4591
|
* Shortest clip worth keeping, in ms.
|
|
@@ -6339,7 +6714,10 @@ function buildRecordingProvider(deps) {
|
|
|
6339
6714
|
}
|
|
6340
6715
|
const locationUsages = await Promise.all([...byPhysical.values()].map(async (group) => {
|
|
6341
6716
|
const first = group[0];
|
|
6342
|
-
const cap = await deps.capacity(first.root)
|
|
6717
|
+
const cap = group.some((l) => l.enabled !== false) ? await deps.capacity(first.root) : {
|
|
6718
|
+
availableBytes: null,
|
|
6719
|
+
totalBytes: null
|
|
6720
|
+
};
|
|
6343
6721
|
const ids = group.map((l) => l.id);
|
|
6344
6722
|
if (cap.error !== void 0) deps.logger.warn("recorder: volume capacity unavailable (statfs failed)", { meta: {
|
|
6345
6723
|
locationId: first.id,
|
|
@@ -6742,6 +7120,15 @@ function buildRecordingProvider(deps) {
|
|
|
6742
7120
|
cancelRelocateJob: async ({ jobId }) => ({ cancelled: deps.relocate?.cancel(jobId) ?? false }),
|
|
6743
7121
|
planStorageRebalance: (input) => runRebalance(deps, input, false),
|
|
6744
7122
|
startStorageRebalance: (input) => runRebalance(deps, input, true),
|
|
7123
|
+
getPlacement: async () => {
|
|
7124
|
+
if (!deps.placementView) throw new Error("placement is not active on this node");
|
|
7125
|
+
return deps.placementView();
|
|
7126
|
+
},
|
|
7127
|
+
setDevicePlacement: async ({ deviceId, locationId }) => {
|
|
7128
|
+
if (!deps.setDevicePlacement) throw new Error("placement is not active on this node");
|
|
7129
|
+
await deps.setDevicePlacement(deviceId, locationId);
|
|
7130
|
+
return { ok: true };
|
|
7131
|
+
},
|
|
6745
7132
|
getDeviceSettingsContribution: async ({ deviceId }) => {
|
|
6746
7133
|
if (deps.isCameraDevice && !await deps.isCameraDevice(deviceId)) return null;
|
|
6747
7134
|
return require_dist.hydrateSchema(buildRecordingDeviceSchema(), {});
|
|
@@ -9613,7 +10000,6 @@ Object.freeze({
|
|
|
9613
10000
|
"getMotionEvents": { "limit": 1e3 },
|
|
9614
10001
|
"getObjectEvents": { "limit": 1e3 },
|
|
9615
10002
|
"getSensorEvents": { "limit": 1e3 },
|
|
9616
|
-
"listGroups": { "limit": 40 },
|
|
9617
10003
|
"listRecentTracks": { "limit": 200 },
|
|
9618
10004
|
"reclaimDebugMedia": { "mode": "report" },
|
|
9619
10005
|
"searchObjectEvents": {
|
|
@@ -10703,6 +11089,11 @@ var RECORDING_PLACEMENT_KEY = "recordingPlacement";
|
|
|
10703
11089
|
var RecordingPlacementBlobSchema = require_dist.object({
|
|
10704
11090
|
/** `<deviceId>:<profile>` → locationId. */
|
|
10705
11091
|
assignments: require_dist.record(require_dist.string(), require_dist.string()).default({}),
|
|
11092
|
+
/**
|
|
11093
|
+
* Operator pin: `String(deviceId)` → a `recordings:*` location id.
|
|
11094
|
+
* Absent = Auto. High and mid follow the pin; low is not pinned here.
|
|
11095
|
+
*/
|
|
11096
|
+
pins: require_dist.record(require_dist.string(), require_dist.string()).default({}),
|
|
10706
11097
|
/** locationId → the volume id last verified at that location's root. */
|
|
10707
11098
|
volumeIds: require_dist.record(require_dist.string(), require_dist.string()).default({}),
|
|
10708
11099
|
/**
|
|
@@ -10721,6 +11112,7 @@ var RecordingPlacementBlobSchema = require_dist.object({
|
|
|
10721
11112
|
/** Cold-start value: nothing assigned, nothing claimed, no authority recorded. */
|
|
10722
11113
|
var EMPTY_PLACEMENT = {
|
|
10723
11114
|
assignments: {},
|
|
11115
|
+
pins: {},
|
|
10724
11116
|
volumeIds: {},
|
|
10725
11117
|
defaultLocations: {}
|
|
10726
11118
|
};
|
|
@@ -10806,6 +11198,25 @@ async function rememberVolumeId(state, locationId, volumeId) {
|
|
|
10806
11198
|
updatedAt: Date.now()
|
|
10807
11199
|
});
|
|
10808
11200
|
}
|
|
11201
|
+
/** Operator pin: `locationId` null clears Auto. High/mid assignment keys follow. */
|
|
11202
|
+
async function writeDevicePin(state, deviceId, locationId) {
|
|
11203
|
+
const blob = await state.get();
|
|
11204
|
+
const pins = { ...blob.pins };
|
|
11205
|
+
const assignments = { ...blob.assignments };
|
|
11206
|
+
const key = String(deviceId);
|
|
11207
|
+
if (locationId === null) delete pins[key];
|
|
11208
|
+
else {
|
|
11209
|
+
pins[key] = locationId;
|
|
11210
|
+
assignments[assignmentKey(deviceId, "high")] = locationId;
|
|
11211
|
+
assignments[assignmentKey(deviceId, "mid")] = locationId;
|
|
11212
|
+
}
|
|
11213
|
+
await state.set({
|
|
11214
|
+
...blob,
|
|
11215
|
+
pins,
|
|
11216
|
+
assignments,
|
|
11217
|
+
updatedAt: Date.now()
|
|
11218
|
+
});
|
|
11219
|
+
}
|
|
10809
11220
|
//#endregion
|
|
10810
11221
|
//#region src/recorder/addon/placement-volume.ts
|
|
10811
11222
|
/**
|
|
@@ -11028,6 +11439,17 @@ function sameStamp(stamped, authority) {
|
|
|
11028
11439
|
for (const [type, locationId] of authority) if (stamped.get(type) !== locationId) return false;
|
|
11029
11440
|
return true;
|
|
11030
11441
|
}
|
|
11442
|
+
/** Operator pins win over the planner for high/mid of the pinned camera. */
|
|
11443
|
+
function overlayPins(planned, pins) {
|
|
11444
|
+
const next = new Map(planned);
|
|
11445
|
+
for (const [id, locationId] of Object.entries(pins)) {
|
|
11446
|
+
const deviceId = Number(id);
|
|
11447
|
+
if (!Number.isFinite(deviceId)) continue;
|
|
11448
|
+
next.set(assignmentKey(deviceId, "high"), locationId);
|
|
11449
|
+
next.set(assignmentKey(deviceId, "mid"), locationId);
|
|
11450
|
+
}
|
|
11451
|
+
return next;
|
|
11452
|
+
}
|
|
11031
11453
|
var PlacementService = class {
|
|
11032
11454
|
deps;
|
|
11033
11455
|
/** Locations whose volume has been probed since the last location change. */
|
|
@@ -11221,6 +11643,11 @@ var PlacementService = class {
|
|
|
11221
11643
|
return null;
|
|
11222
11644
|
}
|
|
11223
11645
|
const assignedId = blob.assignments[assignmentKey(deviceId, profile)];
|
|
11646
|
+
const pinId = blob.pins[String(deviceId)];
|
|
11647
|
+
if (pinId !== void 0 && (profile === "high" || profile === "mid")) {
|
|
11648
|
+
const pinned = pool.find((l) => l.id === pinId && !l.readOnly) ?? null;
|
|
11649
|
+
if (pinned !== null) return pinned;
|
|
11650
|
+
}
|
|
11224
11651
|
if (assignedId === void 0) return null;
|
|
11225
11652
|
const location = pool.find((l) => l.id === assignedId && !l.readOnly) ?? null;
|
|
11226
11653
|
if (location === null) return null;
|
|
@@ -11279,7 +11706,7 @@ var PlacementService = class {
|
|
|
11279
11706
|
return NO_PLAN;
|
|
11280
11707
|
}
|
|
11281
11708
|
const stamped = new Map(Object.entries(blob.defaultLocations));
|
|
11282
|
-
const repoints = this.detectRepoints(blob.assignments, stamped, authority);
|
|
11709
|
+
const repoints = this.detectRepoints(blob.assignments, stamped, authority, blob.pins);
|
|
11283
11710
|
if (repoints.length === 0) this.repointAnnounced.clear();
|
|
11284
11711
|
for (const repoint of repoints) this.announceRepoint(repoint);
|
|
11285
11712
|
const repointed = /* @__PURE__ */ new Map();
|
|
@@ -11300,7 +11727,7 @@ var PlacementService = class {
|
|
|
11300
11727
|
});
|
|
11301
11728
|
const pass = {
|
|
11302
11729
|
repointed,
|
|
11303
|
-
planned: plan.assignments,
|
|
11730
|
+
planned: overlayPins(plan.assignments, blob.pins),
|
|
11304
11731
|
defaultLocations: authority
|
|
11305
11732
|
};
|
|
11306
11733
|
this.reportDivergence(applyPlacementPass(blob.assignments, pass), authority);
|
|
@@ -11337,15 +11764,17 @@ var PlacementService = class {
|
|
|
11337
11764
|
* from an absent stamp would throw away every deliberate assignment on the
|
|
11338
11765
|
* first tick after a deploy.
|
|
11339
11766
|
*/
|
|
11340
|
-
detectRepoints(stored, stamped, authority) {
|
|
11767
|
+
detectRepoints(stored, stamped, authority, pins) {
|
|
11341
11768
|
const out = [];
|
|
11342
11769
|
for (const [type, to] of authority) {
|
|
11343
11770
|
const from = stamped.get(type);
|
|
11344
11771
|
if (from === void 0 || from === to) continue;
|
|
11345
11772
|
const keys = [];
|
|
11346
11773
|
for (const [key, locationId] of Object.entries(stored)) {
|
|
11347
|
-
|
|
11774
|
+
const [devicePart, profilePart] = key.split(":");
|
|
11775
|
+
if (placementTypeForProfile(profilePart ?? "") !== type) continue;
|
|
11348
11776
|
if (locationId === to) continue;
|
|
11777
|
+
if (type === "recordings" && devicePart !== void 0 && pins[devicePart] !== void 0) continue;
|
|
11349
11778
|
keys.push(key);
|
|
11350
11779
|
}
|
|
11351
11780
|
out.push({
|
|
@@ -11481,6 +11910,40 @@ var PlacementService = class {
|
|
|
11481
11910
|
return /* @__PURE__ */ new Map();
|
|
11482
11911
|
}
|
|
11483
11912
|
}
|
|
11913
|
+
async placementView() {
|
|
11914
|
+
const blob = await readPlacementState(this.deps.state);
|
|
11915
|
+
return {
|
|
11916
|
+
assignments: Object.entries(blob.assignments).flatMap(([key, locationId]) => {
|
|
11917
|
+
const [idPart, profile] = key.split(":");
|
|
11918
|
+
const deviceId = Number(idPart);
|
|
11919
|
+
if (!Number.isFinite(deviceId) || profile === void 0) return [];
|
|
11920
|
+
return [{
|
|
11921
|
+
deviceId,
|
|
11922
|
+
profile,
|
|
11923
|
+
locationId
|
|
11924
|
+
}];
|
|
11925
|
+
}),
|
|
11926
|
+
pins: Object.entries(blob.pins).flatMap(([id, locationId]) => {
|
|
11927
|
+
const deviceId = Number(id);
|
|
11928
|
+
if (!Number.isFinite(deviceId)) return [];
|
|
11929
|
+
return [{
|
|
11930
|
+
deviceId,
|
|
11931
|
+
locationId
|
|
11932
|
+
}];
|
|
11933
|
+
}),
|
|
11934
|
+
defaultLocations: blob.defaultLocations
|
|
11935
|
+
};
|
|
11936
|
+
}
|
|
11937
|
+
async setDevicePin(deviceId, locationId) {
|
|
11938
|
+
if (locationId !== null) {
|
|
11939
|
+
if (!this.pool().find((l) => l.id === locationId && l.type === "recordings" && !l.readOnly)) throw new Error(`Cannot pin camera ${String(deviceId)} to "${locationId}" — it is not a writable recordings location`);
|
|
11940
|
+
}
|
|
11941
|
+
await writeDevicePin(this.deps.state, deviceId, locationId);
|
|
11942
|
+
this.deps.logger.info("recorder placement: operator pin updated", {
|
|
11943
|
+
tags: { deviceId },
|
|
11944
|
+
meta: { locationId }
|
|
11945
|
+
});
|
|
11946
|
+
}
|
|
11484
11947
|
/**
|
|
11485
11948
|
* Turn each pool location into a planner candidate. Headroom is
|
|
11486
11949
|
* `min(free − minFree floor, maxUsedGb − used)`, clamped at zero: the first
|
|
@@ -15473,6 +15936,14 @@ function withDeviceTag(logger, deviceId) {
|
|
|
15473
15936
|
* provider until the controller swaps the manifest entry in a later step — this
|
|
15474
15937
|
* file ships dormant (separate tree, not wired into the manifest yet).
|
|
15475
15938
|
*/
|
|
15939
|
+
/**
|
|
15940
|
+
* Hub harvests this named export into CustomActionRegistry. Actions that
|
|
15941
|
+
* exist only on the onInitialize envelope stay invisible to `addons.custom`.
|
|
15942
|
+
*/
|
|
15943
|
+
var customActions = {
|
|
15944
|
+
...recorderDebugActions,
|
|
15945
|
+
...locationDiscardActions
|
|
15946
|
+
};
|
|
15476
15947
|
/** Default designated recording node (`recordingNodeId` global setting). The
|
|
15477
15948
|
* stream-broker is hub-only and recording is a single-node global function, so
|
|
15478
15949
|
* the hub is the safe default — mirrors pipeline-analytics' post-processing
|
|
@@ -15640,6 +16111,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
15640
16111
|
* deferred archive walk reconciles it against disk.
|
|
15641
16112
|
*/
|
|
15642
16113
|
segmentHours = null;
|
|
16114
|
+
/** Frozen-location leftover wipe (`low/` or `high/`+`mid/`). Detached. */
|
|
16115
|
+
footageDiscard = null;
|
|
15643
16116
|
/** Why the last placement pass planned nothing — latched so the line is one
|
|
15644
16117
|
* per episode, not one per 30 s tick. `null` ⇒ the last pass had work. */
|
|
15645
16118
|
placementWorkGap = null;
|
|
@@ -16066,6 +16539,16 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
16066
16539
|
refreshStorageLocationsForMigration: (leaseId) => this.refreshStorageLocationsForMigration(leaseId),
|
|
16067
16540
|
assertStorageMigrationLease: (leaseId) => this.assertStorageMigrationLease(leaseId),
|
|
16068
16541
|
placementSnapshot: () => this.placementSnapshot(),
|
|
16542
|
+
placementView: () => {
|
|
16543
|
+
const placement = this.placement;
|
|
16544
|
+
if (!placement) throw new Error("placement is not active on this node");
|
|
16545
|
+
return placement.placementView();
|
|
16546
|
+
},
|
|
16547
|
+
setDevicePlacement: (deviceId, locationId) => {
|
|
16548
|
+
const placement = this.placement;
|
|
16549
|
+
if (!placement) throw new Error("placement is not active on this node");
|
|
16550
|
+
return placement.setDevicePin(deviceId, locationId);
|
|
16551
|
+
},
|
|
16069
16552
|
dataDir: this.ctx.dataDir,
|
|
16070
16553
|
playbackBaseUrl: () => this.playbackBaseUrl,
|
|
16071
16554
|
isCameraDevice: async (deviceId) => {
|
|
@@ -16228,27 +16711,50 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
16228
16711
|
}
|
|
16229
16712
|
});
|
|
16230
16713
|
warnLostBands(this.configStore(), this.ctx.logger);
|
|
16714
|
+
const providers = [
|
|
16715
|
+
{
|
|
16716
|
+
capability: require_dist.recordingCapability,
|
|
16717
|
+
provider: recordingProvider
|
|
16718
|
+
},
|
|
16719
|
+
{
|
|
16720
|
+
capability: require_dist.storageEvictableCapability,
|
|
16721
|
+
provider: evictableProvider
|
|
16722
|
+
},
|
|
16723
|
+
{
|
|
16724
|
+
capability: require_dist.recordingExportCapability,
|
|
16725
|
+
provider: exportProvider
|
|
16726
|
+
},
|
|
16727
|
+
{
|
|
16728
|
+
capability: require_dist.loadContributionCapability,
|
|
16729
|
+
provider: { list: () => this.costRegistry.contributions() }
|
|
16730
|
+
}
|
|
16731
|
+
];
|
|
16732
|
+
this.footageDiscard = createFootageDiscardJob({
|
|
16733
|
+
listLocations: async () => this.ctx.api.storage.listLocations.query({}),
|
|
16734
|
+
localNodeId: this.nodeId,
|
|
16735
|
+
index: this.index,
|
|
16736
|
+
ledger: this.segmentHours === null ? null : {
|
|
16737
|
+
streamSegments: (scope) => this.segmentHours.streamSegments(scope),
|
|
16738
|
+
dropSegments: (rows) => this.segmentHours.dropSegments(rows)
|
|
16739
|
+
},
|
|
16740
|
+
now: () => Date.now()
|
|
16741
|
+
});
|
|
16231
16742
|
return {
|
|
16232
|
-
providers
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16242
|
-
|
|
16243
|
-
provider: exportProvider
|
|
16743
|
+
providers,
|
|
16744
|
+
customActions: {
|
|
16745
|
+
...recorderDebugActions,
|
|
16746
|
+
...locationDiscardActions
|
|
16747
|
+
},
|
|
16748
|
+
actionHandlers: {
|
|
16749
|
+
"debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input),
|
|
16750
|
+
"storage.discardLocation": async (input) => this.footageDiscard?.start(input) ?? {
|
|
16751
|
+
started: false,
|
|
16752
|
+
alreadyRunning: false,
|
|
16753
|
+
jobId: null
|
|
16244
16754
|
},
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
}
|
|
16249
|
-
],
|
|
16250
|
-
customActions: recorderDebugActions,
|
|
16251
|
-
actionHandlers: { "debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input) }
|
|
16755
|
+
"storage.discardLocationStatus": async () => this.footageDiscard?.status() ?? IDLE_DISCARD,
|
|
16756
|
+
"storage.discardLocationCancel": async () => this.footageDiscard?.cancel() ?? { requested: false }
|
|
16757
|
+
}
|
|
16252
16758
|
};
|
|
16253
16759
|
}
|
|
16254
16760
|
async onShutdown() {
|
|
@@ -17255,4 +17761,5 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17255
17761
|
}
|
|
17256
17762
|
};
|
|
17257
17763
|
//#endregion
|
|
17258
|
-
|
|
17764
|
+
exports.customActions = customActions;
|
|
17765
|
+
exports.default = RecorderV2Addon;
|