@camstack/addon-pipeline 1.2.374 → 1.2.601

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-1LDuDw8S.mjs → custom-model-catalog-CK9GK3N5.mjs} +2 -2
  4. package/dist/{custom-model-catalog-W0DHDhfo.js → custom-model-catalog-DgyafyuW.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-CmdLCb_F.mjs → dist-BaHsDOWa.mjs} +1124 -29
  8. package/dist/{dist-CBEUVjTS.js → dist-BqI10eLV.js} +1129 -28
  9. package/dist/{model-catalogs-BnCdB2A3.mjs → model-catalogs-DM5sVj1T.mjs} +1 -1
  10. package/dist/{model-catalogs-D5YksA5A.js → model-catalogs-DhGVt_sH.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-C3r8kiOo.mjs → node-B0a0dXhg.mjs} +38 -3
  14. package/dist/{node-DMgi6pND.js → node-CgwqjGz0.js} +42 -1
  15. package/dist/pipeline-runner/index.js +4 -4
  16. package/dist/pipeline-runner/index.mjs +4 -4
  17. package/dist/{process-memory-GFR7fxpJ.js → process-memory-C0zbWvwD.js} +1 -1
  18. package/dist/{process-memory-X0ws1SGJ.mjs → process-memory-sfdcLVkW.mjs} +1 -1
  19. package/dist/recorder/index.js +357 -516
  20. package/dist/recorder/index.mjs +357 -516
  21. package/dist/{segment-demux-js-CSQVhbFI.js → segment-demux-js-B9lL2VCC.js} +1 -1
  22. package/dist/{segment-demux-js-IKEO1XBz.mjs → segment-demux-js-BB4CUAB5.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-DPFTlVoW.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbN9DjoK.mjs} +3 -3
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Boq8JqyD.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-B25q1OKG.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-BOp1_Hhk.mjs → hostInit-BSYHAfPt.mjs} +3 -3
  30. package/dist/stream-broker/index.js +556 -32
  31. package/dist/stream-broker/index.mjs +556 -32
  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-B7wsxLrK.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-BohOb2Cl.mjs +0 -26
@@ -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-CBEUVjTS.js");
7
- const require_node = require("../node-DMgi6pND.js");
6
+ const require_dist = require("../dist-BqI10eLV.js");
7
+ const require_node = require("../node-CgwqjGz0.js");
8
8
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
- const require_segment_demux_js = require("../segment-demux-js-CSQVhbFI.js");
10
+ const require_segment_demux_js = require("../segment-demux-js-B9lL2VCC.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");
@@ -858,6 +858,131 @@ function healthEventIsOperatorFacing(camStreamId) {
858
858
  return !PRIVATE_STREAM_PREFIXES.some((prefix) => camStreamId.startsWith(prefix));
859
859
  }
860
860
  //#endregion
