@camstack/addon-pipeline 1.2.96 → 1.2.98
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/{addon-utils-B6usm-YO.js → addon-utils-eUS6n_Zj.js} +1 -1
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +3 -3
- package/dist/{dist-w9QWDvWn.js → dist-dm3t4BOt.js} +29 -3
- package/dist/{dist-LIWfUNC_.mjs → dist-gXdWP96z.mjs} +29 -3
- package/dist/{event-loop-stall-monitor-DndI0S2s.js → event-loop-stall-monitor-Lki2_mYY.js} +1 -1
- package/dist/{event-loop-stall-monitor-BDC8qURn.mjs → event-loop-stall-monitor-TnQ21mu0.mjs} +1 -1
- package/dist/{lazy-sharp-BVqBydNU.js → lazy-sharp-oYppDIZR.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +51 -4
- package/dist/pipeline-runner/index.mjs +50 -3
- package/dist/{process-memory-k45KJaKH.mjs → process-memory-CKWh609B.mjs} +1 -1
- package/dist/{process-memory-C6zWIgos.js → process-memory-vkFVOdkV.js} +1 -1
- package/dist/recorder/index.js +296 -48
- package/dist/recorder/index.mjs +295 -47
- 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 +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CJWMZMhQ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B7ERJocD.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-zT-cYUdF.mjs +26 -0
- package/dist/stream-broker/{hostInit-CGIo3d4K.mjs → hostInit-Cm2rl8-e.mjs} +3 -3
- package/dist/stream-broker/index.js +21 -6
- package/dist/stream-broker/index.mjs +21 -6
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DqrXmX0g.js → worker-protocol-BrlrMxXu.js} +1 -1
- package/dist/{worker-protocol-DTe7Ntat.mjs → worker-protocol-DJWdPLKF.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-D2WOTeYi.mjs +0 -26
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as recordingExportCapability, C as RecordingConfigSchema, Ct as nodePin, Et as selectAssignedProfileSlots, Ft as object, It as record, L as deriveRecordingMode, Lt as string, Pt as number, Q as recordingCapability, S as RECORDING_EXPORT_MAX_READ_BYTES, _t as DeviceType, ct as errMsg, f as EVENT_PAD_MS, it as storageEvictableCapability, kt as array, l as DEFAULT_EVENTS_BAND_BUFFER_SEC, m as ExportRecordSchema, mt as BaseAddon, nt as resolveRecordingProfiles, v as OpsLogEntrySchema, yt as hydrateSchema, zt as EventCategory } from "../dist-
|
|
1
|
+
import { $ as recordingExportCapability, C as RecordingConfigSchema, Ct as nodePin, Et as selectAssignedProfileSlots, Ft as object, It as record, L as deriveRecordingMode, Lt as string, Pt as number, Q as recordingCapability, S as RECORDING_EXPORT_MAX_READ_BYTES, _t as DeviceType, ct as errMsg, f as EVENT_PAD_MS, it as storageEvictableCapability, kt as array, l as DEFAULT_EVENTS_BAND_BUFFER_SEC, m as ExportRecordSchema, mt as BaseAddon, nt as resolveRecordingProfiles, v as OpsLogEntrySchema, yt as hydrateSchema, zt as EventCategory } from "../dist-gXdWP96z.mjs";
|
|
2
2
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
3
3
|
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-A2S9D7pu.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -4332,9 +4332,15 @@ function buildRecordingProvider(deps) {
|
|
|
4332
4332
|
const first = group[0];
|
|
4333
4333
|
const cap = await deps.capacity(first.root);
|
|
4334
4334
|
const ids = group.map((l) => l.id);
|
|
4335
|
+
if (cap.error !== void 0) deps.logger.warn("recorder: volume capacity unavailable (statfs failed)", { meta: {
|
|
4336
|
+
locationId: first.id,
|
|
4337
|
+
root: first.root,
|
|
4338
|
+
error: cap.error
|
|
4339
|
+
} });
|
|
4335
4340
|
return {
|
|
4336
4341
|
locationId: first.id,
|
|
4337
4342
|
locationIds: ids,
|
|
4343
|
+
root: first.root,
|
|
4338
4344
|
usedBytes: deps.index.accountingForLocations(new Set(ids)).bytes,
|
|
4339
4345
|
availableBytes: cap.availableBytes,
|
|
4340
4346
|
totalBytes: cap.totalBytes
|
|
@@ -5568,13 +5574,20 @@ async function probeLocationVolume(location, rememberedVolumeId, deps) {
|
|
|
5568
5574
|
}
|
|
5569
5575
|
/**
|
|
5570
5576
|
* Where to write instead when a location fails its volume probe: the normal
|
|
5571
|
-
* placement chain over every
|
|
5572
|
-
* sibling of the same type; with
|
|
5573
|
-
* a shouting recorder beats a stopped camera,
|
|
5574
|
-
* one outcome no operator asked for.
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5577
|
+
* placement chain over every location NOT already known unusable. With N
|
|
5578
|
+
* locations this is a healthy sibling of the same type; with none left it is
|
|
5579
|
+
* the unusable location itself — a shouting recorder beats a stopped camera,
|
|
5580
|
+
* and refusing to record is the one outcome no operator asked for.
|
|
5581
|
+
*
|
|
5582
|
+
* `alreadyUnusable` is not an optimisation. Without it the search happily
|
|
5583
|
+
* returned a sibling that had ALSO failed its probe, so with two broken disks
|
|
5584
|
+
* every attach handed back the other one and the stored assignment flipped
|
|
5585
|
+
* A→B→A→B forever — one durable write and one audit row per attach, per camera,
|
|
5586
|
+
* per profile. Observed live on 2026-08-19 (the UD disk did not come back after
|
|
5587
|
+
* a reboot): 716 EACCES/min, with the ops-log ring flooded by the flapping.
|
|
5588
|
+
*/
|
|
5589
|
+
function failoverForUnusableLocation(unusable, locations, profile, alreadyUnusable = /* @__PURE__ */ new Set()) {
|
|
5590
|
+
const siblings = locations.filter((l) => l.id !== unusable.id && !alreadyUnusable.has(l.id));
|
|
5578
5591
|
try {
|
|
5579
5592
|
return resolvePlacement(siblings, profile);
|
|
5580
5593
|
} catch {
|
|
@@ -5625,6 +5638,13 @@ var PlacementService = class {
|
|
|
5625
5638
|
verified = /* @__PURE__ */ new Set();
|
|
5626
5639
|
/** Locations the probe declared unusable — re-probed after a change. */
|
|
5627
5640
|
unusable = /* @__PURE__ */ new Set();
|
|
5641
|
+
/** Cameras that have already announced "every location is unusable". One line
|
|
5642
|
+
* per camera per episode: the fault is asked per-camera, and repeating it per
|
|
5643
|
+
* attach is the noise this state exists to end. */
|
|
5644
|
+
allUnusableReported = /* @__PURE__ */ new Set();
|
|
5645
|
+
/** When the current all-unusable episode last (re-)probed. `null` ⇒ no
|
|
5646
|
+
* episode in progress. */
|
|
5647
|
+
allUnusableProbedAt = null;
|
|
5628
5648
|
/** The fair tie-break the PURE planner defers to (D37). Advisory state, and
|
|
5629
5649
|
* consulted only for a tie the capacity signal cannot separate. */
|
|
5630
5650
|
rotation = new PlacementRotation();
|
|
@@ -5634,6 +5654,12 @@ var PlacementService = class {
|
|
|
5634
5654
|
get probeDeps() {
|
|
5635
5655
|
return this.deps.probe ?? fsVolumeProbeDeps;
|
|
5636
5656
|
}
|
|
5657
|
+
get now() {
|
|
5658
|
+
return this.deps.now?.() ?? Date.now();
|
|
5659
|
+
}
|
|
5660
|
+
get reprobeMs() {
|
|
5661
|
+
return this.deps.reprobeMs ?? 6e4;
|
|
5662
|
+
}
|
|
5637
5663
|
/**
|
|
5638
5664
|
* Forget every probe verdict. Called whenever the location set is re-resolved
|
|
5639
5665
|
* (rescan, storage migration) — a remount is exactly the event the probe
|
|
@@ -5642,6 +5668,8 @@ var PlacementService = class {
|
|
|
5642
5668
|
onLocationsChanged() {
|
|
5643
5669
|
this.verified.clear();
|
|
5644
5670
|
this.unusable.clear();
|
|
5671
|
+
this.allUnusableReported.clear();
|
|
5672
|
+
this.allUnusableProbedAt = null;
|
|
5645
5673
|
}
|
|
5646
5674
|
/**
|
|
5647
5675
|
* Resolve the location for one (camera, profile) at an attach boundary.
|
|
@@ -5650,12 +5678,92 @@ var PlacementService = class {
|
|
|
5650
5678
|
*/
|
|
5651
5679
|
async place(deviceId, profile) {
|
|
5652
5680
|
const pool = this.pool();
|
|
5681
|
+
this.reprobeAfterBackoff(pool);
|
|
5653
5682
|
const resolved = await this.assignedLocation(deviceId, profile, pool) ?? resolvePlacement(pool, profile);
|
|
5654
5683
|
const usable = await this.ensureUsable(resolved, pool, profile, deviceId);
|
|
5684
|
+
if (this.everyLocationUnusable(pool)) {
|
|
5685
|
+
this.reportEveryLocationUnusable(deviceId, profile, resolved, pool);
|
|
5686
|
+
return resolved;
|
|
5687
|
+
}
|
|
5688
|
+
this.allUnusableReported.delete(deviceId);
|
|
5655
5689
|
await this.recordDecision(deviceId, profile, usable, resolved.id === usable.id);
|
|
5656
5690
|
return usable;
|
|
5657
5691
|
}
|
|
5658
5692
|
/**
|
|
5693
|
+
* Has the marker probe declared this location's volume NOT the one we last
|
|
5694
|
+
* wrote to?
|
|
5695
|
+
*
|
|
5696
|
+
* Read by the segment-hour ledger: a walk over a root whose volume failed its
|
|
5697
|
+
* probe found nothing because nothing is mounted there, and that must never be
|
|
5698
|
+
* allowed to delete durable rows (D49). Unprobed ⇒ usable, which is honest —
|
|
5699
|
+
* the probe runs at the first writer attach, so at boot it has usually not run
|
|
5700
|
+
* at all, and the ledger's own magnitude guard covers that window.
|
|
5701
|
+
*/
|
|
5702
|
+
isLocationUsable(locationId) {
|
|
5703
|
+
return !this.unusable.has(locationId);
|
|
5704
|
+
}
|
|
5705
|
+
/** True when the pool is non-empty and the probe has failed EVERY member. */
|
|
5706
|
+
everyLocationUnusable(pool) {
|
|
5707
|
+
return pool.length > 0 && pool.every((location) => this.unusable.has(location.id));
|
|
5708
|
+
}
|
|
5709
|
+
/**
|
|
5710
|
+
* Announce the terminal state — ONCE per camera per episode.
|
|
5711
|
+
*
|
|
5712
|
+
* Before this, two unusable locations produced an infinite failover
|
|
5713
|
+
* ping-pong: `failoverForUnusableLocation` did not consult the unusable set,
|
|
5714
|
+
* so each attach handed back the other broken disk, `recordDecision` saw a
|
|
5715
|
+
* changed assignment and wrote the store plus an audit row, and the writer
|
|
5716
|
+
* hit EACCES on a path that was never going to work. Measured on the live hub
|
|
5717
|
+
* on 2026-08-19 with the UD disk unmounted: 716 EACCES/min.
|
|
5718
|
+
*
|
|
5719
|
+
* Recording is NOT refused. With a single location this state is reached the
|
|
5720
|
+
* moment that one location fails, and the recorder keeps attaching to it on
|
|
5721
|
+
* purpose — a shouting recorder beats a stopped camera. What changes is that
|
|
5722
|
+
* it shouts once instead of once per attach, and writes nothing durable while
|
|
5723
|
+
* shouting.
|
|
5724
|
+
*/
|
|
5725
|
+
reportEveryLocationUnusable(deviceId, profile, target, pool) {
|
|
5726
|
+
this.allUnusableProbedAt ??= this.now;
|
|
5727
|
+
if (this.allUnusableReported.has(deviceId)) return;
|
|
5728
|
+
this.allUnusableReported.add(deviceId);
|
|
5729
|
+
this.deps.logger.warn("recorder placement: EVERY recordings location failed its volume probe — the assignment is FROZEN on its current target and nothing more is written until a disk comes back", {
|
|
5730
|
+
tags: { deviceId },
|
|
5731
|
+
meta: {
|
|
5732
|
+
profile,
|
|
5733
|
+
targetLocationId: target.id,
|
|
5734
|
+
root: target.root,
|
|
5735
|
+
locationIds: pool.map((location) => location.id),
|
|
5736
|
+
reprobeMs: this.reprobeMs
|
|
5737
|
+
}
|
|
5738
|
+
});
|
|
5739
|
+
}
|
|
5740
|
+
/**
|
|
5741
|
+
* While every location is unusable, re-probe them all — at most once per
|
|
5742
|
+
* {@link ALL_UNUSABLE_REPROBE_MS}. This is the backoff: without it the probe
|
|
5743
|
+
* memo would hold for the life of the process (only `onLocationsChanged`
|
|
5744
|
+
* clears it) and a disk remounted by an operator would need an addon restart
|
|
5745
|
+
* to be noticed.
|
|
5746
|
+
*/
|
|
5747
|
+
reprobeAfterBackoff(pool) {
|
|
5748
|
+
if (!this.everyLocationUnusable(pool)) {
|
|
5749
|
+
this.allUnusableProbedAt = null;
|
|
5750
|
+
return;
|
|
5751
|
+
}
|
|
5752
|
+
const now = this.now;
|
|
5753
|
+
if (this.allUnusableProbedAt === null) {
|
|
5754
|
+
this.allUnusableProbedAt = now;
|
|
5755
|
+
return;
|
|
5756
|
+
}
|
|
5757
|
+
if (now - this.allUnusableProbedAt < this.reprobeMs) return;
|
|
5758
|
+
this.allUnusableProbedAt = now;
|
|
5759
|
+
this.deps.logger.info("recorder placement: re-probing every storage location after backoff — a disk that came back is picked up here", { meta: {
|
|
5760
|
+
locationIds: pool.map((location) => location.id),
|
|
5761
|
+
backoffMs: this.reprobeMs
|
|
5762
|
+
} });
|
|
5763
|
+
this.unusable.clear();
|
|
5764
|
+
this.verified.clear();
|
|
5765
|
+
}
|
|
5766
|
+
/**
|
|
5659
5767
|
* The locations placement may write to: operator-ENABLED and alias-guarded.
|
|
5660
5768
|
* A location the operator has not opted in is still read, swept and drained —
|
|
5661
5769
|
* it is simply never a write target. Creating a disk must not start writing
|
|
@@ -5804,13 +5912,13 @@ var PlacementService = class {
|
|
|
5804
5912
|
* ONCE per location per location-set generation — it is a claim, not a poll.
|
|
5805
5913
|
*/
|
|
5806
5914
|
async ensureUsable(chosen, locations, profile, deviceId) {
|
|
5807
|
-
if (this.unusable.has(chosen.id)) return failoverForUnusableLocation(chosen, locations, profile);
|
|
5915
|
+
if (this.unusable.has(chosen.id)) return failoverForUnusableLocation(chosen, locations, profile, this.unusable);
|
|
5808
5916
|
if (this.verified.has(chosen.id)) return chosen;
|
|
5809
5917
|
const result = await probeLocationVolume(chosen, (await readPlacementState(this.deps.state)).volumeIds[chosen.id] ?? null, this.probeDeps);
|
|
5810
5918
|
if (result.outcome === "mismatch") {
|
|
5811
5919
|
this.unusable.add(chosen.id);
|
|
5812
5920
|
this.reportMountFault(result, deviceId);
|
|
5813
|
-
return failoverForUnusableLocation(chosen, locations, profile);
|
|
5921
|
+
return failoverForUnusableLocation(chosen, locations, profile, this.unusable);
|
|
5814
5922
|
}
|
|
5815
5923
|
if (result.outcome === "unreadable") {
|
|
5816
5924
|
this.deps.logger.warn("recorder placement: volume probe unreadable — assignment unchanged", {
|
|
@@ -7924,6 +8032,34 @@ function makeBrokerCall(handle) {
|
|
|
7924
8032
|
return (fn) => handle.call(fn, BROKER_READY_TIMEOUT_MS);
|
|
7925
8033
|
}
|
|
7926
8034
|
//#endregion
|
|
8035
|
+
//#region src/recorder/addon/location-capacity.ts
|
|
8036
|
+
/** Human-readable failure text for a thrown value of unknown shape. */
|
|
8037
|
+
function reasonOf(err) {
|
|
8038
|
+
return err instanceof Error ? err.message : String(err);
|
|
8039
|
+
}
|
|
8040
|
+
/**
|
|
8041
|
+
* Free + total bytes on the volume backing `root`.
|
|
8042
|
+
*
|
|
8043
|
+
* Never throws: an unreachable volume is one degraded ROW, not a failed
|
|
8044
|
+
* `getStorageUsage` — a NAS that stops answering must not blank the capacity of
|
|
8045
|
+
* every other disk on the node.
|
|
8046
|
+
*/
|
|
8047
|
+
async function readLocationCapacity(root, statfs) {
|
|
8048
|
+
try {
|
|
8049
|
+
const st = await statfs(root);
|
|
8050
|
+
return {
|
|
8051
|
+
availableBytes: st.bavail * st.bsize,
|
|
8052
|
+
totalBytes: st.blocks * st.bsize
|
|
8053
|
+
};
|
|
8054
|
+
} catch (err) {
|
|
8055
|
+
return {
|
|
8056
|
+
availableBytes: null,
|
|
8057
|
+
totalBytes: null,
|
|
8058
|
+
error: reasonOf(err)
|
|
8059
|
+
};
|
|
8060
|
+
}
|
|
8061
|
+
}
|
|
8062
|
+
//#endregion
|
|
7927
8063
|
//#region src/durable/durable-ledger.ts
|
|
7928
8064
|
/** Default reseed cap — every current consumer's row set is installation-bounded. */
|
|
7929
8065
|
var DEFAULT_LOAD_LIMIT = 1e5;
|
|
@@ -8211,6 +8347,17 @@ var RECORDING_SEGMENT_HOURS_INDEXES = [{
|
|
|
8211
8347
|
name: "idx_recorder_segment_hours_hour",
|
|
8212
8348
|
columns: ["hourStartMs"]
|
|
8213
8349
|
}];
|
|
8350
|
+
/**
|
|
8351
|
+
* The largest share of a device's held hours a single walk may delete.
|
|
8352
|
+
*
|
|
8353
|
+
* On 2026-08-19 the recorder respawned while `/recordings` was an empty tmpfs —
|
|
8354
|
+
* the UD disk had not mounted — and one walk took the ledger from 873 hours to
|
|
8355
|
+
* 32 (a 96% drop). The ledger is precisely what lets the NEXT boot skip a
|
|
8356
|
+
* ~400 s recursive walk, so a single bad walk taxed every boot after it. A
|
|
8357
|
+
* retention sweep does not delete half a camera's archive in one pass; a
|
|
8358
|
+
* `readdir` over an unmounted root deletes all of it.
|
|
8359
|
+
*/
|
|
8360
|
+
var LEDGER_PRUNE_MAX_SHARE = .5;
|
|
8214
8361
|
function hourStartMs(startMs) {
|
|
8215
8362
|
return Math.floor(startMs / HOUR_MS$1) * HOUR_MS$1;
|
|
8216
8363
|
}
|
|
@@ -8284,8 +8431,10 @@ var SPEC = {
|
|
|
8284
8431
|
var SegmentHourLedger = class {
|
|
8285
8432
|
ledger;
|
|
8286
8433
|
logger;
|
|
8434
|
+
isLocationUsable;
|
|
8287
8435
|
constructor(deps) {
|
|
8288
8436
|
this.logger = deps.logger;
|
|
8437
|
+
this.isLocationUsable = deps.isLocationUsable ?? (() => true);
|
|
8289
8438
|
this.ledger = new DurableLedger({
|
|
8290
8439
|
spec: SPEC,
|
|
8291
8440
|
store: deps.store,
|
|
@@ -8375,6 +8524,10 @@ var SegmentHourLedger = class {
|
|
|
8375
8524
|
*
|
|
8376
8525
|
* Devices not in `deviceIds` are left untouched (a partial walk must not
|
|
8377
8526
|
* prune cameras it did not look at).
|
|
8527
|
+
*
|
|
8528
|
+
* And a walk this ledger does not TRUST prunes nothing at all — see
|
|
8529
|
+
* {@link refusedPrunes}. What such a walk FOUND is still real, so its paths
|
|
8530
|
+
* are unioned in; what it did not find decides nothing (D49).
|
|
8378
8531
|
*/
|
|
8379
8532
|
async reconcileFromIndex(index, deviceIds, nowMs) {
|
|
8380
8533
|
const walked = new Set(deviceIds);
|
|
@@ -8396,15 +8549,17 @@ var SegmentHourLedger = class {
|
|
|
8396
8549
|
paths: [...existing.paths, seg.path]
|
|
8397
8550
|
});
|
|
8398
8551
|
}
|
|
8552
|
+
const refused = this.refusedPrunes(walked, fromIndex);
|
|
8399
8553
|
for (const row of this.ledger.snapshot()) {
|
|
8400
8554
|
if (!walked.has(row.deviceId)) continue;
|
|
8401
8555
|
const disk = fromIndex.get(row.key);
|
|
8556
|
+
const additiveOnly = row.hourStartMs === currentHour || refused.devices.has(row.deviceId) || refused.locations.has(locationKey(row.deviceId, row.locationId));
|
|
8402
8557
|
if (disk === void 0) {
|
|
8403
|
-
if (
|
|
8558
|
+
if (additiveOnly) continue;
|
|
8404
8559
|
await this.ledger.forget(row.key);
|
|
8405
8560
|
continue;
|
|
8406
8561
|
}
|
|
8407
|
-
if (
|
|
8562
|
+
if (additiveOnly) {
|
|
8408
8563
|
const union = uniquePaths([...disk.paths, ...row.paths]);
|
|
8409
8564
|
if (!samePaths(union, row.paths)) await this.ledger.put({
|
|
8410
8565
|
...row,
|
|
@@ -8419,7 +8574,85 @@ var SegmentHourLedger = class {
|
|
|
8419
8574
|
await this.ledger.put(disk);
|
|
8420
8575
|
}
|
|
8421
8576
|
}
|
|
8577
|
+
/**
|
|
8578
|
+
* Which of this walk's deletions are REFUSED, and why.
|
|
8579
|
+
*
|
|
8580
|
+
* Two independent reasons, because the live fault of 2026-08-19 would have
|
|
8581
|
+
* escaped either one alone:
|
|
8582
|
+
*
|
|
8583
|
+
* - **the volume said no.** `PlacementService`'s marker probe knows the bytes
|
|
8584
|
+
* under a root are not the bytes we wrote. A walk of such a root found
|
|
8585
|
+
* nothing because nothing is mounted there, not because the footage is
|
|
8586
|
+
* gone.
|
|
8587
|
+
* - **the magnitude said no.** At BOOT the probe has not run yet — it runs at
|
|
8588
|
+
* the first writer attach — so on the morning the recorder respawned onto an
|
|
8589
|
+
* empty tmpfs the volume was not flagged and the prune went through: 873
|
|
8590
|
+
* hours to 32. A walk that deletes more than
|
|
8591
|
+
* {@link LEDGER_PRUNE_MAX_SHARE} of a device's held hours is refused on its
|
|
8592
|
+
* size alone.
|
|
8593
|
+
*
|
|
8594
|
+
* A refusal is one line per device (or per device+location), carrying the
|
|
8595
|
+
* delta, tagged with `deviceId` — a ledger that silently declined to prune and
|
|
8596
|
+
* a ledger with nothing to prune must never read the same.
|
|
8597
|
+
*/
|
|
8598
|
+
refusedPrunes(walked, fromIndex) {
|
|
8599
|
+
const heldByDevice = /* @__PURE__ */ new Map();
|
|
8600
|
+
const unusable = /* @__PURE__ */ new Map();
|
|
8601
|
+
for (const row of this.ledger.snapshot()) {
|
|
8602
|
+
if (!walked.has(row.deviceId)) continue;
|
|
8603
|
+
heldByDevice.set(row.deviceId, (heldByDevice.get(row.deviceId) ?? 0) + 1);
|
|
8604
|
+
if (this.isLocationUsable(row.locationId)) continue;
|
|
8605
|
+
const key = locationKey(row.deviceId, row.locationId);
|
|
8606
|
+
const seen = unusable.get(key);
|
|
8607
|
+
unusable.set(key, seen === void 0 ? {
|
|
8608
|
+
deviceId: row.deviceId,
|
|
8609
|
+
locationId: row.locationId,
|
|
8610
|
+
held: 1
|
|
8611
|
+
} : {
|
|
8612
|
+
...seen,
|
|
8613
|
+
held: seen.held + 1
|
|
8614
|
+
});
|
|
8615
|
+
}
|
|
8616
|
+
const onDiskByDevice = /* @__PURE__ */ new Map();
|
|
8617
|
+
for (const row of fromIndex.values()) onDiskByDevice.set(row.deviceId, (onDiskByDevice.get(row.deviceId) ?? 0) + 1);
|
|
8618
|
+
const devices = /* @__PURE__ */ new Set();
|
|
8619
|
+
for (const [deviceId, held] of heldByDevice) {
|
|
8620
|
+
if (held < 8) continue;
|
|
8621
|
+
const onDisk = onDiskByDevice.get(deviceId) ?? 0;
|
|
8622
|
+
if (onDisk >= held * .5) continue;
|
|
8623
|
+
devices.add(deviceId);
|
|
8624
|
+
this.logger.warn("recorder: segment-hour ledger prune REFUSED — this walk would delete most of the camera archive, which is what an unmounted root looks like; keeping the previous ledger", {
|
|
8625
|
+
tags: { deviceId },
|
|
8626
|
+
meta: {
|
|
8627
|
+
held,
|
|
8628
|
+
onDisk,
|
|
8629
|
+
dropped: held - onDisk,
|
|
8630
|
+
maxShare: LEDGER_PRUNE_MAX_SHARE
|
|
8631
|
+
}
|
|
8632
|
+
});
|
|
8633
|
+
}
|
|
8634
|
+
const locations = /* @__PURE__ */ new Set();
|
|
8635
|
+
for (const [key, entry] of unusable) {
|
|
8636
|
+
if (devices.has(entry.deviceId)) continue;
|
|
8637
|
+
locations.add(key);
|
|
8638
|
+
this.logger.warn("recorder: segment-hour ledger prune REFUSED — the volume under this location failed its probe, so the walk found nothing because nothing is mounted there", {
|
|
8639
|
+
tags: { deviceId: entry.deviceId },
|
|
8640
|
+
meta: {
|
|
8641
|
+
locationId: entry.locationId,
|
|
8642
|
+
held: entry.held,
|
|
8643
|
+
onDisk: onDiskByDevice.get(entry.deviceId) ?? 0
|
|
8644
|
+
}
|
|
8645
|
+
});
|
|
8646
|
+
}
|
|
8647
|
+
return {
|
|
8648
|
+
devices,
|
|
8649
|
+
locations
|
|
8650
|
+
};
|
|
8651
|
+
}
|
|
8422
8652
|
};
|
|
8653
|
+
function locationKey(deviceId, locationId) {
|
|
8654
|
+
return `${deviceId}|${locationId}`;
|
|
8655
|
+
}
|
|
8423
8656
|
//#endregion
|
|
8424
8657
|
//#region src/recorder/addon/recording-export-provider.ts
|
|
8425
8658
|
/** Content type of every rendered export — the engine only ever writes MP4. */
|
|
@@ -8909,7 +9142,9 @@ var macrotask = () => new Promise((resolve) => {
|
|
|
8909
9142
|
* unnoticed for six weeks precisely because nothing said anything.
|
|
8910
9143
|
*/
|
|
8911
9144
|
async function recoverStagingOrphans(dir, input, deps) {
|
|
8912
|
-
|
|
9145
|
+
const chunkSize = deps.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
9146
|
+
const yieldBetween = deps.yieldBetween ?? macrotask;
|
|
9147
|
+
let names = null;
|
|
8913
9148
|
try {
|
|
8914
9149
|
names = await deps.listDir(dir);
|
|
8915
9150
|
} catch (err) {
|
|
@@ -8917,13 +9152,14 @@ async function recoverStagingOrphans(dir, input, deps) {
|
|
|
8917
9152
|
dir,
|
|
8918
9153
|
error: err instanceof Error ? err.message : String(err)
|
|
8919
9154
|
} });
|
|
8920
|
-
return {
|
|
8921
|
-
found: 0,
|
|
8922
|
-
recovered: 0,
|
|
8923
|
-
failed: 0,
|
|
8924
|
-
aborted: false
|
|
8925
|
-
};
|
|
8926
9155
|
}
|
|
9156
|
+
await yieldBetween();
|
|
9157
|
+
if (names === null) return {
|
|
9158
|
+
found: 0,
|
|
9159
|
+
recovered: 0,
|
|
9160
|
+
failed: 0,
|
|
9161
|
+
aborted: false
|
|
9162
|
+
};
|
|
8927
9163
|
let playlistNames;
|
|
8928
9164
|
const body = await deps.readPlaylist(dir).catch(() => null);
|
|
8929
9165
|
if (body !== null) playlistNames = new Set(parseLivePlaylist(body).map((e) => e.segPath.replace(/^.*\//, "")));
|
|
@@ -8945,8 +9181,6 @@ async function recoverStagingOrphans(dir, input, deps) {
|
|
|
8945
9181
|
oldestMs: plan[0]?.startMs,
|
|
8946
9182
|
newestMs: plan[plan.length - 1]?.startMs
|
|
8947
9183
|
} });
|
|
8948
|
-
const chunkSize = deps.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
8949
|
-
const yieldBetween = deps.yieldBetween ?? macrotask;
|
|
8950
9184
|
let recovered = 0;
|
|
8951
9185
|
let failed = 0;
|
|
8952
9186
|
let aborted = false;
|
|
@@ -9046,24 +9280,41 @@ async function recoverAllStagedOrphans(deps) {
|
|
|
9046
9280
|
locations: locations.map((l) => l.id),
|
|
9047
9281
|
bootMs: deps.bootMs
|
|
9048
9282
|
} });
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9283
|
+
/**
|
|
9284
|
+
* One directory listing on the walk, paced through the pass's own pacer.
|
|
9285
|
+
*
|
|
9286
|
+
* "A handful of listings" is what this walk was costed as, and on a WARM
|
|
9287
|
+
* share it is — 4.1 s for the whole thing. Cold, after a power-cycle, each
|
|
9288
|
+
* one is a ~9.4 s FUSE round-trip and 58 of them summed to 547 s with the
|
|
9289
|
+
* loop to themselves, while the controller re-pinned writers 118 times and
|
|
9290
|
+
* the first recording landed at t+714 s.
|
|
9291
|
+
*
|
|
9292
|
+
* Paced per LISTING and not per entry, because that is where the latency
|
|
9293
|
+
* is; and through the SAME `yieldBetween` the recovery uses, so there is one
|
|
9294
|
+
* pacer, one back-off state and one set of counters for the whole pass —
|
|
9295
|
+
* never a second scheduling authority (D167).
|
|
9296
|
+
*/
|
|
9297
|
+
const listPaced = async (dir) => {
|
|
9298
|
+
let entries = null;
|
|
9053
9299
|
try {
|
|
9054
|
-
|
|
9300
|
+
entries = await deps.listDir(dir);
|
|
9055
9301
|
} catch {
|
|
9056
|
-
|
|
9302
|
+
entries = null;
|
|
9057
9303
|
}
|
|
9304
|
+
await yieldBetween();
|
|
9305
|
+
return entries;
|
|
9306
|
+
};
|
|
9307
|
+
const walkStartedMs = Date.now();
|
|
9308
|
+
const work = [];
|
|
9309
|
+
for (const location of locations) {
|
|
9310
|
+
const stagingRoot = `${location.root}/${STAGING_DIR_NAME}`;
|
|
9311
|
+
const deviceDirs = await listPaced(stagingRoot);
|
|
9312
|
+
if (deviceDirs === null) continue;
|
|
9058
9313
|
for (const deviceDir of deviceDirs) {
|
|
9059
9314
|
const deviceId = Number(deviceDir);
|
|
9060
9315
|
if (!Number.isInteger(deviceId) || deviceId <= 0) continue;
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
profileDirs = await deps.listDir(`${stagingRoot}/${deviceDir}`);
|
|
9064
|
-
} catch {
|
|
9065
|
-
continue;
|
|
9066
|
-
}
|
|
9316
|
+
const profileDirs = await listPaced(`${stagingRoot}/${deviceDir}`);
|
|
9317
|
+
if (profileDirs === null) continue;
|
|
9067
9318
|
const segmentSeconds = await deps.segmentSecondsFor(deviceId);
|
|
9068
9319
|
for (const profile of profileDirs) {
|
|
9069
9320
|
if (!KNOWN_PROFILES.has(profile)) continue;
|
|
@@ -9077,6 +9328,7 @@ async function recoverAllStagedOrphans(deps) {
|
|
|
9077
9328
|
}
|
|
9078
9329
|
}
|
|
9079
9330
|
}
|
|
9331
|
+
const walkMs = Date.now() - walkStartedMs;
|
|
9080
9332
|
const queue = [...work];
|
|
9081
9333
|
const lane = async () => {
|
|
9082
9334
|
for (;;) {
|
|
@@ -9119,6 +9371,7 @@ async function recoverAllStagedOrphans(deps) {
|
|
|
9119
9371
|
failed,
|
|
9120
9372
|
aborted,
|
|
9121
9373
|
ms: Date.now() - startedMs,
|
|
9374
|
+
walkMs,
|
|
9122
9375
|
...pacer === null ? {} : { pacing: pacer.stats() }
|
|
9123
9376
|
} });
|
|
9124
9377
|
return {
|
|
@@ -9371,7 +9624,8 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
9371
9624
|
}
|
|
9372
9625
|
this.segmentHours = new SegmentHourLedger({
|
|
9373
9626
|
store: this.ctx.api.settingsStore,
|
|
9374
|
-
logger: this.ctx.logger
|
|
9627
|
+
logger: this.ctx.logger,
|
|
9628
|
+
isLocationUsable: (locationId) => this.placement?.isLocationUsable(locationId) ?? true
|
|
9375
9629
|
});
|
|
9376
9630
|
try {
|
|
9377
9631
|
await this.segmentHours.declare();
|
|
@@ -10200,20 +10454,14 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
10200
10454
|
playbackRoots() {
|
|
10201
10455
|
return [path.join(this.ctx.dataDir, "playback"), ...this.resolvedLocations.map((l) => l.root)];
|
|
10202
10456
|
}
|
|
10203
|
-
/**
|
|
10457
|
+
/**
|
|
10458
|
+
* Free + total bytes on a location's volume (`statfs`); null plus a reason
|
|
10459
|
+
* when the volume will not answer. The reason is NOT logged here — the caller
|
|
10460
|
+
* (`getStorageUsage`) knows which location the root belongs to and logs it
|
|
10461
|
+
* against that location.
|
|
10462
|
+
*/
|
|
10204
10463
|
async locationCapacity(root) {
|
|
10205
|
-
|
|
10206
|
-
const st = await promises.statfs(root);
|
|
10207
|
-
return {
|
|
10208
|
-
availableBytes: st.bavail * st.bsize,
|
|
10209
|
-
totalBytes: st.blocks * st.bsize
|
|
10210
|
-
};
|
|
10211
|
-
} catch {
|
|
10212
|
-
return {
|
|
10213
|
-
availableBytes: null,
|
|
10214
|
-
totalBytes: null
|
|
10215
|
-
};
|
|
10216
|
-
}
|
|
10464
|
+
return readLocationCapacity(root, (target) => promises.statfs(target));
|
|
10217
10465
|
}
|
|
10218
10466
|
/**
|
|
10219
10467
|
* Global settings surfaced in the admin UI (and persisted to the addon
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
3
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
2
|
+
const require_worker_protocol = require("../worker-protocol-BrlrMxXu.js");
|
|
3
|
+
const require_lazy_sharp = require("../lazy-sharp-oYppDIZR.js");
|
|
4
4
|
//#region src/session-decode/color-conversion.ts
|
|
5
5
|
/**
|
|
6
6
|
* Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-
|
|
1
|
+
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-DJWdPLKF.mjs";
|
|
2
2
|
import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
|
|
3
3
|
//#region src/session-decode/color-conversion.ts
|
|
4
4
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DsIi4G5Q.mjs";
|
|
2
2
|
import { a as p, i as m, n as h, o as g, r as _, t as v } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
|
|
3
3
|
import { n as y, r as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
|
|
4
|
-
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-
|
|
4
|
+
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-zT-cYUdF.mjs";
|
|
5
5
|
import { n as C, t as w } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
|
|
6
6
|
//#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
7
7
|
var T = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), E = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), D = (e) => {
|
|
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
|
|
|
3
3
|
var e = {
|
|
4
4
|
"@camstack/sdk": {
|
|
5
5
|
name: "@camstack/sdk",
|
|
6
|
-
version: "1.2.
|
|
6
|
+
version: "1.2.25",
|
|
7
7
|
scope: ["default"],
|
|
8
8
|
loaded: !1,
|
|
9
9
|
from: "addon_stream_broker_widgets",
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.90",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_stream_broker_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.62",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_stream_broker_widgets",
|