@camstack/addon-pipeline 1.2.360 → 1.2.362

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 (38) 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-ihWAlDr4.mjs → custom-model-catalog-C77XAnt9.mjs} +2 -2
  4. package/dist/{custom-model-catalog-K9Hx82GZ.js → custom-model-catalog-CL9Tcn2Y.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-BYCrV0VR.mjs → dist-D629dz5x.mjs} +127 -20
  8. package/dist/{dist-CYakbWA8.js → dist-tclsRHCk.js} +126 -19
  9. package/dist/{model-catalogs-CX9SJ9in.mjs → model-catalogs-BxQBJBt4.mjs} +1 -1
  10. package/dist/{model-catalogs-C9cyLHdH.js → model-catalogs-Y1WJrGyS.js} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-CTCm8Vtw.js → node-BYrYuZdh.js} +4 -2
  14. package/dist/{node-C_qDNN9f.mjs → node-D2D0N7_h.mjs} +3 -2
  15. package/dist/pipeline-runner/index.js +3 -3
  16. package/dist/pipeline-runner/index.mjs +3 -3
  17. package/dist/{process-memory-B3lkW9H-.mjs → process-memory-BR1e6q_Q.mjs} +1 -1
  18. package/dist/{process-memory-Ds5S3L0F.js → process-memory-eM4qLFdC.js} +1 -1
  19. package/dist/recorder/index.js +1679 -2
  20. package/dist/recorder/index.mjs +1679 -2
  21. package/dist/{segment-demux-js-BgDpp15L.js → segment-demux-js-B9EqRfBi.js} +1 -1
  22. package/dist/{segment-demux-js-W5FIgijf.mjs → segment-demux-js-C5IOs4lZ.mjs} +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-v95KZ2Rt.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C4Cp_OAY.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-CICWhpXS.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-qyhFaM-7.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-7VuNrr9l.mjs} +1 -1
  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-CDaDWDva.mjs → hostInit-CZ_HBwki.mjs} +2 -2
  30. package/dist/stream-broker/index.js +159 -20
  31. package/dist/stream-broker/index.mjs +159 -20
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/python/inference_pool.py +12 -13
  35. package/python/pool_platform.py +214 -0
  36. package/python/test_inference_pool_windows.py +86 -0
  37. package/python/test_pool_platform.py +141 -0
  38. 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
