@camstack/addon-pipeline 1.2.632 → 1.2.634

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 (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +3 -3
  4. package/dist/{custom-model-catalog-B2w26VBG.mjs → custom-model-catalog-BjkdcgOE.mjs} +2 -2
  5. package/dist/{custom-model-catalog-DN4xBVQE.js → custom-model-catalog-CgSsz_wK.js} +2 -2
  6. package/dist/detection-pipeline/index.js +4 -4
  7. package/dist/detection-pipeline/index.mjs +4 -4
  8. package/dist/{dist-CLo3QHtG.js → dist-C5sQVajZ.js} +685 -13
  9. package/dist/{dist-DWkdwtle.mjs → dist-DlB7gVjz.mjs} +685 -13
  10. package/dist/{model-catalogs-DIviCtiQ.js → model-catalogs-D6iqJdhC.js} +1 -1
  11. package/dist/{model-catalogs-rs7okKyp.mjs → model-catalogs-XdVK-cpH.mjs} +1 -1
  12. package/dist/motion-wasm/index.js +1 -1
  13. package/dist/motion-wasm/index.mjs +1 -1
  14. package/dist/{node-CAJ1Dal-.mjs → node-DI6cJT3H.mjs} +1 -1
  15. package/dist/{node-CPqDCFas.js → node-DqynP4Sn.js} +1 -1
  16. package/dist/pipeline-runner/index.js +3 -3
  17. package/dist/pipeline-runner/index.mjs +3 -3
  18. package/dist/{process-memory-BrKzYxim.js → process-memory-BWsvhswo.js} +1 -1
  19. package/dist/{process-memory-DJWthb6k.mjs → process-memory-CdsO2dxD.mjs} +1 -1
  20. package/dist/recorder/index.js +8 -2
  21. package/dist/recorder/index.mjs +8 -2
  22. package/dist/{segment-demux-js-Bt5bxfyh.js → segment-demux-js-CEqPvaCD.js} +1 -1
  23. package/dist/{segment-demux-js-CZdgio0n.mjs → segment-demux-js-jzhozTBr.mjs} +1 -1
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CjjdxZUq.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BfutHWHx.mjs} +2 -2
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DfXSkf61.mjs +26 -0
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CGcEN1m1.mjs +26 -0
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-MWvZi8Nq.mjs → hostInit-oZNKK5YV.mjs} +2 -2
  31. package/dist/stream-broker/index.js +115 -19
  32. package/dist/stream-broker/index.mjs +115 -19
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/package.json +1 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DZmxw-we.mjs +0 -26
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CFflpMdb.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { t as demuxSegmentFullJs } from "../segment-demux-js-CZdgio0n.mjs";
1
+ import { t as demuxSegmentFullJs } from "../segment-demux-js-jzhozTBr.mjs";
2
2
  import { parentPort } from "node:worker_threads";
3
3
  //#region src/stream-broker/recorded/demux-worker-child.ts
4
4
  /**
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.435",
39
+ version: "1.2.442",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.343",
84
+ version: "1.2.348",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -3,11 +3,11 @@ 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-CLo3QHtG.js");
7
- const require_node = require("../node-CPqDCFas.js");
6
+ const require_dist = require("../dist-C5sQVajZ.js");
7
+ const require_node = require("../node-DqynP4Sn.js");
8
8
  const require_restream_intent = require("../restream-intent-DzrZ_EHP.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
- const require_segment_demux_js = require("../segment-demux-js-Bt5bxfyh.js");
10
+ const require_segment_demux_js = require("../segment-demux-js-CEqPvaCD.js");
11
11
  let node_os = require("node:os");
12
12
  node_os = require_chunk.__toESM(node_os);
13
13
  let node_crypto = require("node:crypto");
@@ -874,7 +874,10 @@ function healthEventIsOperatorFacing(camStreamId) {
874
874
  *
875
875
  * - asleep and nobody asked for the stream — NOT an outage (`asleep`);
876
876
  * - asleep, a stream was DEMANDED and none arrived — `wake-timeout`;
877
- * - reported awake and the stream is gone — `connection-error`;
877
+ * - reported awake, the stream is gone, and the camera has been awake longer
878
+ * than a wake needs (`WAKE_DEMAND_BOUND_MS`) — `connection-error`; inside
879
+ * that window it is the normal wake (sleeping flipped, first keyframe not
880
+ * yet here) and is suppressed, then re-judged;
878
881
  * - silent past the battery refresh window — `battery-unreachable`.
879
882
  *
880
883
  * A sleep verdict needs a sleep state that was actually READ. A mirror that
@@ -911,10 +914,17 @@ function judgeStaleStream(input) {
911
914
  kind: "outage",
912
915
  reason: "battery-unreachable"
913
916
  };
914
- if (presence === "awake") return {
915
- kind: "outage",
916
- reason: "connection-error"
917
- };
917
+ if (presence === "awake") {
918
+ const awakeSince = input.battery.facts.awakeSince;
919
+ if (awakeSince !== void 0 && input.nowMs - awakeSince < 45e3) return {
920
+ kind: "suppressed",
921
+ presence
922
+ };
923
+ return {
924
+ kind: "outage",
925
+ reason: "connection-error"
926
+ };
927
+ }
918
928
  if (input.demanded) return {
919
929
  kind: "outage",
920
930
  reason: "wake-timeout"
@@ -17620,6 +17630,25 @@ function resolveBrokerTier(brokerId, localLookup, remoteBrokerTiers) {
17620
17630
  */
