@camstack/addon-pipeline 1.2.374 → 1.2.375
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.
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-1LDuDw8S.mjs → custom-model-catalog-DF5qs8u4.mjs} +2 -2
- package/dist/{custom-model-catalog-W0DHDhfo.js → custom-model-catalog-gbLoUFki.js} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-CBEUVjTS.js → dist-C0Mg8e3I.js} +753 -27
- package/dist/{dist-CmdLCb_F.mjs → dist-QyeZXkAO.mjs} +753 -27
- package/dist/{model-catalogs-D5YksA5A.js → model-catalogs-BTuX-P0k.js} +1 -1
- package/dist/{model-catalogs-BnCdB2A3.mjs → model-catalogs-CIE4Dz4x.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-DMgi6pND.js → node-CifGKjsd.js} +1 -1
- package/dist/{node-C3r8kiOo.mjs → node-ndjp9DQO.mjs} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-GFR7fxpJ.js → process-memory-BMfSP5-i.js} +1 -1
- package/dist/{process-memory-X0ws1SGJ.mjs → process-memory-CaMI-Ry2.mjs} +1 -1
- package/dist/recorder/index.js +16 -2
- package/dist/recorder/index.mjs +16 -2
- package/dist/{segment-demux-js-IKEO1XBz.mjs → segment-demux-js-CLXBgUTo.mjs} +1 -1
- package/dist/{segment-demux-js-CSQVhbFI.js → segment-demux-js-LhwVw18z.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFTlVoW.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B4MD_B4k.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BvMrdOCD.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-JVFs9LgI.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-BOp1_Hhk.mjs → hostInit-DBDp5YHD.mjs} +2 -2
- package/dist/stream-broker/index.js +549 -26
- package/dist/stream-broker/index.mjs +549 -26
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- 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
- 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-
|
|
7
|
-
const require_node = require("../node-
|
|
6
|
+
const require_dist = require("../dist-C0Mg8e3I.js");
|
|
7
|
+
const require_node = require("../node-CifGKjsd.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-
|
|
10
|
+
const require_segment_demux_js = require("../segment-demux-js-LhwVw18z.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 {
|
|
@@ -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
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
20432
|
-
|
|
20433
|
-
|
|
20434
|
-
|
|
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
|
-
|
|
20438
|
-
|
|
20439
|
-
|
|
20440
|
-
|
|
20441
|
-
|
|
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:
|
|
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);
|