@camstack/addon-pipeline 1.2.361 → 1.2.363

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-DWFL7iBE.mjs → custom-model-catalog-DHFPSu0E.mjs} +2 -2
  4. package/dist/{custom-model-catalog-BsuqZLaN.js → custom-model-catalog-nx2TXAGS.js} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-73R05PFN.mjs → dist-BX9VTm12.mjs} +278 -4
  8. package/dist/{dist-DFGiI5lr.js → dist-CGOb--t-.js} +278 -4
  9. package/dist/{model-catalogs-BefhGOAc.js → model-catalogs-DHV48cdY.js} +1 -1
  10. package/dist/{model-catalogs-OLyh3TdB.mjs → model-catalogs-DNL4ydAg.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-C0-Ik_JH.mjs → node-C8bbmV3c.mjs} +1 -1
  14. package/dist/{node-CdOP9ELN.js → node-UBMRK8SK.js} +1 -1
  15. package/dist/pipeline-runner/index.js +3 -3
  16. package/dist/pipeline-runner/index.mjs +3 -3
  17. package/dist/{process-memory-DO-ZBWdK.js → process-memory-1BwIq2Ks.js} +1 -1
  18. package/dist/{process-memory-DKKVQV5j.mjs → process-memory-BHG2uCeq.mjs} +1 -1
  19. package/dist/recorder/index.js +7 -3
  20. package/dist/recorder/index.mjs +7 -3
  21. package/dist/{segment-demux-js-C_27CmCq.mjs → segment-demux-js-BhnT__dA.mjs} +1 -1
  22. package/dist/{segment-demux-js-Dr3e9r4m.js → segment-demux-js-PkDP-xQg.js} +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DJeFG_hB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vdmrukBk.mjs} +2 -2
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ScPS8ZWM.mjs +26 -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-Cq5NPjBX.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-Buho1uzb.mjs → hostInit-D_DBY5wu.mjs} +2 -2
  30. package/dist/stream-broker/index.js +247 -41
  31. package/dist/stream-broker/index.mjs +247 -41
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-XWaPHrfA.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-qyhFaM-7.mjs +0 -26
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-DFGiI5lr.js");
7
- const require_node = require("../node-CdOP9ELN.js");
6
+ const require_dist = require("../dist-CGOb--t-.js");
7
+ const require_node = require("../node-UBMRK8SK.js");
8
8
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
- const require_segment_demux_js = require("../segment-demux-js-Dr3e9r4m.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-PkDP-xQg.js");
12
12
  let node_os = require("node:os");
13
13
  node_os = require_chunk.__toESM(node_os);
14
14
  let node_crypto = require("node:crypto");
@@ -565,6 +565,85 @@ var STREAM_BROKER_RETIRED_ROOT_KEYS = [
565
565
  }
566
566
  ];
567
567
  //#endregion
