@camstack/addon-pipeline 1.2.135 → 1.2.137

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/{addon-utils-CKf8kbwh.js → addon-utils-C6b5Tnia.js} +3 -3
  2. package/dist/audio-analyzer/index.js +5 -5
  3. package/dist/audio-analyzer/index.mjs +4 -4
  4. package/dist/detection-pipeline/index.js +55 -17
  5. package/dist/detection-pipeline/index.mjs +53 -15
  6. package/dist/{dist-BzLiQ8Ue.js → dist-4EMKvxCp.js} +275 -25
  7. package/dist/{dist-C1dXLLQq.mjs → dist-g0Or5_pR.mjs} +270 -26
  8. package/dist/{event-loop-stall-monitor-CU-xxX5c.mjs → event-loop-stall-monitor-CskL9Lgu.mjs} +1 -1
  9. package/dist/{event-loop-stall-monitor-DjV4Bow7.js → event-loop-stall-monitor-DDm07Ei6.js} +1 -1
  10. package/dist/{lazy-sharp-DxWmhTqk.js → lazy-sharp-BlA9PxWk.js} +3 -3
  11. package/dist/motion-wasm/index.js +3 -3
  12. package/dist/motion-wasm/index.mjs +3 -3
  13. package/dist/node-BOHn8gpk.js +721 -0
  14. package/dist/node-Bo1g8hLO.mjs +690 -0
  15. package/dist/pipeline-runner/index.js +37 -11
  16. package/dist/pipeline-runner/index.mjs +37 -11
  17. package/dist/{process-memory-CZK3D3kM.js → process-memory-C3084DOt.js} +2 -2
  18. package/dist/{process-memory-CaEwi5Cd.mjs → process-memory-DmrHWybb.mjs} +2 -2
  19. package/dist/recorder/index.js +382 -35
  20. package/dist/recorder/index.mjs +382 -35
  21. package/dist/{segment-demux-js-BbpqygHL.js → segment-demux-js-BrQ_A0oi.js} +1 -1
  22. package/dist/{segment-demux-js-njRO85gd.mjs → segment-demux-js-C-a2pghM.mjs} +1 -1
  23. package/dist/session-decode/decode-worker-child.js +2 -2
  24. package/dist/session-decode/decode-worker-child.mjs +2 -2
  25. package/dist/stream-broker/_stub.js +2 -2
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C14Qqrqn.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-AH6OJ3hJ.mjs} +3 -3
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQyiEd0l.mjs +26 -0
  28. 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
  29. package/dist/stream-broker/demux-worker-child.js +1 -1
  30. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  31. package/dist/stream-broker/{hostInit-kjsveCjG.mjs → hostInit-XNyn-FTM.mjs} +3 -3
  32. package/dist/stream-broker/index.js +88 -515
  33. package/dist/stream-broker/index.mjs +85 -510
  34. package/dist/stream-broker/remoteEntry.js +1 -1
  35. package/dist/{worker-protocol-DvTZBvh2.js → worker-protocol-BLfaD5uC.js} +1 -1
  36. package/dist/{worker-protocol-DxsVx0G7.mjs → worker-protocol-vc8WWhjx.mjs} +1 -1
  37. package/package.json +17 -3
  38. 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
  39. 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
  40. package/dist/{addon-utils-CgBCF-mE.mjs → addon-utils-C8KTuPdE.mjs} +2 -2
  41. package/dist/{lazy-sharp-BtT5x-qP.mjs → lazy-sharp-DXsqwpph.mjs} +1 -1
@@ -2,27 +2,26 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BzLiQ8Ue.js");
5
+ const require_dist = require("../dist-4EMKvxCp.js");
6
+ const require_node = require("../node-BOHn8gpk.js");
6
7
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
7
8
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
9
  const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
9
- const require_segment_demux_js = require("../segment-demux-js-BbpqygHL.js");
10
+ const require_segment_demux_js = require("../segment-demux-js-BrQ_A0oi.js");
10
11
  let node_crypto = require("node:crypto");
11
- node_crypto = require_dist.__toESM(node_crypto, 1);
12
+ node_crypto = require_dist.__toESM(node_crypto);
13
+ let node_fs = require("node:fs");
14
+ node_fs = require_dist.__toESM(node_fs);
15
+ let node_path = require("node:path");
16
+ node_path = require_dist.__toESM(node_path);
12
17
  let node_child_process = require("node:child_process");
18
+ let node_fs_promises = require("node:fs/promises");
13
19
  let node_url = require("node:url");
14
20
  let node_module = require("node:module");
15
- let node_path = require("node:path");
16
- require_dist.__toESM(node_path, 1);
17
- node_path = require_dist.__toESM(node_path);
18
21
  let node_os = require("node:os");
19
22
  node_os = require_dist.__toESM(node_os);
20
- let node_fs = require("node:fs");
21
- require_dist.__toESM(node_fs, 1);
22
- node_fs = require_dist.__toESM(node_fs);
23
23
  let node_net = require("node:net");
24
24
  node_net = require_dist.__toESM(node_net);
25
- let node_fs_promises = require("node:fs/promises");
26
25
  let node_dgram = require("node:dgram");
27
26
  let net = require("net");
