@camstack/addon-pipeline 1.2.135 → 1.2.137
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-CKf8kbwh.js → addon-utils-C6b5Tnia.js} +3 -3
- package/dist/audio-analyzer/index.js +5 -5
- package/dist/audio-analyzer/index.mjs +4 -4
- package/dist/detection-pipeline/index.js +55 -17
- package/dist/detection-pipeline/index.mjs +53 -15
- package/dist/{dist-BzLiQ8Ue.js → dist-4EMKvxCp.js} +275 -25
- package/dist/{dist-C1dXLLQq.mjs → dist-g0Or5_pR.mjs} +270 -26
- package/dist/{event-loop-stall-monitor-CU-xxX5c.mjs → event-loop-stall-monitor-CskL9Lgu.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-DjV4Bow7.js → event-loop-stall-monitor-DDm07Ei6.js} +1 -1
- package/dist/{lazy-sharp-DxWmhTqk.js → lazy-sharp-BlA9PxWk.js} +3 -3
- package/dist/motion-wasm/index.js +3 -3
- package/dist/motion-wasm/index.mjs +3 -3
- package/dist/node-BOHn8gpk.js +721 -0
- package/dist/node-Bo1g8hLO.mjs +690 -0
- package/dist/pipeline-runner/index.js +37 -11
- package/dist/pipeline-runner/index.mjs +37 -11
- package/dist/{process-memory-CZK3D3kM.js → process-memory-C3084DOt.js} +2 -2
- package/dist/{process-memory-CaEwi5Cd.mjs → process-memory-DmrHWybb.mjs} +2 -2
- package/dist/recorder/index.js +382 -35
- package/dist/recorder/index.mjs +382 -35
- package/dist/{segment-demux-js-BbpqygHL.js → segment-demux-js-BrQ_A0oi.js} +1 -1
- package/dist/{segment-demux-js-njRO85gd.mjs → segment-demux-js-C-a2pghM.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +2 -2
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C14Qqrqn.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-AH6OJ3hJ.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQyiEd0l.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-BrTM_tsb.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-kjsveCjG.mjs → hostInit-XNyn-FTM.mjs} +3 -3
- package/dist/stream-broker/index.js +88 -515
- package/dist/stream-broker/index.mjs +85 -510
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DvTZBvh2.js → worker-protocol-BLfaD5uC.js} +1 -1
- package/dist/{worker-protocol-DxsVx0G7.mjs → worker-protocol-vc8WWhjx.mjs} +1 -1
- package/package.json +17 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqplWP1i.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DKMXG57X.mjs +0 -26
- package/dist/{addon-utils-CgBCF-mE.mjs → addon-utils-C8KTuPdE.mjs} +2 -2
- package/dist/{lazy-sharp-BtT5x-qP.mjs → lazy-sharp-DXsqwpph.mjs} +1 -1
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $ as loadContributionCapability, B as defineCustomActions, Ct as errMsg, E as RecordingConfigSchema, Gt as boolean, J as evaluateSensorEdge, L as customAction, Mt as DeviceType, N as batteryCapability, Pt as hydrateSchema, Q as isFirstLevelMacroClass, Qt as string, Rt as nodePin, T as RECORDING_EXPORT_MAX_READ_BYTES, U as deriveRecordingMode, V as deriveBatteryPresence, Vt as selectAssignedProfileSlots, Wt as array, Xt as object, Yt as number, Zt as record, _ as ExportRecordSchema, _t as resolveRecordingProfiles, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as EventCategory, ft as recordingCapability, h as EVENT_PAD_MS, i as BatteryStatusSchema, jt as DeviceFeature, kt as BaseAddon, pt as recordingExportCapability, x as OpsLogEntrySchema, yt as storageEvictableCapability } from "../dist-g0Or5_pR.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-Bo1g8hLO.mjs";
|
|
2
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
3
4
|
import { r as withRecordingIntent } from "../restream-intent-B4BXZra7.mjs";
|
|
4
|
-
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-
|
|
5
|
+
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-C8KTuPdE.mjs";
|
|
5
6
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DMolfhsP.mjs";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import path from "node:path";
|
|
9
8
|
import { createReadStream, promises, realpathSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
10
11
|
import { open } from "node:fs/promises";
|
|
11
12
|
//#region src/recorder/event-capture.ts
|
|
12
13
|
function markerFromMotion(data, pad) {
|
|
@@ -6223,6 +6224,7 @@ Object.freeze({
|
|
|
6223
6224
|
"listProfiles",
|
|
6224
6225
|
"listRuntimeNodes"
|
|
6225
6226
|
],
|
|
6227
|
+
"load-contribution": ["list"],
|
|
6226
6228
|
"log-channels": ["list"],
|
|
6227
6229
|
"log-destination": ["query"],
|
|
6228
6230
|
"login-method": ["getLoginMethods"],
|
|
@@ -8732,6 +8734,38 @@ var RESTART_MAX_MS = 1e4;
|
|
|
8732
8734
|
/** A process that ran at least this long is "stable" → reset the restart count,
|
|
8733
8735
|
* so sporadic blips over a long lifetime never accumulate into a give-up. */
|
|
8734
8736
|
var STABLE_RUN_MS = 3e4;
|
|
8737
|
+
/**
|
|
8738
|
+
* The window the SECOND budget counts over — and it counts EVERY restart,
|
|
8739
|
+
* whatever that run lasted.
|
|
8740
|
+
*
|
|
8741
|
+
* `STABLE_RUN_MS` above is a reset, and a reset is a blind spot: measured on
|
|
8742
|
+
* 2026-08-27, `SegmentWriter restarting` fired **1 814 times in 12 h** and
|
|
8743
|
+
* almost every `ranMs` was over 30 s (122 620 / 165 255 / 345 420 / 394 522 ms
|
|
8744
|
+
* are four consecutive real samples), so `attempt` never left 1, `onGaveUp`
|
|
8745
|
+
* never fired, and `recoverWriterGaveUp` never ran. The storm could have lasted
|
|
8746
|
+
* forever without a single line saying anything was wrong.
|
|
8747
|
+
*
|
|
8748
|
+
* A window cannot be zeroed by a long run, which is the whole point. Modelled
|
|
8749
|
+
* on `CrashSupervisor` (kernel, D6): timestamps age out of `windowMs`, and the
|
|
8750
|
+
* count inside it is the budget.
|
|
8751
|
+
*/
|
|
8752
|
+
var RESTART_WINDOW_MS = 60 * 6e4;
|
|
8753
|
+
/**
|
|
8754
|
+
* Restarts inside {@link RESTART_WINDOW_MS} before the writer stops respawning
|
|
8755
|
+
* and escalates to the controller.
|
|
8756
|
+
*
|
|
8757
|
+
* 15/hour, from the measured distribution. Per writer over the same 12 h:
|
|
8758
|
+
* 1441/high 549 (**45.8/h**), 615/high 241 (20.1/h), 618/high 217 (18.1/h),
|
|
8759
|
+
* 592/high 211 (17.6/h), 590/high 195 (16.3/h) — every one of them a storm and
|
|
8760
|
+
* every one of them silent — against `low` profiles at 4–15 per 12 h (≤1.3/h),
|
|
8761
|
+
* which is what a healthy writer looks like. 15 separates them with room for a
|
|
8762
|
+
* camera that blips hourly and is otherwise fine.
|
|
8763
|
+
*
|
|
8764
|
+
* It never shadows {@link MAX_RESTARTS}: 10 CONSECUTIVE rapid failures happen
|
|
8765
|
+
* inside ~90 s, so the rapid breaker still trips first on a hard failure. This
|
|
8766
|
+
* one exists for the slow storm the rapid breaker structurally cannot see.
|
|
8767
|
+
*/
|
|
8768
|
+
var MAX_RESTARTS_IN_WINDOW = 15;
|
|
8735
8769
|
/** Grace between the SIGTERM in stop() and the escalated SIGKILL. */
|
|
8736
8770
|
var KILL_GRACE_MS = 500;
|
|
8737
8771
|
/** Supervises one passthrough ffmpeg for a single (camera, profile). */
|
|
@@ -8741,10 +8775,22 @@ var SegmentWriter = class {
|
|
|
8741
8775
|
proc = null;
|
|
8742
8776
|
stopped = false;
|
|
8743
8777
|
restarts = 0;
|
|
8778
|
+
/**
|
|
8779
|
+
* Epoch-ms of every termination still inside {@link RESTART_WINDOW_MS}.
|
|
8780
|
+
* Replaced, never mutated in place, so a reader can never observe a
|
|
8781
|
+
* half-pruned window.
|
|
8782
|
+
*/
|
|
8783
|
+
restartWindow = [];
|
|
8744
8784
|
restartTimer = null;
|
|
8745
8785
|
startedAt = 0;
|
|
8746
8786
|
exitWait = null;
|
|
8747
8787
|
resolveExit = null;
|
|
8788
|
+
/**
|
|
8789
|
+
* The cost claim for the CURRENT ffmpeg generation. Taken in `start()` and
|
|
8790
|
+
* released in `onTermination()` — the two edges of one process, so a restart
|
|
8791
|
+
* storm cannot accumulate claims for children that are already gone.
|
|
8792
|
+
*/
|
|
8793
|
+
costClaim = NO_COST_CLAIM;
|
|
8748
8794
|
constructor(cfg, deps) {
|
|
8749
8795
|
this.cfg = cfg;
|
|
8750
8796
|
this.deps = deps;
|
|
@@ -8758,6 +8804,7 @@ var SegmentWriter = class {
|
|
|
8758
8804
|
this.resolveExit = resolve;
|
|
8759
8805
|
});
|
|
8760
8806
|
this.startedAt = Date.now();
|
|
8807
|
+
this.costClaim = this.deps.claimCost?.(proc.pid) ?? NO_COST_CLAIM;
|
|
8761
8808
|
const stderrTail = [];
|
|
8762
8809
|
proc.stderr?.on("data", (chunk) => {
|
|
8763
8810
|
const text = chunk instanceof Buffer ? chunk.toString("utf8") : String(chunk);
|
|
@@ -8774,10 +8821,11 @@ var SegmentWriter = class {
|
|
|
8774
8821
|
settled = true;
|
|
8775
8822
|
this.onTermination(termination, stderrTail);
|
|
8776
8823
|
};
|
|
8777
|
-
proc.on("exit", (code) => {
|
|
8824
|
+
proc.on("exit", (code, signal) => {
|
|
8778
8825
|
terminate({
|
|
8779
8826
|
reason: "exit",
|
|
8780
|
-
exitCode: typeof code === "number" ? code : null
|
|
8827
|
+
exitCode: typeof code === "number" ? code : null,
|
|
8828
|
+
signal: typeof signal === "string" ? signal : null
|
|
8781
8829
|
});
|
|
8782
8830
|
});
|
|
8783
8831
|
proc.on("error", (raw) => {
|
|
@@ -8793,32 +8841,53 @@ var SegmentWriter = class {
|
|
|
8793
8841
|
/**
|
|
8794
8842
|
* The ONE restart policy, reached from both `exit` and `error`.
|
|
8795
8843
|
*
|
|
8796
|
-
* `
|
|
8797
|
-
*
|
|
8798
|
-
*
|
|
8799
|
-
* camera, and a writer that stops recording one camera must be greppable by
|
|
8800
|
-
* that camera.
|
|
8844
|
+
* Every line below carries `tags: { deviceId }` from `deps.deviceId` — "why
|
|
8845
|
+
* is 617 worse than 615" is always asked per camera, and a writer that stops
|
|
8846
|
+
* recording one camera must be greppable by that camera.
|
|
8801
8847
|
*/
|
|
8802
8848
|
onTermination(termination, stderrTail) {
|
|
8849
|
+
this.costClaim.release();
|
|
8850
|
+
this.costClaim = NO_COST_CLAIM;
|
|
8803
8851
|
this.resolveExit?.();
|
|
8804
8852
|
this.resolveExit = null;
|
|
8805
8853
|
this.proc = null;
|
|
8806
8854
|
if (this.stopped) return;
|
|
8807
|
-
const
|
|
8855
|
+
const now = Date.now();
|
|
8856
|
+
const ranMs = now - this.startedAt;
|
|
8808
8857
|
if (ranMs >= STABLE_RUN_MS) this.restarts = 0;
|
|
8809
|
-
|
|
8810
|
-
|
|
8858
|
+
this.restartWindow = [...this.restartWindow.filter((at) => at > now - RESTART_WINDOW_MS), now];
|
|
8859
|
+
const restartsInWindow = this.restartWindow.length;
|
|
8860
|
+
if (termination.reason === "exit") this.deps.logger.warn("SegmentWriter ffmpeg exited", {
|
|
8861
|
+
tags: { deviceId: this.deps.deviceId },
|
|
8862
|
+
meta: {
|
|
8811
8863
|
outDir: this.cfg.outDir,
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8864
|
+
exitCode: termination.exitCode,
|
|
8865
|
+
signal: termination.signal,
|
|
8866
|
+
ranMs,
|
|
8867
|
+
restartsInWindow,
|
|
8868
|
+
windowMs: RESTART_WINDOW_MS,
|
|
8869
|
+
stderrTail: stderrTail.join(" | ")
|
|
8870
|
+
}
|
|
8871
|
+
});
|
|
8872
|
+
if (termination.reason === "spawn-error") {
|
|
8873
|
+
this.deps.logger.warn("SegmentWriter ffmpeg spawn failed", {
|
|
8874
|
+
tags: { deviceId: this.deps.deviceId },
|
|
8875
|
+
meta: {
|
|
8818
8876
|
outDir: this.cfg.outDir,
|
|
8819
8877
|
errorCode: termination.errorCode,
|
|
8820
|
-
error: termination.message
|
|
8821
|
-
|
|
8878
|
+
error: termination.message,
|
|
8879
|
+
permanent: termination.permanent
|
|
8880
|
+
}
|
|
8881
|
+
});
|
|
8882
|
+
if (termination.permanent) {
|
|
8883
|
+
this.deps.logger.error("SegmentWriter giving up: ffmpeg cannot be executed on this node", {
|
|
8884
|
+
tags: { deviceId: this.deps.deviceId },
|
|
8885
|
+
meta: {
|
|
8886
|
+
outDir: this.cfg.outDir,
|
|
8887
|
+
errorCode: termination.errorCode,
|
|
8888
|
+
error: termination.message
|
|
8889
|
+
}
|
|
8890
|
+
});
|
|
8822
8891
|
this.stopped = true;
|
|
8823
8892
|
this.deps.onGaveUp?.();
|
|
8824
8893
|
return;
|
|
@@ -8826,25 +8895,51 @@ var SegmentWriter = class {
|
|
|
8826
8895
|
this.deps.onResourcePressure?.();
|
|
8827
8896
|
}
|
|
8828
8897
|
if (this.restarts >= MAX_RESTARTS) {
|
|
8829
|
-
this.deps.logger.warn("SegmentWriter giving up after max restarts", {
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8898
|
+
this.deps.logger.warn("SegmentWriter giving up after max restarts", {
|
|
8899
|
+
tags: { deviceId: this.deps.deviceId },
|
|
8900
|
+
meta: {
|
|
8901
|
+
outDir: this.cfg.outDir,
|
|
8902
|
+
reason: termination.reason,
|
|
8903
|
+
code: termination.reason === "exit" ? termination.exitCode : termination.errorCode,
|
|
8904
|
+
stderrTail: stderrTail.join(" | ")
|
|
8905
|
+
}
|
|
8906
|
+
});
|
|
8907
|
+
this.stopped = true;
|
|
8908
|
+
this.deps.onGaveUp?.();
|
|
8909
|
+
return;
|
|
8910
|
+
}
|
|
8911
|
+
if (restartsInWindow >= MAX_RESTARTS_IN_WINDOW) {
|
|
8912
|
+
this.deps.logger.warn("SegmentWriter giving up: restart storm", {
|
|
8913
|
+
tags: { deviceId: this.deps.deviceId },
|
|
8914
|
+
meta: {
|
|
8915
|
+
outDir: this.cfg.outDir,
|
|
8916
|
+
restartsInWindow,
|
|
8917
|
+
windowMs: RESTART_WINDOW_MS,
|
|
8918
|
+
budget: MAX_RESTARTS_IN_WINDOW,
|
|
8919
|
+
attempt: this.restarts,
|
|
8920
|
+
ranMs,
|
|
8921
|
+
reason: termination.reason,
|
|
8922
|
+
code: termination.reason === "exit" ? termination.exitCode : termination.errorCode,
|
|
8923
|
+
stderrTail: stderrTail.join(" | ")
|
|
8924
|
+
}
|
|
8925
|
+
});
|
|
8835
8926
|
this.stopped = true;
|
|
8836
8927
|
this.deps.onGaveUp?.();
|
|
8837
8928
|
return;
|
|
8838
8929
|
}
|
|
8839
8930
|
this.restarts++;
|
|
8840
8931
|
const delayMs = Math.min(RESTART_BASE_MS * 2 ** (this.restarts - 1), RESTART_MAX_MS);
|
|
8841
|
-
this.deps.logger.info("SegmentWriter restarting", {
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8932
|
+
this.deps.logger.info("SegmentWriter restarting", {
|
|
8933
|
+
tags: { deviceId: this.deps.deviceId },
|
|
8934
|
+
meta: {
|
|
8935
|
+
outDir: this.cfg.outDir,
|
|
8936
|
+
attempt: this.restarts,
|
|
8937
|
+
restartsInWindow,
|
|
8938
|
+
delayMs,
|
|
8939
|
+
ranMs,
|
|
8940
|
+
reason: termination.reason
|
|
8941
|
+
}
|
|
8942
|
+
});
|
|
8848
8943
|
this.restartTimer = setTimeout(() => {
|
|
8849
8944
|
this.restartTimer = null;
|
|
8850
8945
|
this.start();
|
|
@@ -10108,6 +10203,13 @@ var RecordingController = class {
|
|
|
10108
10203
|
}, {
|
|
10109
10204
|
spawn: this.deps.spawn,
|
|
10110
10205
|
logger: deviceLog,
|
|
10206
|
+
deviceId,
|
|
10207
|
+
claimCost: (pid) => this.deps.costRegistry?.claim({
|
|
10208
|
+
role: "recording",
|
|
10209
|
+
deviceId,
|
|
10210
|
+
unit: `${deviceId}/${profile}`,
|
|
10211
|
+
pid
|
|
10212
|
+
}) ?? NO_COST_CLAIM,
|
|
10111
10213
|
onGaveUp: () => {
|
|
10112
10214
|
this.recoverWriterGaveUp(deviceId, profile);
|
|
10113
10215
|
},
|
|
@@ -10531,6 +10633,209 @@ function buildRecordingExportProvider(deps) {
|
|
|
10531
10633
|
*/
|
|
10532
10634
|
var resolveRecordingHubHostname = resolveHubHostname;
|
|
10533
10635
|
//#endregion
|
|
10636
|
+
//#region src/recorder/redundant-segments.ts
|
|
10637
|
+
/**
|
|
10638
|
+
* Return the indices (into `segs`) of segments safe to delete: those fully
|
|
10639
|
+
* contained within a kept segment `[coverStart, coverEnd]`. Sorting by start
|
|
10640
|
+
* ascending, then by end DESCENDING, guarantees the containing (longer) segment
|
|
10641
|
+
* is seen first and kept, and any shorter segment nested inside it is removed.
|
|
10642
|
+
*/
|
|
10643
|
+
function selectRedundantSegments(segs) {
|
|
10644
|
+
if (segs.length < 2) return [];
|
|
10645
|
+
const order = segs.map((s, i) => ({
|
|
10646
|
+
start: s.startMs,
|
|
10647
|
+
end: s.startMs + s.durMs,
|
|
10648
|
+
i
|
|
10649
|
+
})).toSorted((a, b) => a.start - b.start || b.end - a.end);
|
|
10650
|
+
const remove = [];
|
|
10651
|
+
let coverStart = Number.NEGATIVE_INFINITY;
|
|
10652
|
+
let coverEnd = Number.NEGATIVE_INFINITY;
|
|
10653
|
+
for (const seg of order) if (seg.start >= coverStart && seg.end <= coverEnd && seg.end > seg.start) remove.push(seg.i);
|
|
10654
|
+
else {
|
|
10655
|
+
coverStart = seg.start;
|
|
10656
|
+
coverEnd = seg.end;
|
|
10657
|
+
}
|
|
10658
|
+
return remove;
|
|
10659
|
+
}
|
|
10660
|
+
//#endregion
|
|
10661
|
+
//#region src/recorder/addon/redundancy-sweep.ts
|
|
10662
|
+
/**
|
|
10663
|
+
* Periodic redundancy sweep — the only thing that removes duplicated footage.
|
|
10664
|
+
*
|
|
10665
|
+
* WHY IT EXISTS AGAIN
|
|
10666
|
+
* ───────────────────
|
|
10667
|
+
* Every `SegmentWriter` restart re-dials the broker with `withRecordingIntent`,
|
|
10668
|
+
* and the broker answers a RECORDING dial with its pre-roll ring (≈10 s). So a
|
|
10669
|
+
* restart does not resume at the live edge: it re-writes ~10 s of media that is
|
|
10670
|
+
* already on disk. At the 2026-08-27 measurement — **1 814 restarts in 12 h** —
|
|
10671
|
+
* that is ~5 hours of duplicated `high`-profile footage per day, on a fuse
|
|
10672
|
+
* share that was already at 90 % full.
|
|
10673
|
+
*
|
|
10674
|
+
* The pruner that used to remove it, `runRedundancyJanitor`, was reachable ONLY
|
|
10675
|
+
* from `runFullArchiveWalk`, which was reachable only from
|
|
10676
|
+
* `scheduleDeferredFullWalk`, which had no caller at all — so on 2026-08-27
|
|
10677
|
+
* (commit `9308428b4`) all three were deleted together as dead code. That was
|
|
10678
|
+
* correct about the walk and left the system with **no duplicate pruner
|
|
10679
|
+
* whatsoever**. The precedent for letting this class of waste run unattended is
|
|
10680
|
+
* the 179 GB of stranded staging segments in `staging-reconcile.ts`.
|
|
10681
|
+
*
|
|
10682
|
+
* WHAT IS DIFFERENT FROM THE ONE THAT WAS DELETED
|
|
10683
|
+
* ───────────────────────────────────────────────
|
|
10684
|
+
* The old janitor ran once, after a walk of the WHOLE archive, and asked the
|
|
10685
|
+
* in-RAM index for `segments(deviceId)` — a full copy plus a sort of every row
|
|
10686
|
+
* the device owns. At the live shape (7.1 M rows) that copy alone was measured
|
|
10687
|
+
* at ~560 MB of transient heap, which is precisely the cost D248 had just
|
|
10688
|
+
* finished removing from the pressure sweep. Reconnecting it in that shape
|
|
10689
|
+
* would have traded one regression for another.
|
|
10690
|
+
*
|
|
10691
|
+
* So this one is bounded by a LOOKBACK WINDOW, not by the archive: duplicates
|
|
10692
|
+
* are made by a restart, restarts are now, and `segmentsStartingIn` slices the
|
|
10693
|
+
* sorted view instead of copying it. Cost grows with footage RECORDED in the
|
|
10694
|
+
* window — the invariant the recorder is supposed to satisfy — never with rows
|
|
10695
|
+
* already indexed.
|
|
10696
|
+
*
|
|
10697
|
+
* WHAT IT WILL NOT DO
|
|
10698
|
+
* ───────────────────
|
|
10699
|
+
* It never deletes a segment that contributes unique coverage:
|
|
10700
|
+
* {@link selectRedundantSegments} selects only segments fully contained inside
|
|
10701
|
+
* another segment that is KEPT, and staggered partial overlaps are always kept.
|
|
10702
|
+
* The delete itself is `SegmentStore.evict` — the same audited path
|
|
10703
|
+
* disk-pressure eviction uses, so a removed segment is de-indexed exactly as an
|
|
10704
|
+
* evicted one is. A failure for one (device, profile, location) is logged and
|
|
10705
|
+
* never aborts the pass.
|
|
10706
|
+
*/
|
|
10707
|
+
/** The profiles a recorder writes. Same set `staging-reconcile` sweeps. */
|
|
10708
|
+
var SWEPT_PROFILES = [
|
|
10709
|
+
"high",
|
|
10710
|
+
"mid",
|
|
10711
|
+
"low"
|
|
10712
|
+
];
|
|
10713
|
+
/** Group rows by the location that owns them. `evict` is per-location. */
|
|
10714
|
+
function byLocation(rows) {
|
|
10715
|
+
const out = /* @__PURE__ */ new Map();
|
|
10716
|
+
for (const row of rows) {
|
|
10717
|
+
const found = out.get(row.locationId);
|
|
10718
|
+
if (found) found.push(row);
|
|
10719
|
+
else out.set(row.locationId, [row]);
|
|
10720
|
+
}
|
|
10721
|
+
return out;
|
|
10722
|
+
}
|
|
10723
|
+
/**
|
|
10724
|
+
* Owns its timer and a single-flight guard, the same shape the retention sweep
|
|
10725
|
+
* and the export janitor already use — a slow pass is never overlapped by the
|
|
10726
|
+
* next tick.
|
|
10727
|
+
*/
|
|
10728
|
+
var RedundancySweeper = class {
|
|
10729
|
+
deps;
|
|
10730
|
+
timer = null;
|
|
10731
|
+
sweeping = false;
|
|
10732
|
+
constructor(deps) {
|
|
10733
|
+
this.deps = deps;
|
|
10734
|
+
}
|
|
10735
|
+
/**
|
|
10736
|
+
* Arm the sweep on its interval. Idempotent.
|
|
10737
|
+
*
|
|
10738
|
+
* Deliberately NOT run immediately: boot is when the writers are attaching,
|
|
10739
|
+
* the staging reconcile is relocating thousands of files over shfs and the
|
|
10740
|
+
* first viewer is painting. A duplicate that has waited an hour can wait
|
|
10741
|
+
* another one; the write path cannot.
|
|
10742
|
+
*/
|
|
10743
|
+
start() {
|
|
10744
|
+
if (this.timer !== null) return;
|
|
10745
|
+
this.timer = setInterval(() => {
|
|
10746
|
+
this.sweep();
|
|
10747
|
+
}, this.deps.intervalMs);
|
|
10748
|
+
this.timer.unref?.();
|
|
10749
|
+
}
|
|
10750
|
+
/** Stop the timer. Idempotent. An in-flight pass still completes. */
|
|
10751
|
+
stop() {
|
|
10752
|
+
if (this.timer !== null) {
|
|
10753
|
+
clearInterval(this.timer);
|
|
10754
|
+
this.timer = null;
|
|
10755
|
+
}
|
|
10756
|
+
}
|
|
10757
|
+
/**
|
|
10758
|
+
* One pass. Never throws: a device-list read failure aborts THIS pass
|
|
10759
|
+
* (logged), a per-location evict failure is logged and the pass continues.
|
|
10760
|
+
*/
|
|
10761
|
+
async sweep() {
|
|
10762
|
+
const empty = {
|
|
10763
|
+
files: 0,
|
|
10764
|
+
bytes: 0,
|
|
10765
|
+
devices: 0
|
|
10766
|
+
};
|
|
10767
|
+
if (this.sweeping) {
|
|
10768
|
+
this.deps.logger.debug("recorder: redundancy sweep already running — skipping this tick");
|
|
10769
|
+
return empty;
|
|
10770
|
+
}
|
|
10771
|
+
this.sweeping = true;
|
|
10772
|
+
try {
|
|
10773
|
+
let deviceIds;
|
|
10774
|
+
try {
|
|
10775
|
+
deviceIds = await this.deps.deviceIds();
|
|
10776
|
+
} catch (err) {
|
|
10777
|
+
this.deps.logger.warn("recorder: redundancy sweep could not read the device list", { meta: { error: errMsg(err) } });
|
|
10778
|
+
return empty;
|
|
10779
|
+
}
|
|
10780
|
+
const toMs = (this.deps.now ?? Date.now)();
|
|
10781
|
+
const fromMs = toMs - this.deps.lookbackMs;
|
|
10782
|
+
let files = 0;
|
|
10783
|
+
let bytes = 0;
|
|
10784
|
+
let devices = 0;
|
|
10785
|
+
for (const deviceId of deviceIds) {
|
|
10786
|
+
let removedHere = 0;
|
|
10787
|
+
for (const profile of SWEPT_PROFILES) {
|
|
10788
|
+
const rows = this.deps.segmentsInWindow(deviceId, profile, fromMs, toMs);
|
|
10789
|
+
const redundant = selectRedundantSegments(rows);
|
|
10790
|
+
if (redundant.length === 0) continue;
|
|
10791
|
+
const victims = redundant.map((i) => rows[i]).filter((row) => row !== void 0);
|
|
10792
|
+
for (const [locationId, group] of byLocation(victims)) try {
|
|
10793
|
+
const reclaimed = await this.deps.evict(locationId, group);
|
|
10794
|
+
files += group.length;
|
|
10795
|
+
bytes += reclaimed;
|
|
10796
|
+
removedHere += group.length;
|
|
10797
|
+
} catch (err) {
|
|
10798
|
+
this.deps.logger.warn("recorder: redundancy sweep could not evict duplicates", {
|
|
10799
|
+
tags: { deviceId },
|
|
10800
|
+
meta: {
|
|
10801
|
+
profile,
|
|
10802
|
+
locationId,
|
|
10803
|
+
count: group.length,
|
|
10804
|
+
error: errMsg(err)
|
|
10805
|
+
}
|
|
10806
|
+
});
|
|
10807
|
+
}
|
|
10808
|
+
}
|
|
10809
|
+
if (removedHere > 0) {
|
|
10810
|
+
devices += 1;
|
|
10811
|
+
this.deps.logger.info("recorder: removed duplicated segments for device", {
|
|
10812
|
+
tags: { deviceId },
|
|
10813
|
+
meta: {
|
|
10814
|
+
files: removedHere,
|
|
10815
|
+
fromMs,
|
|
10816
|
+
toMs
|
|
10817
|
+
}
|
|
10818
|
+
});
|
|
10819
|
+
}
|
|
10820
|
+
}
|
|
10821
|
+
this.deps.logger.info("recorder: redundancy sweep complete", { meta: {
|
|
10822
|
+
devices,
|
|
10823
|
+
files,
|
|
10824
|
+
bytes,
|
|
10825
|
+
lookbackMs: this.deps.lookbackMs,
|
|
10826
|
+
deviceCount: deviceIds.length
|
|
10827
|
+
} });
|
|
10828
|
+
return {
|
|
10829
|
+
files,
|
|
10830
|
+
bytes,
|
|
10831
|
+
devices
|
|
10832
|
+
};
|
|
10833
|
+
} finally {
|
|
10834
|
+
this.sweeping = false;
|
|
10835
|
+
}
|
|
10836
|
+
}
|
|
10837
|
+
};
|
|
10838
|
+
//#endregion
|
|
10534
10839
|
//#region src/recorder/addon/retention-sweep.ts
|
|
10535
10840
|
/**
|
|
10536
10841
|
* Periodic footage-retention sweep (recording-spec §6).
|
|
@@ -12091,6 +12396,18 @@ var EXPORT_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
|
12091
12396
|
* hourly cadence keeps the storage.list/evict cost negligible while honouring
|
|
12092
12397
|
* the recording-spec §6 periodic retention. */
|
|
12093
12398
|
var RETENTION_SWEEP_INTERVAL_MS = 60 * 6e4;
|
|
12399
|
+
/** How often the redundancy sweep removes footage a writer restart duplicated. */
|
|
12400
|
+
var REDUNDANCY_SWEEP_INTERVAL_MS = 60 * 6e4;
|
|
12401
|
+
/**
|
|
12402
|
+
* How far back each redundancy pass looks.
|
|
12403
|
+
*
|
|
12404
|
+
* Twice the interval, so a pass that is skipped (single-flight) or delayed by a
|
|
12405
|
+
* slow predecessor still covers the window its predecessor was supposed to.
|
|
12406
|
+
* It is deliberately NOT "the whole archive": duplicates are made by a writer
|
|
12407
|
+
* restart, restarts are recent, and a full-archive selection is the ~560 MB
|
|
12408
|
+
* transient copy D248 removed. See `redundancy-sweep.ts`.
|
|
12409
|
+
*/
|
|
12410
|
+
var REDUNDANCY_SWEEP_LOOKBACK_MS = 2 * REDUNDANCY_SWEEP_INTERVAL_MS;
|
|
12094
12411
|
/** What the per-volume usage row reports when the hour ledger is unavailable:
|
|
12095
12412
|
* nothing measured, rather than a total counted off a partial RAM index. */
|
|
12096
12413
|
var EMPTY_ARCHIVE_ACCOUNTING = {
|
|
@@ -12116,6 +12433,16 @@ function isSegmentStoreLogExtras(extras) {
|
|
|
12116
12433
|
}
|
|
12117
12434
|
var RecorderV2Addon = class extends BaseAddon {
|
|
12118
12435
|
nodeId = "hub";
|
|
12436
|
+
/**
|
|
12437
|
+
* The children this addon started, claimed at the spawn site and released at
|
|
12438
|
+
* their exit. It is what `load-contribution` reports: the recorder knows it
|
|
12439
|
+
* ran an ffmpeg for camera 615 profile `high`, so nobody else has to work it
|
|
12440
|
+
* out from a process table.
|
|
12441
|
+
*
|
|
12442
|
+
* One registry per PROCESS, and it dies with the process — which is correct,
|
|
12443
|
+
* because those children were this process's children.
|
|
12444
|
+
*/
|
|
12445
|
+
costRegistry = new ChildCostRegistry();
|
|
12119
12446
|
/** True when THIS node is the designated recording node (`recordingNodeId`).
|
|
12120
12447
|
* Every other node stays fully inert — registers no providers, serves no
|
|
12121
12448
|
* playback, runs no controller. Resolved in `onInitialize`. */
|
|
@@ -12243,6 +12570,9 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
12243
12570
|
/** Periodic footage-retention sweep (owns its timer + single-flight guard).
|
|
12244
12571
|
* Constructed + started in `onHubReachable`, stopped on shutdown. */
|
|
12245
12572
|
retentionSweeper = null;
|
|
12573
|
+
/** Periodic duplicate-footage sweep — the ONLY pruner of media a writer
|
|
12574
|
+
* restart re-wrote. Constructed + started in `onHubReachable`. */
|
|
12575
|
+
redundancySweeper = null;
|
|
12246
12576
|
/** Reversible migration pause lease; never reflected in RecordingConfig. */
|
|
12247
12577
|
storageMigrationLeaseId = null;
|
|
12248
12578
|
/**
|
|
@@ -12459,6 +12789,7 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
12459
12789
|
placeProfile: (deviceId, profile) => this.placeProfile(deviceId, profile),
|
|
12460
12790
|
onPlacementTick: (deviceIds) => this.recomputePlacement(deviceIds),
|
|
12461
12791
|
spawn,
|
|
12792
|
+
costRegistry: this.costRegistry,
|
|
12462
12793
|
segmentSeconds: this.config.segmentSeconds,
|
|
12463
12794
|
watchIntervalMs: this.config.watchIntervalMs,
|
|
12464
12795
|
...consumerHostname !== void 0 ? { consumerHostname } : {},
|
|
@@ -12707,6 +13038,10 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
12707
13038
|
{
|
|
12708
13039
|
capability: recordingExportCapability,
|
|
12709
13040
|
provider: exportProvider
|
|
13041
|
+
},
|
|
13042
|
+
{
|
|
13043
|
+
capability: loadContributionCapability,
|
|
13044
|
+
provider: { list: () => this.costRegistry.contributions() }
|
|
12710
13045
|
}
|
|
12711
13046
|
],
|
|
12712
13047
|
customActions: recorderDebugActions,
|
|
@@ -12731,6 +13066,8 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
12731
13066
|
}
|
|
12732
13067
|
this.retentionSweeper?.stop();
|
|
12733
13068
|
this.retentionSweeper = null;
|
|
13069
|
+
this.redundancySweeper?.stop();
|
|
13070
|
+
this.redundancySweeper = null;
|
|
12734
13071
|
this.exportEngine = null;
|
|
12735
13072
|
this.recordingProvider = null;
|
|
12736
13073
|
}
|
|
@@ -12870,6 +13207,16 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
12870
13207
|
intervalMs: RETENTION_SWEEP_INTERVAL_MS
|
|
12871
13208
|
});
|
|
12872
13209
|
this.retentionSweeper?.start();
|
|
13210
|
+
const store = this.segmentStore;
|
|
13211
|
+
if (store && this.redundancySweeper === null) this.redundancySweeper = new RedundancySweeper({
|
|
13212
|
+
deviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
|
|
13213
|
+
segmentsInWindow: (deviceId, profile, fromMs, toMs) => this.index.segmentsStartingIn(deviceId, profile, fromMs, toMs),
|
|
13214
|
+
evict: (locationId, rows) => store.evict(locationId, rows),
|
|
13215
|
+
logger: this.ctx.logger,
|
|
13216
|
+
intervalMs: REDUNDANCY_SWEEP_INTERVAL_MS,
|
|
13217
|
+
lookbackMs: REDUNDANCY_SWEEP_LOOKBACK_MS
|
|
13218
|
+
});
|
|
13219
|
+
this.redundancySweeper?.start();
|
|
12873
13220
|
}
|
|
12874
13221
|
/**
|
|
12875
13222
|
* Rebuild the sensor trigger's reverse index from the persisted configs.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-4EMKvxCp.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) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Ct as errMsg } from "./dist-g0Or5_pR.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,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-BLfaD5uC.js");
|
|
3
|
+
const require_lazy_sharp = require("../lazy-sharp-BlA9PxWk.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,5 +1,5 @@
|
|
|
1
|
-
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-
|
|
2
|
-
import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-
|
|
1
|
+
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-vc8WWhjx.mjs";
|
|
2
|
+
import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
3
3
|
//#region src/session-decode/color-conversion.ts
|
|
4
4
|
/**
|
|
5
5
|
* Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
|
|
@@ -1,7 +1,7 @@
|
|
|
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-
|
|
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-BrTM_tsb.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-CQyiEd0l.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.39",
|
|
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.117",
|
|
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.80",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_stream_broker_widgets",
|