@@ -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-CYakbWA8.js");
7
- const require_node = require("../node-CTCm8Vtw.js");
6
+ const require_dist = require("../dist-tclsRHCk.js");
7
+ const require_node = require("../node-BYrYuZdh.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-BgDpp15L.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-B9EqRfBi.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
@@ -14255,6 +14344,13 @@ var StreamBroker = class StreamBroker {
14255
14344
  getLocalStreamUrl() {
14256
14345
  return this.pipeServer.getUrl();
14257
14346
  }
14347
+ /**
14348
+ * External RTSP restream clients playing on this broker right now — the
14349
+ * restream half of the per-camera live-viewer count (`live-viewers.ts`).
14350
+ */
14351
+ countExternalRestreamViewers() {
14352
+ return this.rtspRestreamer.countExternalViewers();
14353
+ }
14258
14354
  /** Get the RTSP restreamer instance (for RtspListenServer registration) */
14259
14355
  getRtspRestreamer() {
14260
14356
  return this.rtspRestreamer;
@@ -18875,6 +18971,25 @@ var StreamBrokerManager = class StreamBrokerManager {
18875
18971
  };
18876
18972
  return broker.listClients();
18877
18973
  }
18974
+ /**
18975
+ * People watching each camera live right now (cap `getLiveViewerCounts`).
18976
+ * Two planes, one definition (`live-viewers.ts`): the WebRTC server's live
18977
+ * viewer sessions plus every broker's external RTSP restream clients, summed
18978
+ * per camera over all of its profile brokers. Both are read off live session
18979
+ * maps — no stored counter — so an ended session cannot be left counted.
18980
+ */
18981
+ async getLiveViewerCounts() {
18982
+ const rtsp = /* @__PURE__ */ new Map();
18983
+ for (const [brokerId, broker] of this.brokers) {
18984
+ const n = broker.countExternalRestreamViewers();
18985
+ const deviceId = deviceIdFromBrokerId$1(brokerId);
18986
+ if (n > 0 && deviceId !== void 0) rtsp.set(deviceId, (rtsp.get(deviceId) ?? 0) + n);
18987
+ }
18988
+ return [...mergeViewerCounts([this.webrtcServer?.countLiveViewers() ?? /* @__PURE__ */ new Map(), rtsp])].map(([deviceId, viewers]) => ({
18989
+ deviceId,
18990
+ viewers
18991
+ })).sort((a, b) => a.deviceId - b.deviceId);
18992
+ }
18878
18993
  async killClient(input) {
18879
18994
  const broker = this.brokers.get(input.brokerId);
18880
18995
  if (!broker) return { killed: false };
@@ -34733,6 +34848,7 @@ var BrokerWebrtcServer = class {
34733
34848
  });
34734
34849
  const entry = {
34735
34850
  session,
34851
+ consumerKind: baseAttribution.kind,
34736
34852
  brokerId,
34737
34853
  unsubBroker,
34738
34854
  unsubRtp,
@@ -35159,6 +35275,29 @@ var BrokerWebrtcServer = class {
35159
35275
  getSessionCount() {
35160
35276
  return this.sessions.size;
35161
35277
  }
35278
+ /**
35279
+ * People watching LIVE through this server, per camera (`live-viewers.ts`):
35280
+ * sessions of a viewer kind whose track is on the live edge. A session on a
35281
+ * recorded playhead is not watching live; the system's own subscriptions
35282
+ * (warm-up, linger ghost, egress-transcode legs) are never a SESSION here, so
35283
+ * they cannot count.
35284
+ *
35285
+ * Derived from `this.sessions` at call time, the map `cleanupSession`
35286
+ * maintains for every teardown path (explicit close, ICE failure, the peer
35287
+ * vanishing — all end in `session.onClosed`), so there is no second counter
35288
+ * that could leak.
35289
+ */
35290
+ countLiveViewers() {
35291
+ const counts = /* @__PURE__ */ new Map();
35292
+ for (const entry of this.sessions.values()) {
35293
+ if (!isViewerConsumerKind(entry.consumerKind)) continue;
35294
+ if ((entry.timeline?.currentMode ?? "live") !== "live") continue;
35295
+ const deviceId = deviceIdFromBrokerId(entry.brokerId);
35296
+ if (deviceId === void 0) continue;
35297
+ counts.set(deviceId, (counts.get(deviceId) ?? 0) + 1);
35298
+ }
35299
+ return counts;
35300
+ }
35162
35301
  /** Live signaling state for a session — used by the webrtc-session
35163
35302
  * provider's getSessionState. Returns a null pending when unknown. */
35164
35303
  getSessionRenegotiationState(sessionId) {
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { $n as discriminatedUnion, $t as rankStreams, An as DeviceType, At as gifFromMp4Args, Bn as expandAudioChunkToF32le, D as G711_SCALE_CORRECTION_DB, Dn as invocationFromEncodeProfile, Dt as ffmpegRate, Fn as CamProfileSchema, Gn as maskUrlCredentials, Hn as isEvent, It as loadContributionCapability, K as RingBuffer, Kn as nodePin, Ln as createEvent, Lt as logChannelsCapability, Mn as BOOT_RECOVERY_BACKOFF_MS, Nn as BaseAddon, On as isSoftwareDecode, Pn as CAM_PROFILE_ORDER, Qn as boolean, R as RATE_CONTROL_RELAXED, Sn as AUDIO_PRESETS, Tn as buildFfmpegArgs, Un as makeProfileBrokerId, Vt as memoizePinnedFfmpeg, Wn as makeSourceBrokerId, X as addonWidgetsSourceCapability, Xn as _enum, Zn as array, _n as streamBrokerCapability, ar as record, at as cameraStreamsCapability, bt as egressTransportFromRequest, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, dt as createLogChannelsProvider, ft as declareLogChannel, jn as errMsg, kn as DeviceFeature, kt as gifDeriveSpeed, mt as deriveBatteryPresence, nr as number, nt as batteryCapability, o as BatteryStatusSchema, on as resolveEgressDecodeHwAccel, or as string, qn as parseProfileBrokerId, rr as object, s as CLIP_BROKER_PACED_RATES, sr as union, st as clipExportFailureOf, tr as literal, ut as createHwAccelCache, w as EncodeProfileSchema, xn as webrtcSessionCapability, yt as egressTranscodeSharingKey, z as RATE_CONTROL_TIGHT } from "../dist-BYCrV0VR.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-C_qDNN9f.mjs";
2
+ import { $n as discriminatedUnion, An as BaseAddon, D as G711_SCALE_CORRECTION_DB, Dn as DeviceType, Dt as gifDeriveSpeed, En as DeviceFeature, Gn as maskUrlCredentials, Hn as isEvent, I as RATE_CONTROL_RELAXED, In as createEvent, J as addonWidgetsSourceCapability, Kn as nodePin, L as RATE_CONTROL_TIGHT, Mn as CamProfileSchema, Nt as loadContributionCapability, On as errMsg, Ot as gifFromMp4Args, Pt as logChannelsCapability, Qn as boolean, Rt as memoizePinnedFfmpeg, Sn as buildFfmpegArgs, Tn as isSoftwareDecode, Tt as ffmpegRate, Un as makeProfileBrokerId, W as RingBuffer, Wn as makeSourceBrokerId, Xn as _enum, Xt as rankStreams, Zn as array, _t as egressTranscodeSharingKey, ar as record, at as clipExportFailureOf, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, ct as createHwAccelCache, et as batteryCapability, ft as deriveBatteryPresence, jn as CAM_PROFILE_ORDER, kn as BOOT_RECOVERY_BACKOFF_MS, lt as createLogChannelsProvider, mn as streamBrokerCapability, nr as number, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rn as resolveEgressDecodeHwAccel, rr as object, rt as cameraStreamsCapability, s as CLIP_BROKER_PACED_RATES, sr as union, tr as literal, ut as declareLogChannel, vn as webrtcSessionCapability, vt as egressTransportFromRequest, w as EncodeProfileSchema, wn as invocationFromEncodeProfile, yn as AUDIO_PRESETS, zn as expandAudioChunkToF32le } from "../dist-D629dz5x.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-D2D0N7_h.mjs";
4
4
  import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
6
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
- import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-W5FIgijf.mjs";
7
+ import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-C5IOs4lZ.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os$1 from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -561,6 +561,85 @@ var STREAM_BROKER_RETIRED_ROOT_KEYS = [
561
561
  }
562
562
  ];
563
563
  //#endregion
564
+ //#region src/stream-broker/live-viewers.ts
565
+ /**
566
+ * WebRTC consumer kinds that are a person (or a person's device) watching.
567
+ * Every other kind (`recording`, `pipeline`, `snapshot`, `warmup`,
568
+ * `derived-broker`, `rtsp-listen`, `unknown`) is the system holding the stream.
569
+ */
570
+ var VIEWER_CONSUMER_KINDS = new Set([
571
+ "webrtc-browser",
572
+ "webrtc-mobile",
573
+ "webrtc-whep",
574
+ "alexa",
575
+ "homekit"
576
+ ]);
577
+ function isViewerConsumerKind(kind) {
578
+ return VIEWER_CONSUMER_KINDS.has(kind);
579
+ }
580
+ /**
581
+ * `User-Agent` prefixes our OWN processes put on their RTSP dials:
582
+ * `recorder/<id>/…` (the writer), `decode/<id>/…` (the detection decode
583
+ * worker), `camstack-…` (the RTSP client an agent's derived broker pulls this
584
+ * restream with), `prebuffer` (the prebuffer hold).
585
+ */
586
+ var INTERNAL_RESTREAM_AGENT_PREFIXES = [
587
+ "recorder",
588
+ "decode",
589
+ "camstack-",
590
+ "prebuffer"
591
+ ];
592
+ var LOOPBACK_PREFIXES = [
593
+ "127.",
594
+ "::1",
595
+ "::ffff:127.",
596
+ "[::1]"
597
+ ];
598
+ /** True when the peer is this host — the system's own ffmpeg children. */
599
+ function isLoopbackAddr(remoteAddr) {
600
+ return LOOPBACK_PREFIXES.some((p) => remoteAddr.startsWith(p));
601
+ }
602
+ /**
603
+ * An RTSP restream client from OUTSIDE: playing, and none of
604
+ * - a declared dial purpose (detection / recording / sentry),
605
+ * - the muted decoder-loopback path,
606
+ * - one of our own `User-Agent`s,
607
+ * - a process on this same host (snapshot, grid compositor, the HomeKit leg —
608
+ * ffmpeg children that dial the restream over loopback).
609
+ */
610
+ function isExternalRestreamClient(facts) {
611
+ if (!facts.playing) return false;
612
+ if (facts.intent !== null) return false;
613
+ if (facts.muted) return false;
614
+ const ua = facts.userAgent;
615
+ if (ua !== null && INTERNAL_RESTREAM_AGENT_PREFIXES.some((p) => ua.startsWith(p))) return false;
616
+ if (isLoopbackAddr(facts.remoteAddr)) return false;
617
+ return true;
618
+ }
619
+ /** Sum several tallies into a new one (inputs are never mutated). */
620
+ function mergeViewerCounts(parts) {
621
+ const out = /* @__PURE__ */ new Map();
622
+ for (const part of parts) for (const [deviceId, n] of part) if (n > 0) out.set(deviceId, (out.get(deviceId) ?? 0) + n);
623
+ return out;
624
+ }
625
+ //#endregion
626
+ //#region src/stream-broker/rtsp/broker-device-id.ts
627
+ /**
628
+ * Parse the numeric deviceId out of a canonical brokerId
629
+ * (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
630
+ * `undefined` for a malformed id — so every device-scoped log line on the RTSP
631
+ * plane can carry `tags.deviceId` without risking a bogus tag value.
632
+ *
633
+ * Every log line about a device carries `tags: { deviceId }`, always the same
634
+ * key and always the numeric id: a miss rate, a media gap or a routing fault is
635
+ * always asked per-camera ("why is 617 worse than 615?"), and a line that omits
636
+ * the tag cannot answer it.
637
+ */
638
+ function deviceIdFromBrokerId$1(brokerId) {
639
+ const parsed = Number(brokerId.split("/")[0]);
640
+ return Number.isFinite(parsed) ? parsed : void 0;
641
+ }
642
+ //#endregion
564
643
  //#region src/shared/ffmpeg-decode-args.ts
565
644
  /** Default DRM render node used for the VA-API / QSV hardware decode path. */
566
645
  var DEFAULT_VAAPI_RENDER_DEVICE = "/dev/dri/renderD128";
@@ -2609,23 +2688,6 @@ var AnnexBDeframer = class {
2609
2688
  }
2610
2689
  };
2611
2690
  //#endregion
2612
- //#region src/stream-broker/rtsp/broker-device-id.ts
2613
- /**
2614
- * Parse the numeric deviceId out of a canonical brokerId
2615
- * (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
2616
- * `undefined` for a malformed id — so every device-scoped log line on the RTSP
2617
- * plane can carry `tags.deviceId` without risking a bogus tag value.
2618
- *
2619
- * Every log line about a device carries `tags: { deviceId }`, always the same
2620
- * key and always the numeric id: a miss rate, a media gap or a routing fault is
2621
- * always asked per-camera ("why is 617 worse than 615?"), and a line that omits
2622
- * the tag cannot answer it.
2623
- */
2624
- function deviceIdFromBrokerId$1(brokerId) {
2625
- const parsed = Number(brokerId.split("/")[0]);
2626
- return Number.isFinite(parsed) ? parsed : void 0;
2627
- }
2628
- //#endregion
2629
2691
  //#region src/stream-broker/rtsp/audio/g711-silence.ts
2630
2692
  var SILENCE_BYTE = {
2631
2693
  /** µ-law digital zero. */
@@ -3597,6 +3659,18 @@ var RtspRestreamer = class {
3597
3659
  return this.sessions.size;
3598
3660
  }
3599
3661
  /**
3662
+ * RTSP clients from OUTSIDE that are playing right now — people with a player
3663
+ * open on the restream, not our recorder, decoder or agents (see
3664
+ * `live-viewers.ts`). Read straight off the live session map and each
3665
+ * session's own `playing && !closed`, so a client that vanished without a
3666
+ * TEARDOWN stops counting the moment its socket closes or errors.
3667
+ */
3668
+ countExternalViewers() {
3669
+ let n = 0;
3670
+ for (const session of this.sessions.values()) if (isExternalRestreamClient(session.getViewerFacts())) n += 1;
3671
+ return n;
3672
+ }
3673
+ /**
3600
3674
  * Sessions that count as LIVE INTENT — everything except a sentry dial.
3601
3675
  *
3602
3676
  * The split exists so a Frigate-style puller can hold a session open for ever
@@ -4378,6 +4452,21 @@ var RtspSession = class {
4378
4452
  return this.userAgent;
4379
4453
  }
4380
4454
  /**
4455
+ * What the live-viewer count needs to classify this client (see
4456
+ * `live-viewers.ts`). `playing` is `isPlaying()`, which is false once the
4457
+ * socket closed or errored — so an abruptly dropped client stops counting at
4458
+ * the same instant, before the restreamer's own `removeSession` reconcile.
4459
+ */
4460
+ getViewerFacts() {
4461
+ return {
4462
+ playing: this.isPlaying(),
4463
+ userAgent: this.userAgent,
4464
+ remoteAddr: `${this.socket.remoteAddress ?? "unknown"}:${String(this.socket.remotePort ?? 0)}`,
4465
+ muted: this.muted,
4466
+ intent: this.declaration.intent
4467
+ };
4468
+ }
4469
+ /**
4381
4470
  * True only for a session that dialed the DETECTION path
4382
4471
  * (`DETECTION_MUTED_PATH_SUFFIX`). Read by
4383
4472
  * `RtspRestreamer.servePendingSessionsRtp` to exempt this session from the
@@ -14251,6 +14340,13 @@ var StreamBroker = class StreamBroker {
14251
14340
  getLocalStreamUrl() {
14252
14341
  return this.pipeServer.getUrl();
14253
14342
  }
14343
+ /**
14344
+ * External RTSP restream clients playing on this broker right now — the
14345
+ * restream half of the per-camera live-viewer count (`live-viewers.ts`).
14346
+ */
14347
+ countExternalRestreamViewers() {
14348
+ return this.rtspRestreamer.countExternalViewers();
14349
+ }
14254
14350
  /** Get the RTSP restreamer instance (for RtspListenServer registration) */
14255
14351
  getRtspRestreamer() {
14256
14352
  return this.rtspRestreamer;
@@ -18871,6 +18967,25 @@ var StreamBrokerManager = class StreamBrokerManager {
18871
18967
  };
18872
18968
  return broker.listClients();
18873
18969
  }
18970
+ /**
18971
+ * People watching each camera live right now (cap `getLiveViewerCounts`).
18972
+ * Two planes, one definition (`live-viewers.ts`): the WebRTC server's live
18973
+ * viewer sessions plus every broker's external RTSP restream clients, summed
18974
+ * per camera over all of its profile brokers. Both are read off live session
18975
+ * maps — no stored counter — so an ended session cannot be left counted.
18976
+ */
18977
+ async getLiveViewerCounts() {
18978
+ const rtsp = /* @__PURE__ */ new Map();
18979
+ for (const [brokerId, broker] of this.brokers) {
18980
+ const n = broker.countExternalRestreamViewers();
18981
+ const deviceId = deviceIdFromBrokerId$1(brokerId);
18982
+ if (n > 0 && deviceId !== void 0) rtsp.set(deviceId, (rtsp.get(deviceId) ?? 0) + n);
18983
+ }
18984
+ return [...mergeViewerCounts([this.webrtcServer?.countLiveViewers() ?? /* @__PURE__ */ new Map(), rtsp])].map(([deviceId, viewers]) => ({
18985
+ deviceId,
18986
+ viewers
18987
+ })).sort((a, b) => a.deviceId - b.deviceId);
18988
+ }
18874
18989
  async killClient(input) {
18875
18990
  const broker = this.brokers.get(input.brokerId);
18876
18991
  if (!broker) return { killed: false };
@@ -34729,6 +34844,7 @@ var BrokerWebrtcServer = class {
34729
34844
  });
34730
34845
  const entry = {
34731
34846
  session,
34847
+ consumerKind: baseAttribution.kind,
34732
34848
  brokerId,
34733
34849
  unsubBroker,
34734
34850
  unsubRtp,
@@ -35155,6 +35271,29 @@ var BrokerWebrtcServer = class {
35155
35271
  getSessionCount() {
35156
35272
  return this.sessions.size;
35157
35273
  }
35274
+ /**
35275
+ * People watching LIVE through this server, per camera (`live-viewers.ts`):
35276
+ * sessions of a viewer kind whose track is on the live edge. A session on a
35277
+ * recorded playhead is not watching live; the system's own subscriptions
35278
+ * (warm-up, linger ghost, egress-transcode legs) are never a SESSION here, so
35279
+ * they cannot count.
35280
+ *
35281
+ * Derived from `this.sessions` at call time, the map `cleanupSession`
35282
+ * maintains for every teardown path (explicit close, ICE failure, the peer
35283
+ * vanishing — all end in `session.onClosed`), so there is no second counter
35284
+ * that could leak.
35285
+ */
35286
+ countLiveViewers() {
35287
+ const counts = /* @__PURE__ */ new Map();
35288
+ for (const entry of this.sessions.values()) {
35289
+ if (!isViewerConsumerKind(entry.consumerKind)) continue;
35290
+ if ((entry.timeline?.currentMode ?? "live") !== "live") continue;
35291
+ const deviceId = deviceIdFromBrokerId(entry.brokerId);
35292
+ if (deviceId === void 0) continue;
35293
+ counts.set(deviceId, (counts.get(deviceId) ?? 0) + 1);
35294
+ }
35295
+ return counts;
35296
+ }
35158
35297
  /** Live signaling state for a session — used by the webrtc-session
35159
35298
  * provider's getSessionState. Returns a null pending when unknown. */
35160
35299
  getSessionRenegotiationState(sessionId) {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-v95KZ2Rt.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C4Cp_OAY.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.360",
3
+ "version": "1.2.362",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -61,7 +61,6 @@ from __future__ import annotations
61
61
  import asyncio
62
62
  import concurrent.futures
63
63
  import contextlib
64
- import fcntl
65
64
  import gc
66
65
  import io
67
66
  import json
@@ -80,6 +79,7 @@ from typing import Any, Awaitable, Callable, Optional
80
79
  import numpy as np
81
80
  from PIL import Image
82
81
 
82
+ import pool_platform
83
83
  from postprocessors import POSTPROCESSORS
84
84
 
85
85
  # ---------------------------------------------------------------------------
@@ -574,9 +574,12 @@ def compile_cache_lock(
574
574
  deadline = time.monotonic() + max(0.0, timeout_sec)
575
575
  while True:
576
576
  try:
577
- fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
578
- held = True
579
- break
577
+ if pool_platform.try_lock_exclusive(handle.fileno()):
578
+ held = True
579
+ break
580
+ if time.monotonic() >= deadline:
581
+ break
582
+ time.sleep(_COMPILE_CACHE_LOCK_POLL_SEC)
580
583
  except OSError:
581
584
  if time.monotonic() >= deadline:
582
585
  break
@@ -589,7 +592,7 @@ def compile_cache_lock(
589
592
  if handle is not None:
590
593
  try:
591
594
  if held:
592
- fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
595
+ pool_platform.unlock(handle.fileno())
593
596
  except OSError:
594
597
  pass
595
598
  try:
@@ -2344,6 +2347,9 @@ def _read_self_memory() -> dict:
2344
2347
  Partial results are fine — an empty dict means the read failed entirely.
2345
2348
  """
2346
2349
  out: dict = {}
2350
+ if sys.platform == "win32":
2351
+ # No `resource` and no /proc on Windows: psapi has the same numbers.
2352
+ return pool_platform.read_windows_memory()
2347
2353
  try:
2348
2354
  import resource
2349
2355
 
@@ -3000,14 +3006,7 @@ class CommandRouter:
3000
3006
 
3001
3007
  async def _run() -> None:
3002
3008
  loop = asyncio.get_event_loop()
3003
- reader = asyncio.StreamReader(loop=loop)
3004
- protocol = asyncio.StreamReaderProtocol(reader, loop=loop)
3005
- await loop.connect_read_pipe(lambda: protocol, sys.stdin.buffer)
3006
-
3007
- transport, stdout_protocol = await loop.connect_write_pipe(
3008
- asyncio.streams.FlowControlMixin, sys.stdout.buffer,
3009
- )
3010
- stream_writer = asyncio.StreamWriter(transport, stdout_protocol, None, loop)
3009
+ reader, stream_writer = await pool_platform.open_stdio(loop)
3011
3010
  writer = ResponseWriter(stream_writer)
3012
3011
 
3013
3012
  t_start = time.perf_counter()