@camstack/addon-pipeline 1.2.215 → 1.2.218
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-AJyhoTLF.mjs} +1 -1
- package/dist/{default-detection-model-f0PImZnh.js → default-detection-model-Bf45EQ98.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-BbvtiBNc.js} +278 -20
- package/dist/{dist-DttqKvsQ.mjs → dist-Dl6GntEh.mjs} +278 -20
- package/dist/{lazy-sharp-BeeQGCUu.js → lazy-sharp-L9Lcr62v.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-CEyw52D2.mjs} +1 -1
- package/dist/{node-6uF72wri.js → node-CbCVHEvJ.js} +1 -1
- package/dist/pipeline-runner/index.js +546 -25
- package/dist/pipeline-runner/index.mjs +545 -24
- package/dist/{process-memory-BRdyl5fE.js → process-memory-DBxdiCSF.js} +1 -1
- package/dist/{process-memory-CddpCDWL.mjs → process-memory-Du00R6-e.mjs} +1 -1
- package/dist/recorder/index.js +238 -52
- package/dist/recorder/index.mjs +238 -52
- package/dist/{segment-demux-js-CsFtPVzH.mjs → segment-demux-js-B278aBtB.mjs} +1 -1
- package/dist/{segment-demux-js-CkJPdyDH.js → segment-demux-js-D7bjzY8M.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-BnmbfnAd.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-MIKQqs86.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-BKwkLZWT.mjs → worker-protocol-BEbD5adq.mjs} +1 -1
- package/dist/{worker-protocol-BWO2WNjw.js → worker-protocol-C4rh5uiS.js} +1 -1
- package/package.json +1 -1
- package/python/__pycache__/inference_pool.cpython-312.pyc +0 -0
- package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
- package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_deadline.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_layout.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_preprocess.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_reconfigure.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-314.pyc +0 -0
- package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/plate_slots.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/plate_slots.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/rfdetr.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/rfdetr.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_plate_slots.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_rfdetr.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_rfdetr.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_softmax.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolonas.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolonas.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolonas.cpython-314.pyc +0 -0
- 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-BbvtiBNc.js");
|
|
6
|
+
const require_node = require("../node-CbCVHEvJ.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)
|
|
@@ -3838,6 +3817,40 @@ async function segmentsOfHour(root, deviceId, profile, hourStartMs) {
|
|
|
3838
3817
|
* the directory route serve the profile the client asked for when it exists,
|
|
3839
3818
|
* and fall back to the availability pick when it does not.
|
|
3840
3819
|
*/
|
|
3820
|
+
/**
|
|
3821
|
+
* EVERY profile this camera holds footage in, across every root.
|
|
3822
|
+
*
|
|
3823
|
+
* {@link availabilityProfile} deliberately returns ONE — the timeline is a
|
|
3824
|
+
* single bar. That is right for the bar and wrong for the ANSWER: on
|
|
3825
|
+
* 2026-09-10 `getAvailability` reported twenty `high` ranges for camera 615
|
|
3826
|
+
* and the conclusion drawn was that this deployment records no `low` at all,
|
|
3827
|
+
* while `/media-fast/recordingsLow/615/low/` held 14 GB on a different
|
|
3828
|
+
* location (D433).
|
|
3829
|
+
*
|
|
3830
|
+
* Unions across roots for exactly that reason: `high` and `low` do not share a
|
|
3831
|
+
* location here, so a per-root answer sees one of them.
|
|
3832
|
+
*
|
|
3833
|
+
* Non-emptiness, not existence — the same rule {@link availabilityProfile}
|
|
3834
|
+
* keeps, so a directory left behind by a config change is not offered. And
|
|
3835
|
+
* only {@link KNOWN_PROFILES}: the camera dir also hosts `events/` and the
|
|
3836
|
+
* retired `strips/` trees, which a walker that trusts readdir would present as
|
|
3837
|
+
* playable.
|
|
3838
|
+
*
|
|
3839
|
+
* `list` is injectable for the test; production passes the module's own
|
|
3840
|
+
* directory read.
|
|
3841
|
+
*/
|
|
3842
|
+
async function profilesWithFootage(roots, deviceId, list = listDir) {
|
|
3843
|
+
const found = /* @__PURE__ */ new Set();
|
|
3844
|
+
for (const root of roots) {
|
|
3845
|
+
const entries = await list(node_path.default.join(root, String(deviceId)));
|
|
3846
|
+
for (const entry of entries) {
|
|
3847
|
+
if (!KNOWN_PROFILES$1.has(entry)) continue;
|
|
3848
|
+
if (found.has(entry)) continue;
|
|
3849
|
+
if ((await list(node_path.default.join(root, String(deviceId), entry))).length > 0) found.add(entry);
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
return [...found].sort();
|
|
3853
|
+
}
|
|
3841
3854
|
async function profileHasFootage(root, deviceId, profile) {
|
|
3842
3855
|
return (await listDir(node_path.default.join(root, String(deviceId), profile))).length > 0;
|
|
3843
3856
|
}
|
|
@@ -3959,6 +3972,8 @@ var CalendarIndex = class {
|
|
|
3959
3972
|
segsReadAt = /* @__PURE__ */ new Map();
|
|
3960
3973
|
/** The profile availability reads from, resolved once per device. */
|
|
3961
3974
|
profileFor = /* @__PURE__ */ new Map();
|
|
3975
|
+
profilesFor = /* @__PURE__ */ new Map();
|
|
3976
|
+
profilesInFlight = /* @__PURE__ */ new Map();
|
|
3962
3977
|
/** `deviceId:profile` → whether the device records that profile, resolved
|
|
3963
3978
|
* once. Same stability trade as {@link profileFor}: a config change that
|
|
3964
3979
|
* adds a profile mid-session is picked up on restart, not on a timer. */
|
|
@@ -4241,6 +4256,31 @@ var CalendarIndex = class {
|
|
|
4241
4256
|
* reads at a time and they ALL ask for the profile before the first answer
|
|
4242
4257
|
* lands, so a cache alone let the pick run eight times. Parallelism found it.
|
|
4243
4258
|
*/
|
|
4259
|
+
/**
|
|
4260
|
+
* Every profile this camera holds footage in, across every root — cached
|
|
4261
|
+
* like the pick, because it is the same shallow read (D433).
|
|
4262
|
+
*
|
|
4263
|
+
* {@link availabilityProfileFor} answers with ONE; this says what the one
|
|
4264
|
+
* was chosen FROM, so a caller cannot mistake the timeline's single bar for
|
|
4265
|
+
* the whole archive.
|
|
4266
|
+
*/
|
|
4267
|
+
async profilesWithFootageFor(deviceId) {
|
|
4268
|
+
const cached = this.profilesFor.get(deviceId);
|
|
4269
|
+
if (cached !== void 0) return cached;
|
|
4270
|
+
const running = this.profilesInFlight.get(deviceId);
|
|
4271
|
+
if (running) return running;
|
|
4272
|
+
const resolve = (async () => {
|
|
4273
|
+
const found = await profilesWithFootage(this.deps.roots(), deviceId);
|
|
4274
|
+
this.profilesFor.set(deviceId, found);
|
|
4275
|
+
return found;
|
|
4276
|
+
})();
|
|
4277
|
+
this.profilesInFlight.set(deviceId, resolve);
|
|
4278
|
+
try {
|
|
4279
|
+
return await resolve;
|
|
4280
|
+
} finally {
|
|
4281
|
+
if (this.profilesInFlight.get(deviceId) === resolve) this.profilesInFlight.delete(deviceId);
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4244
4284
|
async availabilityProfileFor(deviceId) {
|
|
4245
4285
|
const cached = this.profileFor.get(deviceId);
|
|
4246
4286
|
if (cached !== void 0) return cached;
|
|
@@ -5885,6 +5925,8 @@ var TIME_RE = /time=(\d+):(\d+):(\d+(?:\.\d+)?)/;
|
|
|
5885
5925
|
* whatever ffmpeg build is installed.
|
|
5886
5926
|
*/
|
|
5887
5927
|
var EXPORT_MIN_PLAUSIBLE_BYTES = 1024;
|
|
5928
|
+
/** ffmpeg's own announcement of that pass — the only signal it gives. */
|
|
5929
|
+
var FASTSTART_RE = /Starting second pass: moving the moov atom/i;
|
|
5888
5930
|
/** Expected OUTPUT duration (seconds) for progress scaling. */
|
|
5889
5931
|
function expectedOutSeconds(rec) {
|
|
5890
5932
|
const rangeSec = Math.max(.001, (rec.toMs - rec.fromMs) / 1e3);
|
|
@@ -6194,6 +6236,7 @@ var ExportEngine = class {
|
|
|
6194
6236
|
args
|
|
6195
6237
|
} });
|
|
6196
6238
|
const stallMs = this.deps.stallMs ?? 9e4;
|
|
6239
|
+
const faststartStallMs = this.deps.faststartStallMs ?? 9e5;
|
|
6197
6240
|
return new Promise((resolve) => {
|
|
6198
6241
|
const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
|
|
6199
6242
|
this.active.set(id, proc);
|
|
@@ -6205,14 +6248,16 @@ var ExportEngine = class {
|
|
|
6205
6248
|
stallTimer = null;
|
|
6206
6249
|
}
|
|
6207
6250
|
};
|
|
6251
|
+
let budgetMs = stallMs;
|
|
6208
6252
|
const armStall = () => {
|
|
6209
6253
|
clearStall();
|
|
6254
|
+
const armedFor = budgetMs;
|
|
6210
6255
|
stallTimer = setTimeout(() => {
|
|
6211
|
-
stderrTail.push(`ffmpeg stalled: no progress for ${
|
|
6256
|
+
stderrTail.push(`ffmpeg stalled: no progress for ${armedFor}ms`);
|
|
6212
6257
|
try {
|
|
6213
6258
|
proc.kill("SIGKILL");
|
|
6214
6259
|
} catch {}
|
|
6215
|
-
},
|
|
6260
|
+
}, armedFor);
|
|
6216
6261
|
};
|
|
6217
6262
|
armStall();
|
|
6218
6263
|
proc.stderr?.on("data", (chunk) => {
|
|
@@ -6223,6 +6268,10 @@ var ExportEngine = class {
|
|
|
6223
6268
|
stderrTail.push(trimmed);
|
|
6224
6269
|
if (stderrTail.length > STDERR_TAIL_LINES$1) stderrTail.shift();
|
|
6225
6270
|
}
|
|
6271
|
+
if (FASTSTART_RE.test(line)) {
|
|
6272
|
+
budgetMs = faststartStallMs;
|
|
6273
|
+
armStall();
|
|
6274
|
+
}
|
|
6226
6275
|
const m = TIME_RE.exec(line);
|
|
6227
6276
|
if (m) {
|
|
6228
6277
|
armStall();
|
|
@@ -6866,6 +6915,81 @@ function buildRecordingDeviceSchema() {
|
|
|
6866
6915
|
}]
|
|
6867
6916
|
}] };
|
|
6868
6917
|
}
|
|
6918
|
+
//#endregion
|
|
6919
|
+
//#region src/recorder/addon/device-storage-usage.ts
|
|
6920
|
+
/**
|
|
6921
|
+
* Gap between the per-camera sum and the per-volume total that is worth
|
|
6922
|
+
* saying out loud, as a share of the volume total.
|
|
6923
|
+
*
|
|
6924
|
+
* The two are aggregations of the SAME hour rows over the same location set —
|
|
6925
|
+
* one folded per camera, one summed in SQL — so in a healthy recorder they
|
|
6926
|
+
* agree to the byte. A few tenths of a percent is a write landing between the
|
|
6927
|
+
* two reads; 97% (60.5 GB against 1.9 TB) is the defect this file closes,
|
|
6928
|
+
* rebuilt somewhere new.
|
|
6929
|
+
*/
|
|
6930
|
+
var USAGE_MISMATCH_SHARE = .02;
|
|
6931
|
+
/** Floor under {@link USAGE_MISMATCH_SHARE} so a nearly empty disk does not
|
|
6932
|
+
* warn on a single in-flight segment. */
|
|
6933
|
+
var USAGE_MISMATCH_FLOOR_BYTES = 1e9;
|
|
6934
|
+
/**
|
|
6935
|
+
* One row per camera the ARCHIVE holds footage for on `locationIds`, biggest
|
|
6936
|
+
* first (deviceId breaks a tie, so two consecutive calls compare).
|
|
6937
|
+
*
|
|
6938
|
+
* A node with no ledger REFUSES (D295 rule 3): empty rows, `read:
|
|
6939
|
+
* 'unavailable'`, and a line saying so. Falling back to the index would answer
|
|
6940
|
+
* the same question with the wrong quantity and look exactly like a correct
|
|
6941
|
+
* answer, which is how this shipped in the first place.
|
|
6942
|
+
*/
|
|
6943
|
+
function buildDeviceUsage(input) {
|
|
6944
|
+
if (input.archive === null) {
|
|
6945
|
+
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(",") } });
|
|
6946
|
+
return {
|
|
6947
|
+
devices: [],
|
|
6948
|
+
totalUsedBytes: 0,
|
|
6949
|
+
read: "unavailable"
|
|
6950
|
+
};
|
|
6951
|
+
}
|
|
6952
|
+
const devices = [];
|
|
6953
|
+
let totalUsedBytes = 0;
|
|
6954
|
+
for (const [deviceId, archived] of input.archive) {
|
|
6955
|
+
const resident = input.resident(deviceId);
|
|
6956
|
+
totalUsedBytes += archived.bytes;
|
|
6957
|
+
devices.push({
|
|
6958
|
+
deviceId,
|
|
6959
|
+
usedBytes: archived.bytes,
|
|
6960
|
+
oldestMs: archived.oldestMs,
|
|
6961
|
+
segments: archived.segments,
|
|
6962
|
+
residentBytes: resident.bytes,
|
|
6963
|
+
residentOldestMs: resident.oldestMs
|
|
6964
|
+
});
|
|
6965
|
+
}
|
|
6966
|
+
return {
|
|
6967
|
+
devices: devices.toSorted((a, b) => b.usedBytes - a.usedBytes || a.deviceId - b.deviceId),
|
|
6968
|
+
totalUsedBytes,
|
|
6969
|
+
read: "archive"
|
|
6970
|
+
};
|
|
6971
|
+
}
|
|
6972
|
+
/**
|
|
6973
|
+
* Say it when the per-camera sum and the per-volume total disagree.
|
|
6974
|
+
*
|
|
6975
|
+
* The "I saw N of M" rule of D295, applied to a REPORT: these two numbers are
|
|
6976
|
+
* the same rows counted two ways, so a gap means one of the two reads saw a
|
|
6977
|
+
* fraction of the archive — and the whole point of D418 is that a page which
|
|
6978
|
+
* shows both without comparing them lets a 97% gap read as a fact about the
|
|
6979
|
+
* disks.
|
|
6980
|
+
*/
|
|
6981
|
+
function warnOnUsageMismatch(logger, deviceBytes, volumeBytes, read) {
|
|
6982
|
+
if (read !== "archive") return;
|
|
6983
|
+
if (volumeBytes === 0) return;
|
|
6984
|
+
const gap = Math.abs(volumeBytes - deviceBytes);
|
|
6985
|
+
if (gap <= USAGE_MISMATCH_FLOOR_BYTES) return;
|
|
6986
|
+
if (gap <= volumeBytes * USAGE_MISMATCH_SHARE) return;
|
|
6987
|
+
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: {
|
|
6988
|
+
deviceBytes,
|
|
6989
|
+
volumeBytes,
|
|
6990
|
+
gap
|
|
6991
|
+
} });
|
|
6992
|
+
}
|
|
6869
6993
|
/** The placement-store key for one (camera, profile). Mirrors `assignmentKey`
|
|
6870
6994
|
* in `placement-store.ts`; kept local so this module stays dependency-free. */
|
|
6871
6995
|
function sourceKey(source) {
|
|
@@ -7325,13 +7449,15 @@ function buildRecordingProvider(deps) {
|
|
|
7325
7449
|
* and by the batch alike — the batch collapses the TRANSPORT, and sharing the
|
|
7326
7450
|
* function is what makes that literally true instead of aspirational.
|
|
7327
7451
|
*/
|
|
7328
|
-
const availabilityFor = async (deviceId, fromMs, toMs) => {
|
|
7452
|
+
const availabilityFor = async (deviceId, fromMs, toMs, requestedProfile) => {
|
|
7329
7453
|
const calendar = deps.calendar;
|
|
7330
7454
|
if (calendar) {
|
|
7331
|
-
const
|
|
7455
|
+
const profilesWithFootage = await calendar.profilesWithFootageFor(deviceId);
|
|
7456
|
+
const profile = await calendar.servedProfileFor(deviceId, requestedProfile);
|
|
7332
7457
|
if (profile === null) return {
|
|
7333
7458
|
deviceId,
|
|
7334
|
-
ranges: []
|
|
7459
|
+
ranges: [],
|
|
7460
|
+
profilesWithFootage: [...profilesWithFootage]
|
|
7335
7461
|
};
|
|
7336
7462
|
const ranges = [];
|
|
7337
7463
|
for (const r of await calendar.rangesIn(deviceId, profile, fromMs, toMs)) ranges.push({
|
|
@@ -7341,16 +7467,20 @@ function buildRecordingProvider(deps) {
|
|
|
7341
7467
|
});
|
|
7342
7468
|
return {
|
|
7343
7469
|
deviceId,
|
|
7344
|
-
ranges
|
|
7470
|
+
ranges,
|
|
7471
|
+
profilesWithFootage: [...profilesWithFootage]
|
|
7345
7472
|
};
|
|
7346
7473
|
}
|
|
7474
|
+
const profiles = new Set(deps.index.segments(deviceId).map((s) => s.profile));
|
|
7475
|
+
const asked = requestedProfile !== void 0 && profiles.has(requestedProfile);
|
|
7347
7476
|
return {
|
|
7348
7477
|
deviceId,
|
|
7349
|
-
ranges: [...
|
|
7478
|
+
ranges: [...profiles].filter((p) => !asked || p === requestedProfile).flatMap((profile) => deps.index.ranges(deviceId, profile, fromMs, toMs, RANGE_MERGE_GAP_MS).map((r) => ({
|
|
7350
7479
|
profile,
|
|
7351
7480
|
startMs: r.startMs,
|
|
7352
7481
|
endMs: r.endMs
|
|
7353
|
-
})))
|
|
7482
|
+
}))),
|
|
7483
|
+
profilesWithFootage: [...profiles].sort()
|
|
7354
7484
|
};
|
|
7355
7485
|
};
|
|
7356
7486
|
/** The per-camera day read. Same rule as {@link availabilityFor}: one body. */
|
|
@@ -7380,7 +7510,7 @@ function buildRecordingProvider(deps) {
|
|
|
7380
7510
|
getStatus: async ({ deviceId }) => {
|
|
7381
7511
|
return statusFor(deps, deviceId, await loadDeviceConfig(deps.configStore, deviceId));
|
|
7382
7512
|
},
|
|
7383
|
-
getAvailability: ({ deviceId, fromMs, toMs }) => availabilityFor(deviceId, fromMs, toMs),
|
|
7513
|
+
getAvailability: ({ deviceId, fromMs, toMs, profile }) => availabilityFor(deviceId, fromMs, toMs, profile),
|
|
7384
7514
|
/**
|
|
7385
7515
|
* `getAvailability` for a SET of cameras. Same body per camera, run
|
|
7386
7516
|
* concurrently; only the transport collapses.
|
|
@@ -7394,11 +7524,12 @@ function buildRecordingProvider(deps) {
|
|
|
7394
7524
|
refuseOverCap("getAvailabilityBatch", deviceIds.length);
|
|
7395
7525
|
return Promise.all(deviceIds.map(async (deviceId) => {
|
|
7396
7526
|
try {
|
|
7397
|
-
const { ranges } = await availabilityFor(deviceId, fromMs, toMs);
|
|
7527
|
+
const { ranges, profilesWithFootage } = await availabilityFor(deviceId, fromMs, toMs);
|
|
7398
7528
|
return {
|
|
7399
7529
|
deviceId,
|
|
7400
7530
|
read: "read",
|
|
7401
|
-
ranges
|
|
7531
|
+
ranges,
|
|
7532
|
+
profilesWithFootage
|
|
7402
7533
|
};
|
|
7403
7534
|
} catch (err) {
|
|
7404
7535
|
deps.logger.warn("recording availability could not be read", {
|
|
@@ -7412,7 +7543,8 @@ function buildRecordingProvider(deps) {
|
|
|
7412
7543
|
return {
|
|
7413
7544
|
deviceId,
|
|
7414
7545
|
read: "unreadable",
|
|
7415
|
-
ranges: []
|
|
7546
|
+
ranges: [],
|
|
7547
|
+
profilesWithFootage: []
|
|
7416
7548
|
};
|
|
7417
7549
|
}
|
|
7418
7550
|
}));
|
|
@@ -7453,18 +7585,15 @@ function buildRecordingProvider(deps) {
|
|
|
7453
7585
|
getPlaybackManifest: ({ deviceId, fromMs, toMs }) => buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs),
|
|
7454
7586
|
getStorageUsage: async () => {
|
|
7455
7587
|
const locations = deps.locations();
|
|
7456
|
-
const
|
|
7457
|
-
const
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
oldestMs: accounting.oldestMs
|
|
7466
|
-
});
|
|
7467
|
-
}
|
|
7588
|
+
const locationIds = new Set(locations.map((l) => l.id));
|
|
7589
|
+
const deviceUsage = buildDeviceUsage({
|
|
7590
|
+
locationIds,
|
|
7591
|
+
archive: deps.archiveDeviceUsage?.(locationIds) ?? null,
|
|
7592
|
+
resident: (deviceId) => deps.index.accounting(deviceId),
|
|
7593
|
+
logger: deps.logger
|
|
7594
|
+
});
|
|
7595
|
+
const devices = deviceUsage.devices;
|
|
7596
|
+
const totalUsedBytes = deviceUsage.totalUsedBytes;
|
|
7468
7597
|
const byPhysical = /* @__PURE__ */ new Map();
|
|
7469
7598
|
for (const loc of locations) {
|
|
7470
7599
|
const key = physicalRootKey(loc.root);
|
|
@@ -7493,11 +7622,12 @@ function buildRecordingProvider(deps) {
|
|
|
7493
7622
|
totalBytes: cap.totalBytes
|
|
7494
7623
|
};
|
|
7495
7624
|
}));
|
|
7496
|
-
|
|
7625
|
+
warnOnUsageMismatch(deps.logger, totalUsedBytes, locationUsages.reduce((sum, l) => sum + l.usedBytes, 0), deviceUsage.read);
|
|
7497
7626
|
return {
|
|
7498
7627
|
nodeId: deps.nodeId,
|
|
7499
7628
|
totalUsedBytes,
|
|
7500
|
-
devices,
|
|
7629
|
+
devices: [...devices],
|
|
7630
|
+
devicesRead: deviceUsage.read,
|
|
7501
7631
|
locations: locationUsages
|
|
7502
7632
|
};
|
|
7503
7633
|
},
|
|
@@ -8391,6 +8521,14 @@ var DurableLedger = class DurableLedger {
|
|
|
8391
8521
|
//#endregion
|
|
8392
8522
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
8393
8523
|
var HOUR_MS$3 = 36e5;
|
|
8524
|
+
/** `Math.min` where a missing left operand means "nothing yet", not `NaN`. */
|
|
8525
|
+
function minOf(a, b) {
|
|
8526
|
+
return a === null ? b : Math.min(a, b);
|
|
8527
|
+
}
|
|
8528
|
+
/** `Math.max` with the same rule as {@link minOf}. */
|
|
8529
|
+
function maxOf(a, b) {
|
|
8530
|
+
return a === null ? b : Math.max(a, b);
|
|
8531
|
+
}
|
|
8394
8532
|
var macrotask$1 = () => new Promise((resolve) => {
|
|
8395
8533
|
setImmediate(resolve);
|
|
8396
8534
|
});
|
|
@@ -9041,7 +9179,7 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9041
9179
|
*
|
|
9042
9180
|
* The reader repairs the durable index it just reconstructed, so the next
|
|
9043
9181
|
* cold read of this hour is a point read instead of another `readdir`
|
|
9044
|
-
* (`docs/design/2026-08-29-
|
|
9182
|
+
* (`docs/design/2026-08-29-reference-nvr-recording-model.md` §4.2). Two
|
|
9045
9183
|
* constraints, both enforced by the caller and re-stated here because
|
|
9046
9184
|
* getting either wrong turns a READ into durable damage: the hour must be
|
|
9047
9185
|
* CLOSED (a live hour's directory races the writer, so a walk of it is a
|
|
@@ -9274,6 +9412,53 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9274
9412
|
for (const row of this.ledger.snapshot()) if (locationIds.has(row.locationId)) out.add(row.deviceId);
|
|
9275
9413
|
return out;
|
|
9276
9414
|
}
|
|
9415
|
+
/**
|
|
9416
|
+
* Bytes / segments / oldest / newest per CAMERA on these locations — the
|
|
9417
|
+
* per-camera half of the storage page, answered by the ARCHIVE (D418).
|
|
9418
|
+
*
|
|
9419
|
+
* ## Why the T1 fold and not `census({ deviceId })` per camera
|
|
9420
|
+
*
|
|
9421
|
+
* `census` is the right authority for a SCOPE, and it cannot express this
|
|
9422
|
+
* scope: {@link SegmentHourLedger.predicate} takes ONE `locationId`, so
|
|
9423
|
+
* "camera 615 across the three resolvable volumes" is three aggregates, and
|
|
9424
|
+
* `census({ deviceId })` alone would silently count footage stranded on a
|
|
9425
|
+
* location id that no longer resolves — bytes no volume row on the same page
|
|
9426
|
+
* accounts for. Per camera per volume that is `cameras × volumes` SQL
|
|
9427
|
+
* aggregates for one operator click, and it grows with the fleet.
|
|
9428
|
+
*
|
|
9429
|
+
* The T1 hour summary answers all of them in ONE pass with no I/O, exactly
|
|
9430
|
+
* as {@link devicesWithFootage} and {@link profilesWithFootage} already do,
|
|
9431
|
+
* and it is the same rows `accountingForLocations` sums in SQL — the mirror
|
|
9432
|
+
* is write-behind, so it is never BEHIND the store. The cost is one pass
|
|
9433
|
+
* over the hour rows the mirror holds — O(writers × profiles × hours), ~19 k
|
|
9434
|
+
* on the live hub (see {@link REPAIR_MAX_ROWS}) against 98 445 segments,
|
|
9435
|
+
* i.e. it does NOT scale with the segment count — and it is capped for good
|
|
9436
|
+
* by {@link SEGMENT_HOUR_SUMMARY_LIMIT}, the mirror's own ceiling.
|
|
9437
|
+
*
|
|
9438
|
+
* ## What an empty answer means
|
|
9439
|
+
*
|
|
9440
|
+
* The same thing it means for {@link devicesWithFootage}: "the mirror knows
|
|
9441
|
+
* no hour here" — which is also what a ledger whose load failed reports. The
|
|
9442
|
+
* caller must not render that as "no camera has footage"; `getStorageUsage`
|
|
9443
|
+
* cross-checks the per-camera sum against the volume aggregate and SAYS so
|
|
9444
|
+
* when the two disagree.
|
|
9445
|
+
*/
|
|
9446
|
+
usageByDevice(locationIds) {
|
|
9447
|
+
const out = /* @__PURE__ */ new Map();
|
|
9448
|
+
if (locationIds.size === 0) return out;
|
|
9449
|
+
for (const row of this.ledger.snapshot()) {
|
|
9450
|
+
if (!locationIds.has(row.locationId)) continue;
|
|
9451
|
+
const empty = row.segments === 0;
|
|
9452
|
+
const prev = out.get(row.deviceId);
|
|
9453
|
+
out.set(row.deviceId, {
|
|
9454
|
+
bytes: (prev?.bytes ?? 0) + row.bytes,
|
|
9455
|
+
segments: (prev?.segments ?? 0) + row.segments,
|
|
9456
|
+
oldestMs: empty ? prev?.oldestMs ?? null : minOf(prev?.oldestMs ?? null, row.minStartMs),
|
|
9457
|
+
newestMs: empty ? prev?.newestMs ?? null : maxOf(prev?.newestMs ?? null, row.maxStartMs)
|
|
9458
|
+
});
|
|
9459
|
+
}
|
|
9460
|
+
return out;
|
|
9461
|
+
}
|
|
9277
9462
|
async oldestSegmentsScoped(locationIds, deviceIds, targetBytes) {
|
|
9278
9463
|
const ids = [...locationIds];
|
|
9279
9464
|
if (ids.length === 0 || targetBytes <= 0) return {
|
|
@@ -14533,7 +14718,7 @@ var RecordingController = class {
|
|
|
14533
14718
|
/**
|
|
14534
14719
|
* The ADAPTIVE attach bound. A transient `spawn ffmpeg` failure (EAGAIN) is
|
|
14535
14720
|
* the host saying it cannot fork right now, and the answer is to attach
|
|
14536
|
-
* FEWER cameras at once — not to serialise them, which
|
|
14721
|
+
* FEWER cameras at once — not to serialise them, which removed
|
|
14537
14722
|
* for good reason. Every bounded-parallel attach in this file reads it.
|
|
14538
14723
|
*/
|
|
14539
14724
|
spawnPressure;
|
|
@@ -16038,7 +16223,7 @@ function selectRedundantSegments(segs) {
|
|
|
16038
16223
|
* The pruner that used to remove it, `runRedundancyJanitor`, was reachable ONLY
|
|
16039
16224
|
* from `runFullArchiveWalk`, which was reachable only from
|
|
16040
16225
|
* `scheduleDeferredFullWalk`, which had no caller at all — so on 2026-08-27
|
|
16041
|
-
*
|
|
16226
|
+
* all three were deleted together as dead code. That was
|
|
16042
16227
|
* correct about the walk and left the system with **no duplicate pruner
|
|
16043
16228
|
* whatsoever**. The precedent for letting this class of waste run unattended is
|
|
16044
16229
|
* the 179 GB of stranded staging segments in `staging-reconcile.ts`.
|
|
@@ -17449,6 +17634,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17449
17634
|
refreshLocations: () => this.refreshLocations(),
|
|
17450
17635
|
capacity: (root) => this.locationCapacity(root),
|
|
17451
17636
|
archiveAccounting: (locationIds) => this.segmentHours?.accountingForLocations(locationIds) ?? Promise.resolve(EMPTY_ARCHIVE_ACCOUNTING),
|
|
17637
|
+
archiveDeviceUsage: (locationIds) => this.segmentHours?.usageByDevice(locationIds) ?? null,
|
|
17452
17638
|
archive: () => this.segmentHours,
|
|
17453
17639
|
ledgerWalk: this.ledgerWalkReconciler(),
|
|
17454
17640
|
hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
|