17621
17631
  var STREAM_STALE_TIMEOUT_MS = 12e4;
17622
17632
  var STREAM_HEALTH_POLL_MS = 15e3;
17633
+ /**
17634
+ * `awakeSince` for a sleep fact just observed: stamped when `sleeping` flips to
17635
+ * false (from asleep, or from nothing known — the event IS the flip), kept while
17636
+ * it stays awake, absent while asleep.
17637
+ */
17638
+ function awakeSinceFor(previous, sleeping, nowMs) {
17639
+ if (sleeping) return {};
17640
+ if (previous !== null && !previous.sleeping && previous.awakeSince !== void 0) return { awakeSince: previous.awakeSince };
17641
+ return { awakeSince: nowMs };
17642
+ }
17643
+ /** A slice READ carries no flip time: keep the one an event gave while the camera stays awake. */
17644
+ function carryAwakeSince(previous, read) {
17645
+ if (read === null || read.sleeping) return read;
17646
+ if (previous === null || previous.sleeping || previous.awakeSince === void 0) return read;
17647
+ return {
17648
+ ...read,
17649
+ awakeSince: previous.awakeSince
17650
+ };
17651
+ }
17623
17652
  /** How often an unanswered / stale battery probe is asked again. */
17624
17653
  var BATTERY_PROBE_RETRY_MS = 6e4;
17625
17654
  /** A wake-only outage with no further failed wake for this long is over: the demand is gone. */