568
+ //#region src/stream-broker/live-viewers.ts
569
+ /**
570
+ * WebRTC consumer kinds that are a person (or a person's device) watching.
571
+ * Every other kind (`recording`, `pipeline`, `snapshot`, `warmup`,
572
+ * `derived-broker`, `rtsp-listen`, `unknown`) is the system holding the stream.
573
+ */
574
+ var VIEWER_CONSUMER_KINDS = new Set([
575
+ "webrtc-browser",
576
+ "webrtc-mobile",
577
+ "webrtc-whep",
578
+ "alexa",
579
+ "homekit"
580
+ ]);
581
+ function isViewerConsumerKind(kind) {
582
+ return VIEWER_CONSUMER_KINDS.has(kind);
583
+ }
584
+ /**
585
+ * `User-Agent` prefixes our OWN processes put on their RTSP dials:
586
+ * `recorder/<id>/…` (the writer), `decode/<id>/…` (the detection decode
587
+ * worker), `camstack-…` (the RTSP client an agent's derived broker pulls this
588
+ * restream with), `prebuffer` (the prebuffer hold).
589
+ */
590
+ var INTERNAL_RESTREAM_AGENT_PREFIXES = [
591
+ "recorder",
592
+ "decode",
593
+ "camstack-",
594
+ "prebuffer"
595
+ ];
596
+ var LOOPBACK_PREFIXES = [
597
+ "127.",
598
+ "::1",
599
+ "::ffff:127.",
600
+ "[::1]"
601
+ ];
602
+ /** True when the peer is this host — the system's own ffmpeg children. */
603
+ function isLoopbackAddr(remoteAddr) {
604
+ return LOOPBACK_PREFIXES.some((p) => remoteAddr.startsWith(p));
605
+ }
606
+ /**
607
+ * An RTSP restream client from OUTSIDE: playing, and none of
608
+ * - a declared dial purpose (detection / recording / sentry),
609
+ * - the muted decoder-loopback path,
610
+ * - one of our own `User-Agent`s,
611
+ * - a process on this same host (snapshot, grid compositor, the HomeKit leg —
612
+ * ffmpeg children that dial the restream over loopback).
613
+ */
614
+ function isExternalRestreamClient(facts) {
615
+ if (!facts.playing) return false;
616
+ if (facts.intent !== null) return false;
617
+ if (facts.muted) return false;
618
+ const ua = facts.userAgent;
619
+ if (ua !== null && INTERNAL_RESTREAM_AGENT_PREFIXES.some((p) => ua.startsWith(p))) return false;
620
+ if (isLoopbackAddr(facts.remoteAddr)) return false;
621
+ return true;
622
+ }
623
+ /** Sum several tallies into a new one (inputs are never mutated). */
624
+ function mergeViewerCounts(parts) {
625
+ const out = /* @__PURE__ */ new Map();
626
+ for (const part of parts) for (const [deviceId, n] of part) if (n > 0) out.set(deviceId, (out.get(deviceId) ?? 0) + n);
627
+ return out;
628
+ }
629
+ //#endregion
630
+ //#region src/stream-broker/rtsp/broker-device-id.ts
631
+ /**
632
+ * Parse the numeric deviceId out of a canonical brokerId
633
+ * (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
634
+ * `undefined` for a malformed id — so every device-scoped log line on the RTSP
635
+ * plane can carry `tags.deviceId` without risking a bogus tag value.
636
+ *
637
+ * Every log line about a device carries `tags: { deviceId }`, always the same
638
+ * key and always the numeric id: a miss rate, a media gap or a routing fault is
639
+ * always asked per-camera ("why is 617 worse than 615?"), and a line that omits
640
+ * the tag cannot answer it.
641
+ */
642
+ function deviceIdFromBrokerId$1(brokerId) {
643
+ const parsed = Number(brokerId.split("/")[0]);
644
+ return Number.isFinite(parsed) ? parsed : void 0;
645
+ }
646
+ //#endregion
568
647
  //#region src/shared/ffmpeg-decode-args.ts
569
648
  /** Default DRM render node used for the VA-API / QSV hardware decode path. */
570
649
  var DEFAULT_VAAPI_RENDER_DEVICE = "/dev/dri/renderD128";
@@ -2613,23 +2692,6 @@ var AnnexBDeframer = class {
2613
2692
  }
2614
2693
  };
2615
2694
  //#endregion
2616
- //#region src/stream-broker/rtsp/broker-device-id.ts
2617
- /**
2618
- * Parse the numeric deviceId out of a canonical brokerId
2619
- * (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
2620
- * `undefined` for a malformed id — so every device-scoped log line on the RTSP
2621
- * plane can carry `tags.deviceId` without risking a bogus tag value.
2622
- *
2623
- * Every log line about a device carries `tags: { deviceId }`, always the same
2624
- * key and always the numeric id: a miss rate, a media gap or a routing fault is
2625
- * always asked per-camera ("why is 617 worse than 615?"), and a line that omits
2626
- * the tag cannot answer it.
2627
- */
2628
- function deviceIdFromBrokerId$1(brokerId) {
2629
- const parsed = Number(brokerId.split("/")[0]);
2630
- return Number.isFinite(parsed) ? parsed : void 0;
2631
- }
2632
- //#endregion
2633
2695
  //#region src/stream-broker/rtsp/audio/g711-silence.ts
