@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as isFirstLevelMacroClass, $t as record, B as defineCustomActions, Bt as nodePin, E as RecordingConfigSchema, It as hydrateSchema, J as evaluateSensorEdge, Kt as array, L as customAction, N as batteryCapability, Nt as DeviceFeature, Pt as DeviceType, Qt as object, T as RECORDING_EXPORT_MAX_READ_BYTES, U as deriveRecordingMode, Ut as selectAssignedProfileSlots, V as deriveBatteryPresence, Wt as sleep, Zt as number, bt as storageEvictableCapability, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as string, et as loadContributionCapability, g as ExportRecordSchema, i as BatteryStatusSchema, jt as BaseAddon, mt as recordingExportCapability, nn as EventCategory, pt as recordingCapability, qt as boolean, vt as resolveRecordingProfiles, wt as errMsg, x as OpsLogEntrySchema } from "../dist-
|
|
2
|
-
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
1
|
+
import { $ as isFirstLevelMacroClass, $t as record, B as defineCustomActions, Bt as nodePin, E as RecordingConfigSchema, Gt as _enum, It as hydrateSchema, J as evaluateSensorEdge, Kt as array, L as customAction, N as batteryCapability, Nt as DeviceFeature, Pt as DeviceType, Qt as object, T as RECORDING_EXPORT_MAX_READ_BYTES, U as deriveRecordingMode, Ut as selectAssignedProfileSlots, V as deriveBatteryPresence, Wt as sleep$1, Zt as number, bt as storageEvictableCapability, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as string, et as loadContributionCapability, g as ExportRecordSchema, i as BatteryStatusSchema, jt as BaseAddon, mt as recordingExportCapability, nn as EventCategory, pt as recordingCapability, qt as boolean, vt as resolveRecordingProfiles, wt as errMsg, x as OpsLogEntrySchema } from "../dist-C8zVKhzA.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-G9AiO_JJ.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { r as withRecordingIntent } from "../restream-intent-B4BXZra7.mjs";
|
|
5
5
|
import { n as startEventLoopStallMonitor } from "../event-loop-stall-monitor-DXLNBMkY.mjs";
|
|
@@ -4209,6 +4209,377 @@ function sendDirectory(res, payload) {
|
|
|
4209
4209
|
res.end(Buffer.from(body));
|
|
4210
4210
|
}
|
|
4211
4211
|
//#endregion
|
|
4212
|
+
//#region src/recorder/addon/location-discard-actions.ts
|
|
4213
|
+
var DiscardStartInput = object({ locationId: string().min(1) });
|
|
4214
|
+
var DiscardStarted = object({
|
|
4215
|
+
started: boolean(),
|
|
4216
|
+
alreadyRunning: boolean(),
|
|
4217
|
+
jobId: string().nullable()
|
|
4218
|
+
});
|
|
4219
|
+
var DiscardStatusSchema = object({
|
|
4220
|
+
running: boolean(),
|
|
4221
|
+
locationId: string().nullable(),
|
|
4222
|
+
jobId: string().nullable(),
|
|
4223
|
+
phase: _enum([
|
|
4224
|
+
"idle",
|
|
4225
|
+
"scanning",
|
|
4226
|
+
"wiping-fs",
|
|
4227
|
+
"wiping-db",
|
|
4228
|
+
"cascading",
|
|
4229
|
+
"complete",
|
|
4230
|
+
"cancelled",
|
|
4231
|
+
"error"
|
|
4232
|
+
]),
|
|
4233
|
+
camerasTotal: number(),
|
|
4234
|
+
camerasDone: number(),
|
|
4235
|
+
foldersRemoved: number(),
|
|
4236
|
+
dbRowsDeleted: number(),
|
|
4237
|
+
ownersCascaded: number(),
|
|
4238
|
+
detail: string(),
|
|
4239
|
+
startedAtMs: number().nullable(),
|
|
4240
|
+
finishedAtMs: number().nullable(),
|
|
4241
|
+
error: string().nullable()
|
|
4242
|
+
});
|
|
4243
|
+
var DiscardCancel = object({ requested: boolean() });
|
|
4244
|
+
var locationDiscardActions = defineCustomActions({
|
|
4245
|
+
"storage.discardLocation": customAction(DiscardStartInput, DiscardStarted, { auth: "admin" }),
|
|
4246
|
+
"storage.discardLocationStatus": customAction(object({}), DiscardStatusSchema, { auth: "admin" }),
|
|
4247
|
+
"storage.discardLocationCancel": customAction(object({}), DiscardCancel, { auth: "admin" })
|
|
4248
|
+
});
|
|
4249
|
+
//#endregion
|
|
4250
|
+
//#region src/recorder/addon/location-class-wipe.ts
|
|
4251
|
+
/**
|
|
4252
|
+
* Class-subtree wipe — the filesystem half of discarding a frozen location
|
|
4253
|
+
* that SHARES its root with another class (today: Toshiba `/recordings`
|
|
4254
|
+
* holds events + low + high).
|
|
4255
|
+
*
|
|
4256
|
+
* The only directories this module will `rm` are
|
|
4257
|
+
* `<root>/<camera>/<subtree>` for `subtree` in the class map. It refuses
|
|
4258
|
+
* a path that is not exactly two levels under the root, a symlink, or a
|
|
4259
|
+
* subtree the class did not name. `rm` of the location root is not a
|
|
4260
|
+
* code path.
|
|
4261
|
+
*/
|
|
4262
|
+
var DISCARD_SUBTREES$1 = {
|
|
4263
|
+
eventMedia: ["events"],
|
|
4264
|
+
recordingsLow: ["low"],
|
|
4265
|
+
recordings: ["high", "mid"]
|
|
4266
|
+
};
|
|
4267
|
+
function discardSubtreesForType(type) {
|
|
4268
|
+
return DISCARD_SUBTREES$1[type] ?? null;
|
|
4269
|
+
}
|
|
4270
|
+
var DEFAULT_FS = {
|
|
4271
|
+
readdir: (dir) => promises.readdir(dir, { withFileTypes: true }),
|
|
4272
|
+
lstat: (target) => promises.lstat(target),
|
|
4273
|
+
rm: (target) => promises.rm(target, {
|
|
4274
|
+
recursive: true,
|
|
4275
|
+
force: true
|
|
4276
|
+
})
|
|
4277
|
+
};
|
|
4278
|
+
/**
|
|
4279
|
+
* Resolve `<root>/<camera>/<subtree>` and refuse anything that is not
|
|
4280
|
+
* exactly that shape under `root`. The check is lexical (after
|
|
4281
|
+
* `path.resolve`) so `../high` in a camera name cannot escape.
|
|
4282
|
+
*/
|
|
4283
|
+
function resolveClassDir(root, cameraId, subtree) {
|
|
4284
|
+
if (cameraId.length === 0 || cameraId === "." || cameraId === "..") throw new Error(`discard: illegal camera id ${cameraId}`);
|
|
4285
|
+
if (subtree.includes("/") || subtree.includes("\\") || subtree === ".." || subtree === ".") throw new Error(`discard: illegal subtree ${subtree}`);
|
|
4286
|
+
const rootResolved = path.resolve(root);
|
|
4287
|
+
const target = path.resolve(rootResolved, cameraId, subtree);
|
|
4288
|
+
const rel = path.relative(rootResolved, target);
|
|
4289
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`discard: path escaped root: ${target}`);
|
|
4290
|
+
const parts = rel.split(path.sep).filter((p) => p.length > 0);
|
|
4291
|
+
if (parts.length !== 2 || parts[0] !== cameraId || parts[1] !== subtree) throw new Error(`discard: path is not a class subtree: ${target}`);
|
|
4292
|
+
return target;
|
|
4293
|
+
}
|
|
4294
|
+
async function wipeClassSubtrees(input) {
|
|
4295
|
+
const subtrees = discardSubtreesForType(input.type);
|
|
4296
|
+
if (subtrees === null) throw new Error(`discard: unknown location type ${input.type}`);
|
|
4297
|
+
const io = input.io ?? DEFAULT_FS;
|
|
4298
|
+
const rootResolved = path.resolve(input.root);
|
|
4299
|
+
let cameras;
|
|
4300
|
+
try {
|
|
4301
|
+
cameras = await io.readdir(rootResolved);
|
|
4302
|
+
} catch (err) {
|
|
4303
|
+
if (err.code === "ENOENT") return {
|
|
4304
|
+
camerasDone: 0,
|
|
4305
|
+
foldersRemoved: 0,
|
|
4306
|
+
cancelled: false
|
|
4307
|
+
};
|
|
4308
|
+
throw err;
|
|
4309
|
+
}
|
|
4310
|
+
const dirs = cameras.filter((e) => e.isDirectory() && e.name !== "." && e.name !== "..");
|
|
4311
|
+
let camerasDone = 0;
|
|
4312
|
+
let foldersRemoved = 0;
|
|
4313
|
+
for (const dir of dirs) {
|
|
4314
|
+
if (input.stop()) return {
|
|
4315
|
+
camerasDone,
|
|
4316
|
+
foldersRemoved,
|
|
4317
|
+
cancelled: true
|
|
4318
|
+
};
|
|
4319
|
+
for (const subtree of subtrees) {
|
|
4320
|
+
const target = resolveClassDir(rootResolved, dir.name, subtree);
|
|
4321
|
+
let st;
|
|
4322
|
+
try {
|
|
4323
|
+
st = await io.lstat(target);
|
|
4324
|
+
} catch (err) {
|
|
4325
|
+
if (err.code === "ENOENT") continue;
|
|
4326
|
+
throw err;
|
|
4327
|
+
}
|
|
4328
|
+
if (st.isSymbolicLink()) throw new Error(`discard: refusing to wipe symlink ${target}`);
|
|
4329
|
+
if (!st.isDirectory()) continue;
|
|
4330
|
+
await io.rm(target);
|
|
4331
|
+
foldersRemoved += 1;
|
|
4332
|
+
}
|
|
4333
|
+
camerasDone += 1;
|
|
4334
|
+
input.onProgress({
|
|
4335
|
+
camerasTotal: dirs.length,
|
|
4336
|
+
camerasDone,
|
|
4337
|
+
cameraId: dir.name,
|
|
4338
|
+
foldersRemoved
|
|
4339
|
+
});
|
|
4340
|
+
if (input.pace) await input.pace();
|
|
4341
|
+
}
|
|
4342
|
+
return {
|
|
4343
|
+
camerasDone,
|
|
4344
|
+
foldersRemoved,
|
|
4345
|
+
cancelled: false
|
|
4346
|
+
};
|
|
4347
|
+
}
|
|
4348
|
+
//#endregion
|
|
4349
|
+
//#region src/recorder/addon/location-discard-policy.ts
|
|
4350
|
+
/**
|
|
4351
|
+
* Server-side eligibility for discarding a frozen location's leftover
|
|
4352
|
+
* class subtree. Same rules as the admin-ui card: frozen, known class,
|
|
4353
|
+
* writable sibling of the same type. The last writable of a type is
|
|
4354
|
+
* refused here even if the UI is bypassed.
|
|
4355
|
+
*/
|
|
4356
|
+
var DISCARD_SUBTREES = {
|
|
4357
|
+
eventMedia: ["events"],
|
|
4358
|
+
recordingsLow: ["low"],
|
|
4359
|
+
recordings: ["high", "mid"]
|
|
4360
|
+
};
|
|
4361
|
+
function locationIsFrozen(location) {
|
|
4362
|
+
const enabled = location.enabled !== false;
|
|
4363
|
+
const readOnly = location.config["readOnly"] === true;
|
|
4364
|
+
return !enabled || readOnly;
|
|
4365
|
+
}
|
|
4366
|
+
function discardEligible(location, siblings) {
|
|
4367
|
+
if (!(location.type in DISCARD_SUBTREES)) return false;
|
|
4368
|
+
if (!locationIsFrozen(location)) return false;
|
|
4369
|
+
return siblings.some((s) => {
|
|
4370
|
+
if (s.id === location.id) return false;
|
|
4371
|
+
if (s.type !== location.type) return false;
|
|
4372
|
+
const enabled = s.enabled !== false;
|
|
4373
|
+
const readOnly = s.config["readOnly"] === true;
|
|
4374
|
+
return enabled && !readOnly;
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
function assertDiscardAllowed(location, all, localNodeId) {
|
|
4378
|
+
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}`);
|
|
4379
|
+
const nodeId = location.nodeId ?? "hub";
|
|
4380
|
+
if (nodeId !== localNodeId) throw new Error(`discard refused: ${location.id} lives on node ${nodeId}, this process is ${localNodeId}`);
|
|
4381
|
+
}
|
|
4382
|
+
function locationBasePath(location) {
|
|
4383
|
+
const basePath = location.config["basePath"];
|
|
4384
|
+
if (typeof basePath !== "string" || basePath.length === 0) throw new Error(`discard refused: ${location.id} has no basePath`);
|
|
4385
|
+
return basePath;
|
|
4386
|
+
}
|
|
4387
|
+
//#endregion
|
|
4388
|
+
//#region src/recorder/addon/location-discard-job.ts
|
|
4389
|
+
/**
|
|
4390
|
+
* Footage discard job: wipe `<camera>/low/` (or `high/`+`mid/` for the
|
|
4391
|
+
* recordings class) on a frozen location, then forget the hour rows
|
|
4392
|
+
* stamped with that `locationId` and drop them from the RAM index.
|
|
4393
|
+
*
|
|
4394
|
+
* The class-subtree wipe is the load-bearing safety: `recordingsLow:default`
|
|
4395
|
+
* shares `/recordings` with `recordings:default`. This job never `rm`s the
|
|
4396
|
+
* location root. Profiles passed to the ledger forget are the class's own
|
|
4397
|
+
* (`low` vs `high`/`mid`) so a mis-stamped hour of the other class is not
|
|
4398
|
+
* dropped.
|
|
4399
|
+
*
|
|
4400
|
+
* Start returns immediately. Progress is the status snapshot.
|
|
4401
|
+
*/
|
|
4402
|
+
var IDLE_DISCARD = {
|
|
4403
|
+
running: false,
|
|
4404
|
+
locationId: null,
|
|
4405
|
+
jobId: null,
|
|
4406
|
+
phase: "idle",
|
|
4407
|
+
camerasTotal: 0,
|
|
4408
|
+
camerasDone: 0,
|
|
4409
|
+
foldersRemoved: 0,
|
|
4410
|
+
dbRowsDeleted: 0,
|
|
4411
|
+
ownersCascaded: 0,
|
|
4412
|
+
detail: "",
|
|
4413
|
+
startedAtMs: null,
|
|
4414
|
+
finishedAtMs: null,
|
|
4415
|
+
error: null
|
|
4416
|
+
};
|
|
4417
|
+
var USB_PACE_MS = 80;
|
|
4418
|
+
var FOOTAGE_TYPES = new Set(["recordings", "recordingsLow"]);
|
|
4419
|
+
function sleep(ms) {
|
|
4420
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4421
|
+
}
|
|
4422
|
+
function profilesForType(type) {
|
|
4423
|
+
return type === "recordingsLow" ? ["low"] : ["high", "mid"];
|
|
4424
|
+
}
|
|
4425
|
+
function createFootageDiscardJob(deps) {
|
|
4426
|
+
let state = { ...IDLE_DISCARD };
|
|
4427
|
+
let cancelRequested = false;
|
|
4428
|
+
let inflight = null;
|
|
4429
|
+
const report = (patch) => {
|
|
4430
|
+
state = {
|
|
4431
|
+
...state,
|
|
4432
|
+
...patch
|
|
4433
|
+
};
|
|
4434
|
+
};
|
|
4435
|
+
const status = () => ({ ...state });
|
|
4436
|
+
const cancel = () => {
|
|
4437
|
+
if (!state.running) return { requested: false };
|
|
4438
|
+
cancelRequested = true;
|
|
4439
|
+
report({ detail: "cancel requested — finishing current camera" });
|
|
4440
|
+
return { requested: true };
|
|
4441
|
+
};
|
|
4442
|
+
const start = async (input) => {
|
|
4443
|
+
if (state.running) return {
|
|
4444
|
+
started: false,
|
|
4445
|
+
alreadyRunning: true,
|
|
4446
|
+
jobId: state.jobId
|
|
4447
|
+
};
|
|
4448
|
+
const jobId = randomUUID();
|
|
4449
|
+
cancelRequested = false;
|
|
4450
|
+
report({
|
|
4451
|
+
...IDLE_DISCARD,
|
|
4452
|
+
running: true,
|
|
4453
|
+
locationId: input.locationId,
|
|
4454
|
+
jobId,
|
|
4455
|
+
phase: "scanning",
|
|
4456
|
+
startedAtMs: deps.now(),
|
|
4457
|
+
detail: "starting"
|
|
4458
|
+
});
|
|
4459
|
+
try {
|
|
4460
|
+
const all = await deps.listLocations();
|
|
4461
|
+
const location = all.find((row) => row.id === input.locationId);
|
|
4462
|
+
if (!location) throw new Error(`discard refused: unknown location ${input.locationId}`);
|
|
4463
|
+
if (!FOOTAGE_TYPES.has(location.type)) throw new Error(`discard refused: recorder handles footage, not ${location.type}`);
|
|
4464
|
+
assertDiscardAllowed(location, all, deps.localNodeId);
|
|
4465
|
+
const root = locationBasePath(location);
|
|
4466
|
+
report({ detail: `scanning ${root}` });
|
|
4467
|
+
inflight = run(location, root).finally(() => {
|
|
4468
|
+
inflight = null;
|
|
4469
|
+
});
|
|
4470
|
+
return {
|
|
4471
|
+
started: true,
|
|
4472
|
+
alreadyRunning: false,
|
|
4473
|
+
jobId
|
|
4474
|
+
};
|
|
4475
|
+
} catch (err) {
|
|
4476
|
+
report({
|
|
4477
|
+
...IDLE_DISCARD,
|
|
4478
|
+
phase: "error",
|
|
4479
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4480
|
+
finishedAtMs: deps.now()
|
|
4481
|
+
});
|
|
4482
|
+
throw err;
|
|
4483
|
+
}
|
|
4484
|
+
};
|
|
4485
|
+
const run = async (location, root) => {
|
|
4486
|
+
const wipe = deps.wipe ?? wipeClassSubtrees;
|
|
4487
|
+
const paceMs = deps.paceMs ?? USB_PACE_MS;
|
|
4488
|
+
try {
|
|
4489
|
+
report({
|
|
4490
|
+
phase: "wiping-fs",
|
|
4491
|
+
detail: `wiping ${profilesForType(location.type).join("/")} under ${root}`
|
|
4492
|
+
});
|
|
4493
|
+
const fsResult = await wipe({
|
|
4494
|
+
root,
|
|
4495
|
+
type: location.type,
|
|
4496
|
+
stop: () => cancelRequested,
|
|
4497
|
+
onProgress: (p) => {
|
|
4498
|
+
report({
|
|
4499
|
+
phase: "wiping-fs",
|
|
4500
|
+
camerasTotal: p.camerasTotal,
|
|
4501
|
+
camerasDone: p.camerasDone,
|
|
4502
|
+
foldersRemoved: p.foldersRemoved,
|
|
4503
|
+
detail: `camera ${p.cameraId} (${p.camerasDone}/${p.camerasTotal})`
|
|
4504
|
+
});
|
|
4505
|
+
},
|
|
4506
|
+
pace: () => sleep(paceMs)
|
|
4507
|
+
});
|
|
4508
|
+
if (fsResult.cancelled || cancelRequested) {
|
|
4509
|
+
report({
|
|
4510
|
+
running: false,
|
|
4511
|
+
phase: "cancelled",
|
|
4512
|
+
camerasDone: fsResult.camerasDone,
|
|
4513
|
+
foldersRemoved: fsResult.foldersRemoved,
|
|
4514
|
+
finishedAtMs: deps.now(),
|
|
4515
|
+
detail: "cancelled"
|
|
4516
|
+
});
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
report({
|
|
4520
|
+
phase: "wiping-db",
|
|
4521
|
+
camerasDone: fsResult.camerasDone,
|
|
4522
|
+
foldersRemoved: fsResult.foldersRemoved,
|
|
4523
|
+
detail: "forgetting ledger hours"
|
|
4524
|
+
});
|
|
4525
|
+
const forgotten = await forgetLedger(location);
|
|
4526
|
+
if (cancelRequested) {
|
|
4527
|
+
report({
|
|
4528
|
+
running: false,
|
|
4529
|
+
phase: "cancelled",
|
|
4530
|
+
dbRowsDeleted: forgotten,
|
|
4531
|
+
finishedAtMs: deps.now(),
|
|
4532
|
+
detail: "cancelled"
|
|
4533
|
+
});
|
|
4534
|
+
return;
|
|
4535
|
+
}
|
|
4536
|
+
report({
|
|
4537
|
+
running: false,
|
|
4538
|
+
phase: "complete",
|
|
4539
|
+
dbRowsDeleted: forgotten,
|
|
4540
|
+
finishedAtMs: deps.now(),
|
|
4541
|
+
detail: `done — ${fsResult.foldersRemoved} folders, ${forgotten} ledger segments`,
|
|
4542
|
+
error: null
|
|
4543
|
+
});
|
|
4544
|
+
} catch (err) {
|
|
4545
|
+
report({
|
|
4546
|
+
running: false,
|
|
4547
|
+
phase: "error",
|
|
4548
|
+
finishedAtMs: deps.now(),
|
|
4549
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4550
|
+
detail: "failed"
|
|
4551
|
+
});
|
|
4552
|
+
}
|
|
4553
|
+
};
|
|
4554
|
+
const forgetLedger = async (location) => {
|
|
4555
|
+
const ledger = deps.ledger;
|
|
4556
|
+
if (ledger === null) return 0;
|
|
4557
|
+
let forgotten = 0;
|
|
4558
|
+
const profiles = profilesForType(location.type);
|
|
4559
|
+
for await (const page of ledger.streamSegments({
|
|
4560
|
+
locationId: location.id,
|
|
4561
|
+
profiles
|
|
4562
|
+
})) {
|
|
4563
|
+
if (cancelRequested) return forgotten;
|
|
4564
|
+
deps.index.removeSegments(page.map((row) => row.path));
|
|
4565
|
+
await ledger.dropSegments(page);
|
|
4566
|
+
forgotten += page.length;
|
|
4567
|
+
report({
|
|
4568
|
+
dbRowsDeleted: forgotten,
|
|
4569
|
+
detail: `forgot ${forgotten} ledger segments`
|
|
4570
|
+
});
|
|
4571
|
+
await sleep(0);
|
|
4572
|
+
}
|
|
4573
|
+
return forgotten;
|
|
4574
|
+
};
|
|
4575
|
+
return {
|
|
4576
|
+
start,
|
|
4577
|
+
status,
|
|
4578
|
+
cancel,
|
|
4579
|
+
waitForIdle: () => inflight
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
//#endregion
|
|
4212
4583
|
//#region src/recorder/addon/export-dense-map.ts
|
|
4213
4584
|
/**
|
|
4214
4585
|
* Shortest clip worth keeping, in ms.
|
|
@@ -6337,7 +6708,10 @@ function buildRecordingProvider(deps) {
|
|
|
6337
6708
|
}
|
|
6338
6709
|
const locationUsages = await Promise.all([...byPhysical.values()].map(async (group) => {
|
|
6339
6710
|
const first = group[0];
|
|
6340
|
-
const cap = await deps.capacity(first.root)
|
|
6711
|
+
const cap = group.some((l) => l.enabled !== false) ? await deps.capacity(first.root) : {
|
|
6712
|
+
availableBytes: null,
|
|
6713
|
+
totalBytes: null
|
|
6714
|
+
};
|
|
6341
6715
|
const ids = group.map((l) => l.id);
|
|
6342
6716
|
if (cap.error !== void 0) deps.logger.warn("recorder: volume capacity unavailable (statfs failed)", { meta: {
|
|
6343
6717
|
locationId: first.id,
|
|
@@ -6740,6 +7114,15 @@ function buildRecordingProvider(deps) {
|
|
|
6740
7114
|
cancelRelocateJob: async ({ jobId }) => ({ cancelled: deps.relocate?.cancel(jobId) ?? false }),
|
|
6741
7115
|
planStorageRebalance: (input) => runRebalance(deps, input, false),
|
|
6742
7116
|
startStorageRebalance: (input) => runRebalance(deps, input, true),
|
|
7117
|
+
getPlacement: async () => {
|
|
7118
|
+
if (!deps.placementView) throw new Error("placement is not active on this node");
|
|
7119
|
+
return deps.placementView();
|
|
7120
|
+
},
|
|
7121
|
+
setDevicePlacement: async ({ deviceId, locationId }) => {
|
|
7122
|
+
if (!deps.setDevicePlacement) throw new Error("placement is not active on this node");
|
|
7123
|
+
await deps.setDevicePlacement(deviceId, locationId);
|
|
7124
|
+
return { ok: true };
|
|
7125
|
+
},
|
|
6743
7126
|
getDeviceSettingsContribution: async ({ deviceId }) => {
|
|
6744
7127
|
if (deps.isCameraDevice && !await deps.isCameraDevice(deviceId)) return null;
|
|
6745
7128
|
return hydrateSchema(buildRecordingDeviceSchema(), {});
|
|
@@ -9611,7 +9994,6 @@ Object.freeze({
|
|
|
9611
9994
|
"getMotionEvents": { "limit": 1e3 },
|
|
9612
9995
|
"getObjectEvents": { "limit": 1e3 },
|
|
9613
9996
|
"getSensorEvents": { "limit": 1e3 },
|
|
9614
|
-
"listGroups": { "limit": 40 },
|
|
9615
9997
|
"listRecentTracks": { "limit": 200 },
|
|
9616
9998
|
"reclaimDebugMedia": { "mode": "report" },
|
|
9617
9999
|
"searchObjectEvents": {
|
|
@@ -10701,6 +11083,11 @@ var RECORDING_PLACEMENT_KEY = "recordingPlacement";
|
|
|
10701
11083
|
var RecordingPlacementBlobSchema = object({
|
|
10702
11084
|
/** `<deviceId>:<profile>` → locationId. */
|
|
10703
11085
|
assignments: record(string(), string()).default({}),
|
|
11086
|
+
/**
|
|
11087
|
+
* Operator pin: `String(deviceId)` → a `recordings:*` location id.
|
|
11088
|
+
* Absent = Auto. High and mid follow the pin; low is not pinned here.
|
|
11089
|
+
*/
|
|
11090
|
+
pins: record(string(), string()).default({}),
|
|
10704
11091
|
/** locationId → the volume id last verified at that location's root. */
|
|
10705
11092
|
volumeIds: record(string(), string()).default({}),
|
|
10706
11093
|
/**
|
|
@@ -10719,6 +11106,7 @@ var RecordingPlacementBlobSchema = object({
|
|
|
10719
11106
|
/** Cold-start value: nothing assigned, nothing claimed, no authority recorded. */
|
|
10720
11107
|
var EMPTY_PLACEMENT = {
|
|
10721
11108
|
assignments: {},
|
|
11109
|
+
pins: {},
|
|
10722
11110
|
volumeIds: {},
|
|
10723
11111
|
defaultLocations: {}
|
|
10724
11112
|
};
|
|
@@ -10804,6 +11192,25 @@ async function rememberVolumeId(state, locationId, volumeId) {
|
|
|
10804
11192
|
updatedAt: Date.now()
|
|
10805
11193
|
});
|
|
10806
11194
|
}
|
|
11195
|
+
/** Operator pin: `locationId` null clears Auto. High/mid assignment keys follow. */
|
|
11196
|
+
async function writeDevicePin(state, deviceId, locationId) {
|
|
11197
|
+
const blob = await state.get();
|
|
11198
|
+
const pins = { ...blob.pins };
|
|
11199
|
+
const assignments = { ...blob.assignments };
|
|
11200
|
+
const key = String(deviceId);
|
|
11201
|
+
if (locationId === null) delete pins[key];
|
|
11202
|
+
else {
|
|
11203
|
+
pins[key] = locationId;
|
|
11204
|
+
assignments[assignmentKey(deviceId, "high")] = locationId;
|
|
11205
|
+
assignments[assignmentKey(deviceId, "mid")] = locationId;
|
|
11206
|
+
}
|
|
11207
|
+
await state.set({
|
|
11208
|
+
...blob,
|
|
11209
|
+
pins,
|
|
11210
|
+
assignments,
|
|
11211
|
+
updatedAt: Date.now()
|
|
11212
|
+
});
|
|
11213
|
+
}
|
|
10807
11214
|
//#endregion
|
|
10808
11215
|
//#region src/recorder/addon/placement-volume.ts
|
|
10809
11216
|
/**
|
|
@@ -11026,6 +11433,17 @@ function sameStamp(stamped, authority) {
|
|
|
11026
11433
|
for (const [type, locationId] of authority) if (stamped.get(type) !== locationId) return false;
|
|
11027
11434
|
return true;
|
|
11028
11435
|
}
|
|
11436
|
+
/** Operator pins win over the planner for high/mid of the pinned camera. */
|
|
11437
|
+
function overlayPins(planned, pins) {
|
|
11438
|
+
const next = new Map(planned);
|
|
11439
|
+
for (const [id, locationId] of Object.entries(pins)) {
|
|
11440
|
+
const deviceId = Number(id);
|
|
11441
|
+
if (!Number.isFinite(deviceId)) continue;
|
|
11442
|
+
next.set(assignmentKey(deviceId, "high"), locationId);
|
|
11443
|
+
next.set(assignmentKey(deviceId, "mid"), locationId);
|
|
11444
|
+
}
|
|
11445
|
+
return next;
|
|
11446
|
+
}
|
|
11029
11447
|
var PlacementService = class {
|
|
11030
11448
|
deps;
|
|
11031
11449
|
/** Locations whose volume has been probed since the last location change. */
|
|
@@ -11219,6 +11637,11 @@ var PlacementService = class {
|
|
|
11219
11637
|
return null;
|
|
11220
11638
|
}
|
|
11221
11639
|
const assignedId = blob.assignments[assignmentKey(deviceId, profile)];
|
|
11640
|
+
const pinId = blob.pins[String(deviceId)];
|
|
11641
|
+
if (pinId !== void 0 && (profile === "high" || profile === "mid")) {
|
|
11642
|
+
const pinned = pool.find((l) => l.id === pinId && !l.readOnly) ?? null;
|
|
11643
|
+
if (pinned !== null) return pinned;
|
|
11644
|
+
}
|
|
11222
11645
|
if (assignedId === void 0) return null;
|
|
11223
11646
|
const location = pool.find((l) => l.id === assignedId && !l.readOnly) ?? null;
|
|
11224
11647
|
if (location === null) return null;
|
|
@@ -11277,7 +11700,7 @@ var PlacementService = class {
|
|
|
11277
11700
|
return NO_PLAN;
|
|
11278
11701
|
}
|
|
11279
11702
|
const stamped = new Map(Object.entries(blob.defaultLocations));
|
|
11280
|
-
const repoints = this.detectRepoints(blob.assignments, stamped, authority);
|
|
11703
|
+
const repoints = this.detectRepoints(blob.assignments, stamped, authority, blob.pins);
|
|
11281
11704
|
if (repoints.length === 0) this.repointAnnounced.clear();
|
|
11282
11705
|
for (const repoint of repoints) this.announceRepoint(repoint);
|
|
11283
11706
|
const repointed = /* @__PURE__ */ new Map();
|
|
@@ -11298,7 +11721,7 @@ var PlacementService = class {
|
|
|
11298
11721
|
});
|
|
11299
11722
|
const pass = {
|
|
11300
11723
|
repointed,
|
|
11301
|
-
planned: plan.assignments,
|
|
11724
|
+
planned: overlayPins(plan.assignments, blob.pins),
|
|
11302
11725
|
defaultLocations: authority
|
|
11303
11726
|
};
|
|
11304
11727
|
this.reportDivergence(applyPlacementPass(blob.assignments, pass), authority);
|
|
@@ -11335,15 +11758,17 @@ var PlacementService = class {
|
|
|
11335
11758
|
* from an absent stamp would throw away every deliberate assignment on the
|
|
11336
11759
|
* first tick after a deploy.
|
|
11337
11760
|
*/
|
|
11338
|
-
detectRepoints(stored, stamped, authority) {
|
|
11761
|
+
detectRepoints(stored, stamped, authority, pins) {
|
|
11339
11762
|
const out = [];
|
|
11340
11763
|
for (const [type, to] of authority) {
|
|
11341
11764
|
const from = stamped.get(type);
|
|
11342
11765
|
if (from === void 0 || from === to) continue;
|
|
11343
11766
|
const keys = [];
|
|
11344
11767
|
for (const [key, locationId] of Object.entries(stored)) {
|
|
11345
|
-
|
|
11768
|
+
const [devicePart, profilePart] = key.split(":");
|
|
11769
|
+
if (placementTypeForProfile(profilePart ?? "") !== type) continue;
|
|
11346
11770
|
if (locationId === to) continue;
|
|
11771
|
+
if (type === "recordings" && devicePart !== void 0 && pins[devicePart] !== void 0) continue;
|
|
11347
11772
|
keys.push(key);
|
|
11348
11773
|
}
|
|
11349
11774
|
out.push({
|
|
@@ -11479,6 +11904,40 @@ var PlacementService = class {
|
|
|
11479
11904
|
return /* @__PURE__ */ new Map();
|
|
11480
11905
|
}
|
|
11481
11906
|
}
|
|
11907
|
+
async placementView() {
|
|
11908
|
+
const blob = await readPlacementState(this.deps.state);
|
|
11909
|
+
return {
|
|
11910
|
+
assignments: Object.entries(blob.assignments).flatMap(([key, locationId]) => {
|
|
11911
|
+
const [idPart, profile] = key.split(":");
|
|
11912
|
+
const deviceId = Number(idPart);
|
|
11913
|
+
if (!Number.isFinite(deviceId) || profile === void 0) return [];
|
|
11914
|
+
return [{
|
|
11915
|
+
deviceId,
|
|
11916
|
+
profile,
|
|
11917
|
+
locationId
|
|
11918
|
+
}];
|
|
11919
|
+
}),
|
|
11920
|
+
pins: Object.entries(blob.pins).flatMap(([id, locationId]) => {
|
|
11921
|
+
const deviceId = Number(id);
|
|
11922
|
+
if (!Number.isFinite(deviceId)) return [];
|
|
11923
|
+
return [{
|
|
11924
|
+
deviceId,
|
|
11925
|
+
locationId
|
|
11926
|
+
}];
|
|
11927
|
+
}),
|
|
11928
|
+
defaultLocations: blob.defaultLocations
|
|
11929
|
+
};
|
|
11930
|
+
}
|
|
11931
|
+
async setDevicePin(deviceId, locationId) {
|
|
11932
|
+
if (locationId !== null) {
|
|
11933
|
+
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`);
|
|
11934
|
+
}
|
|
11935
|
+
await writeDevicePin(this.deps.state, deviceId, locationId);
|
|
11936
|
+
this.deps.logger.info("recorder placement: operator pin updated", {
|
|
11937
|
+
tags: { deviceId },
|
|
11938
|
+
meta: { locationId }
|
|
11939
|
+
});
|
|
11940
|
+
}
|
|
11482
11941
|
/**
|
|
11483
11942
|
* Turn each pool location into a planner candidate. Headroom is
|
|
11484
11943
|
* `min(free − minFree floor, maxUsedGb − used)`, clamped at zero: the first
|
|
@@ -15471,6 +15930,14 @@ function withDeviceTag(logger, deviceId) {
|
|
|
15471
15930
|
* provider until the controller swaps the manifest entry in a later step — this
|
|
15472
15931
|
* file ships dormant (separate tree, not wired into the manifest yet).
|
|
15473
15932
|
*/
|
|
15933
|
+
/**
|
|
15934
|
+
* Hub harvests this named export into CustomActionRegistry. Actions that
|
|
15935
|
+
* exist only on the onInitialize envelope stay invisible to `addons.custom`.
|
|
15936
|
+
*/
|
|
15937
|
+
var customActions = {
|
|
15938
|
+
...recorderDebugActions,
|
|
15939
|
+
...locationDiscardActions
|
|
15940
|
+
};
|
|
15474
15941
|
/** Default designated recording node (`recordingNodeId` global setting). The
|
|
15475
15942
|
* stream-broker is hub-only and recording is a single-node global function, so
|
|
15476
15943
|
* the hub is the safe default — mirrors pipeline-analytics' post-processing
|
|
@@ -15638,6 +16105,8 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
15638
16105
|
* deferred archive walk reconciles it against disk.
|
|
15639
16106
|
*/
|
|
15640
16107
|
segmentHours = null;
|
|
16108
|
+
/** Frozen-location leftover wipe (`low/` or `high/`+`mid/`). Detached. */
|
|
16109
|
+
footageDiscard = null;
|
|
15641
16110
|
/** Why the last placement pass planned nothing — latched so the line is one
|
|
15642
16111
|
* per episode, not one per 30 s tick. `null` ⇒ the last pass had work. */
|
|
15643
16112
|
placementWorkGap = null;
|
|
@@ -16064,6 +16533,16 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
16064
16533
|
refreshStorageLocationsForMigration: (leaseId) => this.refreshStorageLocationsForMigration(leaseId),
|
|
16065
16534
|
assertStorageMigrationLease: (leaseId) => this.assertStorageMigrationLease(leaseId),
|
|
16066
16535
|
placementSnapshot: () => this.placementSnapshot(),
|
|
16536
|
+
placementView: () => {
|
|
16537
|
+
const placement = this.placement;
|
|
16538
|
+
if (!placement) throw new Error("placement is not active on this node");
|
|
16539
|
+
return placement.placementView();
|
|
16540
|
+
},
|
|
16541
|
+
setDevicePlacement: (deviceId, locationId) => {
|
|
16542
|
+
const placement = this.placement;
|
|
16543
|
+
if (!placement) throw new Error("placement is not active on this node");
|
|
16544
|
+
return placement.setDevicePin(deviceId, locationId);
|
|
16545
|
+
},
|
|
16067
16546
|
dataDir: this.ctx.dataDir,
|
|
16068
16547
|
playbackBaseUrl: () => this.playbackBaseUrl,
|
|
16069
16548
|
isCameraDevice: async (deviceId) => {
|
|
@@ -16226,27 +16705,50 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
16226
16705
|
}
|
|
16227
16706
|
});
|
|
16228
16707
|
warnLostBands(this.configStore(), this.ctx.logger);
|
|
16708
|
+
const providers = [
|
|
16709
|
+
{
|
|
16710
|
+
capability: recordingCapability,
|
|
16711
|
+
provider: recordingProvider
|
|
16712
|
+
},
|
|
16713
|
+
{
|
|
16714
|
+
capability: storageEvictableCapability,
|
|
16715
|
+
provider: evictableProvider
|
|
16716
|
+
},
|
|
16717
|
+
{
|
|
16718
|
+
capability: recordingExportCapability,
|
|
16719
|
+
provider: exportProvider
|
|
16720
|
+
},
|
|
16721
|
+
{
|
|
16722
|
+
capability: loadContributionCapability,
|
|
16723
|
+
provider: { list: () => this.costRegistry.contributions() }
|
|
16724
|
+
}
|
|
16725
|
+
];
|
|
16726
|
+
this.footageDiscard = createFootageDiscardJob({
|
|
16727
|
+
listLocations: async () => this.ctx.api.storage.listLocations.query({}),
|
|
16728
|
+
localNodeId: this.nodeId,
|
|
16729
|
+
index: this.index,
|
|
16730
|
+
ledger: this.segmentHours === null ? null : {
|
|
16731
|
+
streamSegments: (scope) => this.segmentHours.streamSegments(scope),
|
|
16732
|
+
dropSegments: (rows) => this.segmentHours.dropSegments(rows)
|
|
16733
|
+
},
|
|
16734
|
+
now: () => Date.now()
|
|
16735
|
+
});
|
|
16229
16736
|
return {
|
|
16230
|
-
providers
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
provider: exportProvider
|
|
16737
|
+
providers,
|
|
16738
|
+
customActions: {
|
|
16739
|
+
...recorderDebugActions,
|
|
16740
|
+
...locationDiscardActions
|
|
16741
|
+
},
|
|
16742
|
+
actionHandlers: {
|
|
16743
|
+
"debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input),
|
|
16744
|
+
"storage.discardLocation": async (input) => this.footageDiscard?.start(input) ?? {
|
|
16745
|
+
started: false,
|
|
16746
|
+
alreadyRunning: false,
|
|
16747
|
+
jobId: null
|
|
16242
16748
|
},
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
}
|
|
16247
|
-
],
|
|
16248
|
-
customActions: recorderDebugActions,
|
|
16249
|
-
actionHandlers: { "debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input) }
|
|
16749
|
+
"storage.discardLocationStatus": async () => this.footageDiscard?.status() ?? IDLE_DISCARD,
|
|
16750
|
+
"storage.discardLocationCancel": async () => this.footageDiscard?.cancel() ?? { requested: false }
|
|
16751
|
+
}
|
|
16250
16752
|
};
|
|
16251
16753
|
}
|
|
16252
16754
|
async onShutdown() {
|
|
@@ -16330,7 +16832,7 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
16330
16832
|
},
|
|
16331
16833
|
logger: this.ctx.logger,
|
|
16332
16834
|
now: () => Date.now(),
|
|
16333
|
-
pace: () => sleep(LEDGER_WALK_PAUSE_MS)
|
|
16835
|
+
pace: () => sleep$1(LEDGER_WALK_PAUSE_MS)
|
|
16334
16836
|
});
|
|
16335
16837
|
}
|
|
16336
16838
|
/**
|
|
@@ -16479,7 +16981,7 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
16479
16981
|
},
|
|
16480
16982
|
logger: this.ctx.logger,
|
|
16481
16983
|
now: () => Date.now(),
|
|
16482
|
-
pace: () => sleep(GHOST_LOCATION_PROBE_PAUSE_MS)
|
|
16984
|
+
pace: () => sleep$1(GHOST_LOCATION_PROBE_PAUSE_MS)
|
|
16483
16985
|
});
|
|
16484
16986
|
this.ghostLocationPass = startPeriodicPass({
|
|
16485
16987
|
name: "ghost-location-reconcile",
|
|
@@ -17253,4 +17755,4 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
17253
17755
|
}
|
|
17254
17756
|
};
|
|
17255
17757
|
//#endregion
|
|
17256
|
-
export { RecorderV2Addon as default };
|
|
17758
|
+
export { customActions, RecorderV2Addon as default };
|