@@ -20685,7 +20714,7 @@ var StreamBrokerManager = class StreamBrokerManager {
20685
20714
  markSleepSuppressed(brokerId, context, presence, lastPacketAt) {
20686
20715
  this.sleepSuppressedBrokers.add(brokerId);
20687
20716
  this.streamOfflineReasonByBroker.delete(brokerId);
20688
- this.logger.info("stream quiet on a sleeping battery camera — not an outage", {
20717
+ this.logger.info(presence === "awake" ? "stream quiet on a battery camera that just woke — not an outage yet, judged again after the wake bound" : "stream quiet on a sleeping battery camera — not an outage", {
20689
20718
  tags: { deviceId: context.deviceId },
20690
20719
  meta: {
20691
20720
  camStreamId: context.camStreamId,
@@ -20852,7 +20881,7 @@ var StreamBrokerManager = class StreamBrokerManager {
20852
20881
  this.batteryProbe.set(deviceId, {
20853
20882
  state: "done",
20854
20883
  isBattery: true,
20855
- facts: facts ?? current?.facts ?? null,
20884
+ facts: carryAwakeSince(current?.facts ?? null, facts) ?? current?.facts ?? null,
20856
20885
  readAt: Date.now()
20857
20886
  });
20858
20887
  }).finally(() => this.batteryProbeInFlight.delete(deviceId));
@@ -20874,9 +20903,11 @@ var StreamBrokerManager = class StreamBrokerManager {
20874
20903
  }
20875
20904
  recordBatterySleep(deviceId, status) {
20876
20905
  const previous = this.batteryProbe.get(deviceId);
20906
+ const nowMs = Date.now();
20877
20907
  const facts = status && typeof status.sleeping === "boolean" ? {
20878
20908
  sleeping: status.sleeping,
20879
- ...typeof status.lastContactAt === "number" ? { lastContactAt: status.lastContactAt } : {}
20909
+ ...typeof status.lastContactAt === "number" ? { lastContactAt: status.lastContactAt } : {},
20910
+ ...awakeSinceFor(previous?.facts ?? null, status.sleeping, nowMs)
20880
20911
  } : null;
20881
20912
  this.batteryProbe.set(deviceId, {
20882
20913
  state: "done",
@@ -24649,6 +24680,45 @@ var RembWatch = class {
24649
24680
  }
24650
24681
  };
24651
24682
  //#endregion
24683
+ //#region src/stream-broker/webrtc/inbound-stun-probe.ts
24684
+ var MAX_SOURCES = 8;
24685
+ var InboundStunProbe = class {
24686
+ onFirst;
24687
+ now;
24688
+ requests = 0;
24689
+ first = null;
24690
+ sources = /* @__PURE__ */ new Set();
24691
+ createdAt;
24692
+ constructor(onFirst, now = Date.now) {
24693
+ this.onFirst = onFirst;
24694
+ this.now = now;
24695
+ this.createdAt = now();
24696
+ }
24697
+ /** Pass as werift's `iceFilterStunResponse`. Always lets the request through. */
24698
+ filter = (_message, addr, _protocol) => {
24699
+ const from = `${addr[0]}:${addr[1]}`;
24700
+ this.requests += 1;
24701
+ if (this.sources.size < MAX_SOURCES) this.sources.add(from);
24702
+ if (this.first === null) {
24703
+ const afterMs = this.now() - this.createdAt;
24704
+ this.first = {
24705
+ from,
24706
+ atMs: afterMs
24707
+ };
24708
+ this.onFirst(from, afterMs);
24709
+ }
24710
+ return true;
24711
+ };
24712
+ summary() {
24713
+ return {
24714
+ requests: this.requests,
24715
+ firstFrom: this.first?.from ?? null,
24716
+ firstAfterMs: this.first?.atMs ?? null,
24717
+ sources: [...this.sources]
24718
+ };
24719
+ }
24720
+ };
24721
+ //#endregion
24652
24722
  //#region src/stream-broker/webrtc/transcode-egress-leg.ts
24653
24723
  /**
24654
24724
  * The request a browser session makes. Structured only: the primitive's schema
@@ -25182,6 +25252,8 @@ var AdaptiveSession = class AdaptiveSession {
25182
25252
  audioSender = null;
25183
25253
  feedAbort = null;
25184
25254
  closed = false;
25255
+ /** Armed by `handleOffer`; counts the peer's inbound ICE checks. */
25256
+ inboundStun = null;
25185
25257
  statsTimer = null;
25186
25258
  /**
25187
25259
  * Notification hook invoked exactly once when `close()` finishes. The
@@ -25875,6 +25947,18 @@ var AdaptiveSession = class AdaptiveSession {
25875
25947
  async handleOffer(clientOffer, opts) {
25876
25948
  const negotiateStart = Date.now();
25877
25949
  const { werift, pcOptions } = await this.buildPcOptions();
25950
+ this.inboundStun = new InboundStunProbe((from, afterMs) => {
25951
+ this.logger.info("ICE first inbound STUN binding request from the peer", {
25952
+ tags: { deviceId: this.deviceId },
25953
+ meta: {
25954
+ phase: "session",
25955
+ sessionId: this.sessionId,
25956
+ from,
25957
+ afterMs
25958
+ }
25959
+ });
25960
+ });
25961
+ pcOptions.iceFilterStunResponse = this.inboundStun.filter;
25878
25962
  this.pc = new werift.RTCPeerConnection(pcOptions);
25879
25963
  const pcSetupMs = Date.now() - negotiateStart;
25880
25964
  this.wireControlChannel();
@@ -25913,14 +25997,17 @@ var AdaptiveSession = class AdaptiveSession {
25913
25997
  const resolvedOfferSdp = await resolveMdnsCandidatesInSdp(clientOffer.sdp, this.logger, `session:${this.sessionId}`);
25914
25998
  const mdnsMs = Date.now() - mdnsStart;
25915
25999
  const offerCandidates = resolvedOfferSdp.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("a=candidate:"));
25916
- if (this.debug) this.logger.info("client offer REMOTE ICE candidates", { meta: {
25917
- phase: "session",
25918
- sessionId: this.sessionId,
25919
- deviceId: this.deviceId,
25920
- count: offerCandidates.length,
25921
- candidateTypes: summarizeCandidates(resolvedOfferSdp),
25922
- candidates: offerCandidates
25923
- } });
26000
+ this.logger.info("client offer REMOTE ICE candidates", {
26001
+ tags: { deviceId: this.deviceId },
26002
+ meta: {
26003
+ phase: "session",
26004
+ sessionId: this.sessionId,
26005
+ deviceId: this.deviceId,
26006
+ count: offerCandidates.length,
26007
+ candidateTypes: summarizeCandidates(resolvedOfferSdp),
26008
+ candidates: offerCandidates
26009
+ }
26010
+ });
25924
26011
  const remoteDesc = new werift.RTCSessionDescription(resolvedOfferSdp, clientOffer.type);
25925
26012
  const setRemoteStart = Date.now();
25926
26013
  await this.pc.setRemoteDescription(remoteDesc);
@@ -26178,6 +26265,15 @@ var AdaptiveSession = class AdaptiveSession {
26178
26265
  phase: "session",
26179
26266
  sessionId: this.sessionId
26180
26267
  } });
26268
+ if (this.inboundStun !== null) this.logger.info("ICE inbound STUN summary at close", {
26269
+ tags: { deviceId: this.deviceId },
26270
+ meta: {
26271
+ phase: "session",
26272
+ sessionId: this.sessionId,
26273
+ iceConnectionState: this.pc?.iceConnectionState ?? "unknown",
26274
+ ...this.inboundStun.summary()
26275
+ }
26276
+ });
26181
26277
  if (this.statsTimer) {
26182
26278
  clearInterval(this.statsTimer);
26183
26279
  this.statsTimer = null;
@@ -1,9 +1,9 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { Bn as isSoftwareDecode, Cr as record, D as G711_SCALE_CORRECTION_DB, Er as EventCategory, Et as egressTransportFromRequest, Gn as BaseAddon, Hn as DeviceType, Jt as memoizePinnedFfmpeg, K as RingBuffer, Kn as CAM_PROFILE_ORDER, L as RATE_CONTROL_RELAXED, Ln as buildFfmpegArgs, Lt as gifDeriveSpeed, Nn as webrtcSessionCapability, Pn as AUDIO_PRESETS, Pt as ffmpegRate, R as RATE_CONTROL_TIGHT, Rt as gifFromMp4Args, St as deviceRemovalHookCapability, Tr as union, Tt as egressTranscodeSharingKey, Un as errMsg, Ut as loadContributionCapability, Vn as DeviceFeature, Wn as BOOT_RECOVERY_BACKOFF_MS, Wt as logChannelsCapability, X as addonWidgetsSourceCapability, Zn as createEvent, _r as discriminatedUnion, _t as deriveBatteryPresence, ar as isSecretMarker, br as number, c as CLIP_STILL_REASON_CODE_HEADER, cr as maskUrlCredentials, et as armRemovalReconcile, gn as resolveEgressDecodeHwAccel, gr as boolean, hr as array, ht as declareLogChannel, ir as isEvent, it as batteryCapability, kn as streamBrokerCapability, lr as nodePin, lt as clipExportFailureOf, mr as _enum, mt as createLogChannelsProvider, o as BatteryStatusSchema, or as makeProfileBrokerId, pt as createHwAccelCache, qn as CamProfileSchema, s as CLIP_BROKER_PACED_RATES, sn as rankStreams, sr as makeSourceBrokerId, st as cameraStreamsCapability, tr as expandAudioChunkToF32le, ur as parseProfileBrokerId, w as EncodeProfileSchema, wr as string, xr as object, yr as literal, zn as invocationFromEncodeProfile } from "../dist-DWkdwtle.mjs";
3
- import { a as defaultFfmpegSpawn, i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, s as probeFfmpegBinary, t as ChildCostRegistry } from "../node-CAJ1Dal-.mjs";
2
+ import { Bn as isSoftwareDecode, Cr as record, D as G711_SCALE_CORRECTION_DB, Er as EventCategory, Et as egressTransportFromRequest, Gn as BaseAddon, Hn as DeviceType, Jt as memoizePinnedFfmpeg, K as RingBuffer, Kn as CAM_PROFILE_ORDER, L as RATE_CONTROL_RELAXED, Ln as buildFfmpegArgs, Lt as gifDeriveSpeed, Nn as webrtcSessionCapability, Pn as AUDIO_PRESETS, Pt as ffmpegRate, R as RATE_CONTROL_TIGHT, Rt as gifFromMp4Args, St as deviceRemovalHookCapability, Tr as union, Tt as egressTranscodeSharingKey, Un as errMsg, Ut as loadContributionCapability, Vn as DeviceFeature, Wn as BOOT_RECOVERY_BACKOFF_MS, Wt as logChannelsCapability, X as addonWidgetsSourceCapability, Zn as createEvent, _r as discriminatedUnion, _t as deriveBatteryPresence, ar as isSecretMarker, br as number, c as CLIP_STILL_REASON_CODE_HEADER, cr as maskUrlCredentials, et as armRemovalReconcile, gn as resolveEgressDecodeHwAccel, gr as boolean, hr as array, ht as declareLogChannel, ir as isEvent, it as batteryCapability, kn as streamBrokerCapability, lr as nodePin, lt as clipExportFailureOf, mr as _enum, mt as createLogChannelsProvider, o as BatteryStatusSchema, or as makeProfileBrokerId, pt as createHwAccelCache, qn as CamProfileSchema, s as CLIP_BROKER_PACED_RATES, sn as rankStreams, sr as makeSourceBrokerId, st as cameraStreamsCapability, tr as expandAudioChunkToF32le, ur as parseProfileBrokerId, w as EncodeProfileSchema, wr as string, xr as object, yr as literal, zn as invocationFromEncodeProfile } from "../dist-DlB7gVjz.mjs";
3
+ import { a as defaultFfmpegSpawn, i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, s as probeFfmpegBinary, t as ChildCostRegistry } from "../node-DI6cJT3H.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-DvzCnthK.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
- 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-CZdgio0n.mjs";
6
+ 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-jzhozTBr.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import * as os$1 from "node:os";
9
9
  import { networkInterfaces, tmpdir } from "node:os";
@@ -870,7 +870,10 @@ function healthEventIsOperatorFacing(camStreamId) {
870
870
  *
871
871
  * - asleep and nobody asked for the stream — NOT an outage (`asleep`);
872
872
  * - asleep, a stream was DEMANDED and none arrived — `wake-timeout`;
873
- * - reported awake and the stream is gone — `connection-error`;
873
+ * - reported awake, the stream is gone, and the camera has been awake longer
874
+ * than a wake needs (`WAKE_DEMAND_BOUND_MS`) — `connection-error`; inside
875
+ * that window it is the normal wake (sleeping flipped, first keyframe not
876
+ * yet here) and is suppressed, then re-judged;
874
877
  * - silent past the battery refresh window — `battery-unreachable`.
875
878
  *
876
879
  * A sleep verdict needs a sleep state that was actually READ. A mirror that
@@ -907,10 +910,17 @@ function judgeStaleStream(input) {
907
910
  kind: "outage",
908
911
  reason: "battery-unreachable"
909
912
  };
910
- if (presence === "awake") return {
911
- kind: "outage",
912
- reason: "connection-error"
913
- };
913
+ if (presence === "awake") {
914
+ const awakeSince = input.battery.facts.awakeSince;
915
+ if (awakeSince !== void 0 && input.nowMs - awakeSince < 45e3) return {
916
+ kind: "suppressed",
917
+ presence
918
+ };
919
+ return {
920
+ kind: "outage",
921
+ reason: "connection-error"
922
+ };
923
+ }
914
924
  if (input.demanded) return {
915
925
  kind: "outage",
916
926
  reason: "wake-timeout"
@@ -17616,6 +17626,25 @@ function resolveBrokerTier(brokerId, localLookup, remoteBrokerTiers) {
17616
17626
  */
17617
17627
  var STREAM_STALE_TIMEOUT_MS = 12e4;
17618
17628
  var STREAM_HEALTH_POLL_MS = 15e3;
17629
+ /**
17630
+ * `awakeSince` for a sleep fact just observed: stamped when `sleeping` flips to
17631
+ * false (from asleep, or from nothing known — the event IS the flip), kept while
17632
+ * it stays awake, absent while asleep.
17633
+ */
17634
+ function awakeSinceFor(previous, sleeping, nowMs) {
17635
+ if (sleeping) return {};
17636
+ if (previous !== null && !previous.sleeping && previous.awakeSince !== void 0) return { awakeSince: previous.awakeSince };
17637
+ return { awakeSince: nowMs };
17638
+ }
17639
+ /** A slice READ carries no flip time: keep the one an event gave while the camera stays awake. */
17640
+ function carryAwakeSince(previous, read) {
17641
+ if (read === null || read.sleeping) return read;
17642
+ if (previous === null || previous.sleeping || previous.awakeSince === void 0) return read;
17643
+ return {
17644
+ ...read,
17645
+ awakeSince: previous.awakeSince
17646
+ };
17647
+ }
17619
17648
  /** How often an unanswered / stale battery probe is asked again. */
17620
17649
  var BATTERY_PROBE_RETRY_MS = 6e4;
17621
17650
  /** A wake-only outage with no further failed wake for this long is over: the demand is gone. */
@@ -20681,7 +20710,7 @@ var StreamBrokerManager = class StreamBrokerManager {
20681
20710
  markSleepSuppressed(brokerId, context, presence, lastPacketAt) {
20682
20711
  this.sleepSuppressedBrokers.add(brokerId);
20683
20712
  this.streamOfflineReasonByBroker.delete(brokerId);
20684
- this.logger.info("stream quiet on a sleeping battery camera — not an outage", {
20713
+ this.logger.info(presence === "awake" ? "stream quiet on a battery camera that just woke — not an outage yet, judged again after the wake bound" : "stream quiet on a sleeping battery camera — not an outage", {
20685
20714
  tags: { deviceId: context.deviceId },
20686
20715
  meta: {
20687
20716
  camStreamId: context.camStreamId,
@@ -20848,7 +20877,7 @@ var StreamBrokerManager = class StreamBrokerManager {
20848
20877
  this.batteryProbe.set(deviceId, {
20849
20878
  state: "done",
20850
20879
  isBattery: true,
20851
- facts: facts ?? current?.facts ?? null,
20880
+ facts: carryAwakeSince(current?.facts ?? null, facts) ?? current?.facts ?? null,
20852
20881
  readAt: Date.now()
20853
20882
  });
20854
20883
  }).finally(() => this.batteryProbeInFlight.delete(deviceId));
@@ -20870,9 +20899,11 @@ var StreamBrokerManager = class StreamBrokerManager {
20870
20899
  }
20871
20900
  recordBatterySleep(deviceId, status) {
20872
20901
  const previous = this.batteryProbe.get(deviceId);
20902
+ const nowMs = Date.now();
20873
20903
  const facts = status && typeof status.sleeping === "boolean" ? {
20874
20904
  sleeping: status.sleeping,
20875
- ...typeof status.lastContactAt === "number" ? { lastContactAt: status.lastContactAt } : {}
20905
+ ...typeof status.lastContactAt === "number" ? { lastContactAt: status.lastContactAt } : {},
20906
+ ...awakeSinceFor(previous?.facts ?? null, status.sleeping, nowMs)
20876
20907
  } : null;
20877
20908
  this.batteryProbe.set(deviceId, {
20878
20909
  state: "done",
@@ -24645,6 +24676,45 @@ var RembWatch = class {
24645
24676
  }
24646
24677
  };
24647
24678
  //#endregion
24679
+ //#region src/stream-broker/webrtc/inbound-stun-probe.ts
24680
+ var MAX_SOURCES = 8;
24681
+ var InboundStunProbe = class {
24682
+ onFirst;
24683
+ now;
24684
+ requests = 0;
24685
+ first = null;
24686
+ sources = /* @__PURE__ */ new Set();
24687
+ createdAt;
24688
+ constructor(onFirst, now = Date.now) {
24689
+ this.onFirst = onFirst;
24690
+ this.now = now;
24691
+ this.createdAt = now();
24692
+ }
24693
+ /** Pass as werift's `iceFilterStunResponse`. Always lets the request through. */
24694
+ filter = (_message, addr, _protocol) => {
24695
+ const from = `${addr[0]}:${addr[1]}`;
24696
+ this.requests += 1;
24697
+ if (this.sources.size < MAX_SOURCES) this.sources.add(from);
24698
+ if (this.first === null) {
24699
+ const afterMs = this.now() - this.createdAt;
24700
+ this.first = {
24701
+ from,
24702
+ atMs: afterMs
24703
+ };
24704
+ this.onFirst(from, afterMs);
24705
+ }
24706
+ return true;
24707
+ };
24708
+ summary() {
24709
+ return {
24710
+ requests: this.requests,
24711
+ firstFrom: this.first?.from ?? null,
24712
+ firstAfterMs: this.first?.atMs ?? null,
24713
+ sources: [...this.sources]
24714
+ };
24715
+ }
24716
+ };
24717
+ //#endregion
24648
24718
  //#region src/stream-broker/webrtc/transcode-egress-leg.ts
24649
24719
  /**
24650
24720
  * The request a browser session makes. Structured only: the primitive's schema
@@ -25178,6 +25248,8 @@ var AdaptiveSession = class AdaptiveSession {
25178
25248
  audioSender = null;
25179
25249
  feedAbort = null;
25180
25250
  closed = false;
25251
+ /** Armed by `handleOffer`; counts the peer's inbound ICE checks. */
25252
+ inboundStun = null;
25181
25253
  statsTimer = null;
25182
25254
  /**
25183
25255
  * Notification hook invoked exactly once when `close()` finishes. The
@@ -25871,6 +25943,18 @@ var AdaptiveSession = class AdaptiveSession {
25871
25943
  async handleOffer(clientOffer, opts) {
25872
25944
  const negotiateStart = Date.now();
25873
25945
  const { werift, pcOptions } = await this.buildPcOptions();
25946
+ this.inboundStun = new InboundStunProbe((from, afterMs) => {
25947
+ this.logger.info("ICE first inbound STUN binding request from the peer", {
25948
+ tags: { deviceId: this.deviceId },
25949
+ meta: {
25950
+ phase: "session",
25951
+ sessionId: this.sessionId,
25952
+ from,
25953
+ afterMs
25954
+ }
25955
+ });
25956
+ });
25957
+ pcOptions.iceFilterStunResponse = this.inboundStun.filter;
25874
25958
  this.pc = new werift.RTCPeerConnection(pcOptions);
25875
25959
  const pcSetupMs = Date.now() - negotiateStart;
25876
25960
  this.wireControlChannel();
@@ -25909,14 +25993,17 @@ var AdaptiveSession = class AdaptiveSession {
25909
25993
  const resolvedOfferSdp = await resolveMdnsCandidatesInSdp(clientOffer.sdp, this.logger, `session:${this.sessionId}`);
25910
25994
  const mdnsMs = Date.now() - mdnsStart;
25911
25995
  const offerCandidates = resolvedOfferSdp.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("a=candidate:"));
25912
- if (this.debug) this.logger.info("client offer REMOTE ICE candidates", { meta: {
25913
- phase: "session",
25914
- sessionId: this.sessionId,
25915
- deviceId: this.deviceId,
25916
- count: offerCandidates.length,
25917
- candidateTypes: summarizeCandidates(resolvedOfferSdp),
25918
- candidates: offerCandidates
25919
- } });
25996
+ this.logger.info("client offer REMOTE ICE candidates", {
25997
+ tags: { deviceId: this.deviceId },
25998
+ meta: {
25999
+ phase: "session",
26000
+ sessionId: this.sessionId,
26001
+ deviceId: this.deviceId,
26002
+ count: offerCandidates.length,
26003
+ candidateTypes: summarizeCandidates(resolvedOfferSdp),
26004
+ candidates: offerCandidates
26005
+ }
26006
+ });
25920
26007
  const remoteDesc = new werift.RTCSessionDescription(resolvedOfferSdp, clientOffer.type);
25921
26008
  const setRemoteStart = Date.now();
25922
26009
  await this.pc.setRemoteDescription(remoteDesc);
@@ -26174,6 +26261,15 @@ var AdaptiveSession = class AdaptiveSession {
26174
26261
  phase: "session",
26175
26262
  sessionId: this.sessionId
26176
26263
  } });
26264
+ if (this.inboundStun !== null) this.logger.info("ICE inbound STUN summary at close", {
26265
+ tags: { deviceId: this.deviceId },
26266
+ meta: {
26267
+ phase: "session",
26268
+ sessionId: this.sessionId,
26269
+ iceConnectionState: this.pc?.iceConnectionState ?? "unknown",
26270
+ ...this.inboundStun.summary()
26271
+ }
26272
+ });
26177
26273
  if (this.statsTimer) {
26178
26274
  clearInterval(this.statsTimer);
26179
26275
  this.statsTimer = null;
@@ -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-CjjdxZUq.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BfutHWHx.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.632",
3
+ "version": "1.2.634",
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",