2634
2696
  var SILENCE_BYTE = {
2635
2697
  /** µ-law digital zero. */
@@ -3601,6 +3663,18 @@ var RtspRestreamer = class {
3601
3663
  return this.sessions.size;
3602
3664
  }
3603
3665
  /**
3666
+ * RTSP clients from OUTSIDE that are playing right now — people with a player
3667
+ * open on the restream, not our recorder, decoder or agents (see
3668
+ * `live-viewers.ts`). Read straight off the live session map and each
3669
+ * session's own `playing && !closed`, so a client that vanished without a
3670
+ * TEARDOWN stops counting the moment its socket closes or errors.
3671
+ */
3672
+ countExternalViewers() {
3673
+ let n = 0;
3674
+ for (const session of this.sessions.values()) if (isExternalRestreamClient(session.getViewerFacts())) n += 1;
3675
+ return n;
3676
+ }
3677
+ /**
3604
3678
  * Sessions that count as LIVE INTENT — everything except a sentry dial.
3605
3679
  *
3606
3680
  * The split exists so a Frigate-style puller can hold a session open for ever
@@ -4382,6 +4456,21 @@ var RtspSession = class {
4382
4456
  return this.userAgent;
4383
4457
  }
4384
4458
  /**
4459
+ * What the live-viewer count needs to classify this client (see
4460
+ * `live-viewers.ts`). `playing` is `isPlaying()`, which is false once the
4461
+ * socket closed or errored — so an abruptly dropped client stops counting at
4462
+ * the same instant, before the restreamer's own `removeSession` reconcile.
4463
+ */
4464
+ getViewerFacts() {
4465
+ return {
4466
+ playing: this.isPlaying(),
4467
+ userAgent: this.userAgent,
4468
+ remoteAddr: `${this.socket.remoteAddress ?? "unknown"}:${String(this.socket.remotePort ?? 0)}`,
4469
+ muted: this.muted,
4470
+ intent: this.declaration.intent
4471
+ };
4472
+ }
4473
+ /**
4385
4474
  * True only for a session that dialed the DETECTION path
4386
4475
  * (`DETECTION_MUTED_PATH_SUFFIX`). Read by
4387
4476
  * `RtspRestreamer.servePendingSessionsRtp` to exempt this session from the
@@ -4541,15 +4630,16 @@ var RtspSession = class {
4541
4630
  *
4542
4631
  * ## Why sharing the payload is safe, and what would make it unsafe
4543
4632
  *
4544
- * The payload buffer handed here is ALREADY retained, by reference, in the
4545
- * broker's `rtpRing` for the life of a GOP (`captureRtpForPreBuffer` pushes
4546
- * the same object). A pending `socket.write` cannot outlive that, so the
4547
- * lifetime requirement this adds is strictly weaker than one the broker
4548
- * already meets. Its provenance is a private allocation per packet —
4633
+ * The payload buffer handed here is a private allocation per packet —
4549
4634
  * `Buffer.from(readBuffer.subarray(...))` in `rtsp-client.ts`'s `onData`, for
4550
4635
  * video and audio alike — never a view of the reader's socket buffer, which
4551
4636
  * advances on the next read. Nothing in the broker writes into a captured
4552
4637
  * packet: every mutation in this path targets a freshly allocated buffer.
4638
+ * (The broker's `rtpRing` keeps its OWN copy of a pooled packet —
4639
+ * `detachFromPool`, so a ring entry never pins a shared pool slab — so the
4640
+ * ring no longer vouches for THIS object's lifetime, and it does not need
4641
+ * to: a pending write holds its own reference, and the rule below is about
4642
+ * mutation, not retention.)
4553
4643
  *
4554
4644
  * The rule to keep: **`socket.write` forbids MUTATION before the write
4555
4645
  * completes, not retention.** So the header, which IS built per call, must
@@ -11206,6 +11296,49 @@ var AudioCodecSession = class {
11206
11296
  }
11207
11297
  };
11208
11298
  //#endregion
11299
+ //#region src/stream-broker/stream-broker/owned-buffer.ts
11300
+ /**
11301
+ * Buffers that enter a LONG-LIVED ring own their backing store.
11302
+ *
11303
+ * Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
11304
+ * below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
11305
+ * That is the right choice for the ingest path — a start code, a 4-byte
11306
+ * interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
11307
+ * within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
11308
+ * for 60 s in a clip ring pins its whole slab, and with it every short-lived
11309
+ * packet that happened to be allocated beside it. On the hub that held
11310
+ * 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
11311
+ * pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
11312
+ *
11313
+ * The ring byte caps counted payload bytes, so they never saw it. Both rings
11314
+ * now pass every entry through {@link detachFromPool} on the way in and
11315
+ * account it with {@link backingStoreBytes}, so the cap measures what the
11316
+ * entry costs. Short-lived traffic stays pooled.
11317
+ */
11318
+ /** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
11319
+ function ownsBackingStore(buf) {
11320
+ return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
11321
+ }
11322
+ /**
11323
+ * What retaining `buf` really costs: its own length when it owns its
11324
+ * ArrayBuffer, the whole shared slab when it is a view of one.
11325
+ */
11326
+ function backingStoreBytes(buf) {
11327
+ return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
11328
+ }
11329
+ /**
11330
+ * `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
11331
+ * buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
11332
+ * runtime detail), else a copy that does. One copy, never two: a buffer that is
11333
+ * already unpooled is returned as-is.
11334
+ */
11335
+ function detachFromPool(buf) {
11336
+ if (ownsBackingStore(buf)) return buf;
11337
+ const owned = Buffer.allocUnsafeSlow(buf.length);
11338
+ buf.copy(owned);
11339
+ return owned;
11340
+ }
11341
+ //#endregion
11209
11342
  //#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
11210
11343
  /**
11211
11344
  * Time-based ring buffer for encoded video packets.
@@ -11216,6 +11349,12 @@ var AudioCodecSession = class {
11216
11349
  *
11217
11350
  * The buffer always starts from the most recent keyframe to ensure
11218
11351
  * consumers can decode the returned data.
11352
+ *
11353
+ * Every entry OWNS its backing store (`owned-buffer.ts`): a sub-stream NAL
11354
+ * arrives as a view of a shared 8 KiB pool slab, and a ring that kept it for
11355
+ * 60 s pinned the slab and every short-lived packet beside it — the clip
11356
+ * rings of ~70 brokers were the largest holder of the hub broker's
11357
+ * 550–980 MB of ArrayBuffers. The byte budget charges what an entry pins.
11219
11358
  */
11220
11359
  var EncodedRingBuffer = class {
11221
11360
  packets = [];
@@ -11247,13 +11386,25 @@ var EncodedRingBuffer = class {
11247
11386
  getDuration() {
11248
11387
  return this.maxDurationMs / 1e3;
11249
11388
  }
11250
- /** Push a new packet into the buffer and evict expired ones. */
11389
+ /**
11390
+ * Push a new packet into the buffer and evict expired ones.
11391
+ *
11392
+ * A pooled payload is copied into its own allocation on the way in; one
11393
+ * that already owns its ArrayBuffer is stored as-is. The consumers fanned
11394
+ * out from the same packet keep the original, so the copy is the ring's
11395
+ * alone and is freed with it.
11396
+ */
11251
11397
  push(packet) {
11252
- this.packets.push(packet);
11253
- this.bytes += packet.data.length;
11398
+ const data = detachFromPool(packet.data);
11399
+ const stored = data === packet.data ? packet : {
11400
+ ...packet,
11401
+ data
11402
+ };
11403
+ this.packets.push(stored);
11404
+ this.bytes += backingStoreBytes(data);
11254
11405
  this.evict();
11255
11406
  }
11256
- /** What the retained window currently costs, in bytes. */
11407
+ /** What the retained window currently costs, in bytes of backing store. */
11257
11408
  getBufferedBytes() {
11258
11409
  return this.bytes;
11259
11410
  }
@@ -11331,14 +11482,14 @@ var EncodedRingBuffer = class {
11331
11482
  let removeCount = 0;
11332
11483
  while (removeCount < this.packets.length && (this.packets[removeCount].pts < cutoff || this.packets[removeCount].pts > newest)) removeCount++;
11333
11484
  if (removeCount > 0) {
11334
- for (let i = 0; i < removeCount; i++) this.bytes -= this.packets[i].data.length;
11485
+ for (let i = 0; i < removeCount; i++) this.bytes -= backingStoreBytes(this.packets[i].data);
11335
11486
  this.packets.splice(0, removeCount);
11336
11487
  }
11337
11488
  if (this.maxBytes > 0 && this.bytes > this.maxBytes) {
11338
11489
  this.clampedByBytes = true;
11339
11490
  let dropped = 0;
11340
11491
  while (dropped < this.packets.length && this.bytes > this.maxBytes) {
11341
- this.bytes -= this.packets[dropped].data.length;
11492
+ this.bytes -= backingStoreBytes(this.packets[dropped].data);
11342
11493
  dropped++;
11343
11494
  }
11344
11495
  if (dropped > 0) this.packets.splice(0, dropped);
@@ -13774,8 +13925,9 @@ var StreamBroker = class StreamBroker {
13774
13925
  captureRtpForPreBuffer(rtpData) {
13775
13926
  const marker = rtpData.length > 1 && (rtpData[1] & 128) !== 0;
13776
13927
  if (this.rtpRingPrevMarker || this.rtpRing.length === 0) this.rtpRingCurAuStart = this.rtpRing.length;
13777
- this.rtpRing.push(rtpData);
13778
- this.rtpRingBytes += rtpData.length;
13928
+ const owned = detachFromPool(rtpData);
13929
+ this.rtpRing.push(owned);
13930
+ this.rtpRingBytes += backingStoreBytes(owned);
13779
13931
  if (this._lastNalParamSet || this._lastNalKeyframe) {
13780
13932
  const auStart = this.rtpRingCurAuStart;
13781
13933
  const lastMark = this.rtpRingKeyframeMarks[this.rtpRingKeyframeMarks.length - 1];
@@ -13953,18 +14105,18 @@ var StreamBroker = class StreamBroker {
13953
14105
  this.reportPreRollClamp(budget, retentionMs);
13954
14106
  const historyEnd = this.rtpRingKeyframeMarks[this.rtpRingKeyframeMarks.length - 1].index;
13955
14107
  let retained = 0;
13956
- for (let i = cut; i < historyEnd; i++) retained += this.rtpRing[i].length;
14108
+ for (let i = cut; i < historyEnd; i++) retained += backingStoreBytes(this.rtpRing[i]);
13957
14109
  let markIdx = this.rtpRingKeyframeMarks.findIndex((m) => m.index === cut);
13958
14110
  while (retained > budget.bytes && markIdx >= 0 && markIdx < this.rtpRingKeyframeMarks.length - 1) {
13959
14111
  const nextCut = this.rtpRingKeyframeMarks[markIdx + 1].index;
13960
- for (let i = cut; i < nextCut; i++) retained -= this.rtpRing[i].length;
14112
+ for (let i = cut; i < nextCut; i++) retained -= backingStoreBytes(this.rtpRing[i]);
13961
14113
  cut = nextCut;
13962
14114
  markIdx++;
13963
14115
  }
13964
14116
  }
13965
14117
  if (cut <= 0) return;
13966
14118
  const dropped = this.rtpRing.splice(0, cut);
13967
- for (const b of dropped) this.rtpRingBytes -= b.length;
14119
+ for (const b of dropped) this.rtpRingBytes -= backingStoreBytes(b);
13968
14120
  this.rtpRingKeyframeMarks = this.rtpRingKeyframeMarks.filter((mark) => mark.index >= cut).map((mark) => ({
13969
14121
  index: mark.index - cut,
13970
14122
  ts: mark.ts
@@ -14255,6 +14407,13 @@ var StreamBroker = class StreamBroker {
14255
14407
  getLocalStreamUrl() {
14256
14408
  return this.pipeServer.getUrl();
14257
14409
  }
14410
+ /**
14411
+ * External RTSP restream clients playing on this broker right now — the
14412
+ * restream half of the per-camera live-viewer count (`live-viewers.ts`).
14413
+ */
14414
+ countExternalRestreamViewers() {
14415
+ return this.rtspRestreamer.countExternalViewers();
14416
+ }
14258
14417
  /** Get the RTSP restreamer instance (for RtspListenServer registration) */
14259
14418
  getRtspRestreamer() {
14260
14419
  return this.rtspRestreamer;
@@ -14297,11 +14456,15 @@ var StreamBroker = class StreamBroker {
14297
14456
  if (clamped === this.clipRetentionSec) return;
14298
14457
  const previous = this.clipRetentionSec;
14299
14458
  this.clipRetentionSec = clamped;
14300
- this.logger?.info(clamped === 0 ? "Clip retention off" : "Clip retention set", { meta: {
14301
- seconds: clamped,
14302
- requested: seconds,
14303
- previousSeconds: previous
14304
- } });
14459
+ this.logger?.info(clamped === 0 ? "Clip retention off" : "Clip retention set", {
14460
+ tags: { deviceId: this.numericDeviceId },
14461
+ meta: {
14462
+ seconds: clamped,
14463
+ requested: seconds,
14464
+ previousSeconds: previous,
14465
+ capBytes: StreamBroker.CLIP_RING_MAX_BYTES
14466
+ }
14467
+ });
14305
14468
  if (clamped === 0) {
14306
14469
  this.clipBuffer = null;
14307
14470
  return;
@@ -18875,6 +19038,25 @@ var StreamBrokerManager = class StreamBrokerManager {
18875
19038
  };
18876
19039
  return broker.listClients();
18877
19040
  }
19041
+ /**
19042
+ * People watching each camera live right now (cap `getLiveViewerCounts`).
19043
+ * Two planes, one definition (`live-viewers.ts`): the WebRTC server's live
19044
+ * viewer sessions plus every broker's external RTSP restream clients, summed
19045
+ * per camera over all of its profile brokers. Both are read off live session
19046
+ * maps — no stored counter — so an ended session cannot be left counted.
19047
+ */
19048
+ async getLiveViewerCounts() {
19049
+ const rtsp = /* @__PURE__ */ new Map();
19050
+ for (const [brokerId, broker] of this.brokers) {
19051
+ const n = broker.countExternalRestreamViewers();
19052
+ const deviceId = deviceIdFromBrokerId$1(brokerId);
19053
+ if (n > 0 && deviceId !== void 0) rtsp.set(deviceId, (rtsp.get(deviceId) ?? 0) + n);
19054
+ }
19055
+ return [...mergeViewerCounts([this.webrtcServer?.countLiveViewers() ?? /* @__PURE__ */ new Map(), rtsp])].map(([deviceId, viewers]) => ({
19056
+ deviceId,
19057
+ viewers
19058
+ })).sort((a, b) => a.deviceId - b.deviceId);
19059
+ }
18878
19060
  async killClient(input) {
18879
19061
  const broker = this.brokers.get(input.brokerId);
18880
19062
  if (!broker) return { killed: false };
@@ -34733,6 +34915,7 @@ var BrokerWebrtcServer = class {
34733
34915
  });
34734
34916
  const entry = {
34735
34917
  session,
34918
+ consumerKind: baseAttribution.kind,
34736
34919
  brokerId,
34737
34920
  unsubBroker,
34738
34921
  unsubRtp,
@@ -35159,6 +35342,29 @@ var BrokerWebrtcServer = class {
35159
35342
  getSessionCount() {
35160
35343
  return this.sessions.size;
35161
35344
  }
35345
+ /**
35346
+ * People watching LIVE through this server, per camera (`live-viewers.ts`):
35347
+ * sessions of a viewer kind whose track is on the live edge. A session on a
35348
+ * recorded playhead is not watching live; the system's own subscriptions
35349
+ * (warm-up, linger ghost, egress-transcode legs) are never a SESSION here, so
35350
+ * they cannot count.
35351
+ *
35352
+ * Derived from `this.sessions` at call time, the map `cleanupSession`
35353
+ * maintains for every teardown path (explicit close, ICE failure, the peer
35354
+ * vanishing — all end in `session.onClosed`), so there is no second counter
35355
+ * that could leak.
35356
+ */
35357
+ countLiveViewers() {
35358
+ const counts = /* @__PURE__ */ new Map();
35359
+ for (const entry of this.sessions.values()) {
35360
+ if (!isViewerConsumerKind(entry.consumerKind)) continue;
35361
+ if ((entry.timeline?.currentMode ?? "live") !== "live") continue;
35362
+ const deviceId = deviceIdFromBrokerId(entry.brokerId);
35363
+ if (deviceId === void 0) continue;
35364
+ counts.set(deviceId, (counts.get(deviceId) ?? 0) + 1);
35365
+ }
35366
+ return counts;
35367
+ }
35162
35368
  /** Live signaling state for a session — used by the webrtc-session
35163
35369
  * provider's getSessionState. Returns a null pending when unknown. */
35164
35370
  getSessionRenegotiationState(sessionId) {