@camstack/addon-pipeline 1.2.215 → 1.2.217
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/{default-detection-model-C2vYSX3O.mjs → default-detection-model-BwNEV8Wl.mjs} +1 -1
- package/dist/{default-detection-model-f0PImZnh.js → default-detection-model-DzzuINe-.js} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-DoHcimdu.js → dist-D7hxiBnC.js} +237 -16
- package/dist/{dist-DttqKvsQ.mjs → dist-DgeXZfwn.mjs} +237 -16
- package/dist/{lazy-sharp-BeeQGCUu.js → lazy-sharp-BSkbiuz5.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-NtM7__8q.mjs → node-C9GcBrwk.mjs} +1 -1
- package/dist/{node-6uF72wri.js → node-DDhmPcOx.js} +1 -1
- package/dist/pipeline-runner/index.js +538 -24
- package/dist/pipeline-runner/index.mjs +537 -23
- package/dist/{process-memory-CddpCDWL.mjs → process-memory-Dw3CGS3D.mjs} +1 -1
- package/dist/{process-memory-BRdyl5fE.js → process-memory-ENqmiiis.js} +1 -1
- package/dist/recorder/index.js +148 -40
- package/dist/recorder/index.mjs +148 -40
- package/dist/{segment-demux-js-CsFtPVzH.mjs → segment-demux-js-BGbKlotF.mjs} +1 -1
- package/dist/{segment-demux-js-CkJPdyDH.js → segment-demux-js-sLs8mbZQ.js} +1 -1
- package/dist/session-decode/decode-worker-child.js +58 -45
- package/dist/session-decode/decode-worker-child.mjs +57 -44
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZIja2AOe.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BULotDCb.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-yJ2CyixO.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-Uxkwkgy5.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DICVQkNi.mjs} +1 -1
- 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-DpatMS4g.mjs → hostInit-_RQxF72h.mjs} +3 -3
- package/dist/stream-broker/index.js +20 -21
- package/dist/stream-broker/index.mjs +20 -21
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-BWO2WNjw.js → worker-protocol-DGql9R4g.js} +1 -1
- package/dist/{worker-protocol-BKwkLZWT.mjs → worker-protocol-SzmaLNnP.mjs} +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-0MF-KL1v.mjs +0 -26
package/dist/recorder/index.js
CHANGED
|
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node-
|
|
5
|
+
const require_dist = require("../dist-D7hxiBnC.js");
|
|
6
|
+
const require_node = require("../node-DDhmPcOx.js");
|
|
7
7
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
8
8
|
const require_restream_intent = require("../restream-intent-B1Difcnq.js");
|
|
9
9
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
@@ -1333,27 +1333,6 @@ var RecordingIndex = class {
|
|
|
1333
1333
|
return [...out.values()];
|
|
1334
1334
|
}
|
|
1335
1335
|
/**
|
|
1336
|
-
* WHICH devices hold footage on any of `locationIds` — the census, without
|
|
1337
|
-
* the archive.
|
|
1338
|
-
*
|
|
1339
|
-
* `getStorageUsage` used to answer this with `segmentsOnLocation(id)` per
|
|
1340
|
-
* location: a full multi-device COPY of every row on the disk plus a sort of
|
|
1341
|
-
* it (294 k rows on the live hub), to read one number off each row and throw
|
|
1342
|
-
* the array away. This visits each device only until its first matching row.
|
|
1343
|
-
* Same membership rule as before — a device is listed when at least one of
|
|
1344
|
-
* its rows sits on one of the given locations, so footage stranded on a
|
|
1345
|
-
* location id that no longer resolves stays invisible here exactly as it was.
|
|
1346
|
-
* @residency archive-shaped
|
|
1347
|
-
*/
|
|
1348
|
-
deviceIdsOnLocations(locationIds) {
|
|
1349
|
-
const out = /* @__PURE__ */ new Set();
|
|
1350
|
-
for (const [deviceId, m] of this.byDevice) for (const s of m.values()) if (locationIds.has(s.locationId)) {
|
|
1351
|
-
out.add(deviceId);
|
|
1352
|
-
break;
|
|
1353
|
-
}
|
|
1354
|
-
return out;
|
|
1355
|
-
}
|
|
1356
|
-
/**
|
|
1357
1336
|
* Every RESIDENT segment on a storage location, across devices, oldest-first.
|
|
1358
1337
|
*
|
|
1359
1338
|
* ## The one that bit (D295)
|
|
@@ -6866,6 +6845,81 @@ function buildRecordingDeviceSchema() {
|
|
|
6866
6845
|
}]
|
|
6867
6846
|
}] };
|
|
6868
6847
|
}
|
|
6848
|
+
//#endregion
|
|
6849
|
+
//#region src/recorder/addon/device-storage-usage.ts
|
|
6850
|
+
/**
|
|
6851
|
+
* Gap between the per-camera sum and the per-volume total that is worth
|
|
6852
|
+
* saying out loud, as a share of the volume total.
|
|
6853
|
+
*
|
|
6854
|
+
* The two are aggregations of the SAME hour rows over the same location set —
|
|
6855
|
+
* one folded per camera, one summed in SQL — so in a healthy recorder they
|
|
6856
|
+
* agree to the byte. A few tenths of a percent is a write landing between the
|
|
6857
|
+
* two reads; 97% (60.5 GB against 1.9 TB) is the defect this file closes,
|
|
6858
|
+
* rebuilt somewhere new.
|
|
6859
|
+
*/
|
|
6860
|
+
var USAGE_MISMATCH_SHARE = .02;
|
|
6861
|
+
/** Floor under {@link USAGE_MISMATCH_SHARE} so a nearly empty disk does not
|
|
6862
|
+
* warn on a single in-flight segment. */
|
|
6863
|
+
var USAGE_MISMATCH_FLOOR_BYTES = 1e9;
|
|
6864
|
+
/**
|
|
6865
|
+
* One row per camera the ARCHIVE holds footage for on `locationIds`, biggest
|
|
6866
|
+
* first (deviceId breaks a tie, so two consecutive calls compare).
|
|
6867
|
+
*
|
|
6868
|
+
* A node with no ledger REFUSES (D295 rule 3): empty rows, `read:
|
|
6869
|
+
* 'unavailable'`, and a line saying so. Falling back to the index would answer
|
|
6870
|
+
* the same question with the wrong quantity and look exactly like a correct
|
|
6871
|
+
* answer, which is how this shipped in the first place.
|
|
6872
|
+
*/
|
|
6873
|
+
function buildDeviceUsage(input) {
|
|
6874
|
+
if (input.archive === null) {
|
|
6875
|
+
input.logger.warn("recorder: no hour ledger on this node — the per-camera storage rows are UNANSWERED, not empty", { meta: { locations: [...input.locationIds].toSorted().join(",") } });
|
|
6876
|
+
return {
|
|
6877
|
+
devices: [],
|
|
6878
|
+
totalUsedBytes: 0,
|
|
6879
|
+
read: "unavailable"
|
|
6880
|
+
};
|
|
6881
|
+
}
|
|
6882
|
+
const devices = [];
|
|
6883
|
+
let totalUsedBytes = 0;
|
|
6884
|
+
for (const [deviceId, archived] of input.archive) {
|
|
6885
|
+
const resident = input.resident(deviceId);
|
|
6886
|
+
totalUsedBytes += archived.bytes;
|
|
6887
|
+
devices.push({
|
|
6888
|
+
deviceId,
|
|
6889
|
+
usedBytes: archived.bytes,
|
|
6890
|
+
oldestMs: archived.oldestMs,
|
|
6891
|
+
segments: archived.segments,
|
|
6892
|
+
residentBytes: resident.bytes,
|
|
6893
|
+
residentOldestMs: resident.oldestMs
|
|
6894
|
+
});
|
|
6895
|
+
}
|
|
6896
|
+
return {
|
|
6897
|
+
devices: devices.toSorted((a, b) => b.usedBytes - a.usedBytes || a.deviceId - b.deviceId),
|
|
6898
|
+
totalUsedBytes,
|
|
6899
|
+
read: "archive"
|
|
6900
|
+
};
|
|
6901
|
+
}
|
|
6902
|
+
/**
|
|
6903
|
+
* Say it when the per-camera sum and the per-volume total disagree.
|
|
6904
|
+
*
|
|
6905
|
+
* The "I saw N of M" rule of D295, applied to a REPORT: these two numbers are
|
|
6906
|
+
* the same rows counted two ways, so a gap means one of the two reads saw a
|
|
6907
|
+
* fraction of the archive — and the whole point of D418 is that a page which
|
|
6908
|
+
* shows both without comparing them lets a 97% gap read as a fact about the
|
|
6909
|
+
* disks.
|
|
6910
|
+
*/
|
|
6911
|
+
function warnOnUsageMismatch(logger, deviceBytes, volumeBytes, read) {
|
|
6912
|
+
if (read !== "archive") return;
|
|
6913
|
+
if (volumeBytes === 0) return;
|
|
6914
|
+
const gap = Math.abs(volumeBytes - deviceBytes);
|
|
6915
|
+
if (gap <= USAGE_MISMATCH_FLOOR_BYTES) return;
|
|
6916
|
+
if (gap <= volumeBytes * USAGE_MISMATCH_SHARE) return;
|
|
6917
|
+
logger.warn("recorder: the per-camera storage sum disagrees with the per-volume total — one of the two reads saw a fraction of the archive", { meta: {
|
|
6918
|
+
deviceBytes,
|
|
6919
|
+
volumeBytes,
|
|
6920
|
+
gap
|
|
6921
|
+
} });
|
|
6922
|
+
}
|
|
6869
6923
|
/** The placement-store key for one (camera, profile). Mirrors `assignmentKey`
|
|
6870
6924
|
* in `placement-store.ts`; kept local so this module stays dependency-free. */
|
|
6871
6925
|
function sourceKey(source) {
|
|
@@ -7453,18 +7507,15 @@ function buildRecordingProvider(deps) {
|
|
|
7453
7507
|
getPlaybackManifest: ({ deviceId, fromMs, toMs }) => buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs),
|
|
7454
7508
|
getStorageUsage: async () => {
|
|
7455
7509
|
const locations = deps.locations();
|
|
7456
|
-
const
|
|
7457
|
-
const
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
oldestMs: accounting.oldestMs
|
|
7466
|
-
});
|
|
7467
|
-
}
|
|
7510
|
+
const locationIds = new Set(locations.map((l) => l.id));
|
|
7511
|
+
const deviceUsage = buildDeviceUsage({
|
|
7512
|
+
locationIds,
|
|
7513
|
+
archive: deps.archiveDeviceUsage?.(locationIds) ?? null,
|
|
7514
|
+
resident: (deviceId) => deps.index.accounting(deviceId),
|
|
7515
|
+
logger: deps.logger
|
|
7516
|
+
});
|
|
7517
|
+
const devices = deviceUsage.devices;
|
|
7518
|
+
const totalUsedBytes = deviceUsage.totalUsedBytes;
|
|
7468
7519
|
const byPhysical = /* @__PURE__ */ new Map();
|
|
7469
7520
|
for (const loc of locations) {
|
|
7470
7521
|
const key = physicalRootKey(loc.root);
|
|
@@ -7493,11 +7544,12 @@ function buildRecordingProvider(deps) {
|
|
|
7493
7544
|
totalBytes: cap.totalBytes
|
|
7494
7545
|
};
|
|
7495
7546
|
}));
|
|
7496
|
-
|
|
7547
|
+
warnOnUsageMismatch(deps.logger, totalUsedBytes, locationUsages.reduce((sum, l) => sum + l.usedBytes, 0), deviceUsage.read);
|
|
7497
7548
|
return {
|
|
7498
7549
|
nodeId: deps.nodeId,
|
|
7499
7550
|
totalUsedBytes,
|
|
7500
|
-
devices,
|
|
7551
|
+
devices: [...devices],
|
|
7552
|
+
devicesRead: deviceUsage.read,
|
|
7501
7553
|
locations: locationUsages
|
|
7502
7554
|
};
|
|
7503
7555
|
},
|
|
@@ -8391,6 +8443,14 @@ var DurableLedger = class DurableLedger {
|
|
|
8391
8443
|
//#endregion
|
|
8392
8444
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
8393
8445
|
var HOUR_MS$3 = 36e5;
|
|
8446
|
+
/** `Math.min` where a missing left operand means "nothing yet", not `NaN`. */
|
|
8447
|
+
function minOf(a, b) {
|
|
8448
|
+
return a === null ? b : Math.min(a, b);
|
|
8449
|
+
}
|
|
8450
|
+
/** `Math.max` with the same rule as {@link minOf}. */
|
|
8451
|
+
function maxOf(a, b) {
|
|
8452
|
+
return a === null ? b : Math.max(a, b);
|
|
8453
|
+
}
|
|
8394
8454
|
var macrotask$1 = () => new Promise((resolve) => {
|
|
8395
8455
|
setImmediate(resolve);
|
|
8396
8456
|
});
|
|
@@ -9041,7 +9101,7 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9041
9101
|
*
|
|
9042
9102
|
* The reader repairs the durable index it just reconstructed, so the next
|
|
9043
9103
|
* cold read of this hour is a point read instead of another `readdir`
|
|
9044
|
-
* (`docs/design/2026-08-29-
|
|
9104
|
+
* (`docs/design/2026-08-29-reference-nvr-recording-model.md` §4.2). Two
|
|
9045
9105
|
* constraints, both enforced by the caller and re-stated here because
|
|
9046
9106
|
* getting either wrong turns a READ into durable damage: the hour must be
|
|
9047
9107
|
* CLOSED (a live hour's directory races the writer, so a walk of it is a
|
|
@@ -9274,6 +9334,53 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9274
9334
|
for (const row of this.ledger.snapshot()) if (locationIds.has(row.locationId)) out.add(row.deviceId);
|
|
9275
9335
|
return out;
|
|
9276
9336
|
}
|
|
9337
|
+
/**
|
|
9338
|
+
* Bytes / segments / oldest / newest per CAMERA on these locations — the
|
|
9339
|
+
* per-camera half of the storage page, answered by the ARCHIVE (D418).
|
|
9340
|
+
*
|
|
9341
|
+
* ## Why the T1 fold and not `census({ deviceId })` per camera
|
|
9342
|
+
*
|
|
9343
|
+
* `census` is the right authority for a SCOPE, and it cannot express this
|
|
9344
|
+
* scope: {@link SegmentHourLedger.predicate} takes ONE `locationId`, so
|
|
9345
|
+
* "camera 615 across the three resolvable volumes" is three aggregates, and
|
|
9346
|
+
* `census({ deviceId })` alone would silently count footage stranded on a
|
|
9347
|
+
* location id that no longer resolves — bytes no volume row on the same page
|
|
9348
|
+
* accounts for. Per camera per volume that is `cameras × volumes` SQL
|
|
9349
|
+
* aggregates for one operator click, and it grows with the fleet.
|
|
9350
|
+
*
|
|
9351
|
+
* The T1 hour summary answers all of them in ONE pass with no I/O, exactly
|
|
9352
|
+
* as {@link devicesWithFootage} and {@link profilesWithFootage} already do,
|
|
9353
|
+
* and it is the same rows `accountingForLocations` sums in SQL — the mirror
|
|
9354
|
+
* is write-behind, so it is never BEHIND the store. The cost is one pass
|
|
9355
|
+
* over the hour rows the mirror holds — O(writers × profiles × hours), ~19 k
|
|
9356
|
+
* on the live hub (see {@link REPAIR_MAX_ROWS}) against 98 445 segments,
|
|
9357
|
+
* i.e. it does NOT scale with the segment count — and it is capped for good
|
|
9358
|
+
* by {@link SEGMENT_HOUR_SUMMARY_LIMIT}, the mirror's own ceiling.
|
|
9359
|
+
*
|
|
9360
|
+
* ## What an empty answer means
|
|
9361
|
+
*
|
|
9362
|
+
* The same thing it means for {@link devicesWithFootage}: "the mirror knows
|
|
9363
|
+
* no hour here" — which is also what a ledger whose load failed reports. The
|
|
9364
|
+
* caller must not render that as "no camera has footage"; `getStorageUsage`
|
|
9365
|
+
* cross-checks the per-camera sum against the volume aggregate and SAYS so
|
|
9366
|
+
* when the two disagree.
|
|
9367
|
+
*/
|
|
9368
|
+
usageByDevice(locationIds) {
|
|
9369
|
+
const out = /* @__PURE__ */ new Map();
|
|
9370
|
+
if (locationIds.size === 0) return out;
|
|
9371
|
+
for (const row of this.ledger.snapshot()) {
|
|
9372
|
+
if (!locationIds.has(row.locationId)) continue;
|
|
9373
|
+
const empty = row.segments === 0;
|
|
9374
|
+
const prev = out.get(row.deviceId);
|
|
9375
|
+
out.set(row.deviceId, {
|
|
9376
|
+
bytes: (prev?.bytes ?? 0) + row.bytes,
|
|
9377
|
+
segments: (prev?.segments ?? 0) + row.segments,
|
|
9378
|
+
oldestMs: empty ? prev?.oldestMs ?? null : minOf(prev?.oldestMs ?? null, row.minStartMs),
|
|
9379
|
+
newestMs: empty ? prev?.newestMs ?? null : maxOf(prev?.newestMs ?? null, row.maxStartMs)
|
|
9380
|
+
});
|
|
9381
|
+
}
|
|
9382
|
+
return out;
|
|
9383
|
+
}
|
|
9277
9384
|
async oldestSegmentsScoped(locationIds, deviceIds, targetBytes) {
|
|
9278
9385
|
const ids = [...locationIds];
|
|
9279
9386
|
if (ids.length === 0 || targetBytes <= 0) return {
|
|
@@ -14533,7 +14640,7 @@ var RecordingController = class {
|
|
|
14533
14640
|
/**
|
|
14534
14641
|
* The ADAPTIVE attach bound. A transient `spawn ffmpeg` failure (EAGAIN) is
|
|
14535
14642
|
* the host saying it cannot fork right now, and the answer is to attach
|
|
14536
|
-
* FEWER cameras at once — not to serialise them, which
|
|
14643
|
+
* FEWER cameras at once — not to serialise them, which removed
|
|
14537
14644
|
* for good reason. Every bounded-parallel attach in this file reads it.
|
|
14538
14645
|
*/
|
|
14539
14646
|
spawnPressure;
|
|
@@ -16038,7 +16145,7 @@ function selectRedundantSegments(segs) {
|
|
|
16038
16145
|
* The pruner that used to remove it, `runRedundancyJanitor`, was reachable ONLY
|
|
16039
16146
|
* from `runFullArchiveWalk`, which was reachable only from
|
|
16040
16147
|
* `scheduleDeferredFullWalk`, which had no caller at all — so on 2026-08-27
|
|
16041
|
-
*
|
|
16148
|
+
* all three were deleted together as dead code. That was
|
|
16042
16149
|
* correct about the walk and left the system with **no duplicate pruner
|
|
16043
16150
|
* whatsoever**. The precedent for letting this class of waste run unattended is
|
|
16044
16151
|
* the 179 GB of stranded staging segments in `staging-reconcile.ts`.
|
|
@@ -17449,6 +17556,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17449
17556
|
refreshLocations: () => this.refreshLocations(),
|
|
17450
17557
|
capacity: (root) => this.locationCapacity(root),
|
|
17451
17558
|
archiveAccounting: (locationIds) => this.segmentHours?.accountingForLocations(locationIds) ?? Promise.resolve(EMPTY_ARCHIVE_ACCOUNTING),
|
|
17559
|
+
archiveDeviceUsage: (locationIds) => this.segmentHours?.usageByDevice(locationIds) ?? null,
|
|
17452
17560
|
archive: () => this.segmentHours,
|
|
17453
17561
|
ledgerWalk: this.ledgerWalkReconciler(),
|
|
17454
17562
|
hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { At as storageEvictableCapability, Ct as recordingSignalCapability, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Et as resolveLocationMode, Gt as DeviceFeature, H as deriveBatteryPresence, It as errMsg, Jt as hydrateSchema, Kt as DeviceType, Nt as stripRetiredBandPreBufferSec, Ot as resolveRecordingProfiles, P as batteryCapability, Q as evictionPolicyOfLocation, Qt as nodePin, R as customAction, St as recordingExportCapability, T as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as defineCustomActions, W as deriveRecordingMode, Y as evaluateSensorEdge, Z as evictionPolicyForMode, an as boolean, ct as mayReadLocation, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, dn as preprocess, dt as modeMayWrite, et as gbToBytes, fn as record, g as ExportRecordSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, jt as storageOccupancyCapability, ln as number, lt as mayWriteToLocation, nn as sleep$1, pn as string, rn as _enum, rt as isFirstLevelMacroClass, tn as selectAssignedProfileSlots, un as object, ut as migrateLegacyDeviceSignalConfig, x as OpsLogEntrySchema, xt as recordingCapability } from "../dist-
|
|
2
|
-
import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
1
|
+
import { At as storageEvictableCapability, Ct as recordingSignalCapability, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Et as resolveLocationMode, Gt as DeviceFeature, H as deriveBatteryPresence, It as errMsg, Jt as hydrateSchema, Kt as DeviceType, Nt as stripRetiredBandPreBufferSec, Ot as resolveRecordingProfiles, P as batteryCapability, Q as evictionPolicyOfLocation, Qt as nodePin, R as customAction, St as recordingExportCapability, T as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as defineCustomActions, W as deriveRecordingMode, Y as evaluateSensorEdge, Z as evictionPolicyForMode, an as boolean, ct as mayReadLocation, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, dn as preprocess, dt as modeMayWrite, et as gbToBytes, fn as record, g as ExportRecordSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, jt as storageOccupancyCapability, ln as number, lt as mayWriteToLocation, nn as sleep$1, pn as string, rn as _enum, rt as isFirstLevelMacroClass, tn as selectAssignedProfileSlots, un as object, ut as migrateLegacyDeviceSignalConfig, x as OpsLogEntrySchema, xt as recordingCapability } from "../dist-DgeXZfwn.mjs";
|
|
2
|
+
import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-C9GcBrwk.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { c as PREBUFFER_MAX_MS, r as RECORDING_PRE_ROLL_REQUEST_MS, s as withRecordingIntent } from "../restream-intent-DWe2kInY.mjs";
|
|
5
5
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
@@ -1328,27 +1328,6 @@ var RecordingIndex = class {
|
|
|
1328
1328
|
return [...out.values()];
|
|
1329
1329
|
}
|
|
1330
1330
|
/**
|
|
1331
|
-
* WHICH devices hold footage on any of `locationIds` — the census, without
|
|
1332
|
-
* the archive.
|
|
1333
|
-
*
|
|
1334
|
-
* `getStorageUsage` used to answer this with `segmentsOnLocation(id)` per
|
|
1335
|
-
* location: a full multi-device COPY of every row on the disk plus a sort of
|
|
1336
|
-
* it (294 k rows on the live hub), to read one number off each row and throw
|
|
1337
|
-
* the array away. This visits each device only until its first matching row.
|
|
1338
|
-
* Same membership rule as before — a device is listed when at least one of
|
|
1339
|
-
* its rows sits on one of the given locations, so footage stranded on a
|
|
1340
|
-
* location id that no longer resolves stays invisible here exactly as it was.
|
|
1341
|
-
* @residency archive-shaped
|
|
1342
|
-
*/
|
|
1343
|
-
deviceIdsOnLocations(locationIds) {
|
|
1344
|
-
const out = /* @__PURE__ */ new Set();
|
|
1345
|
-
for (const [deviceId, m] of this.byDevice) for (const s of m.values()) if (locationIds.has(s.locationId)) {
|
|
1346
|
-
out.add(deviceId);
|
|
1347
|
-
break;
|
|
1348
|
-
}
|
|
1349
|
-
return out;
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
1331
|
* Every RESIDENT segment on a storage location, across devices, oldest-first.
|
|
1353
1332
|
*
|
|
1354
1333
|
* ## The one that bit (D295)
|
|
@@ -6860,6 +6839,81 @@ function buildRecordingDeviceSchema() {
|
|
|
6860
6839
|
}]
|
|
6861
6840
|
}] };
|
|
6862
6841
|
}
|
|
6842
|
+
//#endregion
|
|
6843
|
+
//#region src/recorder/addon/device-storage-usage.ts
|
|
6844
|
+
/**
|
|
6845
|
+
* Gap between the per-camera sum and the per-volume total that is worth
|
|
6846
|
+
* saying out loud, as a share of the volume total.
|
|
6847
|
+
*
|
|
6848
|
+
* The two are aggregations of the SAME hour rows over the same location set —
|
|
6849
|
+
* one folded per camera, one summed in SQL — so in a healthy recorder they
|
|
6850
|
+
* agree to the byte. A few tenths of a percent is a write landing between the
|
|
6851
|
+
* two reads; 97% (60.5 GB against 1.9 TB) is the defect this file closes,
|
|
6852
|
+
* rebuilt somewhere new.
|
|
6853
|
+
*/
|
|
6854
|
+
var USAGE_MISMATCH_SHARE = .02;
|
|
6855
|
+
/** Floor under {@link USAGE_MISMATCH_SHARE} so a nearly empty disk does not
|
|
6856
|
+
* warn on a single in-flight segment. */
|
|
6857
|
+
var USAGE_MISMATCH_FLOOR_BYTES = 1e9;
|
|
6858
|
+
/**
|
|
6859
|
+
* One row per camera the ARCHIVE holds footage for on `locationIds`, biggest
|
|
6860
|
+
* first (deviceId breaks a tie, so two consecutive calls compare).
|
|
6861
|
+
*
|
|
6862
|
+
* A node with no ledger REFUSES (D295 rule 3): empty rows, `read:
|
|
6863
|
+
* 'unavailable'`, and a line saying so. Falling back to the index would answer
|
|
6864
|
+
* the same question with the wrong quantity and look exactly like a correct
|
|
6865
|
+
* answer, which is how this shipped in the first place.
|
|
6866
|
+
*/
|
|
6867
|
+
function buildDeviceUsage(input) {
|
|
6868
|
+
if (input.archive === null) {
|
|
6869
|
+
input.logger.warn("recorder: no hour ledger on this node — the per-camera storage rows are UNANSWERED, not empty", { meta: { locations: [...input.locationIds].toSorted().join(",") } });
|
|
6870
|
+
return {
|
|
6871
|
+
devices: [],
|
|
6872
|
+
totalUsedBytes: 0,
|
|
6873
|
+
read: "unavailable"
|
|
6874
|
+
};
|
|
6875
|
+
}
|
|
6876
|
+
const devices = [];
|
|
6877
|
+
let totalUsedBytes = 0;
|
|
6878
|
+
for (const [deviceId, archived] of input.archive) {
|
|
6879
|
+
const resident = input.resident(deviceId);
|
|
6880
|
+
totalUsedBytes += archived.bytes;
|
|
6881
|
+
devices.push({
|
|
6882
|
+
deviceId,
|
|
6883
|
+
usedBytes: archived.bytes,
|
|
6884
|
+
oldestMs: archived.oldestMs,
|
|
6885
|
+
segments: archived.segments,
|
|
6886
|
+
residentBytes: resident.bytes,
|
|
6887
|
+
residentOldestMs: resident.oldestMs
|
|
6888
|
+
});
|
|
6889
|
+
}
|
|
6890
|
+
return {
|
|
6891
|
+
devices: devices.toSorted((a, b) => b.usedBytes - a.usedBytes || a.deviceId - b.deviceId),
|
|
6892
|
+
totalUsedBytes,
|
|
6893
|
+
read: "archive"
|
|
6894
|
+
};
|
|
6895
|
+
}
|
|
6896
|
+
/**
|
|
6897
|
+
* Say it when the per-camera sum and the per-volume total disagree.
|
|
6898
|
+
*
|
|
6899
|
+
* The "I saw N of M" rule of D295, applied to a REPORT: these two numbers are
|
|
6900
|
+
* the same rows counted two ways, so a gap means one of the two reads saw a
|
|
6901
|
+
* fraction of the archive — and the whole point of D418 is that a page which
|
|
6902
|
+
* shows both without comparing them lets a 97% gap read as a fact about the
|
|
6903
|
+
* disks.
|
|
6904
|
+
*/
|
|
6905
|
+
function warnOnUsageMismatch(logger, deviceBytes, volumeBytes, read) {
|
|
6906
|
+
if (read !== "archive") return;
|
|
6907
|
+
if (volumeBytes === 0) return;
|
|
6908
|
+
const gap = Math.abs(volumeBytes - deviceBytes);
|
|
6909
|
+
if (gap <= USAGE_MISMATCH_FLOOR_BYTES) return;
|
|
6910
|
+
if (gap <= volumeBytes * USAGE_MISMATCH_SHARE) return;
|
|
6911
|
+
logger.warn("recorder: the per-camera storage sum disagrees with the per-volume total — one of the two reads saw a fraction of the archive", { meta: {
|
|
6912
|
+
deviceBytes,
|
|
6913
|
+
volumeBytes,
|
|
6914
|
+
gap
|
|
6915
|
+
} });
|
|
6916
|
+
}
|
|
6863
6917
|
/** The placement-store key for one (camera, profile). Mirrors `assignmentKey`
|
|
6864
6918
|
* in `placement-store.ts`; kept local so this module stays dependency-free. */
|
|
6865
6919
|
function sourceKey(source) {
|
|
@@ -7447,18 +7501,15 @@ function buildRecordingProvider(deps) {
|
|
|
7447
7501
|
getPlaybackManifest: ({ deviceId, fromMs, toMs }) => buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs),
|
|
7448
7502
|
getStorageUsage: async () => {
|
|
7449
7503
|
const locations = deps.locations();
|
|
7450
|
-
const
|
|
7451
|
-
const
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
oldestMs: accounting.oldestMs
|
|
7460
|
-
});
|
|
7461
|
-
}
|
|
7504
|
+
const locationIds = new Set(locations.map((l) => l.id));
|
|
7505
|
+
const deviceUsage = buildDeviceUsage({
|
|
7506
|
+
locationIds,
|
|
7507
|
+
archive: deps.archiveDeviceUsage?.(locationIds) ?? null,
|
|
7508
|
+
resident: (deviceId) => deps.index.accounting(deviceId),
|
|
7509
|
+
logger: deps.logger
|
|
7510
|
+
});
|
|
7511
|
+
const devices = deviceUsage.devices;
|
|
7512
|
+
const totalUsedBytes = deviceUsage.totalUsedBytes;
|
|
7462
7513
|
const byPhysical = /* @__PURE__ */ new Map();
|
|
7463
7514
|
for (const loc of locations) {
|
|
7464
7515
|
const key = physicalRootKey(loc.root);
|
|
@@ -7487,11 +7538,12 @@ function buildRecordingProvider(deps) {
|
|
|
7487
7538
|
totalBytes: cap.totalBytes
|
|
7488
7539
|
};
|
|
7489
7540
|
}));
|
|
7490
|
-
|
|
7541
|
+
warnOnUsageMismatch(deps.logger, totalUsedBytes, locationUsages.reduce((sum, l) => sum + l.usedBytes, 0), deviceUsage.read);
|
|
7491
7542
|
return {
|
|
7492
7543
|
nodeId: deps.nodeId,
|
|
7493
7544
|
totalUsedBytes,
|
|
7494
|
-
devices,
|
|
7545
|
+
devices: [...devices],
|
|
7546
|
+
devicesRead: deviceUsage.read,
|
|
7495
7547
|
locations: locationUsages
|
|
7496
7548
|
};
|
|
7497
7549
|
},
|
|
@@ -8385,6 +8437,14 @@ var DurableLedger = class DurableLedger {
|
|
|
8385
8437
|
//#endregion
|
|
8386
8438
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
8387
8439
|
var HOUR_MS$3 = 36e5;
|
|
8440
|
+
/** `Math.min` where a missing left operand means "nothing yet", not `NaN`. */
|
|
8441
|
+
function minOf(a, b) {
|
|
8442
|
+
return a === null ? b : Math.min(a, b);
|
|
8443
|
+
}
|
|
8444
|
+
/** `Math.max` with the same rule as {@link minOf}. */
|
|
8445
|
+
function maxOf(a, b) {
|
|
8446
|
+
return a === null ? b : Math.max(a, b);
|
|
8447
|
+
}
|
|
8388
8448
|
var macrotask$1 = () => new Promise((resolve) => {
|
|
8389
8449
|
setImmediate(resolve);
|
|
8390
8450
|
});
|
|
@@ -9035,7 +9095,7 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9035
9095
|
*
|
|
9036
9096
|
* The reader repairs the durable index it just reconstructed, so the next
|
|
9037
9097
|
* cold read of this hour is a point read instead of another `readdir`
|
|
9038
|
-
* (`docs/design/2026-08-29-
|
|
9098
|
+
* (`docs/design/2026-08-29-reference-nvr-recording-model.md` §4.2). Two
|
|
9039
9099
|
* constraints, both enforced by the caller and re-stated here because
|
|
9040
9100
|
* getting either wrong turns a READ into durable damage: the hour must be
|
|
9041
9101
|
* CLOSED (a live hour's directory races the writer, so a walk of it is a
|
|
@@ -9268,6 +9328,53 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9268
9328
|
for (const row of this.ledger.snapshot()) if (locationIds.has(row.locationId)) out.add(row.deviceId);
|
|
9269
9329
|
return out;
|
|
9270
9330
|
}
|
|
9331
|
+
/**
|
|
9332
|
+
* Bytes / segments / oldest / newest per CAMERA on these locations — the
|
|
9333
|
+
* per-camera half of the storage page, answered by the ARCHIVE (D418).
|
|
9334
|
+
*
|
|
9335
|
+
* ## Why the T1 fold and not `census({ deviceId })` per camera
|
|
9336
|
+
*
|
|
9337
|
+
* `census` is the right authority for a SCOPE, and it cannot express this
|
|
9338
|
+
* scope: {@link SegmentHourLedger.predicate} takes ONE `locationId`, so
|
|
9339
|
+
* "camera 615 across the three resolvable volumes" is three aggregates, and
|
|
9340
|
+
* `census({ deviceId })` alone would silently count footage stranded on a
|
|
9341
|
+
* location id that no longer resolves — bytes no volume row on the same page
|
|
9342
|
+
* accounts for. Per camera per volume that is `cameras × volumes` SQL
|
|
9343
|
+
* aggregates for one operator click, and it grows with the fleet.
|
|
9344
|
+
*
|
|
9345
|
+
* The T1 hour summary answers all of them in ONE pass with no I/O, exactly
|
|
9346
|
+
* as {@link devicesWithFootage} and {@link profilesWithFootage} already do,
|
|
9347
|
+
* and it is the same rows `accountingForLocations` sums in SQL — the mirror
|
|
9348
|
+
* is write-behind, so it is never BEHIND the store. The cost is one pass
|
|
9349
|
+
* over the hour rows the mirror holds — O(writers × profiles × hours), ~19 k
|
|
9350
|
+
* on the live hub (see {@link REPAIR_MAX_ROWS}) against 98 445 segments,
|
|
9351
|
+
* i.e. it does NOT scale with the segment count — and it is capped for good
|
|
9352
|
+
* by {@link SEGMENT_HOUR_SUMMARY_LIMIT}, the mirror's own ceiling.
|
|
9353
|
+
*
|
|
9354
|
+
* ## What an empty answer means
|
|
9355
|
+
*
|
|
9356
|
+
* The same thing it means for {@link devicesWithFootage}: "the mirror knows
|
|
9357
|
+
* no hour here" — which is also what a ledger whose load failed reports. The
|
|
9358
|
+
* caller must not render that as "no camera has footage"; `getStorageUsage`
|
|
9359
|
+
* cross-checks the per-camera sum against the volume aggregate and SAYS so
|
|
9360
|
+
* when the two disagree.
|
|
9361
|
+
*/
|
|
9362
|
+
usageByDevice(locationIds) {
|
|
9363
|
+
const out = /* @__PURE__ */ new Map();
|
|
9364
|
+
if (locationIds.size === 0) return out;
|
|
9365
|
+
for (const row of this.ledger.snapshot()) {
|
|
9366
|
+
if (!locationIds.has(row.locationId)) continue;
|
|
9367
|
+
const empty = row.segments === 0;
|
|
9368
|
+
const prev = out.get(row.deviceId);
|
|
9369
|
+
out.set(row.deviceId, {
|
|
9370
|
+
bytes: (prev?.bytes ?? 0) + row.bytes,
|
|
9371
|
+
segments: (prev?.segments ?? 0) + row.segments,
|
|
9372
|
+
oldestMs: empty ? prev?.oldestMs ?? null : minOf(prev?.oldestMs ?? null, row.minStartMs),
|
|
9373
|
+
newestMs: empty ? prev?.newestMs ?? null : maxOf(prev?.newestMs ?? null, row.maxStartMs)
|
|
9374
|
+
});
|
|
9375
|
+
}
|
|
9376
|
+
return out;
|
|
9377
|
+
}
|
|
9271
9378
|
async oldestSegmentsScoped(locationIds, deviceIds, targetBytes) {
|
|
9272
9379
|
const ids = [...locationIds];
|
|
9273
9380
|
if (ids.length === 0 || targetBytes <= 0) return {
|
|
@@ -14527,7 +14634,7 @@ var RecordingController = class {
|
|
|
14527
14634
|
/**
|
|
14528
14635
|
* The ADAPTIVE attach bound. A transient `spawn ffmpeg` failure (EAGAIN) is
|
|
14529
14636
|
* the host saying it cannot fork right now, and the answer is to attach
|
|
14530
|
-
* FEWER cameras at once — not to serialise them, which
|
|
14637
|
+
* FEWER cameras at once — not to serialise them, which removed
|
|
14531
14638
|
* for good reason. Every bounded-parallel attach in this file reads it.
|
|
14532
14639
|
*/
|
|
14533
14640
|
spawnPressure;
|
|
@@ -16032,7 +16139,7 @@ function selectRedundantSegments(segs) {
|
|
|
16032
16139
|
* The pruner that used to remove it, `runRedundancyJanitor`, was reachable ONLY
|
|
16033
16140
|
* from `runFullArchiveWalk`, which was reachable only from
|
|
16034
16141
|
* `scheduleDeferredFullWalk`, which had no caller at all — so on 2026-08-27
|
|
16035
|
-
*
|
|
16142
|
+
* all three were deleted together as dead code. That was
|
|
16036
16143
|
* correct about the walk and left the system with **no duplicate pruner
|
|
16037
16144
|
* whatsoever**. The precedent for letting this class of waste run unattended is
|
|
16038
16145
|
* the 179 GB of stranded staging segments in `staging-reconcile.ts`.
|
|
@@ -17443,6 +17550,7 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
17443
17550
|
refreshLocations: () => this.refreshLocations(),
|
|
17444
17551
|
capacity: (root) => this.locationCapacity(root),
|
|
17445
17552
|
archiveAccounting: (locationIds) => this.segmentHours?.accountingForLocations(locationIds) ?? Promise.resolve(EMPTY_ARCHIVE_ACCOUNTING),
|
|
17553
|
+
archiveDeviceUsage: (locationIds) => this.segmentHours?.usageByDevice(locationIds) ?? null,
|
|
17446
17554
|
archive: () => this.segmentHours,
|
|
17447
17555
|
ledgerWalk: this.ledgerWalkReconciler(),
|
|
17448
17556
|
hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { It as errMsg } from "./dist-
|
|
2
|
+
import { It as errMsg } from "./dist-DgeXZfwn.mjs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
//#region ../../node_modules/mp4box/dist/mp4box.all.js
|
|
5
5
|
var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-D7hxiBnC.js");
|
|
2
2
|
let node_child_process = require("node:child_process");
|
|
3
3
|
//#region ../../node_modules/mp4box/dist/mp4box.all.js
|
|
4
4
|
var require_mp4box_all = /* @__PURE__ */ require_dist.__commonJSMin(((exports) => {
|