@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.
Files changed (41) hide show
  1. package/dist/audio-analyzer/index.js +7 -7
  2. package/dist/audio-analyzer/index.mjs +4 -4
  3. package/dist/detection-pipeline/index.js +61 -23
  4. package/dist/detection-pipeline/index.mjs +53 -15
  5. package/dist/{dist-BzLiQ8Ue.js → dist-BuIP0499.js} +315 -25
  6. package/dist/{dist-C1dXLLQq.mjs → dist-CmEYRK7G.mjs} +304 -26
  7. package/dist/{event-loop-stall-monitor-DjV4Bow7.js → event-loop-stall-monitor-Bef2jXMX.js} +1 -1
  8. package/dist/{event-loop-stall-monitor-CU-xxX5c.mjs → event-loop-stall-monitor-JGTFb0Uv.mjs} +1 -1
  9. package/dist/{lazy-sharp-DxWmhTqk.js → lazy-sharp-DH2eVehm.js} +3 -3
  10. package/dist/motion-wasm/index.js +3 -3
  11. package/dist/motion-wasm/index.mjs +3 -3
  12. package/dist/node-ChdX9lv7.js +721 -0
  13. package/dist/node-J07GBWT3.mjs +690 -0
  14. package/dist/pipeline-runner/index.js +87 -18
  15. package/dist/pipeline-runner/index.mjs +87 -18
  16. package/dist/{process-memory-CZK3D3kM.js → process-memory-CJoZemF4.js} +2 -2
  17. package/dist/{process-memory-CaEwi5Cd.mjs → process-memory-DAKxUb9d.mjs} +2 -2
  18. package/dist/recorder/index.js +39 -7
  19. package/dist/recorder/index.mjs +36 -4
  20. package/dist/{segment-demux-js-BbpqygHL.js → segment-demux-js-BgXbXZnp.js} +1 -1
  21. package/dist/{segment-demux-js-njRO85gd.mjs → segment-demux-js-Ck9ly4bX.mjs} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +115 -5
  23. package/dist/session-decode/decode-worker-child.mjs +115 -5
  24. package/dist/stream-broker/_stub.js +2 -2
  25. 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
  26. 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
  27. 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
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-CP7Xhd8J.mjs → hostInit-Gd3OcDIa.mjs} +3 -3
  31. package/dist/stream-broker/index.js +245 -535
  32. package/dist/stream-broker/index.mjs +242 -530
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/{worker-protocol-DvTZBvh2.js → worker-protocol-Bpz62eyB.js} +2 -1
  35. package/dist/{worker-protocol-DxsVx0G7.mjs → worker-protocol-wMS29PG9.mjs} +2 -1
  36. package/package.json +17 -3
  37. package/dist/addon-utils-CKf8kbwh.js +0 -625
  38. package/dist/addon-utils-CgBCF-mE.mjs +0 -583
  39. 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
  40. 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
  41. 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-BzLiQ8Ue.js");
6
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DjV4Bow7.js");
7
- const require_worker_protocol = require("../worker-protocol-DvTZBvh2.js");
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-DxWmhTqk.js");
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
- /** Adapt a Node {@link ChildProcess} to the coordinator's {@link ForkedChildLike}. */
33
- function wrapChild(child) {
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(_source, leaseOverride = {}) {
79
- return wrapChild((0, node_child_process.fork)(CHILD_ENTRY_PATH, [], {
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, 8.0 s IDR interval, `Invalid NAL unit` / `corrupt decoded frame` in its
878
- * bitstream), dial → first decoded frame over the broker's own muted restream
879
- * was 1.8 / 6.5 / 7.0 / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 /
880
- * >40 s across 12 trials — against a healthy 587's 2.0 s ± 0.02.
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 EventCategory, B as defineCustomActions, H as deriveDetailCropRect, Ht as _enum, Jt as number, Kt as lazy, L as customAction, Lt as nodePin, Mt as createEvent, Ot as BaseAddon, St as errMsg, Ut as array, Wt as boolean, Yt as object, Zt as string, ct as pickNativeLeaseOverride, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, qt as literal, st as pickDetailCropConvention, u as DEFAULT_DETAIL_CROP_CONVENTION, ut as pipelineRunnerCapability } from "../dist-C1dXLLQq.mjs";
2
- import { i as resolveFrameViewGeometry, r as localFrameRegistry, t as attributeStall, u as getStepDefinition } from "../event-loop-stall-monitor-CU-xxX5c.mjs";
3
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-DxsVx0G7.mjs";
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-BtT5x-qP.mjs";
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
- /** Adapt a Node {@link ChildProcess} to the coordinator's {@link ForkedChildLike}. */
27
- function wrapChild(child) {
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(_source, leaseOverride = {}) {
73
- return wrapChild(fork(CHILD_ENTRY_PATH, [], {
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, 8.0 s IDR interval, `Invalid NAL unit` / `corrupt decoded frame` in its
872
- * bitstream), dial → first decoded frame over the broker's own muted restream
873
- * was 1.8 / 6.5 / 7.0 / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 /
874
- * >40 s across 12 trials — against a healthy 587's 2.0 s ± 0.02.
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-BzLiQ8Ue.js");
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 { it as parseProcStatus } from "./dist-C1dXLLQq.mjs";
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
@@ -1,14 +1,15 @@
1
- const require_dist = require("../dist-BzLiQ8Ue.js");
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 node_child_process = require("node:child_process");
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 node_fs = require("node:fs");
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
- }, require_addon_utils.createFileDataPlaneHandler({ getRoots: () => this.playbackRoots() }));
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": require_addon_utils.contentTypeFor(absPath),
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 = require_addon_utils.parseRangeHeader(req.headers.range, size);
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;