@camstack/addon-pipeline 1.2.219 → 1.2.220
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 +49 -5
- package/dist/audio-analyzer/index.mjs +46 -3
- package/dist/chunk-emK7D4bc.js +35 -0
- package/dist/{default-detection-model-Dsk1a5pY.mjs → default-detection-model-DBNEc3iw.mjs} +1 -1
- package/dist/{default-detection-model-d7-Q_LMo.js → default-detection-model-x6oNMBDH.js} +1 -1
- package/dist/detection-pipeline/index.js +10 -9
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-wtNhtVVo.js → dist-ByPsWlTa.js} +2 -37
- package/dist/{dist-CKK0CBUW.mjs → dist-ClihWGOZ.mjs} +2 -2
- package/dist/{lazy-sharp-zoQ07I43.js → lazy-sharp-BuwKlQLR.js} +2 -2
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-BduNi21Z.js → node-CNPnaux4.js} +4 -3
- package/dist/{node-CrNBWFbA.mjs → node-DiaTmz84.mjs} +1 -1
- package/dist/pipeline-runner/index.js +222 -157
- package/dist/pipeline-runner/index.mjs +218 -154
- package/dist/{process-memory-Dwl0MFPH.js → process-memory-9KqRtUHt.js} +3 -2
- package/dist/{process-memory-BRQOtDN1.mjs → process-memory-D4t8hpJd.mjs} +1 -1
- package/dist/recorder/index.js +334 -123
- package/dist/recorder/index.mjs +332 -122
- package/dist/{segment-demux-js-D2o9ShNL.mjs → segment-demux-js-CFFXVae4.mjs} +1 -1
- package/dist/{segment-demux-js-CFOU3biG.js → segment-demux-js-CZefXDLx.js} +4 -3
- package/dist/session-decode/decode-worker-child.js +155 -74
- package/dist/session-decode/decode-worker-child.mjs +154 -73
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DN_IoShh.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CtdpN7Sz.mjs} +3 -3
- 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-n7c9pNlt.mjs → hostInit-McoUiljf.mjs} +3 -3
- package/dist/stream-broker/index.js +24 -25
- package/dist/stream-broker/index.mjs +17 -19
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-B2MfQLlu.js +184 -0
- package/dist/worker-protocol-C_W-P_g-.mjs +161 -0
- package/package.json +1 -1
- package/dist/worker-protocol-Cx3Nqreq.mjs +0 -264
- package/dist/worker-protocol-iWTFnnUM.js +0 -299
|
@@ -2,19 +2,20 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
5
|
+
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
+
const require_dist = require("../dist-ByPsWlTa.js");
|
|
7
|
+
const require_node = require("../node-CNPnaux4.js");
|
|
8
|
+
const require_default_detection_model = require("../default-detection-model-x6oNMBDH.js");
|
|
9
|
+
const require_worker_protocol = require("../worker-protocol-B2MfQLlu.js");
|
|
9
10
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
11
|
+
const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
|
|
11
12
|
const require_restream_intent = require("../restream-intent-B1Difcnq.js");
|
|
12
13
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
13
14
|
let node_os = require("node:os");
|
|
14
|
-
node_os =
|
|
15
|
+
node_os = require_chunk.__toESM(node_os);
|
|
15
16
|
let node_child_process = require("node:child_process");
|
|
16
17
|
let sharp = require("sharp");
|
|
17
|
-
sharp =
|
|
18
|
+
sharp = require_chunk.__toESM(sharp);
|
|
18
19
|
let node_url = require("node:url");
|
|
19
20
|
let node_perf_hooks = require("node:perf_hooks");
|
|
20
21
|
let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
|
|
@@ -73,26 +74,23 @@ function wrapChild(child, onExit) {
|
|
|
73
74
|
* signature (it forwards the same source in the subsequent `start` message);
|
|
74
75
|
* the child entry itself is source-independent, so it is not consumed here.
|
|
75
76
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
77
|
+
* The fork carries NO configuration. Until D453 it spread a set of
|
|
78
|
+
* `*_SETTING` env vars in, because the child resolved its native-lease knobs
|
|
79
|
+
* at module load; the knobs now travel on the `start` message
|
|
80
|
+
* (`WorkerRequest.lease`), resolved in this process. That is what makes a
|
|
81
|
+
* decode worker forkable BEFORE anyone knows which camera it will serve — a
|
|
82
|
+
* child configured at spawn is a child bound to the configuration it was born
|
|
83
|
+
* with — and it is what let the child bundle drop the Zod schemas that cost
|
|
84
|
+
* every fork ~170 ms and ~100 MB of heap (D453).
|
|
84
85
|
*
|
|
85
86
|
* The inherited environment is SPREAD, never replaced: this child resolves
|
|
86
87
|
* node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
|
|
87
88
|
* agents, so a replaced env would silently break decoding off the hub.
|
|
88
89
|
*/
|
|
89
|
-
function forkDecodeWorker(source,
|
|
90
|
+
function forkDecodeWorker(source, costRegistry) {
|
|
90
91
|
const child = (0, node_child_process.fork)(CHILD_ENTRY_PATH, [], {
|
|
91
92
|
serialization: "advanced",
|
|
92
|
-
env: {
|
|
93
|
-
...process.env,
|
|
94
|
-
...require_worker_protocol.nativeLeaseSettingEnv(leaseOverride)
|
|
95
|
-
}
|
|
93
|
+
env: { ...process.env }
|
|
96
94
|
});
|
|
97
95
|
const costClaim = costRegistry?.claim({
|
|
98
96
|
role: "decode",
|
|
@@ -103,6 +101,145 @@ function forkDecodeWorker(source, leaseOverride = {}, costRegistry) {
|
|
|
103
101
|
return wrapChild(child, () => costClaim.release());
|
|
104
102
|
}
|
|
105
103
|
//#endregion
|
|
104
|
+
//#region src/session-decode/native-lease-config.ts
|
|
105
|
+
/**
|
|
106
|
+
* Resolution of the native-frame hold / tile knobs for one decode session —
|
|
107
|
+
* PARENT side. The result travels to the decode worker on its `start` message
|
|
108
|
+
* (`native-lease-knobs.ts` is the wire half, and the only half the child sees).
|
|
109
|
+
*
|
|
110
|
+
* ## Where the values come from
|
|
111
|
+
*
|
|
112
|
+
* The knobs are cluster-wide `pipeline-orchestrator` settings
|
|
113
|
+
* (`@camstack/types` → `pipeline/native-lease.ts`). The pipeline-runner reads
|
|
114
|
+
* them over the hub-routed `addon-settings` cap
|
|
115
|
+
* (`pipeline-runner/native-lease-settings-source.ts`) and resolves them HERE,
|
|
116
|
+
* once per session, at the moment the session starts — so a changed setting
|
|
117
|
+
* reaches the next session and leaves running ones alone, exactly as before.
|
|
118
|
+
*
|
|
119
|
+
* Until D453 the resolution ran inside the CHILD, from `*_SETTING` env vars the
|
|
120
|
+
* parent wrote at fork time. That bound a worker to the knobs it was forked
|
|
121
|
+
* with (a pre-forked worker cannot be), and it dragged the Zod schemas — the
|
|
122
|
+
* whole `@camstack/types` index — into the child bundle: ~130 ms and ~140 MB
|
|
123
|
+
* per fork, measured. Moving the resolution up one process removed both.
|
|
124
|
+
*
|
|
125
|
+
* ## Precedence — setting > env > default
|
|
126
|
+
*
|
|
127
|
+
* 1. **setting** — the operator set the knob in the orchestrator's global
|
|
128
|
+
* settings. Wins, because a value someone deliberately chose in the UI must
|
|
129
|
+
* not be silently overruled by a shell variable nobody can see from there.
|
|
130
|
+
* 2. **env** — {@link NATIVE_LEASE_ENV_VARS}, the pre-settings mechanism, kept
|
|
131
|
+
* as an emergency / dev override. It applies only while the corresponding
|
|
132
|
+
* setting is UNSET. "Unset" includes a knob sitting AT its shipped default:
|
|
133
|
+
* `addon-settings.getGlobalSettings` hydrates an unstored field with the
|
|
134
|
+
* schema default and offers no way to tell the two apart, so treating the
|
|
135
|
+
* default as a choice would have retired every env var on the day this
|
|
136
|
+
* shipped. Move the slider off the default and the setting takes over; move
|
|
137
|
+
* it back and the env var returns. See `readNativeLeaseOverride`.
|
|
138
|
+
* 3. **default** — {@link DEFAULT_NATIVE_LEASE_SETTINGS}, byte-for-byte the
|
|
139
|
+
* constants the worker shipped with.
|
|
140
|
+
*
|
|
141
|
+
* A value that fails its bounds at ANY level falls through to the next one
|
|
142
|
+
* rather than being clamped: clamping turns a typo into a number nobody chose,
|
|
143
|
+
* and this one governs how much RAM a busy camera holds.
|
|
144
|
+
*
|
|
145
|
+
* The GPU surface ring (`CAMSTACK_SESSION_NATIVE_CROP_RING`) is deliberately
|
|
146
|
+
* NOT here — see its docblock in `decode-worker-child.ts` for why it stays
|
|
147
|
+
* env-only.
|
|
148
|
+
*/
|
|
149
|
+
/** Emergency / dev override env vars (pre-settings; lowest precedence but the default). */
|
|
150
|
+
var NATIVE_LEASE_ENV_VARS = {
|
|
151
|
+
holdFrames: "CAMSTACK_SESSION_NATIVE_LEASE_HOLD_FRAMES",
|
|
152
|
+
budgetMb: "CAMSTACK_SESSION_NATIVE_LEASE_BUDGET_MB",
|
|
153
|
+
activityMs: "CAMSTACK_SESSION_NATIVE_LEASE_ACTIVITY_MS",
|
|
154
|
+
admission: "CAMSTACK_SESSION_NATIVE_LEASE_ADMISSION",
|
|
155
|
+
tileBudgetMb: "CAMSTACK_SESSION_NATIVE_TILE_BUDGET_MB",
|
|
156
|
+
sceneBudgetMb: "CAMSTACK_SESSION_NATIVE_SCENE_BUDGET_MB"
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Validate one candidate through the knob's own Zod bound. `null` for absent,
|
|
160
|
+
* non-numeric, or out-of-bounds input — the caller then falls through to the
|
|
161
|
+
* next precedence level.
|
|
162
|
+
*/
|
|
163
|
+
function parseKnob(knob, raw) {
|
|
164
|
+
if (raw === void 0) return null;
|
|
165
|
+
if (typeof raw === "string" && raw.trim() === "") return null;
|
|
166
|
+
const parsed = require_dist.NativeLeaseSettingsSchema.shape[knob].safeParse(Number(raw));
|
|
167
|
+
return parsed.success ? parsed.data : null;
|
|
168
|
+
}
|
|
169
|
+
/** {@link parseKnob} for the one knob whose value is a word, not a number. */
|
|
170
|
+
function parseAdmission(raw) {
|
|
171
|
+
if (raw === void 0) return null;
|
|
172
|
+
const parsed = require_dist.NativeLeaseAdmissionSchema.safeParse(raw.trim());
|
|
173
|
+
return parsed.success ? parsed.data : null;
|
|
174
|
+
}
|
|
175
|
+
/** Resolve one NUMERIC knob across the three precedence levels. */
|
|
176
|
+
function resolveKnob(knob, env, override) {
|
|
177
|
+
const fromSetting = parseKnob(knob, override[knob]);
|
|
178
|
+
if (fromSetting !== null) return {
|
|
179
|
+
value: fromSetting,
|
|
180
|
+
source: "setting"
|
|
181
|
+
};
|
|
182
|
+
const fromEnv = parseKnob(knob, env[NATIVE_LEASE_ENV_VARS[knob]]);
|
|
183
|
+
if (fromEnv !== null) return {
|
|
184
|
+
value: fromEnv,
|
|
185
|
+
source: "env"
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
value: require_dist.DEFAULT_NATIVE_LEASE_SETTINGS[knob],
|
|
189
|
+
source: "default"
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** {@link resolveKnob} for `admission`. Same precedence, different parser. */
|
|
193
|
+
function resolveAdmission(env, override) {
|
|
194
|
+
const fromSetting = parseAdmission(override.admission);
|
|
195
|
+
if (fromSetting !== null) return {
|
|
196
|
+
value: fromSetting,
|
|
197
|
+
source: "setting"
|
|
198
|
+
};
|
|
199
|
+
const fromEnv = parseAdmission(env[NATIVE_LEASE_ENV_VARS.admission]);
|
|
200
|
+
if (fromEnv !== null) return {
|
|
201
|
+
value: fromEnv,
|
|
202
|
+
source: "env"
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
value: require_dist.DEFAULT_NATIVE_LEASE_SETTINGS.admission,
|
|
206
|
+
source: "default"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve every lease knob for one session. Pure.
|
|
211
|
+
*
|
|
212
|
+
* `override` carries ONLY the knobs the operator actually SET (see
|
|
213
|
+
* `native-lease-settings-source.ts`); an empty object leaves the env override
|
|
214
|
+
* fully functional, which is the whole point of the distinction.
|
|
215
|
+
*/
|
|
216
|
+
function resolveNativeLeaseKnobs(env, override) {
|
|
217
|
+
const holdFrames = resolveKnob("holdFrames", env, override);
|
|
218
|
+
const budgetMb = resolveKnob("budgetMb", env, override);
|
|
219
|
+
const activityMs = resolveKnob("activityMs", env, override);
|
|
220
|
+
const admission = resolveAdmission(env, override);
|
|
221
|
+
const tileBudgetMb = resolveKnob("tileBudgetMb", env, override);
|
|
222
|
+
const sceneBudgetMb = resolveKnob("sceneBudgetMb", env, override);
|
|
223
|
+
return {
|
|
224
|
+
values: {
|
|
225
|
+
holdFrames: holdFrames.value,
|
|
226
|
+
budgetMb: budgetMb.value,
|
|
227
|
+
activityMs: activityMs.value,
|
|
228
|
+
admission: admission.value,
|
|
229
|
+
tileBudgetMb: tileBudgetMb.value,
|
|
230
|
+
sceneBudgetMb: sceneBudgetMb.value
|
|
231
|
+
},
|
|
232
|
+
sources: {
|
|
233
|
+
holdFrames: holdFrames.source,
|
|
234
|
+
budgetMb: budgetMb.source,
|
|
235
|
+
activityMs: activityMs.source,
|
|
236
|
+
admission: admission.source,
|
|
237
|
+
tileBudgetMb: tileBudgetMb.source,
|
|
238
|
+
sceneBudgetMb: sceneBudgetMb.source
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
106
243
|
//#region src/session-decode/guarded-session-decode.ts
|
|
107
244
|
/**
|
|
108
245
|
* 2s → 5s → 15s → 30s, then 30s forever.
|
|
@@ -1349,11 +1486,12 @@ var SessionDecodeSession = class {
|
|
|
1349
1486
|
this.child.on("message", (m) => this.handleMessage(m));
|
|
1350
1487
|
this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
|
|
1351
1488
|
}
|
|
1352
|
-
start(source, opts) {
|
|
1489
|
+
start(source, opts, lease) {
|
|
1353
1490
|
this.trySend({
|
|
1354
1491
|
kind: "start",
|
|
1355
1492
|
source,
|
|
1356
|
-
opts
|
|
1493
|
+
opts,
|
|
1494
|
+
lease
|
|
1357
1495
|
});
|
|
1358
1496
|
}
|
|
1359
1497
|
/**
|
|
@@ -1521,6 +1659,7 @@ var SessionDecodeSession = class {
|
|
|
1521
1659
|
...raw.reason !== void 0 ? { workerReason: raw.reason } : {}
|
|
1522
1660
|
});
|
|
1523
1661
|
return;
|
|
1662
|
+
case "ready": return;
|
|
1524
1663
|
case "error":
|
|
1525
1664
|
this.settleError(raw.message, raw.frameId);
|
|
1526
1665
|
return;
|
|
@@ -1772,7 +1911,7 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1772
1911
|
const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
|
|
1773
1912
|
return { async *generateVideoFrames(source, opts) {
|
|
1774
1913
|
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming, deps.onPreRollReport);
|
|
1775
|
-
session.start(source, opts);
|
|
1914
|
+
session.start(source, opts, deps.lease());
|
|
1776
1915
|
try {
|
|
1777
1916
|
while (true) {
|
|
1778
1917
|
const outcome = await session.pullNext();
|
|
@@ -1839,6 +1978,7 @@ function startSessionDecodePump(deps) {
|
|
|
1839
1978
|
let dialTiming = null;
|
|
1840
1979
|
const iterator = createSessionDecodeCoordinator({
|
|
1841
1980
|
fork: deps.fork,
|
|
1981
|
+
lease: deps.lease,
|
|
1842
1982
|
logger: deps.logger,
|
|
1843
1983
|
onDialTiming: (timing) => {
|
|
1844
1984
|
dialTiming = timing;
|
|
@@ -6555,7 +6695,6 @@ function writeRetainedDetectionFrame(store, deviceId, frame, retention) {
|
|
|
6555
6695
|
if (!isContinuousRetainEnabled()) return store.allocateHandle(deviceId, frame) ?? void 0;
|
|
6556
6696
|
return store.write(deviceId, frame, frame.frameRef ? createRetainedContentFallback(require_default_detection_model.localFrameRegistry, frame.frameRef) : void 0) ?? void 0;
|
|
6557
6697
|
}
|
|
6558
|
-
var DETECTION_KEEPWARM_MS = 15e3;
|
|
6559
6698
|
/**
|
|
6560
6699
|
* Occupancy-burst safety timeout (both the dial path and the analyzer-tap
|
|
6561
6700
|
* path — Task 10). Bounds worst-case burst latency when frames stop
|
|
@@ -6582,6 +6721,31 @@ var DETECTION_REDIAL_DELAY_MS = 2e3;
|
|
|
6582
6721
|
*/
|
|
6583
6722
|
var DETECTION_REDIAL_GIVEUP_WINDOW_MS = 6e4;
|
|
6584
6723
|
/**
|
|
6724
|
+
* ## There is no decode keep-warm any more — D453 removed it
|
|
6725
|
+
*
|
|
6726
|
+
* A `WarmDetachedDecode` used to hold a detection worker alive for 15 s across
|
|
6727
|
+
* a detach, hoping the next motion event would adopt it. It did not park: it
|
|
6728
|
+
* kept its broker restream lease and kept DECODING, because the child's loop is
|
|
6729
|
+
* `for await (... decoder.frames(demuxer.packets(...)))`, driven by the demuxer
|
|
6730
|
+
* and not by the parent's `pull` — `minIntervalMs` throttles only inside
|
|
6731
|
+
* `FrameSlot.publish`, after the decode. `framesDecoded` advanced in 122 of 122
|
|
6732
|
+
* park windows at full source rate, and a decode-worker child costs 40-80% of a
|
|
6733
|
+
* core.
|
|
6734
|
+
*
|
|
6735
|
+
* So the park cost about what running costs, and it bought a re-fork back in
|
|
6736
|
+
* 12% of cases: on this fleet, 272 of 440 park windows expired having adopted
|
|
6737
|
+
* nothing, while the two decode-worker children stood at 319% of the 522% of
|
|
6738
|
+
* CPU CamStack drew on the host. D282 put the arithmetic at ~25x net-negative
|
|
6739
|
+
* against the forks it avoided, and D453 made the fork itself ~150 ms and
|
|
6740
|
+
* ~100 MB cheaper, which only widens it.
|
|
6741
|
+
*
|
|
6742
|
+
* Do not reintroduce a keep-warm shaped like this one, and do not "fix" it with
|
|
6743
|
+
* a longer TTL: the measured p50 gap between two sessions of one camera is
|
|
6744
|
+
* 61 s, so a longer window scales the waste and not the hit rate. If a warm
|
|
6745
|
+
* process is wanted, it must be a process that holds NO stream and decodes
|
|
6746
|
+
* NOTHING — see D453 for what that costs and why it was not taken either.
|
|
6747
|
+
*/
|
|
6748
|
+
/**
|
|
6585
6749
|
* Pure decision helper: returns `true` when a dynamic frame-diff analyzer
|
|
6586
6750
|
* subscription should be opened for an onboard-motion camera.
|
|
6587
6751
|
*
|
|
@@ -6907,12 +7071,6 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
6907
7071
|
retention = null;
|
|
6908
7072
|
attached = /* @__PURE__ */ new Map();
|
|
6909
7073
|
/**
|
|
6910
|
-
* Detection decode workers parked across a detach, keyed by deviceId — see
|
|
6911
|
-
* {@link WarmDetachedDecode}. Bounded by construction: at most one entry per
|
|
6912
|
-
* camera, each self-expiring after {@link DETECTION_KEEPWARM_MS}.
|
|
6913
|
-
*/
|
|
6914
|
-
warmDetachedDetection = /* @__PURE__ */ new Map();
|
|
6915
|
-
/**
|
|
6916
7074
|
* Detail step state parked across a SOFT detach, keyed by deviceId — see
|
|
6917
7075
|
* `detail-park.ts` for the fault this closes. Bounded by construction: at
|
|
6918
7076
|
* most one entry per camera, each self-expiring after
|
|
@@ -7271,14 +7429,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7271
7429
|
this.runner = null;
|
|
7272
7430
|
}
|
|
7273
7431
|
for (const attachment of this.attached.values()) {
|
|
7274
|
-
if (attachment.detectionKeepWarmTimer !== null) {
|
|
7275
|
-
clearTimeout(attachment.detectionKeepWarmTimer);
|
|
7276
|
-
attachment.detectionKeepWarmTimer = null;
|
|
7277
|
-
}
|
|
7278
7432
|
attachment.motionUnsubscribe?.();
|
|
7279
7433
|
attachment.detectionUnsubscribe?.();
|
|
7280
7434
|
}
|
|
7281
|
-
for (const deviceId of [...this.warmDetachedDetection.keys()]) this.dropWarmDetectionDecode(deviceId, "shutdown");
|
|
7282
7435
|
for (const deviceId of [...this.parkedDetail.keys()]) this.dropParkedDetail(deviceId);
|
|
7283
7436
|
this.attached.clear();
|
|
7284
7437
|
this.lastMotionFrameAt.clear();
|
|
@@ -7989,7 +8142,6 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7989
8142
|
config,
|
|
7990
8143
|
motionUnsubscribe: null,
|
|
7991
8144
|
detectionUnsubscribe: null,
|
|
7992
|
-
detectionKeepWarmTimer: null,
|
|
7993
8145
|
detailSteps: deriveDetailSteps(config.steps)
|
|
7994
8146
|
};
|
|
7995
8147
|
this.attached.set(config.deviceId, attachment);
|
|
@@ -8645,12 +8797,24 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8645
8797
|
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
8646
8798
|
} });
|
|
8647
8799
|
/**
|
|
8648
|
-
* Fork a decode worker
|
|
8649
|
-
*
|
|
8650
|
-
*
|
|
8800
|
+
* Fork a decode worker. Used in place of a bare `forkDecodeWorker` reference
|
|
8801
|
+
* at every session-decode spawn site so the cost registry is never forgotten.
|
|
8802
|
+
*
|
|
8803
|
+
* The fork carries NO configuration: since D453 the lease knobs travel on the
|
|
8804
|
+
* `start` message ({@link resolveSessionLeaseKnobs}), not in the child's
|
|
8805
|
+
* environment. A worker is therefore forkable before anyone knows which
|
|
8806
|
+
* camera it will serve.
|
|
8807
|
+
*/
|
|
8808
|
+
forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.costRegistry);
|
|
8809
|
+
/**
|
|
8810
|
+
* The lease knobs for the session that is about to start — `setting > env >
|
|
8811
|
+
* default`, resolved HERE because this process is the one that carries the
|
|
8812
|
+
* Zod schemas (the child no longer does; D453).
|
|
8813
|
+
*
|
|
8814
|
+
* Read per session, so a changed operator setting reaches NEW sessions and
|
|
8651
8815
|
* leaves running ones alone (a live session would have to be re-dialled).
|
|
8652
8816
|
*/
|
|
8653
|
-
|
|
8817
|
+
resolveSessionLeaseKnobs = () => resolveNativeLeaseKnobs(process.env, this.nativeLease.current());
|
|
8654
8818
|
/**
|
|
8655
8819
|
* Refresh the lease knobs before a decode worker is spawned. Awaited on the
|
|
8656
8820
|
* `acquire` leg of `startGuardedSessionDecode` (which always completes before
|
|
@@ -8855,19 +9019,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8855
9019
|
*/
|
|
8856
9020
|
handleDetectionDecodeEnded(deviceId) {
|
|
8857
9021
|
const attachment = this.attached.get(deviceId);
|
|
8858
|
-
if (!attachment)
|
|
8859
|
-
this.dropWarmDetectionDecode(deviceId, "worker-died-while-parked");
|
|
8860
|
-
return;
|
|
8861
|
-
}
|
|
9022
|
+
if (!attachment) return;
|
|
8862
9023
|
const log = this.ctx.logger.withTags({ deviceId });
|
|
8863
9024
|
attachment.detectionUnsubscribe?.();
|
|
8864
9025
|
attachment.detectionUnsubscribe = null;
|
|
8865
|
-
if (attachment.detectionKeepWarmTimer !== null) {
|
|
8866
|
-
clearTimeout(attachment.detectionKeepWarmTimer);
|
|
8867
|
-
attachment.detectionKeepWarmTimer = null;
|
|
8868
|
-
log.warn("detection decode worker died while kept warm — next open re-forks cold");
|
|
8869
|
-
return;
|
|
8870
|
-
}
|
|
8871
9026
|
const now = Date.now();
|
|
8872
9027
|
const last = this.lastDecodeUnexpectedEndAt.get(deviceId) ?? 0;
|
|
8873
9028
|
this.lastDecodeUnexpectedEndAt.set(deviceId, now);
|
|
@@ -8892,13 +9047,14 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8892
9047
|
/**
|
|
8893
9048
|
* Tear a camera down.
|
|
8894
9049
|
*
|
|
8895
|
-
* `
|
|
8896
|
-
*
|
|
8897
|
-
*
|
|
8898
|
-
*
|
|
8899
|
-
*
|
|
8900
|
-
*
|
|
8901
|
-
*
|
|
9050
|
+
* `softDetach` is the {@link detachCamera} cap entry point (the
|
|
9051
|
+
* orchestrator's motion-cooldown teardown). It keeps the DETAIL step tree
|
|
9052
|
+
* parked — a config slice, no process, no CPU — so a track outliving this
|
|
9053
|
+
* session can still resolve its enrichment steps. It does NOT keep the decode
|
|
9054
|
+
* worker: since D453 every detach releases it, because a held worker went on
|
|
9055
|
+
* decoding at full rate for the whole window (see the note above
|
|
9056
|
+
* {@link AttachedCamera}). Every internal detach — an attach replacing a
|
|
9057
|
+
* stale config, a decode that died twice, shutdown — is hard on both counts.
|
|
8902
9058
|
*/
|
|
8903
9059
|
/**
|
|
8904
9060
|
* Park the detaching camera's detail-relevant config slice for the grace
|
|
@@ -8945,18 +9101,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8945
9101
|
}
|
|
8946
9102
|
return parked.state;
|
|
8947
9103
|
}
|
|
8948
|
-
detachInternal(deviceId,
|
|
9104
|
+
detachInternal(deviceId, softDetach = false) {
|
|
8949
9105
|
const attachment = this.attached.get(deviceId);
|
|
8950
9106
|
if (!attachment) return;
|
|
8951
9107
|
this.clearOnboardAnalyzer(deviceId);
|
|
8952
|
-
if (attachment.detectionKeepWarmTimer !== null) {
|
|
8953
|
-
clearTimeout(attachment.detectionKeepWarmTimer);
|
|
8954
|
-
attachment.detectionKeepWarmTimer = null;
|
|
8955
|
-
}
|
|
8956
9108
|
attachment.motionUnsubscribe?.();
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
if (keepDecodeWarm) this.parkDetailState(deviceId, attachment.config);
|
|
9109
|
+
attachment.detectionUnsubscribe?.();
|
|
9110
|
+
if (softDetach) this.parkDetailState(deviceId, attachment.config);
|
|
8960
9111
|
else this.dropParkedDetail(deviceId);
|
|
8961
9112
|
this.lastMotionFrameAt.delete(deviceId);
|
|
8962
9113
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
@@ -8981,83 +9132,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8981
9132
|
this.runner?.unregisterCamera(deviceId);
|
|
8982
9133
|
this.ctx?.logger.info("detachCamera", {
|
|
8983
9134
|
tags: { deviceId },
|
|
8984
|
-
meta: {
|
|
8985
|
-
});
|
|
8986
|
-
}
|
|
8987
|
-
/**
|
|
8988
|
-
* The decode-relevant identity of a camera's detection decode. Two configs
|
|
8989
|
-
* that agree on all of it can share one worker; anything else must re-fork.
|
|
8990
|
-
* Kept in ONE place so a future decode-shaping option cannot be added to the
|
|
8991
|
-
* pump without the adoption check noticing.
|
|
8992
|
-
*/
|
|
8993
|
-
detectionDecodeIdentity(config) {
|
|
8994
|
-
return {
|
|
8995
|
-
streamId: config.detectionStreamId,
|
|
8996
|
-
fps: config.detectionFps,
|
|
8997
|
-
hostname: resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]),
|
|
8998
|
-
ownerNodeId: resolveSessionDecodeOwnerNodeId(config)
|
|
8999
|
-
};
|
|
9000
|
-
}
|
|
9001
|
-
/**
|
|
9002
|
-
* Hold a detached camera's detection decode worker for
|
|
9003
|
-
* {@link DETECTION_KEEPWARM_MS} so the next motion event reuses it.
|
|
9004
|
-
* Replacing an existing park releases the older worker first — two parked
|
|
9005
|
-
* workers for one camera would be two live decodes and two broker leases.
|
|
9006
|
-
*/
|
|
9007
|
-
parkWarmDetectionDecode(deviceId, config, unsubscribe) {
|
|
9008
|
-
this.dropWarmDetectionDecode(deviceId, "replaced");
|
|
9009
|
-
const timer = setTimeout(() => {
|
|
9010
|
-
this.warmDetachedDetection.delete(deviceId);
|
|
9011
|
-
unsubscribe();
|
|
9012
|
-
this.ctx?.logger.info("session-decode: warm detection decode expired — worker released", {
|
|
9013
|
-
tags: { deviceId },
|
|
9014
|
-
meta: { keepWarmMs: DETECTION_KEEPWARM_MS }
|
|
9015
|
-
});
|
|
9016
|
-
}, DETECTION_KEEPWARM_MS);
|
|
9017
|
-
timer.unref?.();
|
|
9018
|
-
this.warmDetachedDetection.set(deviceId, {
|
|
9019
|
-
unsubscribe,
|
|
9020
|
-
timer,
|
|
9021
|
-
...this.detectionDecodeIdentity(config)
|
|
9022
|
-
});
|
|
9023
|
-
}
|
|
9024
|
-
/**
|
|
9025
|
-
* Adopt a parked decode worker for a camera that is attaching again, or
|
|
9026
|
-
* `null` when there is nothing to adopt.
|
|
9027
|
-
*
|
|
9028
|
-
* A parked worker whose identity no longer matches is DROPPED here rather
|
|
9029
|
-
* than left to expire: the caller is about to fork a replacement, and letting
|
|
9030
|
-
* both run would double this camera's decode cost for the rest of the window.
|
|
9031
|
-
*/
|
|
9032
|
-
adoptWarmDetectionDecode(deviceId, config) {
|
|
9033
|
-
const warm = this.warmDetachedDetection.get(deviceId);
|
|
9034
|
-
if (!warm) return null;
|
|
9035
|
-
const wanted = this.detectionDecodeIdentity(config);
|
|
9036
|
-
if (warm.streamId !== wanted.streamId || warm.fps !== wanted.fps || warm.hostname !== wanted.hostname || warm.ownerNodeId !== wanted.ownerNodeId) {
|
|
9037
|
-
this.dropWarmDetectionDecode(deviceId, "config-changed");
|
|
9038
|
-
return null;
|
|
9039
|
-
}
|
|
9040
|
-
clearTimeout(warm.timer);
|
|
9041
|
-
this.warmDetachedDetection.delete(deviceId);
|
|
9042
|
-
this.ctx?.logger.info("session-decode: reusing the warm detection decode worker (no re-fork)", {
|
|
9043
|
-
tags: { deviceId },
|
|
9044
|
-
meta: {
|
|
9045
|
-
streamId: warm.streamId,
|
|
9046
|
-
fps: warm.fps
|
|
9047
|
-
}
|
|
9048
|
-
});
|
|
9049
|
-
return warm.unsubscribe;
|
|
9050
|
-
}
|
|
9051
|
-
/** Release a parked worker now. `reason` names why in the log. */
|
|
9052
|
-
dropWarmDetectionDecode(deviceId, reason) {
|
|
9053
|
-
const warm = this.warmDetachedDetection.get(deviceId);
|
|
9054
|
-
if (!warm) return;
|
|
9055
|
-
clearTimeout(warm.timer);
|
|
9056
|
-
this.warmDetachedDetection.delete(deviceId);
|
|
9057
|
-
warm.unsubscribe();
|
|
9058
|
-
this.ctx?.logger.info("session-decode: warm detection decode released", {
|
|
9059
|
-
tags: { deviceId },
|
|
9060
|
-
meta: { reason }
|
|
9135
|
+
meta: { detailParked: this.parkedDetail.has(deviceId) }
|
|
9061
9136
|
});
|
|
9062
9137
|
}
|
|
9063
9138
|
/**
|
|
@@ -9368,6 +9443,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
9368
9443
|
let reservation = null;
|
|
9369
9444
|
return startSessionDecodePump({
|
|
9370
9445
|
fork: this.forkDecodeWorkerWithLease,
|
|
9446
|
+
lease: this.resolveSessionLeaseKnobs,
|
|
9371
9447
|
logger: log,
|
|
9372
9448
|
source,
|
|
9373
9449
|
opts: {
|
|
@@ -9526,6 +9602,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
9526
9602
|
} });
|
|
9527
9603
|
return startSessionDecodePump({
|
|
9528
9604
|
fork: this.forkDecodeWorkerWithLease,
|
|
9605
|
+
lease: this.resolveSessionLeaseKnobs,
|
|
9529
9606
|
logger: log,
|
|
9530
9607
|
source,
|
|
9531
9608
|
opts: {
|
|
@@ -9572,16 +9649,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
9572
9649
|
const attachment = this.attached.get(deviceId);
|
|
9573
9650
|
if (!attachment) return;
|
|
9574
9651
|
if (action === "subscribe") {
|
|
9575
|
-
if (attachment.detectionKeepWarmTimer !== null) {
|
|
9576
|
-
clearTimeout(attachment.detectionKeepWarmTimer);
|
|
9577
|
-
attachment.detectionKeepWarmTimer = null;
|
|
9578
|
-
}
|
|
9579
9652
|
if (attachment.detectionUnsubscribe !== null) return;
|
|
9580
|
-
const adopted = this.adoptWarmDetectionDecode(deviceId, attachment.config);
|
|
9581
|
-
if (adopted) {
|
|
9582
|
-
attachment.detectionUnsubscribe = adopted;
|
|
9583
|
-
return;
|
|
9584
|
-
}
|
|
9585
9653
|
this.subscribeDetectionFrames(attachment.config).then((unsub) => {
|
|
9586
9654
|
if (!this.attached.has(deviceId)) {
|
|
9587
9655
|
unsub?.();
|
|
@@ -9590,13 +9658,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
9590
9658
|
attachment.detectionUnsubscribe = unsub;
|
|
9591
9659
|
});
|
|
9592
9660
|
} else {
|
|
9593
|
-
if (attachment.detectionKeepWarmTimer !== null) return;
|
|
9594
9661
|
if (attachment.detectionUnsubscribe === null) return;
|
|
9595
|
-
attachment.
|
|
9596
|
-
|
|
9597
|
-
attachment.detectionUnsubscribe?.();
|
|
9598
|
-
attachment.detectionUnsubscribe = null;
|
|
9599
|
-
}, DETECTION_KEEPWARM_MS);
|
|
9662
|
+
attachment.detectionUnsubscribe();
|
|
9663
|
+
attachment.detectionUnsubscribe = null;
|
|
9600
9664
|
}
|
|
9601
9665
|
}
|
|
9602
9666
|
/**
|
|
@@ -10348,6 +10412,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
10348
10412
|
const useLocalFrameRef = this.executorCoLocated() && process.env["CAMSTACK_DISABLE_LOCAL_FRAME_REF"] !== "1";
|
|
10349
10413
|
return startSessionDecodePump({
|
|
10350
10414
|
fork: this.forkDecodeWorkerWithLease,
|
|
10415
|
+
lease: this.resolveSessionLeaseKnobs,
|
|
10351
10416
|
logger: log,
|
|
10352
10417
|
source,
|
|
10353
10418
|
opts: {
|