861
+ //#region src/stream-broker/stream-broker/stream-health-policy.ts
862
+ /**
863
+ * WHAT a stale stream means for a battery camera (D883).
864
+ *
865
+ * A mains camera whose stream goes quiet for the stale window has lost its
866
+ * stream: that is an outage. A battery camera whose stream goes quiet has,
867
+ * ordinarily, gone to sleep — the designed steady state, not a fault. The
868
+ * broker used to tell the two apart by nothing, so every sleep produced a
869
+ * `stream.offline` per profile and every wake a `stream.online`, and the
870
+ * operator's system-event rule pushed each one.
871
+ *
872
+ * The verdict is made HERE, at the emitter, from the battery's own sleep
873
+ * mirror — never by a filter downstream:
874
+ *
875
+ * - asleep and nobody asked for the stream — NOT an outage (`asleep`);
876
+ * - asleep, a stream was DEMANDED and none arrived — `wake-timeout`;
877
+ * - reported awake and the stream is gone — `connection-error`;
878
+ * - silent past the battery refresh window — `battery-unreachable`.
879
+ *
880
+ * A sleep verdict needs a sleep state that was actually READ. A mirror that
881
+ * was never fed (a restart, a dropped event) is not "asleep": the caller passes
882
+ * `battery: null` and the stream is judged exactly as a mains one (D49 —
883
+ * unknown excludes nothing, and a silent suppression on an unread mirror hid
884
+ * a camera that slept and then died).
885
+ */
886
+ var OUTAGE_REASONS = [
887
+ "stale-timeout",
888
+ "wake-timeout",
889
+ "connection-error",
890
+ "battery-unreachable"
891
+ ];
892
+ /**
893
+ * How long a demand may wait for a sleeping camera's stream before it counts
894
+ * as a failed wake: the firmware wake bound plus the broker settle window of
895
+ * wake-on-play (8 s + 20 s), with margin for the sweep cadence.
896
+ */
897
+ var WAKE_DEMAND_BOUND_MS = 45e3;
898
+ function isStreamOutageReason(reason) {
899
+ return reason !== void 0 && OUTAGE_REASONS.includes(reason);
900
+ }
901
+ function judgeStaleStream(input) {
902
+ if (input.battery === null) return {
903
+ kind: "outage",
904
+ reason: "stale-timeout"
905
+ };
906
+ const presence = require_dist.deriveBatteryPresence({
907
+ status: input.battery.facts,
908
+ nowMs: input.nowMs
909
+ });
910
+ if (presence === "unreachable") return {
911
+ kind: "outage",
912
+ reason: "battery-unreachable"
913
+ };
914
+ if (presence === "awake") return {
915
+ kind: "outage",
916
+ reason: "connection-error"
917
+ };
918
+ if (input.demanded) return {
919
+ kind: "outage",
920
+ reason: "wake-timeout"
921
+ };
922
+ return {
923
+ kind: "suppressed",
924
+ presence
925
+ };
926
+ }
927
+ //#endregion
928
+ //#region src/stream-broker/stream-broker/device-stream-outages.ts
929
+ var DeviceStreamOutages = class {
930
+ byDevice = /* @__PURE__ */ new Map();
931
+ get(deviceId) {
932
+ return this.byDevice.get(deviceId);
933
+ }
934
+ /** Open the device's outage, or add this stream to the one already open. */
935
+ open(deviceId, camStreamId, reason, now) {
936
+ const existing = this.byDevice.get(deviceId);
937
+ if (existing) {
938
+ existing.camStreamIds.add(camStreamId);
939
+ existing.wakeOnly = false;
940
+ existing.lastSignalAt = now;
941
+ return existing;
942
+ }
943
+ const created = {
944
+ reason,
945
+ since: now,
946
+ camStreamIds: new Set([camStreamId]),
947
+ announced: false,
948
+ wakeOnly: false,
949
+ lastSignalAt: now
950
+ };
951
+ this.byDevice.set(deviceId, created);
952
+ return created;
953
+ }
954
+ /**
955
+ * A failed wake with no broker to carry it. Joins the device's open outage
956
+ * (one outage per device) or opens a wake-only one over its REAL streams.
957
+ */
958
+ openWake(deviceId, camStreamIds, reason, now) {
959
+ const existing = this.byDevice.get(deviceId);
960
+ if (existing) {
961
+ existing.lastSignalAt = now;
962
+ return existing;
963
+ }
964
+ const created = {
965
+ reason,
966
+ since: now,
967
+ camStreamIds: new Set(camStreamIds),
968
+ announced: false,
969
+ wakeOnly: true,
970
+ lastSignalAt: now
971
+ };
972
+ this.byDevice.set(deviceId, created);
973
+ return created;
974
+ }
975
+ entries() {
976
+ return this.byDevice.entries();
977
+ }
978
+ close(deviceId) {
979
+ this.byDevice.delete(deviceId);
980
+ }
981
+ clear() {
982
+ this.byDevice.clear();
983
+ }
984
+ };
985
+ //#endregion
861
986
  //#region src/stream-broker/stream-broker/sentry-eligibility.ts
