@camstack/addon-pipeline 1.2.136 → 1.2.138
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 +7 -7
- package/dist/audio-analyzer/index.mjs +4 -4
- package/dist/detection-pipeline/index.js +61 -23
- package/dist/detection-pipeline/index.mjs +53 -15
- package/dist/{dist-BzLiQ8Ue.js → dist-BuIP0499.js} +315 -25
- package/dist/{dist-C1dXLLQq.mjs → dist-CmEYRK7G.mjs} +304 -26
- package/dist/{event-loop-stall-monitor-DjV4Bow7.js → event-loop-stall-monitor-Bef2jXMX.js} +1 -1
- package/dist/{event-loop-stall-monitor-CU-xxX5c.mjs → event-loop-stall-monitor-JGTFb0Uv.mjs} +1 -1
- package/dist/{lazy-sharp-DxWmhTqk.js → lazy-sharp-DH2eVehm.js} +3 -3
- package/dist/motion-wasm/index.js +3 -3
- package/dist/motion-wasm/index.mjs +3 -3
- package/dist/node-ChdX9lv7.js +721 -0
- package/dist/node-J07GBWT3.mjs +690 -0
- package/dist/pipeline-runner/index.js +87 -18
- package/dist/pipeline-runner/index.mjs +87 -18
- package/dist/{process-memory-CZK3D3kM.js → process-memory-CJoZemF4.js} +2 -2
- package/dist/{process-memory-CaEwi5Cd.mjs → process-memory-DAKxUb9d.mjs} +2 -2
- package/dist/recorder/index.js +39 -7
- package/dist/recorder/index.mjs +36 -4
- package/dist/{segment-demux-js-BbpqygHL.js → segment-demux-js-BgXbXZnp.js} +1 -1
- package/dist/{segment-demux-js-njRO85gd.mjs → segment-demux-js-Ck9ly4bX.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +115 -5
- package/dist/session-decode/decode-worker-child.mjs +115 -5
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLtMmF1E.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cad8960O.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-xdmO8wYp.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-CP7Xhd8J.mjs → hostInit-Gd3OcDIa.mjs} +3 -3
- package/dist/stream-broker/index.js +245 -535
- package/dist/stream-broker/index.mjs +242 -530
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DvTZBvh2.js → worker-protocol-Bpz62eyB.js} +2 -1
- package/dist/{worker-protocol-DxsVx0G7.mjs → worker-protocol-wMS29PG9.mjs} +2 -1
- package/package.json +17 -3
- package/dist/addon-utils-CKf8kbwh.js +0 -625
- package/dist/addon-utils-CgBCF-mE.mjs +0 -583
- 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/{lazy-sharp-BtT5x-qP.mjs → lazy-sharp-DXsqwpph.mjs} +1 -1
|
@@ -2,16 +2,17 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
5
|
+
const require_dist = require("../dist-BuIP0499.js");
|
|
6
|
+
const require_node = require("../node-ChdX9lv7.js");
|
|
7
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-Bef2jXMX.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-Bpz62eyB.js");
|
|
8
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
9
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
10
|
+
const require_lazy_sharp = require("../lazy-sharp-DH2eVehm.js");
|
|
10
11
|
const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
|
|
11
12
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
13
|
+
let node_child_process = require("node:child_process");
|
|
12
14
|
let sharp = require("sharp");
|
|
13
15
|
sharp = require_dist.__toESM(sharp);
|
|
14
|
-
let node_child_process = require("node:child_process");
|
|
15
16
|
let node_url = require("node:url");
|
|
16
17
|
let node_perf_hooks = require("node:perf_hooks");
|
|
17
18
|
let node_os = require("node:os");
|
|
@@ -29,9 +30,18 @@ var KILL_SIGNALS = {
|
|
|
29
30
|
SIGKILL: "SIGKILL",
|
|
30
31
|
SIGTERM: "SIGTERM"
|
|
31
32
|
};
|
|
32
|
-
/**
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Adapt a Node {@link ChildProcess} to the coordinator's
|
|
35
|
+
* {@link ForkedChildLike}.
|
|
36
|
+
*
|
|
37
|
+
* `onExit` is invoked exactly once, on the child's real exit, before the
|
|
38
|
+
* coordinator's own listener — it is how the cost claim is released. It is a
|
|
39
|
+
* parameter rather than a second `child.on('exit')` in the caller because
|
|
40
|
+
* every listener added out here is one somebody can forget to remove.
|
|
41
|
+
*/
|
|
42
|
+
function wrapChild(child, onExit) {
|
|
34
43
|
child.on("error", () => void 0);
|
|
44
|
+
child.on("exit", onExit);
|
|
35
45
|
return {
|
|
36
46
|
send: (msg) => {
|
|
37
47
|
if (!child.connected) return;
|
|
@@ -75,14 +85,21 @@ function wrapChild(child) {
|
|
|
75
85
|
* node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
|
|
76
86
|
* agents, so a replaced env would silently break decoding off the hub.
|
|
77
87
|
*/
|
|
78
|
-
function forkDecodeWorker(
|
|
79
|
-
|
|
88
|
+
function forkDecodeWorker(source, leaseOverride = {}, costRegistry) {
|
|
89
|
+
const child = (0, node_child_process.fork)(CHILD_ENTRY_PATH, [], {
|
|
80
90
|
serialization: "advanced",
|
|
81
91
|
env: {
|
|
82
92
|
...process.env,
|
|
83
93
|
...require_worker_protocol.nativeLeaseSettingEnv(leaseOverride)
|
|
84
94
|
}
|
|
85
|
-
})
|
|
95
|
+
});
|
|
96
|
+
const costClaim = costRegistry?.claim({
|
|
97
|
+
role: "decode",
|
|
98
|
+
deviceId: source.deviceId,
|
|
99
|
+
unit: `${source.deviceId}/decode`,
|
|
100
|
+
pid: child.pid
|
|
101
|
+
}) ?? require_node.NO_COST_CLAIM;
|
|
102
|
+
return wrapChild(child, () => costClaim.release());
|
|
86
103
|
}
|
|
87
104
|
//#endregion
|
|
88
105
|
//#region src/session-decode/guarded-session-decode.ts
|
|
@@ -874,10 +891,29 @@ var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
|
|
|
874
891
|
*
|
|
875
892
|
* The first decodable frame is not a fixed cost: it is one IDR interval plus
|
|
876
893
|
* every GOP the decoder has to discard. Measured on device 618 (2560x1440
|
|
877
|
-
* h264,
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
894
|
+
* h264, `Invalid NAL unit` / `corrupt decoded frame` in its bitstream), dial →
|
|
895
|
+
* first decoded frame over the broker's own muted restream was 1.8 / 6.5 / 7.0
|
|
896
|
+
* / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 / >40 s across 12
|
|
897
|
+
* trials — against a healthy 587's 2.0 s ± 0.02.
|
|
898
|
+
*
|
|
899
|
+
* **The "8.0 s IDR interval" this used to attribute 618's spread to was wrong,
|
|
900
|
+
* and is corrected here rather than deleted** (2026-08-28): a stated number
|
|
901
|
+
* nobody re-derived reads as verification, and this one was used to argue that
|
|
902
|
+
* 618 was an encoder outlier. The fleet runs 100-frame GOPs at 25 fps — a
|
|
903
|
+
* **4 000 ms** IDR interval — and two independent readings of 6 h of live logs
|
|
904
|
+
* agree with the operator rather than with this docblock. First, the join-burst
|
|
905
|
+
* span logged on `restream prime (lat-probe)` is the time since the last IDR,
|
|
906
|
+
* uniform in `[0, IDR]` and censored at `PRIME_MAX_SPAN_MS`: its median is
|
|
907
|
+
* ~1 000 ms on eleven cameras, which is what `uniform[0, 4000]` conditioned on
|
|
908
|
+
* `≤2000` predicts. Second, `detectionIntentServed` over detection dials puts
|
|
909
|
+
* the SERVED fraction at 44-61 % on the bulk of the fleet, i.e. the ~50 %
|
|
910
|
+
* `P(span > 2000)` a 4 000 ms interval implies. 618 measures 51 %, which is the
|
|
911
|
+
* fleet norm. The genuine long-GOP outlier is **637** at 30 % served (⇒ ~6.7 s).
|
|
912
|
+
*
|
|
913
|
+
* So the spread is not this camera's encoder. It is D57's fixed 2 000 ms bound
|
|
914
|
+
* withholding the join burst WHOLE on roughly half of every 4 s-GOP camera's
|
|
915
|
+
* dials — see `join-burst-bound.ts`, which states the arithmetic, and
|
|
916
|
+
* `docs/architecture/load-ledger.md` (2026-08-28) for the measurement.
|
|
881
917
|
*
|
|
882
918
|
* Abandoning at 20 s was therefore not a timeout, it was a coin flip, and the
|
|
883
919
|
* recovery made it worse: ending the session RE-DIALS, which throws the
|
|
@@ -913,6 +949,7 @@ var SessionDecodeSession = class {
|
|
|
913
949
|
firstFrameTimeoutMs;
|
|
914
950
|
frameTimeoutMs;
|
|
915
951
|
firstFrameMaxWaitMs;
|
|
952
|
+
onDialTiming;
|
|
916
953
|
pendingPull = null;
|
|
917
954
|
pendingToBuffers = /* @__PURE__ */ new Map();
|
|
918
955
|
/** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
|
|
@@ -926,13 +963,14 @@ var SessionDecodeSession = class {
|
|
|
926
963
|
firstFrameReceived = false;
|
|
927
964
|
/** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
|
|
928
965
|
firstPullAt = 0;
|
|
929
|
-
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs) {
|
|
966
|
+
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming) {
|
|
930
967
|
this.child = child;
|
|
931
968
|
this.logger = logger;
|
|
932
969
|
this.deviceId = deviceId;
|
|
933
970
|
this.firstFrameTimeoutMs = firstFrameTimeoutMs;
|
|
934
971
|
this.frameTimeoutMs = frameTimeoutMs;
|
|
935
972
|
this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
|
|
973
|
+
this.onDialTiming = onDialTiming;
|
|
936
974
|
this.child.on("message", (m) => this.handleMessage(m));
|
|
937
975
|
this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
|
|
938
976
|
}
|
|
@@ -1108,6 +1146,15 @@ var SessionDecodeSession = class {
|
|
|
1108
1146
|
case "log":
|
|
1109
1147
|
this.logger[raw.level](raw.message);
|
|
1110
1148
|
return;
|
|
1149
|
+
case "dialTiming":
|
|
1150
|
+
this.onDialTiming?.({
|
|
1151
|
+
hwContextMs: raw.hwContextMs,
|
|
1152
|
+
demuxerOpenMs: raw.demuxerOpenMs,
|
|
1153
|
+
decoderCreateMs: raw.decoderCreateMs,
|
|
1154
|
+
firstDecodeMs: raw.firstDecodeMs,
|
|
1155
|
+
streamInfoProbed: raw.streamInfoProbed
|
|
1156
|
+
});
|
|
1157
|
+
return;
|
|
1111
1158
|
}
|
|
1112
1159
|
}
|
|
1113
1160
|
handleExit(info) {
|
|
@@ -1332,7 +1379,7 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1332
1379
|
const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
|
|
1333
1380
|
const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
|
|
1334
1381
|
return { async *generateVideoFrames(source, opts) {
|
|
1335
|
-
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs);
|
|
1382
|
+
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming);
|
|
1336
1383
|
session.start(source, opts);
|
|
1337
1384
|
try {
|
|
1338
1385
|
while (true) {
|
|
@@ -1386,9 +1433,13 @@ async function toDecodedFrame(frame, format, viewSpec) {
|
|
|
1386
1433
|
*/
|
|
1387
1434
|
function startSessionDecodePump(deps) {
|
|
1388
1435
|
const format = deps.opts.format ?? "rgb";
|
|
1436
|
+
let dialTiming = null;
|
|
1389
1437
|
const iterator = createSessionDecodeCoordinator({
|
|
1390
1438
|
fork: deps.fork,
|
|
1391
1439
|
logger: deps.logger,
|
|
1440
|
+
onDialTiming: (timing) => {
|
|
1441
|
+
dialTiming = timing;
|
|
1442
|
+
},
|
|
1392
1443
|
...deps.firstFrameTimeoutMs !== void 0 ? { firstFrameTimeoutMs: deps.firstFrameTimeoutMs } : {},
|
|
1393
1444
|
...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {}
|
|
1394
1445
|
}).generateVideoFrames(deps.source, deps.opts);
|
|
@@ -1419,9 +1470,11 @@ function startSessionDecodePump(deps) {
|
|
|
1419
1470
|
if (!firstFrameSeen) {
|
|
1420
1471
|
firstFrameSeen = true;
|
|
1421
1472
|
releaseSpawnSlotOnce();
|
|
1473
|
+
const dial = dialTiming;
|
|
1422
1474
|
deps.onFirstFrame?.({
|
|
1423
1475
|
spawnWaitMs: spawnGrantedAt - pumpStartedAt,
|
|
1424
|
-
forkToFrameMs: Date.now() - spawnGrantedAt
|
|
1476
|
+
forkToFrameMs: Date.now() - spawnGrantedAt,
|
|
1477
|
+
...dial !== null ? { dial } : {}
|
|
1425
1478
|
});
|
|
1426
1479
|
}
|
|
1427
1480
|
if (stopped) {
|
|
@@ -5210,6 +5263,12 @@ var METRICS_HEARTBEAT_MS = 3e4;
|
|
|
5210
5263
|
var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAddon {
|
|
5211
5264
|
runner = null;
|
|
5212
5265
|
/**
|
|
5266
|
+
* The decode-worker children this addon forked, claimed at the fork and
|
|
5267
|
+
* released at their exit. One registry per PROCESS; it dies with the
|
|
5268
|
+
* process, which is correct — those workers were this process's children.
|
|
5269
|
+
*/
|
|
5270
|
+
costRegistry = new require_node.ChildCostRegistry();
|
|
5271
|
+
/**
|
|
5213
5272
|
* Per-runner (per-node) decode-worker SPAWN-CONCURRENCY GATE (P5). A single
|
|
5214
5273
|
* counting semaphore every session-decode fork (detection + motion, all
|
|
5215
5274
|
* cameras on this node) passes through, sized to
|
|
@@ -5511,6 +5570,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5511
5570
|
providers: [{
|
|
5512
5571
|
capability: require_dist.pipelineRunnerCapability,
|
|
5513
5572
|
provider: this
|
|
5573
|
+
}, {
|
|
5574
|
+
capability: require_dist.loadContributionCapability,
|
|
5575
|
+
provider: { list: () => this.costRegistry.contributions() }
|
|
5514
5576
|
}],
|
|
5515
5577
|
customActions: pipelineRunnerBenchActions,
|
|
5516
5578
|
actionHandlers: {
|
|
@@ -6660,7 +6722,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
6660
6722
|
* knobs are read at spawn, so a changed setting reaches NEW sessions and
|
|
6661
6723
|
* leaves running ones alone (a live session would have to be re-dialled).
|
|
6662
6724
|
*/
|
|
6663
|
-
forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current());
|
|
6725
|
+
forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current(), this.costRegistry);
|
|
6664
6726
|
/**
|
|
6665
6727
|
* Refresh the lease knobs before a decode worker is spawned. Awaited on the
|
|
6666
6728
|
* `acquire` leg of `startGuardedSessionDecode` (which always completes before
|
|
@@ -7320,6 +7382,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7320
7382
|
dialMs,
|
|
7321
7383
|
spawnWaitMs: timing.spawnWaitMs,
|
|
7322
7384
|
forkToFrameMs: timing.forkToFrameMs,
|
|
7385
|
+
...timing.dial ? {
|
|
7386
|
+
hwContextMs: timing.dial.hwContextMs,
|
|
7387
|
+
demuxerOpenMs: timing.dial.demuxerOpenMs,
|
|
7388
|
+
decoderCreateMs: timing.dial.decoderCreateMs,
|
|
7389
|
+
firstDecodeMs: timing.dial.firstDecodeMs,
|
|
7390
|
+
streamInfoProbed: timing.dial.streamInfoProbed
|
|
7391
|
+
} : {},
|
|
7323
7392
|
firstFrameMs: Date.now() - requestedAt,
|
|
7324
7393
|
...phaseActiveAt !== void 0 ? { sincePhaseActiveMs: Date.now() - phaseActiveAt } : {}
|
|
7325
7394
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { $t as
|
|
2
|
-
import { i as
|
|
3
|
-
import {
|
|
1
|
+
import { $ as loadContributionCapability, $t as string, At as BaseAddon, B as defineCustomActions, Ct as errMsg, Gt as array, H as deriveDetailCropRect, Jt as lazy, Kt as boolean, L as customAction, Pt as createEvent, Wt as _enum, Xt as number, Yt as literal, Zt as object, ct as pickDetailCropConvention, dt as pipelineRunnerCapability, lt as pickNativeLeaseOverride, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, tn as EventCategory, u as DEFAULT_DETAIL_CROP_CONVENTION, zt as nodePin } from "../dist-CmEYRK7G.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-J07GBWT3.mjs";
|
|
3
|
+
import { i as resolveFrameViewGeometry, r as localFrameRegistry, t as attributeStall, u as getStepDefinition } from "../event-loop-stall-monitor-JGTFb0Uv.mjs";
|
|
4
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-wMS29PG9.mjs";
|
|
4
5
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
5
|
-
import { t as getSharp } from "../lazy-sharp-
|
|
6
|
+
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
6
7
|
import { n as withDetectionIntent } from "../restream-intent-B4BXZra7.mjs";
|
|
7
8
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
8
|
-
import sharp from "sharp";
|
|
9
9
|
import { fork } from "node:child_process";
|
|
10
|
+
import sharp from "sharp";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { PerformanceObserver } from "node:perf_hooks";
|
|
12
13
|
import * as os from "node:os";
|
|
@@ -23,9 +24,18 @@ var KILL_SIGNALS = {
|
|
|
23
24
|
SIGKILL: "SIGKILL",
|
|
24
25
|
SIGTERM: "SIGTERM"
|
|
25
26
|
};
|
|
26
|
-
/**
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Adapt a Node {@link ChildProcess} to the coordinator's
|
|
29
|
+
* {@link ForkedChildLike}.
|
|
30
|
+
*
|
|
31
|
+
* `onExit` is invoked exactly once, on the child's real exit, before the
|
|
32
|
+
* coordinator's own listener — it is how the cost claim is released. It is a
|
|
33
|
+
* parameter rather than a second `child.on('exit')` in the caller because
|
|
34
|
+
* every listener added out here is one somebody can forget to remove.
|
|
35
|
+
*/
|
|
36
|
+
function wrapChild(child, onExit) {
|
|
28
37
|
child.on("error", () => void 0);
|
|
38
|
+
child.on("exit", onExit);
|
|
29
39
|
return {
|
|
30
40
|
send: (msg) => {
|
|
31
41
|
if (!child.connected) return;
|
|
@@ -69,14 +79,21 @@ function wrapChild(child) {
|
|
|
69
79
|
* node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
|
|
70
80
|
* agents, so a replaced env would silently break decoding off the hub.
|
|
71
81
|
*/
|
|
72
|
-
function forkDecodeWorker(
|
|
73
|
-
|
|
82
|
+
function forkDecodeWorker(source, leaseOverride = {}, costRegistry) {
|
|
83
|
+
const child = fork(CHILD_ENTRY_PATH, [], {
|
|
74
84
|
serialization: "advanced",
|
|
75
85
|
env: {
|
|
76
86
|
...process.env,
|
|
77
87
|
...nativeLeaseSettingEnv(leaseOverride)
|
|
78
88
|
}
|
|
79
|
-
})
|
|
89
|
+
});
|
|
90
|
+
const costClaim = costRegistry?.claim({
|
|
91
|
+
role: "decode",
|
|
92
|
+
deviceId: source.deviceId,
|
|
93
|
+
unit: `${source.deviceId}/decode`,
|
|
94
|
+
pid: child.pid
|
|
95
|
+
}) ?? NO_COST_CLAIM;
|
|
96
|
+
return wrapChild(child, () => costClaim.release());
|
|
80
97
|
}
|
|
81
98
|
//#endregion
|
|
82
99
|
//#region src/session-decode/guarded-session-decode.ts
|
|
@@ -868,10 +885,29 @@ var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
|
|
|
868
885
|
*
|
|
869
886
|
* The first decodable frame is not a fixed cost: it is one IDR interval plus
|
|
870
887
|
* every GOP the decoder has to discard. Measured on device 618 (2560x1440
|
|
871
|
-
* h264,
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
888
|
+
* h264, `Invalid NAL unit` / `corrupt decoded frame` in its bitstream), dial →
|
|
889
|
+
* first decoded frame over the broker's own muted restream was 1.8 / 6.5 / 7.0
|
|
890
|
+
* / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 / >40 s across 12
|
|
891
|
+
* trials — against a healthy 587's 2.0 s ± 0.02.
|
|
892
|
+
*
|
|
893
|
+
* **The "8.0 s IDR interval" this used to attribute 618's spread to was wrong,
|
|
894
|
+
* and is corrected here rather than deleted** (2026-08-28): a stated number
|
|
895
|
+
* nobody re-derived reads as verification, and this one was used to argue that
|
|
896
|
+
* 618 was an encoder outlier. The fleet runs 100-frame GOPs at 25 fps — a
|
|
897
|
+
* **4 000 ms** IDR interval — and two independent readings of 6 h of live logs
|
|
898
|
+
* agree with the operator rather than with this docblock. First, the join-burst
|
|
899
|
+
* span logged on `restream prime (lat-probe)` is the time since the last IDR,
|
|
900
|
+
* uniform in `[0, IDR]` and censored at `PRIME_MAX_SPAN_MS`: its median is
|
|
901
|
+
* ~1 000 ms on eleven cameras, which is what `uniform[0, 4000]` conditioned on
|
|
902
|
+
* `≤2000` predicts. Second, `detectionIntentServed` over detection dials puts
|
|
903
|
+
* the SERVED fraction at 44-61 % on the bulk of the fleet, i.e. the ~50 %
|
|
904
|
+
* `P(span > 2000)` a 4 000 ms interval implies. 618 measures 51 %, which is the
|
|
905
|
+
* fleet norm. The genuine long-GOP outlier is **637** at 30 % served (⇒ ~6.7 s).
|
|
906
|
+
*
|
|
907
|
+
* So the spread is not this camera's encoder. It is D57's fixed 2 000 ms bound
|
|
908
|
+
* withholding the join burst WHOLE on roughly half of every 4 s-GOP camera's
|
|
909
|
+
* dials — see `join-burst-bound.ts`, which states the arithmetic, and
|
|
910
|
+
* `docs/architecture/load-ledger.md` (2026-08-28) for the measurement.
|
|
875
911
|
*
|
|
876
912
|
* Abandoning at 20 s was therefore not a timeout, it was a coin flip, and the
|
|
877
913
|
* recovery made it worse: ending the session RE-DIALS, which throws the
|
|
@@ -907,6 +943,7 @@ var SessionDecodeSession = class {
|
|
|
907
943
|
firstFrameTimeoutMs;
|
|
908
944
|
frameTimeoutMs;
|
|
909
945
|
firstFrameMaxWaitMs;
|
|
946
|
+
onDialTiming;
|
|
910
947
|
pendingPull = null;
|
|
911
948
|
pendingToBuffers = /* @__PURE__ */ new Map();
|
|
912
949
|
/** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
|
|
@@ -920,13 +957,14 @@ var SessionDecodeSession = class {
|
|
|
920
957
|
firstFrameReceived = false;
|
|
921
958
|
/** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
|
|
922
959
|
firstPullAt = 0;
|
|
923
|
-
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs) {
|
|
960
|
+
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming) {
|
|
924
961
|
this.child = child;
|
|
925
962
|
this.logger = logger;
|
|
926
963
|
this.deviceId = deviceId;
|
|
927
964
|
this.firstFrameTimeoutMs = firstFrameTimeoutMs;
|
|
928
965
|
this.frameTimeoutMs = frameTimeoutMs;
|
|
929
966
|
this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
|
|
967
|
+
this.onDialTiming = onDialTiming;
|
|
930
968
|
this.child.on("message", (m) => this.handleMessage(m));
|
|
931
969
|
this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
|
|
932
970
|
}
|
|
@@ -1102,6 +1140,15 @@ var SessionDecodeSession = class {
|
|
|
1102
1140
|
case "log":
|
|
1103
1141
|
this.logger[raw.level](raw.message);
|
|
1104
1142
|
return;
|
|
1143
|
+
case "dialTiming":
|
|
1144
|
+
this.onDialTiming?.({
|
|
1145
|
+
hwContextMs: raw.hwContextMs,
|
|
1146
|
+
demuxerOpenMs: raw.demuxerOpenMs,
|
|
1147
|
+
decoderCreateMs: raw.decoderCreateMs,
|
|
1148
|
+
firstDecodeMs: raw.firstDecodeMs,
|
|
1149
|
+
streamInfoProbed: raw.streamInfoProbed
|
|
1150
|
+
});
|
|
1151
|
+
return;
|
|
1105
1152
|
}
|
|
1106
1153
|
}
|
|
1107
1154
|
handleExit(info) {
|
|
@@ -1326,7 +1373,7 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1326
1373
|
const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
|
|
1327
1374
|
const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
|
|
1328
1375
|
return { async *generateVideoFrames(source, opts) {
|
|
1329
|
-
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs);
|
|
1376
|
+
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming);
|
|
1330
1377
|
session.start(source, opts);
|
|
1331
1378
|
try {
|
|
1332
1379
|
while (true) {
|
|
@@ -1380,9 +1427,13 @@ async function toDecodedFrame(frame, format, viewSpec) {
|
|
|
1380
1427
|
*/
|
|
1381
1428
|
function startSessionDecodePump(deps) {
|
|
1382
1429
|
const format = deps.opts.format ?? "rgb";
|
|
1430
|
+
let dialTiming = null;
|
|
1383
1431
|
const iterator = createSessionDecodeCoordinator({
|
|
1384
1432
|
fork: deps.fork,
|
|
1385
1433
|
logger: deps.logger,
|
|
1434
|
+
onDialTiming: (timing) => {
|
|
1435
|
+
dialTiming = timing;
|
|
1436
|
+
},
|
|
1386
1437
|
...deps.firstFrameTimeoutMs !== void 0 ? { firstFrameTimeoutMs: deps.firstFrameTimeoutMs } : {},
|
|
1387
1438
|
...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {}
|
|
1388
1439
|
}).generateVideoFrames(deps.source, deps.opts);
|
|
@@ -1413,9 +1464,11 @@ function startSessionDecodePump(deps) {
|
|
|
1413
1464
|
if (!firstFrameSeen) {
|
|
1414
1465
|
firstFrameSeen = true;
|
|
1415
1466
|
releaseSpawnSlotOnce();
|
|
1467
|
+
const dial = dialTiming;
|
|
1416
1468
|
deps.onFirstFrame?.({
|
|
1417
1469
|
spawnWaitMs: spawnGrantedAt - pumpStartedAt,
|
|
1418
|
-
forkToFrameMs: Date.now() - spawnGrantedAt
|
|
1470
|
+
forkToFrameMs: Date.now() - spawnGrantedAt,
|
|
1471
|
+
...dial !== null ? { dial } : {}
|
|
1419
1472
|
});
|
|
1420
1473
|
}
|
|
1421
1474
|
if (stopped) {
|
|
@@ -5203,6 +5256,12 @@ var METRICS_HEARTBEAT_MS = 3e4;
|
|
|
5203
5256
|
var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
5204
5257
|
runner = null;
|
|
5205
5258
|
/**
|
|
5259
|
+
* The decode-worker children this addon forked, claimed at the fork and
|
|
5260
|
+
* released at their exit. One registry per PROCESS; it dies with the
|
|
5261
|
+
* process, which is correct — those workers were this process's children.
|
|
5262
|
+
*/
|
|
5263
|
+
costRegistry = new ChildCostRegistry();
|
|
5264
|
+
/**
|
|
5206
5265
|
* Per-runner (per-node) decode-worker SPAWN-CONCURRENCY GATE (P5). A single
|
|
5207
5266
|
* counting semaphore every session-decode fork (detection + motion, all
|
|
5208
5267
|
* cameras on this node) passes through, sized to
|
|
@@ -5504,6 +5563,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5504
5563
|
providers: [{
|
|
5505
5564
|
capability: pipelineRunnerCapability,
|
|
5506
5565
|
provider: this
|
|
5566
|
+
}, {
|
|
5567
|
+
capability: loadContributionCapability,
|
|
5568
|
+
provider: { list: () => this.costRegistry.contributions() }
|
|
5507
5569
|
}],
|
|
5508
5570
|
customActions: pipelineRunnerBenchActions,
|
|
5509
5571
|
actionHandlers: {
|
|
@@ -6653,7 +6715,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6653
6715
|
* knobs are read at spawn, so a changed setting reaches NEW sessions and
|
|
6654
6716
|
* leaves running ones alone (a live session would have to be re-dialled).
|
|
6655
6717
|
*/
|
|
6656
|
-
forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current());
|
|
6718
|
+
forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current(), this.costRegistry);
|
|
6657
6719
|
/**
|
|
6658
6720
|
* Refresh the lease knobs before a decode worker is spawned. Awaited on the
|
|
6659
6721
|
* `acquire` leg of `startGuardedSessionDecode` (which always completes before
|
|
@@ -7313,6 +7375,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7313
7375
|
dialMs,
|
|
7314
7376
|
spawnWaitMs: timing.spawnWaitMs,
|
|
7315
7377
|
forkToFrameMs: timing.forkToFrameMs,
|
|
7378
|
+
...timing.dial ? {
|
|
7379
|
+
hwContextMs: timing.dial.hwContextMs,
|
|
7380
|
+
demuxerOpenMs: timing.dial.demuxerOpenMs,
|
|
7381
|
+
decoderCreateMs: timing.dial.decoderCreateMs,
|
|
7382
|
+
firstDecodeMs: timing.dial.firstDecodeMs,
|
|
7383
|
+
streamInfoProbed: timing.dial.streamInfoProbed
|
|
7384
|
+
} : {},
|
|
7316
7385
|
firstFrameMs: Date.now() - requestedAt,
|
|
7317
7386
|
...phaseActiveAt !== void 0 ? { sincePhaseActiveMs: Date.now() - phaseActiveAt } : {}
|
|
7318
7387
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
2
|
-
let node_child_process = require("node:child_process");
|
|
1
|
+
const require_dist = require("./dist-BuIP0499.js");
|
|
3
2
|
let node_fs = require("node:fs");
|
|
4
3
|
node_fs = require_dist.__toESM(node_fs);
|
|
4
|
+
let node_child_process = require("node:child_process");
|
|
5
5
|
//#region src/shared/node-topology-platform.ts
|
|
6
6
|
/**
|
|
7
7
|
* Pick a node's `{ platform, arch }` from the cluster topology. Returns an empty
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { at as parseProcStatus } from "./dist-CmEYRK7G.mjs";
|
|
3
2
|
import * as fs from "node:fs";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
4
|
//#region src/shared/node-topology-platform.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pick a node's `{ platform, arch }` from the cluster topology. Returns an empty
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
const require_dist = require("../dist-
|
|
1
|
+
const require_dist = require("../dist-BuIP0499.js");
|
|
2
|
+
const require_node = require("../node-ChdX9lv7.js");
|
|
2
3
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
3
4
|
const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
|
|
4
|
-
const require_addon_utils = require("../addon-utils-CKf8kbwh.js");
|
|
5
5
|
const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
|
-
let
|
|
7
|
+
let node_fs = require("node:fs");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_dist.__toESM(node_path);
|
|
10
|
-
let
|
|
10
|
+
let node_child_process = require("node:child_process");
|
|
11
11
|
let node_fs_promises = require("node:fs/promises");
|
|
12
|
+
let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
|
|
12
13
|
//#region src/recorder/event-capture.ts
|
|
13
14
|
function markerFromMotion(data, pad) {
|
|
14
15
|
if (!data.detected) return null;
|
|
@@ -6225,6 +6226,7 @@ Object.freeze({
|
|
|
6225
6226
|
"listProfiles",
|
|
6226
6227
|
"listRuntimeNodes"
|
|
6227
6228
|
],
|
|
6229
|
+
"load-contribution": ["list"],
|
|
6228
6230
|
"log-channels": ["list"],
|
|
6229
6231
|
"log-destination": ["query"],
|
|
6230
6232
|
"login-method": ["getLoginMethods"],
|
|
@@ -8785,6 +8787,12 @@ var SegmentWriter = class {
|
|
|
8785
8787
|
startedAt = 0;
|
|
8786
8788
|
exitWait = null;
|
|
8787
8789
|
resolveExit = null;
|
|
8790
|
+
/**
|
|
8791
|
+
* The cost claim for the CURRENT ffmpeg generation. Taken in `start()` and
|
|
8792
|
+
* released in `onTermination()` — the two edges of one process, so a restart
|
|
8793
|
+
* storm cannot accumulate claims for children that are already gone.
|
|
8794
|
+
*/
|
|
8795
|
+
costClaim = require_node.NO_COST_CLAIM;
|
|
8788
8796
|
constructor(cfg, deps) {
|
|
8789
8797
|
this.cfg = cfg;
|
|
8790
8798
|
this.deps = deps;
|
|
@@ -8798,6 +8806,7 @@ var SegmentWriter = class {
|
|
|
8798
8806
|
this.resolveExit = resolve;
|
|
8799
8807
|
});
|
|
8800
8808
|
this.startedAt = Date.now();
|
|
8809
|
+
this.costClaim = this.deps.claimCost?.(proc.pid) ?? require_node.NO_COST_CLAIM;
|
|
8801
8810
|
const stderrTail = [];
|
|
8802
8811
|
proc.stderr?.on("data", (chunk) => {
|
|
8803
8812
|
const text = chunk instanceof Buffer ? chunk.toString("utf8") : String(chunk);
|
|
@@ -8839,6 +8848,8 @@ var SegmentWriter = class {
|
|
|
8839
8848
|
* recording one camera must be greppable by that camera.
|
|
8840
8849
|
*/
|
|
8841
8850
|
onTermination(termination, stderrTail) {
|
|
8851
|
+
this.costClaim.release();
|
|
8852
|
+
this.costClaim = require_node.NO_COST_CLAIM;
|
|
8842
8853
|
this.resolveExit?.();
|
|
8843
8854
|
this.resolveExit = null;
|
|
8844
8855
|
this.proc = null;
|
|
@@ -10195,6 +10206,12 @@ var RecordingController = class {
|
|
|
10195
10206
|
spawn: this.deps.spawn,
|
|
10196
10207
|
logger: deviceLog,
|
|
10197
10208
|
deviceId,
|
|
10209
|
+
claimCost: (pid) => this.deps.costRegistry?.claim({
|
|
10210
|
+
role: "recording",
|
|
10211
|
+
deviceId,
|
|
10212
|
+
unit: `${deviceId}/${profile}`,
|
|
10213
|
+
pid
|
|
10214
|
+
}) ?? require_node.NO_COST_CLAIM,
|
|
10198
10215
|
onGaveUp: () => {
|
|
10199
10216
|
this.recoverWriterGaveUp(deviceId, profile);
|
|
10200
10217
|
},
|
|
@@ -12418,6 +12435,16 @@ function isSegmentStoreLogExtras(extras) {
|
|
|
12418
12435
|
}
|
|
12419
12436
|
var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
12420
12437
|
nodeId = "hub";
|
|
12438
|
+
/**
|
|
12439
|
+
* The children this addon started, claimed at the spawn site and released at
|
|
12440
|
+
* their exit. It is what `load-contribution` reports: the recorder knows it
|
|
12441
|
+
* ran an ffmpeg for camera 615 profile `high`, so nobody else has to work it
|
|
12442
|
+
* out from a process table.
|
|
12443
|
+
*
|
|
12444
|
+
* One registry per PROCESS, and it dies with the process — which is correct,
|
|
12445
|
+
* because those children were this process's children.
|
|
12446
|
+
*/
|
|
12447
|
+
costRegistry = new require_node.ChildCostRegistry();
|
|
12421
12448
|
/** True when THIS node is the designated recording node (`recordingNodeId`).
|
|
12422
12449
|
* Every other node stays fully inert — registers no providers, serves no
|
|
12423
12450
|
* playback, runs no controller. Resolved in `onInitialize`. */
|
|
@@ -12764,6 +12791,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
12764
12791
|
placeProfile: (deviceId, profile) => this.placeProfile(deviceId, profile),
|
|
12765
12792
|
onPlacementTick: (deviceIds) => this.recomputePlacement(deviceIds),
|
|
12766
12793
|
spawn: node_child_process.spawn,
|
|
12794
|
+
costRegistry: this.costRegistry,
|
|
12767
12795
|
segmentSeconds: this.config.segmentSeconds,
|
|
12768
12796
|
watchIntervalMs: this.config.watchIntervalMs,
|
|
12769
12797
|
...consumerHostname !== void 0 ? { consumerHostname } : {},
|
|
@@ -12944,7 +12972,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
12944
12972
|
source: this.calendar,
|
|
12945
12973
|
mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
|
|
12946
12974
|
onLog: (msg, meta) => this.ctx.logger.debug(msg, { meta })
|
|
12947
|
-
},
|
|
12975
|
+
}, (0, _camstack_system_addon_utils.createFileDataPlaneHandler)({ getRoots: () => this.playbackRoots() }));
|
|
12948
12976
|
const served = await this.ctx.dataPlane?.serve({
|
|
12949
12977
|
prefix: PLAYBACK_PREFIX,
|
|
12950
12978
|
access: "authenticated",
|
|
@@ -13012,6 +13040,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
13012
13040
|
{
|
|
13013
13041
|
capability: require_dist.recordingExportCapability,
|
|
13014
13042
|
provider: exportProvider
|
|
13043
|
+
},
|
|
13044
|
+
{
|
|
13045
|
+
capability: require_dist.loadContributionCapability,
|
|
13046
|
+
provider: { list: () => this.costRegistry.contributions() }
|
|
13015
13047
|
}
|
|
13016
13048
|
],
|
|
13017
13049
|
customActions: recorderDebugActions,
|
|
@@ -13784,11 +13816,11 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
13784
13816
|
return;
|
|
13785
13817
|
}
|
|
13786
13818
|
const baseHeaders = {
|
|
13787
|
-
"content-type":
|
|
13819
|
+
"content-type": (0, _camstack_system_addon_utils.contentTypeFor)(absPath),
|
|
13788
13820
|
"accept-ranges": "bytes",
|
|
13789
13821
|
"cache-control": "no-cache"
|
|
13790
13822
|
};
|
|
13791
|
-
const range =
|
|
13823
|
+
const range = (0, _camstack_system_addon_utils.parseRangeHeader)(req.headers.range, size);
|
|
13792
13824
|
const servesFinalByte = range ? range.end === size - 1 : true;
|
|
13793
13825
|
const recordDownloadOnFinish = () => {
|
|
13794
13826
|
if (req.method !== "GET" || !servesFinalByte) return;
|