28
27
  let events = require("events");
@@ -1266,6 +1265,9 @@ var FfmpegPushReader = class {
1266
1265
  spawnFn;
1267
1266
  callbacks;
1268
1267
  logLabel;
1268
+ claimCost;
1269
+ /** The claim for the child currently running, if any. */
1270
+ costClaim = require_node.NO_COST_CLAIM;
1269
1271
  child = null;
1270
1272
  demux = null;
1271
1273
  audioCodec;
@@ -1283,6 +1285,7 @@ var FfmpegPushReader = class {
1283
1285
  this.spawnFn = options.spawnFn ?? node_child_process.spawn;
1284
1286
  this.callbacks = callbacks;
1285
1287
  this.logLabel = logLabel;
1288
+ this.claimCost = options.claimCost;
1286
1289
  }
1287
1290
  /** Spawn ffmpeg and pipe its MPEG-TS stdout into the demux. */
1288
1291
  connect() {
@@ -1304,6 +1307,7 @@ var FfmpegPushReader = class {
1304
1307
  return;
1305
1308
  }
1306
1309
  this.child = child;
1310
+ this.costClaim = this.claimCost?.(child.pid) ?? require_node.NO_COST_CLAIM;
1307
1311
  child.stdin?.on("error", () => {});
1308
1312
  child.stdout?.on("data", (chunk) => demux.push(chunk));
1309
1313
  child.stderr?.on("data", (chunk) => this.appendStderr(chunk));
@@ -1397,6 +1401,8 @@ var FfmpegPushReader = class {
1397
1401
  fireTeardown() {
1398
1402
  if (this.toreDown) return;
1399
1403
  this.toreDown = true;
1404
+ this.costClaim.release();
1405
+ this.costClaim = require_node.NO_COST_CLAIM;
1400
1406
  this.callbacks.onTeardown?.();
1401
1407
  }
1402
1408
  /** SIGTERM, then SIGKILL if the child has not actually exited (gate on exit
@@ -4564,506 +4570,6 @@ function pushDimension(into, constraint, requested, known) {
4564
4570
  reason: `source ${known} > requested ${requested}`
4565
4571
  });
4566
4572
  }
4567
- /**
4568
- * Fmp4FragmentPlane — a SUBSCRIBABLE fragmented-MP4 plane, fed by one
4569
- * {@link import('./fmp4-box-splitter.js').Fmp4BoxSplitter}.
4570
- *
4571
- * ## Why a plane and not a callback
4572
- *
4573
- * The operator's requirement for HKSV was explicit: the live fMP4 source built
4574
- * for it must be **dual-use**, so a HomeKit-triggered recording also lands in
4575
- * CamStack as an additional videoclip source alongside the recorder and the NC
4576
- * clip ring — *one fragmenter, two consumers; do not build an HKSV-only pipe*
4577
- * (`docs/roadmap.md` item 4b). A single-callback pipe makes the second consumer
4578
- * a second ffmpeg child of the same camera. So this is the same shape the
4579
- * broker's other multi-consumer surfaces already have
4580
- * (`AudioChunkPlane`, the push packet plane): N independent subscriptions over
4581
- * one producer.
4582
- *
4583
- * **Nothing consumes it yet.** Phase 4 brings the HKSV delegate and phase 4b the
4584
- * clip source; both are named here so the seam is not re-invented, and neither
4585
- * is built.
4586
- *
4587
- * ## The init segment is RETAINED
4588
- *
4589
- * A subscriber that attaches mid-stream — the clip consumer joining an already
4590
- * running HKSV session, which is the whole dual-use case — receives the
4591
- * retained `ftyp`+`moov` as its first packet and then live fragments. Without
4592
- * retention its fragments are undecodable and the failure looks like a codec
4593
- * problem.
4594
- *
4595
- * ## A slow subscriber is CLOSED, never silently gapped
4596
- *
4597
- * `AudioChunkPlane` drops its oldest chunk on overflow, which for audio costs a
4598
- * click. An fMP4 stream with a hole is not a shorter clip, it is a corrupt one:
4599
- * `moof` sequence numbers jump, the consumer's demuxer desynchronises, and HKSV
4600
- * shows a clip that fails to play with nothing anywhere saying why. So a
4601
- * subscription whose queue overflows is ENDED with a reason, loudly, and the
4602
- * other subscriptions are untouched.
4603
- *
4604
- * ## The PREBUFFER (phase 3)
4605
- *
4606
- * HKSV asks for context BEFORE the trigger — `CameraRecordingOptions.prebufferLength`
4607
- * is a HAP-mandated minimum of 4000 ms — and a subscriber that attaches at the
4608
- * motion edge has none. So the plane optionally retains the last few fragments
4609
- * and replays them to a subscriber that asks for them.
4610
- *
4611
- * Three things this ring gets right, each of which is a measured fact rather
4612
- * than a preference (see [D84](../../../../docs/decisions/adr-0084.md)):
4613
- *
4614
- * - **It is bounded by TIME *and* BYTES.** On the live fleet a 720p copy
4615
- * fragment is ~255 KB and a 4K one is ~6.35 MB — a 25× spread over the same
4616
- * window. A time-only bound is a per-camera RAM figure nobody can predict.
4617
- * - **The window is measured on ARRIVAL, not parsed from `tfdt`.** The
4618
- * splitter deliberately never computes a fragment's duration (a second
4619
- * opinion about a fact the muxer owns), and a prebuffer cares about how long
4620
- * ago the bytes turned up, which is exactly what arrival time answers.
4621
- * - **A replay is not backlog.** A subscriber taking N retained fragments gets
4622
- * its queue capacity raised by N for them, because closing a subscriber as a
4623
- * slow consumer for the prebuffer it explicitly asked for would be the
4624
- * stupidest possible failure — and, with `DEFAULT_QUEUE_CAPACITY` of 4 and a
4625
- * ring of 4, the guaranteed one.
4626
- *
4627
- * ## `isLast`
4628
- *
4629
- * hap-nodejs requires the delegate to mark exactly one `RecordingPacket` with
4630
- * `isLast` — a generator that finishes without it produces the twelve-second
4631
- * timeout loop [D50](../../../../../docs/decisions/adr-0050.md) deleted. The
4632
- * plane therefore computes it at DELIVERY time: a packet is last when the plane
4633
- * has ended and nothing remains queued behind it. A subscription that ends
4634
- * having delivered NOTHING says so through {@link Fmp4Subscription.delivered};
4635
- * the future delegate must not open an HDS stream it cannot feed.
4636
- */
4637
- var DEFAULT_QUEUE_CAPACITY = 4;
4638
- var Fmp4FragmentPlane = class {
4639
- logger;
4640
- prebuffer;
4641
- now;
4642
- subscriptions = /* @__PURE__ */ new Map();
4643
- /** The last init unit seen, handed to every later subscriber. */
4644
- retainedInit = null;
4645
- ended = false;
4646
- /** Oldest first. Empty unless {@link Fmp4PrebufferOptions} was supplied. */
4647
- ring = [];
4648
- ringBytes = 0;
4649
- constructor(logger, prebuffer, now = Date.now) {
4650
- this.logger = logger;
4651
- this.prebuffer = prebuffer;
4652
- this.now = now;
4653
- }
4654
- get subscriberCount() {
4655
- return this.subscriptions.size;
4656
- }
4657
- /** True once {@link end} has been called — no further units are accepted. */
4658
- get isEnded() {
4659
- return this.ended;
4660
- }
4661
- /** What the prebuffer ring holds right now. All zeroes when disabled. */
4662
- prebufferStats() {
4663
- const oldest = this.ring[0];
4664
- return {
4665
- fragments: this.ring.length,
4666
- bytes: this.ringBytes,
4667
- spanMs: oldest === void 0 ? 0 : this.now() - oldest.arrivedAt
4668
- };
4669
- }
4670
- subscribe(input) {
4671
- const replay = input.withPrebuffer === true ? this.trimmedRing() : [];
4672
- const requested = Math.max(1, input.queueCapacity ?? DEFAULT_QUEUE_CAPACITY);
4673
- const sub = {
4674
- id: `fmp4-${(0, node_crypto.randomUUID)()}`,
4675
- tag: input.tag,
4676
- subscribedAt: this.now(),
4677
- capacity: requested + replay.length,
4678
- queue: [],
4679
- delivered: 0,
4680
- closedReason: null,
4681
- wake: null,
4682
- iterating: false
4683
- };
4684
- this.subscriptions.set(sub.id, sub);
4685
- if (this.retainedInit !== null) this.enqueue(sub, this.retainedInit);
4686
- for (const retained of replay) this.enqueue(sub, retained.unit);
4687
- if (this.ended) this.closeSubscription(sub, "ended");
4688
- this.logger?.info("fmp4 plane: subscribed", { meta: {
4689
- subscriptionId: sub.id,
4690
- tag: sub.tag,
4691
- hasRetainedInit: this.retainedInit !== null,
4692
- prebufferFragments: replay.length,
4693
- prebufferBytes: replay.reduce((n, r) => n + r.unit.data.length, 0)
4694
- } });
4695
- return this.facade(sub);
4696
- }
4697
- /**
4698
- * Fan one splitter unit out. An `init` REPLACES the retained one — ffmpeg
4699
- * emits exactly one per child, and a second means the child was respawned, in
4700
- * which case the old one describes a stream that no longer exists.
4701
- */
4702
- publish(unit) {
4703
- if (this.ended) return;
4704
- if (unit.kind === "init") {
4705
- this.retainedInit = unit;
4706
- this.ring.length = 0;
4707
- this.ringBytes = 0;
4708
- } else this.retain(unit);
4709
- for (const sub of this.subscriptions.values()) {
4710
- if (sub.closedReason !== null) continue;
4711
- this.enqueue(sub, unit);
4712
- }
4713
- }
4714
- /**
4715
- * The producer stopped. Every subscriber drains what it holds; its final
4716
- * packet carries `isLast`, and its generator then completes.
4717
- */
4718
- end(reason = "producer ended") {
4719
- if (this.ended) return;
4720
- this.ended = true;
4721
- this.logger?.info("fmp4 plane: ended", { meta: {
4722
- reason,
4723
- subscribers: this.subscriptions.size
4724
- } });
4725
- for (const sub of this.subscriptions.values()) if (sub.closedReason === null) this.closeSubscription(sub, "ended");
4726
- }
4727
- listSubscribers() {
4728
- return [...this.subscriptions.values()].map((s) => ({
4729
- tag: s.tag,
4730
- subscribedAt: s.subscribedAt,
4731
- delivered: s.delivered,
4732
- closedReason: s.closedReason
4733
- }));
4734
- }
4735
- /** End and forget everything. Idempotent. */
4736
- dispose() {
4737
- this.end("disposed");
4738
- this.subscriptions.clear();
4739
- this.retainedInit = null;
4740
- this.ring.length = 0;
4741
- this.ringBytes = 0;
4742
- }
4743
- /**
4744
- * Add one fragment to the ring and evict from the front until BOTH bounds
4745
- * hold. Eviction is oldest-first, which is the one place in this file where
4746
- * dropping is correct: the ring is context, not stream — nobody is mid-decode
4747
- * on it, and a subscriber only ever receives a contiguous tail of it.
4748
- */
4749
- retain(unit) {
4750
- const prebuffer = this.prebuffer;
4751
- if (prebuffer === void 0) return;
4752
- const arrivedAt = this.now();
4753
- this.ring.push({
4754
- unit,
4755
- arrivedAt
4756
- });
4757
- this.ringBytes += unit.data.length;
4758
- const cutoff = arrivedAt - prebuffer.windowMs;
4759
- while (this.ring.length > 0) {
4760
- const oldest = this.ring[0];
4761
- if (oldest === void 0) break;
4762
- const tooOld = oldest.arrivedAt < cutoff;
4763
- const tooBig = this.ringBytes > prebuffer.maxBytes;
4764
- if (!tooOld && !tooBig || this.ring.length === 1) break;
4765
- this.ring.shift();
4766
- this.ringBytes -= oldest.unit.data.length;
4767
- }
4768
- }
4769
- /**
4770
- * The ring as a subscriber should receive it — window applied AT SUBSCRIBE
4771
- * time, not only at publish time. A camera that went quiet keeps its last
4772
- * fragment in the ring indefinitely (see the never-evict-the-newest rule),
4773
- * and replaying a 40-second-old fragment as "prebuffer" would put stale video
4774
- * at the head of a clip iOS presents as the moment of the event.
4775
- */
4776
- trimmedRing() {
4777
- const prebuffer = this.prebuffer;
4778
- if (prebuffer === void 0) return [];
4779
- const cutoff = this.now() - prebuffer.windowMs;
4780
- return this.ring.filter((r) => r.arrivedAt >= cutoff);
4781
- }
4782
- enqueue(sub, unit) {
4783
- if (sub.queue.length >= sub.capacity) {
4784
- this.logger?.warn("fmp4 plane: subscriber fell behind — CLOSING it rather than gapping it", { meta: {
4785
- subscriptionId: sub.id,
4786
- tag: sub.tag,
4787
- capacity: sub.capacity,
4788
- delivered: sub.delivered
4789
- } });
4790
- this.closeSubscription(sub, "slow-consumer");
4791
- return;
4792
- }
4793
- sub.queue.push({
4794
- kind: unit.kind,
4795
- data: unit.data,
4796
- sequence: unit.sequence,
4797
- isLast: false
4798
- });
4799
- this.wake(sub);
4800
- }
4801
- closeSubscription(sub, reason) {
4802
- if (sub.closedReason !== null) return;
4803
- sub.closedReason = reason;
4804
- if (reason === "slow-consumer") sub.queue.length = 0;
4805
- this.wake(sub);
4806
- }
4807
- wake(sub) {
4808
- const resume = sub.wake;
4809
- sub.wake = null;
4810
- resume?.();
4811
- }
4812
- facade(sub) {
4813
- const plane = this;
4814
- return {
4815
- id: sub.id,
4816
- tag: sub.tag,
4817
- get delivered() {
4818
- return sub.delivered;
4819
- },
4820
- get closedReason() {
4821
- return sub.closedReason;
4822
- },
4823
- packets: () => plane.iterate(sub),
4824
- release: () => {
4825
- plane.closeSubscription(sub, "released");
4826
- plane.subscriptions.delete(sub.id);
4827
- }
4828
- };
4829
- }
4830
- async *iterate(sub) {
4831
- if (sub.iterating) throw new Error(`fmp4 plane: subscription ${sub.tag} is already being consumed — take a second subscription`);
4832
- sub.iterating = true;
4833
- for (;;) {
4834
- const next = sub.queue.shift();
4835
- if (next === void 0) {
4836
- if (sub.closedReason !== null) return;
4837
- await new Promise((resolve) => {
4838
- sub.wake = resolve;
4839
- });
4840
- continue;
4841
- }
4842
- const isLast = sub.closedReason === "ended" && sub.queue.length === 0;
4843
- sub.delivered += 1;
4844
- yield {
4845
- ...next,
4846
- isLast
4847
- };
4848
- if (isLast) return;
4849
- }
4850
- }
4851
- };
4852
- var DEFAULT_FIRST_UNIT_TIMEOUT_MS = 12e3;
4853
- /** Heartbeat cadence — ~2 minutes of 4 s fragments. */
4854
- var FRAGMENT_LOG_EVERY = 30;
4855
- var Fmp4FragmentChild = class {
4856
- deps;
4857
- args;
4858
- child = null;
4859
- splitter = new require_dist.Fmp4BoxSplitter();
4860
- stopped = false;
4861
- unitsOut = 0;
4862
- activeHwAccel = null;
4863
- constructor(deps, args) {
4864
- this.deps = deps;
4865
- this.args = args;
4866
- }
4867
- /** Spawn, and resolve once the INIT segment has been cut out of stdout. */
4868
- async start() {
4869
- const requested = this.args.invocation.decodeHwAccel;
4870
- this.activeHwAccel = requested;
4871
- try {
4872
- await this.spawnAttempt(requested);
4873
- return;
4874
- } catch (err) {
4875
- if (this.stopped) throw err;
4876
- if (requested === null || require_dist.isSoftwareDecode(requested)) throw err;
4877
- this.deps.logger.warn("fmp4 fragment child: hardware decode produced NO fragment — retrying in SOFTWARE", {
4878
- tags: { deviceId: this.args.deviceId },
4879
- meta: {
4880
- sourceId: this.args.sourceId,
4881
- decodeHwAccel: requested,
4882
- error: require_dist.errMsg(err)
4883
- }
4884
- });
4885
- this.killChild();
4886
- this.splitter = new require_dist.Fmp4BoxSplitter();
4887
- this.activeHwAccel = null;
4888
- await this.spawnAttempt(null);
4889
- }
4890
- }
4891
- /** The backend the child ACTUALLY ran with — `null` for software. */
4892
- activeDecodeHwAccel() {
4893
- const value = this.activeHwAccel;
4894
- return value === null || value === "none" || value === "copy" ? null : value;
4895
- }
4896
- /** Kill ffmpeg and end the plane. Idempotent. */
4897
- async stop() {
4898
- if (this.stopped) return;
4899
- this.stopped = true;
4900
- this.killChild();
4901
- this.args.plane.end("the fragment child stopped");
4902
- }
4903
- spawnAttempt(decodeHwAccel) {
4904
- const args = require_dist.buildFfmpegArgs({
4905
- ...this.args.invocation,
4906
- decodeHwAccel,
4907
- sink: {
4908
- kind: "stdout",
4909
- container: "mp4",
4910
- fragmentMs: this.args.fragmentMs
4911
- }
4912
- });
4913
- this.deps.logger.info("fmp4 fragment child: spawning ffmpeg", {
4914
- tags: { deviceId: this.args.deviceId },
4915
- meta: {
4916
- sourceId: this.args.sourceId,
4917
- fragmentMs: this.args.fragmentMs,
4918
- decodeHwAccel: decodeHwAccel ?? "software",
4919
- argv: args.join(" ")
4920
- }
4921
- });
4922
- return new Promise((resolve, reject) => {
4923
- const child = this.deps.spawnFn(this.deps.ffmpegBinaryPath, args, { stdio: [
4924
- "ignore",
4925
- "pipe",
4926
- "pipe"
4927
- ] });
4928
- this.child = child;
4929
- let settled = false;
4930
- /**
4931
- * This attempt FAILED. Set before the kill, because SIGTERM makes the
4932
- * child exit and that exit must not be reported as a death: the retry —
4933
- * or the caller's rejection — already owns what happens next. Without it
4934
- * the timeout path ends the plane the software retry is about to fill,
4935
- * and the consumer sees a stream that stopped for no reason. A "which
4936
- * spawn is current" counter does NOT cover this: the retry has not been
4937
- * spawned when the kill's exit arrives.
4938
- */
4939
- let failed = false;
4940
- /**
4941
- * This attempt is still the live producer: it has not failed (a failure
4942
- * hands ownership to the retry, or to the caller's rejection) and nothing
4943
- * has stopped the child. Those two cover every way an attempt stops being
4944
- * current — `start` only respawns after a rejection.
4945
- */
4946
- const isCurrent = () => !this.stopped && !failed;
4947
- const timeoutMs = this.deps.firstUnitTimeoutMs ?? DEFAULT_FIRST_UNIT_TIMEOUT_MS;
4948
- const settle = (fail) => {
4949
- if (settled) return;
4950
- settled = true;
4951
- clearTimeout(timer);
4952
- if (fail) {
4953
- failed = true;
4954
- reject(fail);
4955
- } else resolve();
4956
- };
4957
- const timer = setTimeout(() => {
4958
- settle(/* @__PURE__ */ new Error(`fmp4 fragment child: no fragment within ${timeoutMs}ms`));
4959
- this.killChild();
4960
- }, timeoutMs);
4961
- timer.unref?.();
4962
- child.stdout?.on("data", (chunk) => {
4963
- for (const unit of this.splitter.push(chunk)) {
4964
- this.unitsOut += 1;
4965
- this.args.plane.publish(unit);
4966
- if (unit.kind === "init") {
4967
- this.deps.logger.info("fmp4 fragment child: INIT segment cut", {
4968
- tags: { deviceId: this.args.deviceId },
4969
- meta: {
4970
- sourceId: this.args.sourceId,
4971
- bytes: unit.data.length
4972
- }
4973
- });
4974
- settle();
4975
- } else if (this.unitsOut % FRAGMENT_LOG_EVERY === 0) this.deps.logger.info("fmp4 fragment child: fragments still flowing", {
4976
- tags: { deviceId: this.args.deviceId },
4977
- meta: {
4978
- sourceId: this.args.sourceId,
4979
- unitsOut: this.unitsOut,
4980
- bytes: unit.data.length,
4981
- subscribers: this.args.plane.subscriberCount
4982
- }
4983
- });
4984
- }
4985
- const fault = this.splitter.fault;
4986
- if (fault !== null) this.onFault(fault, settled, isCurrent(), settle);
4987
- });
4988
- child.stderr?.setEncoding("utf8");
4989
- child.stderr?.on("data", (line) => {
4990
- this.deps.logger.debug("fmp4 fragment child ffmpeg", {
4991
- tags: { deviceId: this.args.deviceId },
4992
- meta: {
4993
- sourceId: this.args.sourceId,
4994
- line: line.trim()
4995
- }
4996
- });
4997
- });
4998
- child.once("error", (err) => {
4999
- if (!settled) {
5000
- settle(err);
5001
- return;
5002
- }
5003
- if (!isCurrent()) return;
5004
- this.args.plane.end("the fragment child errored");
5005
- this.deps.onChildExit?.(err);
5006
- });
5007
- child.once("exit", (code, signal) => {
5008
- if (!settled) {
5009
- settle(/* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited before any fragment (code=${code} signal=${signal})`));
5010
- return;
5011
- }
5012
- if (!isCurrent()) return;
5013
- const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited while live (code=${code} signal=${signal})`);
5014
- this.deps.logger.warn("fmp4 fragment child: ffmpeg exited while live", {
5015
- tags: { deviceId: this.args.deviceId },
5016
- meta: {
5017
- sourceId: this.args.sourceId,
5018
- code,
5019
- signal,
5020
- unitsOut: this.unitsOut
5021
- }
5022
- });
5023
- this.args.plane.end("the fragment child exited");
5024
- this.deps.onChildExit?.(error);
5025
- });
5026
- });
5027
- }
5028
- /**
5029
- * The byte stream stopped being splittable. Not recoverable — the splitter
5030
- * cannot resynchronise mid-box — so the child is a corpse and every consumer
5031
- * has to be told, loudly, with the reason.
5032
- */
5033
- onFault(reason, wasLive, current, settle) {
5034
- const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ${reason}`);
5035
- this.deps.logger.error("fmp4 fragment child: the ffmpeg output stopped parsing as fMP4", {
5036
- tags: { deviceId: this.args.deviceId },
5037
- meta: {
5038
- sourceId: this.args.sourceId,
5039
- unitsOut: this.unitsOut,
5040
- interstitial: this.splitter.discardedInterstitialTypes,
5041
- reason
5042
- }
5043
- });
5044
- this.killChild();
5045
- settle(error);
5046
- if (wasLive && current) {
5047
- this.args.plane.end("the fragment child produced unsplittable output");
5048
- this.deps.onChildExit?.(error);
5049
- }
5050
- }
5051
- killChild() {
5052
- const child = this.child;
5053
- this.child = null;
5054
- if (child && !child.killed) try {
5055
- child.kill("SIGTERM");
5056
- } catch (err) {
5057
- this.deps.logger.warn("fmp4 fragment child: kill error", {
5058
- tags: { deviceId: this.args.deviceId },
5059
- meta: {
5060
- sourceId: this.args.sourceId,
5061
- error: require_dist.errMsg(err)
5062
- }
5063
- });
5064
- }
5065
- }
5066
- };
5067
4573
  //#endregion
5068
4574
  //#region src/stream-broker/stream-broker/rtsp-host.ts
5069
4575
  /**
@@ -5268,6 +4774,7 @@ var TranscodeEgress = class {
5268
4774
  ] });
5269
4775
  this.child = child;
5270
4776
  this.startedAtMs = Date.now();
4777
+ const costClaim = this.deps.claimCost?.(child.pid) ?? require_node.NO_COST_CLAIM;
5271
4778
  let settled = false;
5272
4779
  const timeoutMs = this.deps.firstDataTimeoutMs ?? DEFAULT_FIRST_DATA_TIMEOUT_MS;
5273
4780
  const timer = setTimeout(() => {
@@ -5304,12 +4811,14 @@ var TranscodeEgress = class {
5304
4811
  } });
5305
4812
  });
5306
4813
  child.once("error", (err) => {
4814
+ costClaim.release();
5307
4815
  if (settled) return;
5308
4816
  settled = true;
5309
4817
  clearTimeout(timer);
5310
4818
  reject(err);
5311
4819
  });
5312
4820
  child.once("exit", (code, signal) => {
4821
+ costClaim.release();
5313
4822
  if (settled) {
5314
4823
  if (this.stopped) return;
5315
4824
  const error = /* @__PURE__ */ new Error(`transcode egress: ffmpeg exited while live (code=${code} signal=${signal})`);
@@ -5639,7 +5148,7 @@ var EgressTranscodeManager = class {
5639
5148
  });
5640
5149
  const targetCodec = request.encode.video.codec === "h265" ? "H265" : "H264";
5641
5150
  const brokerId = `egress:${deviceId}:${(0, node_crypto.randomUUID)().slice(0, 8)}`;
5642
- const plane = fragments ? new Fmp4FragmentPlane(this.opts.logger.child("egress-fmp4")) : null;
5151
+ const plane = fragments ? new require_node.Fmp4FragmentPlane(this.opts.logger.child("egress-fmp4")) : null;
5643
5152
  const channel = pushSink === null && plane === null ? this.opts.acquireChannel(brokerId) : null;
5644
5153
  const consumerUrl = channel !== null ? channel.consumerUrl : plane !== null ? fragmentsOnlyUrl(key) : pushOnlyUrl(publishedCamStreamId);
5645
5154
  const child = this.createChild({
@@ -5870,7 +5379,13 @@ var EgressTranscodeManager = class {
5870
5379
  ffmpegBinaryPath: this.opts.ffmpegConfig().binaryPath,
5871
5380
  spawnFn: this.opts.spawnFn ?? node_child_process.spawn,
5872
5381
  acquireChannel: () => channel,
5873
- ...spec.onDied ? { onChildExit: spec.onDied } : {}
5382
+ ...spec.onDied ? { onChildExit: spec.onDied } : {},
5383
+ claimCost: (pid) => this.opts.costRegistry?.claim({
5384
+ role: "transcode",
5385
+ deviceId: spec.deviceId,
5386
+ unit: spec.brokerId,
5387
+ pid
5388
+ }) ?? require_node.NO_COST_CLAIM
5874
5389
  }, {
5875
5390
  brokerId: spec.brokerId,
5876
5391
  targetCodec: spec.targetCodec,
@@ -5894,7 +5409,7 @@ var EgressTranscodeManager = class {
5894
5409
  * the logic. See the primitive's `ffmpeg/fmp4-fragment-child.ts`.
5895
5410
  */
5896
5411
  createFragmentChild(spec, fragments) {
5897
- const child = new Fmp4FragmentChild({
5412
+ const child = new require_node.Fmp4FragmentChild({
5898
5413
  logger: this.opts.logger.child("egress-fmp4"),
5899
5414
  ffmpegBinaryPath: this.opts.ffmpegConfig().binaryPath,
5900
5415
  spawnFn: this.opts.spawnFn ?? node_child_process.spawn,
@@ -11376,6 +10891,14 @@ var StreamBroker = class StreamBroker {
11376
10891
  * and a line without the tag cannot be grouped to answer it.
11377
10892
  */
11378
10893
  numericDeviceId;
10894
+ /**
10895
+ * Where this broker records the ffmpeg ingest it spawns, so the stream-broker
10896
+ * addon reports its OWN per-camera cost through `load-contribution` instead
10897
+ * of leaving an observer to join a pid against a camera. Null until the
10898
+ * manager injects it; a broker without one still streams, and its ffmpeg is
10899
+ * simply unclaimed in the node's process snapshot.
10900
+ */
10901
+ costRegistry = null;
11379
10902
  _status = "idle";
11380
10903
  source;
11381
10904
  /**
@@ -11909,6 +11432,30 @@ var StreamBroker = class StreamBroker {
11909
11432
  this.ffmpegBinaryPath = binaryPath;
11910
11433
  }
11911
11434
  /**
11435
+ * Inject the addon's child-cost registry. Injected rather than constructed
11436
+ * here because the registry is per-ADDON-PROCESS (it is what the addon's
11437
+ * `load-contribution` provider reads) while a broker is per-camera.
11438
+ */
11439
+ setCostRegistry(registry) {
11440
+ this.costRegistry = registry;
11441
+ }
11442
+ /**
11443
+ * Claim one ffmpeg ingest generation for this camera, or nothing when the
11444
+ * camera cannot be named. Never a placeholder device id: an unnamed
11445
+ * per-camera claim is indistinguishable from a shared cost and would quietly
11446
+ * turn one camera's ffmpeg into everybody's.
11447
+ */
11448
+ claimIngestCost(pid) {
11449
+ const deviceId = this.numericDeviceId;
11450
+ if (this.costRegistry === null || deviceId === void 0) return require_node.NO_COST_CLAIM;
11451
+ return this.costRegistry.claim({
11452
+ role: "streaming",
11453
+ deviceId,
11454
+ unit: this.deviceId,
11455
+ pid
11456
+ });
11457
+ }
11458
+ /**
11912
11459
  * Inject a manager-owned closure that derives the current
11913
11460
  * `StreamSource` from the live cam-stream registry. Every dial
11914
11461
  * attempt (initial start, scheduled reconnect, demand-driven resume)
@@ -13524,7 +13071,8 @@ var StreamBroker = class StreamBroker {
13524
13071
  const reader = new FfmpegSourceReader({
13525
13072
  ffmpegPath: this.ffmpegBinaryPath,
13526
13073
  args,
13527
- ...this.logger ? { logger: this.logger.child("ffmpeg-source") } : {}
13074
+ ...this.logger ? { logger: this.logger.child("ffmpeg-source") } : {},
13075
+ claimCost: (pid) => this.claimIngestCost(pid)
13528
13076
  }, {
13529
13077
  onVideoCodec: (codec) => {
13530
13078
  this.detectedCodec = codec;
@@ -15302,6 +14850,8 @@ var StreamBrokerManager = class StreamBrokerManager {
15302
14850
  * that reclaims a refcount whose holder died without releasing.
15303
14851
  */
15304
14852
  egressTranscodeManager = null;
14853
+ /** See {@link StreamBrokerManager.setCostRegistry}. */
14854
+ costRegistry = null;
15305
14855
  /**
15306
14856
  * Test/injection seam for the egress primitive's child. Production leaves it
15307
14857
  * null and `EgressTranscodeManager` builds a real `TranscodeEgress` /
@@ -15344,6 +14894,15 @@ var StreamBrokerManager = class StreamBrokerManager {
15344
14894
  this.rtspProvider = new RtspRestreamProviderImpl(this.rtspServer, logger.child("rtsp-restream"));
15345
14895
  }
15346
14896
  /**
14897
+ * The addon's child-cost registry, threaded down to every broker and to the
14898
+ * egress transcode manager so each spawn site claims its OWN child. Injected
14899
+ * (not constructed) because it is per-addon-process and is what the addon's
14900
+ * `load-contribution` provider reads.
14901
+ */
14902
+ setCostRegistry(registry) {
14903
+ this.costRegistry = registry;
14904
+ }
14905
+ /**
15347
14906
  * Lazily create the transcode manager wired with closures rather than
15348
14907
  * direct refs so it only sees the surface it needs.
15349
14908
  */
@@ -15411,6 +14970,7 @@ var StreamBrokerManager = class StreamBrokerManager {
15411
14970
  return entry ? { url: entry.url } : null;
15412
14971
  },
15413
14972
  ffmpegConfig: () => this.ffmpegConfig,
14973
+ ...this.costRegistry === null ? {} : { costRegistry: this.costRegistry },
15414
14974
  resolveDecodeHwAccel: (deviceId, override) => this.resolveEgressHwAccel(deviceId, override),
15415
14975
  acquireChannel: (brokerId) => {
15416
14976
  const restreamer = new RtspRestreamer(brokerId);
@@ -19061,6 +18621,7 @@ var StreamBrokerManager = class StreamBrokerManager {
19061
18621
  const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
19062
18622
  broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
19063
18623
  broker.setFfmpegBinaryPath(this.ffmpegConfig.binaryPath);
18624
+ if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
19064
18625
  this.applyPreBufferConfig(broker, deviceId, camStreamId);
19065
18626
  this.applyAudioMuteConfig(broker, deviceId);
19066
18627
  this.applyJoinBurstBound(broker, deviceId, camStreamId);
@@ -29507,6 +29068,12 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
29507
29068
  * diagnostic timer never keeps a runner from exiting. */
29508
29069
  logChannels = null;
29509
29070
  /**
29071
+ * The ffmpeg children this addon started, claimed at their spawn sites and
29072
+ * released at their exits. One registry per PROCESS: it dies with the
29073
+ * process, which is correct — those children were this process's children.
29074
+ */
29075
+ costRegistry = new require_node.ChildCostRegistry();
29076
+ /**
29510
29077
  * Snapshot-equality cache for the broker-metrics emit. Each
29511
29078
  * broker emits a stats snapshot every BROKER_METRICS_SNAPSHOT_-
29512
29079
  * INTERVAL_MS; for an idle broker (status=streaming, fps=0,
@@ -29592,11 +29159,16 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
29592
29159
  capability: require_dist.logChannelsCapability,
29593
29160
  provider: this.logChannels
29594
29161
  };
29162
+ const loadContributionRegistration = {
29163
+ capability: require_dist.loadContributionCapability,
29164
+ provider: { list: () => this.costRegistry.contributions() }
29165
+ };
29595
29166
  this.loopStallMonitor = new EventLoopStallMonitor({ onStall: (stallMs) => {
29596
29167
  this.ctx?.logger?.warn?.("event-loop stall — every session in this process froze", { meta: { stallMs } });
29597
29168
  } });
29598
29169
  this.loopStallMonitor.start();
29599
29170
  this.brokerManager = new StreamBrokerManager(void 0, this.ctx.logger);
29171
+ this.brokerManager.setCostRegistry(this.costRegistry);
29600
29172
  this.brokerManager.setDefaultPreBufferSec(this.config.defaultPreBufferSec);
29601
29173
  this.brokerManager.setDefaultJoinBurstMaxSpanMs(this.config.joinBurstMaxSpanMs);
29602
29174
  this.brokerManager.setDefaultPreRollMs(this.config.preRollMs);
@@ -29949,7 +29521,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
29949
29521
  capability: require_dist.addonWidgetsSourceCapability,
29950
29522
  provider: widgetsProvider
29951
29523
  },
29952
- logChannelsRegistration
29524
+ logChannelsRegistration,
29525
+ loadContributionRegistration
29953
29526
  ];
29954
29527
  }
29955
29528
  async onShutdown() {