862
987
  function sentryEligible(input) {
863
988
  if (input.batteryOperated === null || input.declaresSignals === null) return {
@@ -1741,7 +1866,7 @@ var FfmpegPushReader = class {
1741
1866
  this.ffmpegPath = options.ffmpegPath;
1742
1867
  this.args = options.args;
1743
1868
  this.logger = options.logger;
1744
- this.spawnFn = options.spawnFn ?? node_child_process.spawn;
1869
+ this.spawnFn = options.spawnFn ?? require_node.defaultFfmpegSpawn;
1745
1870
  this.callbacks = callbacks;
1746
1871
  this.logLabel = logLabel;
1747
1872
  this.claimCost = options.claimCost;
@@ -7650,7 +7775,7 @@ var EgressTranscodeManager = class {
7650
7775
  const egress = new TranscodeEgress({
7651
7776
  logger: this.opts.logger.child("egress"),
7652
7777
  ffmpegBinaryPath,
7653
- spawnFn: this.opts.spawnFn ?? node_child_process.spawn,
7778
+ spawnFn: this.opts.spawnFn ?? require_node.defaultFfmpegSpawn,
7654
7779
  acquireChannel: () => channel,
7655
7780
  ...spec.onDied ? { onChildExit: spec.onDied } : {},
7656
7781
  claimCost: (pid) => this.opts.costRegistry?.claim({
@@ -7685,7 +7810,7 @@ var EgressTranscodeManager = class {
7685
7810
  const child = new require_node.Fmp4FragmentChild({
7686
7811
  logger: this.opts.logger.child("egress-fmp4"),
7687
7812
  ffmpegBinaryPath,
7688
- spawnFn: this.opts.spawnFn ?? node_child_process.spawn,
7813
+ spawnFn: this.opts.spawnFn ?? require_node.defaultFfmpegSpawn,
7689
7814
  ...this.opts.fragmentFirstUnitTimeoutMs === void 0 ? {} : { firstUnitTimeoutMs: this.opts.fragmentFirstUnitTimeoutMs },
7690
7815
  ...spec.onDied ? { onChildExit: spec.onDied } : {}
7691
7816
  }, {
@@ -14555,6 +14680,15 @@ var StreamBroker = class StreamBroker {
14555
14680
  return false;
14556
14681
  }
14557
14682
  /**
14683
+ * Does a consumer want this stream right now (a viewer, a recording, a
14684
+ * detection decode, a camera-reported wake hold)? Sentry sessions and idle
14685
+ * prestream do not count. The stream-health sweep reads it to tell a battery
14686
+ * camera that slept unasked from one that failed to wake for somebody (D883).
14687
+ */
14688
+ hasLiveDemand() {
14689
+ return this.hasLiveIntent();
14690
+ }
14691
+ /**
14558
14692
  * True when at least one consumer needs DECODED audio. The audio-specific
14559
14693
  * subset of {@link hasDemand} — deliberately excludes video-only
14560
14694
  * consumers so a video-only viewer never spins up the AAC/Opus decode
@@ -16784,7 +16918,7 @@ var TranscodePipelineManager = class {
16784
16918
  this.onFfmpegMissing = opts.onFfmpegMissing ?? null;
16785
16919
  this.probeFfmpeg = opts.probeFfmpeg ?? require_node.probeFfmpegBinary;
16786
16920
  this.resolveStreamHwAccel = opts.resolveStreamHwAccel ?? null;
16787
- this.spawnFn = opts.spawnFn ?? node_child_process.spawn;
16921
+ this.spawnFn = opts.spawnFn ?? require_node.defaultFfmpegSpawn;
16788
16922
  this.acquireChannel = opts.acquireChannel ?? null;
16789
16923
  this.sentryOffered = opts.sentryOffered ?? null;
16790
16924
  }
@@ -17454,6 +17588,10 @@ function resolveBrokerTier(brokerId, localLookup, remoteBrokerTiers) {
17454
17588
  */
17455
17589
  var STREAM_STALE_TIMEOUT_MS = 12e4;
17456
17590
  var STREAM_HEALTH_POLL_MS = 15e3;
17591
+ /** How often an unanswered / stale battery probe is asked again. */
17592
+ var BATTERY_PROBE_RETRY_MS = 6e4;
17593
+ /** A wake-only outage with no further failed wake for this long is over: the demand is gone. */
17594
+ var WAKE_ONLY_OUTAGE_TTL_MS = 5 * 6e4;
17457
17595
  /** Compose a SOURCE broker id from a `(deviceId, camStreamId)` tuple. Thin
17458
17596
  * wrapper over the canonical `makeSourceBrokerId` — single source of truth. */
17459
17597
  function brokerIdFor(deviceId, camStreamId) {
@@ -17646,6 +17784,25 @@ var StreamBrokerManager = class StreamBrokerManager {
17646
17784
  * guess which one it was looking at.
17647
17785
  */
17648
17786
  streamOfflineReasonByBroker = /* @__PURE__ */ new Map();
17787
+ /**
17788
+ * Brokers whose stream went quiet because their BATTERY camera is asleep.
17789
+ * Not an outage and never announced (D883); remembered so the wake that ends
17790
+ * it is recognised as a plain wake instead of a `recovered`.
17791
+ */
17792
+ sleepSuppressedBrokers = /* @__PURE__ */ new Set();
17793
+ /**
17794
+ * What the sweep knows of a device's battery: is it one, and what did its
17795
+ * `battery` slice last say. Fed by `BatteryOnStatusChanged` (droppable, empty
17796
+ * after a restart) AND read from the device registry / slice by the sweep
17797
+ * itself — the same two reads wake-on-play makes — so a restart or a lost
17798
+ * event never leaves the verdict standing on an unread mirror (D49).
17799
+ */
17800
+ batteryProbe = /* @__PURE__ */ new Map();
17801
+ batteryProbeInFlight = /* @__PURE__ */ new Set();
17802
+ /** When a consumer's demand on a quiet battery stream was first seen (the wake bound starts there). */
17803
+ demandSince = /* @__PURE__ */ new Map();
17804
+ /** The stream outage a DEVICE is in (one entry per device, however many profiles). */
17805
+ deviceOutages = new DeviceStreamOutages();
17649
17806
  streamHealthTimer;
17650
17807
  /**
17651
17808
  * Routes an audio-chunk `subscriptionId` (Phase 5 / D9) back to its owning
@@ -18586,6 +18743,7 @@ var StreamBrokerManager = class StreamBrokerManager {
18586
18743
  const deviceId = typeof data.deviceId === "number" ? data.deviceId : null;
18587
18744
  if (deviceId === null) return;
18588
18745
  const sleeping = data.status?.sleeping === true;
18746
+ this.recordBatterySleep(deviceId, data.status);
18589
18747
  this.applyPlaceholderReasonToDevice(deviceId, sleeping ? "sleeping" : "reconnecting");
18590
18748
  });
18591
18749
  bus.subscribe({ category: require_dist.EventCategory.BatteryOnWakeStarted }, (event) => {
@@ -18749,18 +18907,7 @@ var StreamBrokerManager = class StreamBrokerManager {
18749
18907
  await this.batteryWakeOnPlay.run(deviceId, {
18750
18908
  logger: this.logger,
18751
18909
  isBatteryDevice: (id) => this.isBatteryOperated(id),
18752
- readBatteryFacts: async (id) => {
18753
- const slice = await api.deviceState.getCapSlice.query({
18754
- deviceId: id,
18755
- capName: require_dist.batteryCapability.name
18756
- }).catch(() => null);
18757
- const parsed = require_dist.BatteryStatusSchema.safeParse(slice);
18758
- if (!parsed.success) return null;
18759
- return {
18760
- sleeping: parsed.data.sleeping,
18761
- lastContactAt: parsed.data.lastContactAt
18762
- };
18763
- },
18910
+ readBatteryFacts: (id) => this.readBatterySleepFacts(id),
18764
18911
  wakeForStream: async (id, timeoutMs) => api.battery.wakeForStream.mutate({
18765
18912
  deviceId: id,
18766
18913
  timeoutMs
@@ -18768,6 +18915,9 @@ var StreamBrokerManager = class StreamBrokerManager {
18768
18915
  reconcileCatalog: (id) => this.reconcileDeviceCatalog(id),
18769
18916
  ensureBroker: () => this.ensureBrokerForWebrtcSession(deviceId, brokerId, camStreamId),
18770
18917
  hasBroker: demandHasBroker
18918
+ }).catch((err) => {
18919
+ if (err instanceof BatteryStreamUnavailableError) this.announceBatteryWakeFailure(deviceId, camStreamId, err.presence === "unreachable" ? "battery-unreachable" : "wake-timeout");
18920
+ throw err;
18771
18921
  });
18772
18922
  }
18773
18923
  /** Any broker (local or remote-adapter) registered for this device? The
@@ -20391,6 +20541,10 @@ var StreamBrokerManager = class StreamBrokerManager {
20391
20541
  this.brokers.clear();
20392
20542
  this.streamHealthByBroker.clear();
20393
20543
  this.streamOfflineReasonByBroker.clear();
20544
+ this.sleepSuppressedBrokers.clear();
20545
+ this.batteryProbe.clear();
20546
+ this.demandSince.clear();
20547
+ this.deviceOutages.clear();
20394
20548
  this.audioSubscriptionBroker.clear();
20395
20549
  await this.rtspServer.stop();
20396
20550
  }
@@ -20413,6 +20567,8 @@ var StreamBrokerManager = class StreamBrokerManager {
20413
20567
  evaluateStreamHealth() {
20414
20568
  if (!this.eventBus) return;
20415
20569
  const now = Date.now();
20570
+ const outageOpenedOn = /* @__PURE__ */ new Map();
20571
+ const cameBackOn = /* @__PURE__ */ new Map();
20416
20572
  for (const [brokerId, broker] of this.brokers) {
20417
20573
  const parsed = this.parseBrokerId(brokerId);
20418
20574
  if (!parsed) continue;
@@ -20422,30 +20578,386 @@ var StreamBrokerManager = class StreamBrokerManager {
20422
20578
  const wasHealthy = this.streamHealthByBroker.get(brokerId);
20423
20579
  const isHealthy = lastPacketAt > 0 && now - lastPacketAt <= STREAM_STALE_TIMEOUT_MS;
20424
20580
  if (!isHealthy && broker.getStats().status === "streaming") broker.redial("stream-health sweep: phantom streaming, no packets within stale window");
20425
- if (wasHealthy === isHealthy) continue;
20581
+ const context = {
20582
+ deviceId,
20583
+ camStreamId,
20584
+ brokerId,
20585
+ sourceType: broker.getActiveSourceType(),
20586
+ lastPacketAt
20587
+ };
20588
+ if (wasHealthy === isHealthy) {
20589
+ if (!isHealthy) this.rejudgeQuietBroker(brokerId, broker, context, now, outageOpenedOn);
20590
+ continue;
20591
+ }
20426
20592
  if (wasHealthy === void 0 && !isHealthy && !broker.suspended) {
20427
20593
  this.streamHealthByBroker.set(brokerId, false);
20428
20594
  continue;
20429
20595
  }
20596
+ let verdict = null;
20597
+ if (!isHealthy && !broker.suspended) {
20598
+ verdict = this.judgeQuietStream(brokerId, deviceId, broker, now);
20599
+ if (verdict === "defer") continue;
20600
+ }
20430
20601
  this.streamHealthByBroker.set(brokerId, isHealthy);
20431
- const reason = isHealthy ? wasHealthy === void 0 || !this.streamOfflineReasonByBroker.has(brokerId) ? "first-packet" : this.streamOfflineReasonByBroker.get(brokerId) === "suspended" ? "resumed" : "recovered" : broker.suspended ? "suspended" : "stale-timeout";
20432
- if (isHealthy) this.streamOfflineReasonByBroker.delete(brokerId);
20433
- else this.streamOfflineReasonByBroker.set(brokerId, reason);
20434
- this.emitStreamHealth(isHealthy, {
20602
+ const wasSleepSuppressed = this.sleepSuppressedBrokers.delete(brokerId);
20603
+ const previousReason = this.streamOfflineReasonByBroker.get(brokerId);
20604
+ if (verdict !== null) {
20605
+ if (verdict.kind === "suppressed") {
20606
+ this.markSleepSuppressed(brokerId, context, verdict.presence, lastPacketAt);
20607
+ continue;
20608
+ }
20609
+ this.openStreamOutage(brokerId, context, verdict.reason, now, outageOpenedOn);
20610
+ continue;
20611
+ }
20612
+ if (!isHealthy) {
20613
+ this.demandSince.delete(brokerId);
20614
+ this.streamOfflineReasonByBroker.set(brokerId, "suspended");
20615
+ this.emitStreamHealth(false, {
20616
+ ...context,
20617
+ reason: "suspended"
20618
+ });
20619
+ continue;
20620
+ }
20621
+ this.streamOfflineReasonByBroker.delete(brokerId);
20622
+ this.demandSince.delete(brokerId);
20623
+ if (wasSleepSuppressed) this.logger.info("battery camera woke and delivers a stream — plain wake, no event", {
20624
+ tags: { deviceId },
20625
+ meta: { camStreamId }
20626
+ });
20627
+ if (this.deviceOutages.get(deviceId) !== void 0) {
20628
+ if (!cameBackOn.has(deviceId)) cameBackOn.set(deviceId, context);
20629
+ continue;
20630
+ }
20631
+ if (wasSleepSuppressed) {
20632
+ this.writeCameraStreamsSlice(deviceId, this.snapshotProfileSlots(deviceId));
20633
+ continue;
20634
+ }
20635
+ const reason = isStreamOutageReason(previousReason) ? "recovered" : previousReason === "suspended" ? "resumed" : "first-packet";
20636
+ this.emitStreamHealth(true, {
20637
+ ...context,
20638
+ reason
20639
+ });
20640
+ }
20641
+ for (const [deviceId, context] of outageOpenedOn) this.announceDeviceOutage(deviceId, context);
20642
+ for (const [deviceId, context] of cameBackOn) this.settleDeviceOutage(deviceId, context);
20643
+ this.expireWakeOnlyOutages(now);
20644
+ }
20645
+ /** Record one stream as part of its device's outage (announced after the pass). */
20646
+ openStreamOutage(brokerId, context, reason, now, outageOpenedOn) {
20647
+ this.sleepSuppressedBrokers.delete(brokerId);
20648
+ this.streamOfflineReasonByBroker.set(brokerId, reason);
20649
+ this.deviceOutages.open(context.deviceId, context.camStreamId, reason, now);
20650
+ if (!outageOpenedOn.has(context.deviceId)) outageOpenedOn.set(context.deviceId, context);
20651
+ }
20652
+ markSleepSuppressed(brokerId, context, presence, lastPacketAt) {
20653
+ this.sleepSuppressedBrokers.add(brokerId);
20654
+ this.streamOfflineReasonByBroker.delete(brokerId);
20655
+ this.logger.info("stream quiet on a sleeping battery camera — not an outage", {
20656
+ tags: { deviceId: context.deviceId },
20657
+ meta: {
20658
+ camStreamId: context.camStreamId,
20659
+ presence,
20660
+ lastPacketAt
20661
+ }
20662
+ });
20663
+ this.writeCameraStreamsSlice(context.deviceId, this.snapshotProfileSlots(context.deviceId));
20664
+ }
20665
+ /**
20666
+ * The verdict on a quiet, non-idle stream. `'defer'` = the device's battery
20667
+ * class is still being asked of the registry; the edge is kept and judged
20668
+ * again. A class that could not be answered, or a slice never read, judges as
20669
+ * a mains camera — never as a sleeper (D49).
20670
+ */
20671
+ judgeQuietStream(brokerId, deviceId, broker, now) {
20672
+ const probe = this.ensureBatteryProbe(deviceId, now);
20673
+ if (probe.state === "pending") return "defer";
20674
+ return judgeStaleStream({
20675
+ battery: probe.isBattery === true && probe.facts !== null ? { facts: probe.facts } : null,
20676
+ demanded: this.demandHeldPastBound(brokerId, broker, now),
20677
+ nowMs: now
20678
+ });
20679
+ }
20680
+ demandHeldPastBound(brokerId, broker, now) {
20681
+ if (!broker.hasLiveDemand()) {
20682
+ this.demandSince.delete(brokerId);
20683
+ return false;
20684
+ }
20685
+ const since = this.demandSince.get(brokerId) ?? now;
20686
+ this.demandSince.set(brokerId, since);
20687
+ return now - since >= WAKE_DEMAND_BOUND_MS;
20688
+ }
20689
+ /**
20690
+ * A broker that stays quiet, pass after pass. Three things can be true of it:
20691
+ * it was suppressed as a sleeper (the camera may have died, or a wake may
20692
+ * have been demanded since); it went idle while holding an outage open; or it
20693
+ * resumed from an idle suspend on a battery camera that never answered.
20694
+ */
20695
+ rejudgeQuietBroker(brokerId, broker, context, now, outageOpenedOn) {
20696
+ const { deviceId } = context;
20697
+ const reason = this.streamOfflineReasonByBroker.get(brokerId);
20698
+ if (isStreamOutageReason(reason) && broker.suspended) {
20699
+ this.dropSuspendedOutage(brokerId, context);
20700
+ return;
20701
+ }
20702
+ const suppressed = this.sleepSuppressedBrokers.has(brokerId);
20703
+ const resumedQuiet = reason === "suspended" && !broker.suspended;
20704
+ if (!suppressed && !resumedQuiet) return;
20705
+ if (broker.suspended) {
20706
+ this.demandSince.delete(brokerId);
20707
+ return;
20708
+ }
20709
+ this.refreshBatteryFactsIfDue(deviceId, now);
20710
+ const probe = this.batteryProbe.get(deviceId);
20711
+ if (probe?.state !== "done" || probe.isBattery !== true || probe.facts === null) return;
20712
+ const verdict = judgeStaleStream({
20713
+ battery: { facts: probe.facts },
20714
+ demanded: this.demandHeldPastBound(brokerId, broker, now),
20715
+ nowMs: now
20716
+ });
20717
+ if (verdict.kind === "suppressed") return;
20718
+ this.logger.warn("quiet battery stream is now an outage", {
20719
+ tags: { deviceId },
20720
+ meta: {
20721
+ camStreamId: context.camStreamId,
20722
+ reason: verdict.reason
20723
+ }
20724
+ });
20725
+ this.openStreamOutage(brokerId, context, verdict.reason, now, outageOpenedOn);
20726
+ }
20727
+ /**
20728
+ * An outage stream whose last consumer left (the broker went idle). Nobody is
20729
+ * waiting for it any more, so the outage ends there — closed cleanly, with
20730
+ * the one online that lets every consumer clear its row (reason `suspended`,
20731
+ * which the notification gate discards).
20732
+ */
20733
+ dropSuspendedOutage(brokerId, context) {
20734
+ const { deviceId, camStreamId } = context;
20735
+ this.demandSince.delete(brokerId);
20736
+ this.streamOfflineReasonByBroker.set(brokerId, "suspended");
20737
+ const outage = this.deviceOutages.get(deviceId);
20738
+ this.logger.info("stream outage dropped — the broker went idle, nobody is waiting", {
20739
+ tags: { deviceId },
20740
+ meta: { camStreamId }
20741
+ });
20742
+ if (!outage) return;
20743
+ outage.camStreamIds.delete(camStreamId);
20744
+ if (outage.camStreamIds.size > 0 || outage.wakeOnly) return;
20745
+ this.closeDeviceOutage(deviceId, outage, "suspended", context);
20746
+ }
20747
+ /** Close a device outage and, when it was announced, say so once (any online closes the rows). */
20748
+ closeDeviceOutage(deviceId, outage, reason, context) {
20749
+ this.deviceOutages.close(deviceId);
20750
+ if (!outage.announced) return;
20751
+ this.emitDeviceStreamHealth(true, deviceId, reason, /* @__PURE__ */ new Set(), context);
20752
+ }
20753
+ /** Wake-only outages end when the failed wakes stop (demand released). */
20754
+ expireWakeOnlyOutages(now) {
20755
+ for (const [deviceId, outage] of [...this.deviceOutages.entries()]) {
20756
+ if (!outage.wakeOnly || now - outage.lastSignalAt < WAKE_ONLY_OUTAGE_TTL_MS) continue;
20757
+ this.logger.info("wake failure outage ended — no further demand", {
20758
+ tags: { deviceId },
20759
+ meta: { reason: outage.reason }
20760
+ });
20761
+ this.closeDeviceOutage(deviceId, outage, "suspended", {
20435
20762
  deviceId,
20763
+ brokerId: String(deviceId),
20764
+ sourceType: "battery-wake",
20765
+ lastPacketAt: 0
20766
+ });
20767
+ }
20768
+ }
20769
+ /** The device's battery probe; starts the read when there is none (or a retry is due). */
20770
+ ensureBatteryProbe(deviceId, now) {
20771
+ const existing = this.batteryProbe.get(deviceId);
20772
+ if (existing === void 0) {
20773
+ if (!this.api) {
20774
+ const none = {
20775
+ state: "done",
20776
+ isBattery: null,
20777
+ facts: null,
20778
+ readAt: now
20779
+ };
20780
+ this.batteryProbe.set(deviceId, none);
20781
+ return none;
20782
+ }
20783
+ const pending = {
20784
+ state: "pending",
20785
+ isBattery: null,
20786
+ facts: null,
20787
+ readAt: now
20788
+ };
20789
+ this.batteryProbe.set(deviceId, pending);
20790
+ this.probeBattery(deviceId);
20791
+ return pending;
20792
+ }
20793
+ if (existing.state === "done" && existing.isBattery === null && this.api && now - existing.readAt >= BATTERY_PROBE_RETRY_MS && !this.batteryProbeInFlight.has(deviceId)) this.probeBattery(deviceId);
20794
+ return existing;
20795
+ }
20796
+ async probeBattery(deviceId) {
20797
+ this.batteryProbeInFlight.add(deviceId);
20798
+ try {
20799
+ const isBattery = await this.isBatteryOperated(deviceId);
20800
+ const facts = isBattery === true ? await this.readBatterySleepFacts(deviceId) : null;
20801
+ const current = this.batteryProbe.get(deviceId);
20802
+ this.batteryProbe.set(deviceId, {
20803
+ state: "done",
20804
+ isBattery: current?.isBattery === true ? true : isBattery,
20805
+ facts: current?.facts ?? facts,
20806
+ readAt: Date.now()
20807
+ });
20808
+ } finally {
20809
+ this.batteryProbeInFlight.delete(deviceId);
20810
+ }
20811
+ }
20812
+ refreshBatteryFactsIfDue(deviceId, now) {
20813
+ const probe = this.batteryProbe.get(deviceId);
20814
+ if (probe?.state !== "done" || probe.isBattery !== true) return;
20815
+ if (now - probe.readAt < BATTERY_PROBE_RETRY_MS || this.batteryProbeInFlight.has(deviceId)) return;
20816
+ this.batteryProbeInFlight.add(deviceId);
20817
+ this.readBatterySleepFacts(deviceId).then((facts) => {
20818
+ const current = this.batteryProbe.get(deviceId);
20819
+ this.batteryProbe.set(deviceId, {
20820
+ state: "done",
20821
+ isBattery: true,
20822
+ facts: facts ?? current?.facts ?? null,
20823
+ readAt: Date.now()
20824
+ });
20825
+ }).finally(() => this.batteryProbeInFlight.delete(deviceId));
20826
+ }
20827
+ /** The device's `battery` slice as sleep facts, or `null` when it cannot be read. */
20828
+ async readBatterySleepFacts(deviceId) {
20829
+ const api = this.api;
20830
+ if (!api) return null;
20831
+ const slice = await api.deviceState.getCapSlice.query({
20832
+ deviceId,
20833
+ capName: require_dist.batteryCapability.name
20834
+ }).catch(() => null);
20835
+ const parsed = require_dist.BatteryStatusSchema.safeParse(slice);
20836
+ if (!parsed.success) return null;
20837
+ return {
20838
+ sleeping: parsed.data.sleeping,
20839
+ lastContactAt: parsed.data.lastContactAt
20840
+ };
20841
+ }
20842
+ recordBatterySleep(deviceId, status) {
20843
+ const previous = this.batteryProbe.get(deviceId);
20844
+ const facts = status && typeof status.sleeping === "boolean" ? {
20845
+ sleeping: status.sleeping,
20846
+ ...typeof status.lastContactAt === "number" ? { lastContactAt: status.lastContactAt } : {}
20847
+ } : null;
20848
+ this.batteryProbe.set(deviceId, {
20849
+ state: "done",
20850
+ isBattery: true,
20851
+ facts: facts ?? previous?.facts ?? null,
20852
+ readAt: Date.now()
20853
+ });
20854
+ }
20855
+ /**
20856
+ * A released broker can no longer carry an outage. When it was the last
20857
+ * stream holding an ANNOUNCED outage open, the outage is closed with an
20858
+ * online (`broker-destroyed`, which the notification gate discards but every
20859
+ * ledger and alert card clears on) — otherwise the row stays open for ever.
20860
+ */
20861
+ forgetReleasedStreamOutage(context) {
20862
+ const { deviceId, camStreamId } = context;
20863
+ const outage = this.deviceOutages.get(deviceId);
20864
+ if (!outage || !outage.camStreamIds.delete(camStreamId)) return;
20865
+ if (outage.camStreamIds.size > 0) return;
20866
+ this.logger.info("stream outage closed — its last stream was released", {
20867
+ tags: { deviceId },
20868
+ meta: {
20869
+ reason: outage.reason,
20436
20870
  camStreamId,
20437
- profile: findProfileForStream(this.assignments.get(deviceId)?.map ?? {}, camStreamId),
20438
- brokerId,
20439
- sourceType: broker.getActiveSourceType(),
20440
- lastPacketAt,
20441
- reason
20871
+ announced: outage.announced
20872
+ }
20873
+ });
20874
+ this.closeDeviceOutage(deviceId, outage, "broker-destroyed", context);
20875
+ }
20876
+ /** The ONE `stream.offline` of a device outage, naming every stream it covers. */
20877
+ announceDeviceOutage(deviceId, context) {
20878
+ const outage = this.deviceOutages.get(deviceId);
20879
+ if (!outage) return;
20880
+ if (outage.announced) {
20881
+ this.logger.debug("stream outage joined an already announced one — no second event", {
20882
+ tags: { deviceId },
20883
+ meta: {
20884
+ camStreamIds: [...outage.camStreamIds],
20885
+ reason: outage.reason
20886
+ }
20442
20887
  });
20888
+ return;
20889
+ }
20890
+ outage.announced = true;
20891
+ this.logger.warn("stream outage", {
20892
+ tags: { deviceId },
20893
+ meta: {
20894
+ reason: outage.reason,
20895
+ camStreamIds: [...outage.camStreamIds]
20896
+ }
20897
+ });
20898
+ this.emitDeviceStreamHealth(false, deviceId, outage.reason, outage.camStreamIds, context);
20899
+ }
20900
+ /**
20901
+ * A broker of a device in outage is delivering again. The device is back when
20902
+ * no stream of it is still down for an outage reason; then, and only then,
20903
+ * the single `stream.online` (`recovered`) goes out.
20904
+ */
20905
+ settleDeviceOutage(deviceId, context) {
20906
+ const outage = this.deviceOutages.get(deviceId);
20907
+ if (!outage) return;
20908
+ for (const camStreamId of [...outage.camStreamIds]) {
20909
+ const brokerId = brokerIdFor(deviceId, camStreamId);
20910
+ if (!isStreamOutageReason(this.streamOfflineReasonByBroker.get(brokerId))) outage.camStreamIds.delete(camStreamId);
20443
20911
  }
20912
+ if (outage.camStreamIds.size > 0) return;
20913
+ this.deviceOutages.close(deviceId);
20914
+ if (!outage.announced) return;
20915
+ this.logger.info("stream outage over — device delivers again", {
20916
+ tags: { deviceId },
20917
+ meta: {
20918
+ reason: outage.reason,
20919
+ outageMs: Date.now() - outage.since
20920
+ }
20921
+ });
20922
+ this.emitDeviceStreamHealth(true, deviceId, "recovered", new Set(context.camStreamId !== void 0 ? [context.camStreamId] : []), context);
20923
+ }
20924
+ /**
20925
+ * A wake somebody ASKED for did not produce a stream (the viewer's play
20926
+ * button, found no broker and a wake that settled nothing). This is the one
20927
+ * failure with no broker to carry it, so it opens the device outage itself.
20928
+ */
20929
+ announceBatteryWakeFailure(deviceId, camStreamId, reason) {
20930
+ if (!this.eventBus) return;
20931
+ const published = [...this.cameraStreams.get(deviceId)?.keys() ?? []];
20932
+ const streamIds = camStreamId !== void 0 && published.includes(camStreamId) ? [camStreamId] : published;
20933
+ this.deviceOutages.openWake(deviceId, streamIds, reason, Date.now());
20934
+ this.announceDeviceOutage(deviceId, {
20935
+ deviceId,
20936
+ ...streamIds[0] !== void 0 ? { camStreamId: streamIds[0] } : {},
20937
+ brokerId: streamIds[0] !== void 0 ? brokerIdFor(deviceId, streamIds[0]) : String(deviceId),
20938
+ sourceType: "battery-wake",
20939
+ lastPacketAt: 0
20940
+ });
20941
+ }
20942
+ emitDeviceStreamHealth(online, deviceId, reason, camStreamIds, context) {
20943
+ const ids = [...camStreamIds];
20944
+ const first = ids[0] ?? context.camStreamId;
20945
+ const assignments = this.assignments.get(deviceId)?.map ?? {};
20946
+ this.emitStreamHealth(online, {
20947
+ ...context,
20948
+ deviceId,
20949
+ ...first !== void 0 ? { camStreamId: first } : {},
20950
+ brokerId: first !== void 0 ? brokerIdFor(deviceId, first) : context.brokerId,
20951
+ profile: first !== void 0 ? findProfileForStream(assignments, first) : null,
20952
+ reason,
20953
+ camStreamIds: ids
20954
+ });
20444
20955
  }
20445
20956
  emitStreamHealth(online, payload) {
20446
20957
  const bus = this.eventBus;
20447
20958
  if (!bus) return;
20448
20959
  const category = online ? "stream.online" : "stream.offline";
20960
+ const profile = payload.profile !== void 0 ? payload.profile : payload.camStreamId !== void 0 ? findProfileForStream(this.assignments.get(payload.deviceId)?.map ?? {}, payload.camStreamId) : null;
20449
20961
  bus.emit({
20450
20962
  id: `${category}-${payload.brokerId}-${Date.now()}`,
20451
20963
  timestamp: /* @__PURE__ */ new Date(),
@@ -20454,7 +20966,10 @@ var StreamBrokerManager = class StreamBrokerManager {
20454
20966
  type: "stream-broker",
20455
20967
  id: payload.brokerId
20456
20968
  },
20457
- data: payload
20969
+ data: {
20970
+ ...payload,
20971
+ profile
20972
+ }
20458
20973
  });
20459
20974
  this.writeCameraStreamsSlice(payload.deviceId, this.snapshotProfileSlots(payload.deviceId));
20460
20975
  }
@@ -22270,6 +22785,14 @@ var StreamBrokerManager = class StreamBrokerManager {
22270
22785
  });
22271
22786
  this.streamHealthByBroker.delete(brokerId);
22272
22787
  this.streamOfflineReasonByBroker.delete(brokerId);
22788
+ this.sleepSuppressedBrokers.delete(brokerId);
22789
+ this.forgetReleasedStreamOutage({
22790
+ deviceId,
22791
+ camStreamId,
22792
+ brokerId,
22793
+ sourceType: broker.getActiveSourceType(),
22794
+ lastPacketAt: broker.getLastPacketAt()
22795
+ });
22273
22796
  await broker.stop();
22274
22797
  this.brokers.delete(brokerId);
22275
22798
  this.sentryFrames.delete(brokerId);
@@ -33777,9 +34300,10 @@ var ClipStreamFeeder = class {
33777
34300
  asked: "high",
33778
34301
  served: answer.served
33779
34302
  });
33780
- if (this.deps.handle.profile !== void 0 && this.deps.handle.profile !== "high" && answer.served === "high") this.deps.onLog?.("clip stream: only the main copy exists — serving it for a cheaper ask", {
34303
+ const askedProfile = this.deps.handle.profile;
34304
+ if (askedProfile !== void 0 && askedProfile !== null && askedProfile !== "high" && answer.served === "high") this.deps.onLog?.("clip stream: only the main copy exists — serving it for a cheaper ask", {
33781
34305
  deviceId: this.deps.handle.deviceId,
33782
- asked: this.deps.handle.profile,
34306
+ asked: askedProfile,
33783
34307
  served: answer.served
33784
34308
  });
33785
34309
  if (this.audioAsk !== "none" && answer.servedAudio !== this.audioAsk) {