@mega-yfue/eufy-sdk 0.6.0-beta.13 → 0.6.0-beta.15
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/client/eufy-mega.d.ts +42 -8
- package/dist/client/types.d.ts +3 -3
- package/dist/core/contracts.d.ts +3 -1
- package/dist/core/store.d.ts +12 -1
- package/dist/index.js +506 -221
- package/dist/index.js.map +4 -4
- package/dist/transport/http/mega-client.d.ts +3 -2
- package/dist/transport/p2p/codec.d.ts +2 -2
- package/dist/transport/p2p/command-router.d.ts +26 -3
- package/dist/transport/p2p/database-wire.d.ts +30 -0
- package/dist/transport/p2p/live-trace.d.ts +4 -4
- package/dist/transport/p2p/p2p-session.d.ts +25 -15
- package/dist/transport/p2p/readable-egress.d.ts +3 -1
- package/dist/transport/p2p/shared-live-source.d.ts +2 -1
- package/dist/transport/push/store.d.ts +7 -6
- package/dist/transport/rtc/command-router.d.ts +63 -3
- package/dist/transport/rtc/portal-packet.d.ts +3 -13
- package/dist/transport/stored-image-cache.d.ts +14 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -106,8 +106,9 @@ function encryptLoginPassword(password) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// dist/core/store.js
|
|
109
|
-
import {
|
|
110
|
-
import {
|
|
109
|
+
import { randomUUID } from "node:crypto";
|
|
110
|
+
import { lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
111
|
+
import { dirname, resolve } from "node:path";
|
|
111
112
|
var MemorySessionStore = class {
|
|
112
113
|
s = null;
|
|
113
114
|
load() {
|
|
@@ -120,6 +121,22 @@ var MemorySessionStore = class {
|
|
|
120
121
|
this.s = null;
|
|
121
122
|
}
|
|
122
123
|
};
|
|
124
|
+
var IN_PLACE_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EBUSY", "EXDEV", "EROFS"]);
|
|
125
|
+
function realTarget(path) {
|
|
126
|
+
try {
|
|
127
|
+
return realpathSync(path);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (e.code !== "ENOENT")
|
|
130
|
+
throw e;
|
|
131
|
+
try {
|
|
132
|
+
if (!lstatSync(path).isSymbolicLink())
|
|
133
|
+
return path;
|
|
134
|
+
} catch {
|
|
135
|
+
return path;
|
|
136
|
+
}
|
|
137
|
+
return realTarget(resolve(dirname(path), readlinkSync(path)));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
123
140
|
var FileSessionStore = class {
|
|
124
141
|
path;
|
|
125
142
|
constructor(path) {
|
|
@@ -133,11 +150,22 @@ var FileSessionStore = class {
|
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
save(s) {
|
|
153
|
+
const target = realTarget(this.path);
|
|
136
154
|
try {
|
|
137
|
-
mkdirSync(dirname(
|
|
155
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
138
156
|
} catch {
|
|
139
157
|
}
|
|
140
|
-
|
|
158
|
+
const data = JSON.stringify(s, null, 2);
|
|
159
|
+
const tmp = `${target}.${randomUUID()}.tmp`;
|
|
160
|
+
try {
|
|
161
|
+
writeFileSync(tmp, data, { mode: 384, flag: "wx" });
|
|
162
|
+
renameSync(tmp, target);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
rmSync(tmp, { force: true });
|
|
165
|
+
if (!IN_PLACE_CODES.has(e.code ?? ""))
|
|
166
|
+
throw e;
|
|
167
|
+
writeFileSync(target, data, { mode: 384 });
|
|
168
|
+
}
|
|
141
169
|
}
|
|
142
170
|
clear() {
|
|
143
171
|
try {
|
|
@@ -1973,22 +2001,22 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1973
2001
|
return this.postSigned(`app-${service}-${this.region}.eufy.com`, path, body, true, {}, headerOverrides);
|
|
1974
2002
|
}
|
|
1975
2003
|
/**
|
|
1976
|
-
* Fetch the per-station DSK key
|
|
2004
|
+
* Fetch the per-station DSK key using each station's reported cloud category. Mirrors
|
|
1977
2005
|
* the legacy `get_dsk_keys` body shape on the mega devicerelation service.
|
|
1978
2006
|
* The P2P *local* lookup path does not need this; only the cloud path does.
|
|
1979
2007
|
*/
|
|
1980
|
-
async getDskKeys(
|
|
2008
|
+
async getDskKeys(stations, priorDsks = {}) {
|
|
1981
2009
|
const invalid = {};
|
|
1982
2010
|
const deviceDsks = [];
|
|
1983
|
-
for (const sn of
|
|
2011
|
+
for (const { sn, category } of stations) {
|
|
1984
2012
|
const prev = priorDsks[sn] ?? "";
|
|
1985
2013
|
invalid[sn] = prev;
|
|
1986
|
-
deviceDsks.push({ invalid_dsk: prev, device_sn: sn, category: "eufy_security" });
|
|
2014
|
+
deviceDsks.push({ invalid_dsk: prev, device_sn: sn, category: category || "eufy_security" });
|
|
1987
2015
|
}
|
|
1988
2016
|
const res = await this.post("devicerelation", "/app/devicerelation/get_dsk_keys", {
|
|
1989
2017
|
device_dsks: deviceDsks,
|
|
1990
2018
|
invalid_dsks: invalid,
|
|
1991
|
-
station_sns:
|
|
2019
|
+
station_sns: stations.map(({ sn }) => sn)
|
|
1992
2020
|
});
|
|
1993
2021
|
const out = {};
|
|
1994
2022
|
for (const k of res.device_dsks ?? [])
|
|
@@ -2665,7 +2693,7 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2665
2693
|
const port = c.endpoint_port ?? 8883;
|
|
2666
2694
|
const pinned = this.o.instanceIp;
|
|
2667
2695
|
const reconnectPeriod = this.o.reconnectPeriod ?? 5e3;
|
|
2668
|
-
return await new Promise((
|
|
2696
|
+
return await new Promise((resolve2, reject) => {
|
|
2669
2697
|
const client = pinned ? mqtt.connect({
|
|
2670
2698
|
host: pinned,
|
|
2671
2699
|
port,
|
|
@@ -2697,7 +2725,7 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2697
2725
|
client.on("connect", () => {
|
|
2698
2726
|
if (!settled) {
|
|
2699
2727
|
settled = true;
|
|
2700
|
-
|
|
2728
|
+
resolve2();
|
|
2701
2729
|
}
|
|
2702
2730
|
this.emit("connect");
|
|
2703
2731
|
});
|
|
@@ -2840,7 +2868,7 @@ function parseStateInfoSignal(topic, raw) {
|
|
|
2840
2868
|
}
|
|
2841
2869
|
|
|
2842
2870
|
// dist/transport/mqtt/dp-codec.js
|
|
2843
|
-
import { randomUUID } from "node:crypto";
|
|
2871
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2844
2872
|
var DP_MAGIC = [255, 9];
|
|
2845
2873
|
var DP_HEADER_LEN = 9;
|
|
2846
2874
|
function xorChecksum(buf) {
|
|
@@ -2878,7 +2906,7 @@ function buildDpEnvelope(opts) {
|
|
|
2878
2906
|
};
|
|
2879
2907
|
const head = {
|
|
2880
2908
|
version: "1.0.0.1",
|
|
2881
|
-
client_id: `ios-eufy_mega-${opts.accountId}-${
|
|
2909
|
+
client_id: `ios-eufy_mega-${opts.accountId}-${randomUUID2()}`,
|
|
2882
2910
|
sess_id: "0000",
|
|
2883
2911
|
msg_seq: 1,
|
|
2884
2912
|
seed: "",
|
|
@@ -15560,7 +15588,7 @@ function readAddress(buf, offset) {
|
|
|
15560
15588
|
};
|
|
15561
15589
|
}
|
|
15562
15590
|
function parseLookupAddr(msg) {
|
|
15563
|
-
return readAddress(msg, 4);
|
|
15591
|
+
return msg.length >= 12 ? readAddress(msg, 4) : void 0;
|
|
15564
15592
|
}
|
|
15565
15593
|
function parseRelayList(msg) {
|
|
15566
15594
|
const out = [];
|
|
@@ -16209,6 +16237,27 @@ function traceLiveStart(logger, trace, source) {
|
|
|
16209
16237
|
// dist/transport/p2p/level2-settle.js
|
|
16210
16238
|
var LEVEL2_SETTLE_MS = 8e3;
|
|
16211
16239
|
|
|
16240
|
+
// dist/transport/p2p/database-wire.js
|
|
16241
|
+
var CMD_DATABASE_IMAGE = 1308;
|
|
16242
|
+
var CMD_DATABASE = 1306;
|
|
16243
|
+
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011, QUERY_LOCAL: 10017 };
|
|
16244
|
+
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
16245
|
+
function fullTableQuery(count2 = 2e3) {
|
|
16246
|
+
return {
|
|
16247
|
+
count: count2,
|
|
16248
|
+
start_date: "",
|
|
16249
|
+
end_date: "",
|
|
16250
|
+
start_id: 0,
|
|
16251
|
+
end_id: 1,
|
|
16252
|
+
flag: 0,
|
|
16253
|
+
need_ai: 1,
|
|
16254
|
+
res_unzip: 1,
|
|
16255
|
+
update_time: "0",
|
|
16256
|
+
start_time: "0",
|
|
16257
|
+
alarm_id: ""
|
|
16258
|
+
};
|
|
16259
|
+
}
|
|
16260
|
+
|
|
16212
16261
|
// dist/transport/p2p/p2p-session.js
|
|
16213
16262
|
var LOCAL_LOOKUP_PORT = 32108;
|
|
16214
16263
|
var HEARTBEAT_MS = 5e3;
|
|
@@ -16262,9 +16311,6 @@ var CMD_SET_PAYLOAD = 1350;
|
|
|
16262
16311
|
var CMD_NOTIFY_PAYLOAD = 1351;
|
|
16263
16312
|
var PADDED_RESULT_BYTES = 132;
|
|
16264
16313
|
var CMD_CAMERA_INFO = 1103;
|
|
16265
|
-
var CMD_DATABASE_IMAGE = 1308;
|
|
16266
|
-
var CMD_DATABASE = 1306;
|
|
16267
|
-
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011, QUERY_LOCAL: 10017 };
|
|
16268
16314
|
var GCM_AAD = Buffer.from("eufy security");
|
|
16269
16315
|
var LEVEL2_SEQ_BASE = 16909056;
|
|
16270
16316
|
function paramReport(payload) {
|
|
@@ -16281,7 +16327,6 @@ function paramReport(payload) {
|
|
|
16281
16327
|
return Object.keys(out).length ? out : void 0;
|
|
16282
16328
|
}
|
|
16283
16329
|
var traceSequence = 0;
|
|
16284
|
-
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
16285
16330
|
function firstTableRows(text2) {
|
|
16286
16331
|
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
16287
16332
|
for (let start = decoded.indexOf("{"); start >= 0; start = decoded.indexOf("{", start + 1)) {
|
|
@@ -16408,11 +16453,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16408
16453
|
level2Seq = 0;
|
|
16409
16454
|
rsaPrivateKey;
|
|
16410
16455
|
rsaModulusHex;
|
|
16411
|
-
/**
|
|
16456
|
+
/**
|
|
16457
|
+
* Whether a level-2 key negotiation kicked off by a GATEWAYINFO reply is in flight. Cleared when it
|
|
16458
|
+
* settles, so a later reply — the answer to {@link repromptLevel2Key} — negotiates again.
|
|
16459
|
+
*/
|
|
16412
16460
|
level2Negotiating = false;
|
|
16413
16461
|
/**
|
|
16414
16462
|
* Whether waiting for a level-2 key can still change the answer: `false` once one has been negotiated,
|
|
16415
|
-
* once
|
|
16463
|
+
* once a negotiation concluded without one, and from the start when nothing can negotiate one.
|
|
16416
16464
|
*/
|
|
16417
16465
|
level2Pending;
|
|
16418
16466
|
/** Waiters parked in {@link awaitLevel2Key}, woken the moment the negotiation settles either way. */
|
|
@@ -16517,7 +16565,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16517
16565
|
* The session grace runs from connect, when the station is prompted for `CMD_GATEWAYINFO`. A negotiation
|
|
16518
16566
|
* beginning later does not restart it: best-effort media can proceed without the key, and restarting
|
|
16519
16567
|
* would charge another grace to a source that may already be streaming. A session whose negotiation has
|
|
16520
|
-
* settled answers without waiting at all
|
|
16568
|
+
* settled answers without waiting at all: the answer stands until {@link repromptLevel2Key} re-opens the
|
|
16569
|
+
* wait, since the station is prompted for `CMD_GATEWAYINFO` only on connect and on that re-prompt.
|
|
16521
16570
|
*
|
|
16522
16571
|
* An own-session camera whose grace expires here is NOT thereby a camera that will fail to stream. Measured
|
|
16523
16572
|
* on one account: own-session cameras of two device types negotiated a key, three others never did, and
|
|
@@ -16550,7 +16599,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16550
16599
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} waiting up to ${remaining}ms for the level-2 key`);
|
|
16551
16600
|
this.trace({ phase: "level2-wait", waitMs: remaining });
|
|
16552
16601
|
const waiters = this.level2Waiters;
|
|
16553
|
-
const outcome = await new Promise((
|
|
16602
|
+
const outcome = await new Promise((resolve2) => {
|
|
16554
16603
|
let deadline;
|
|
16555
16604
|
let settled = false;
|
|
16556
16605
|
const finish = (result) => {
|
|
@@ -16561,7 +16610,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16561
16610
|
const at = waiters.indexOf(wake);
|
|
16562
16611
|
if (at >= 0)
|
|
16563
16612
|
waiters.splice(at, 1);
|
|
16564
|
-
|
|
16613
|
+
resolve2(result);
|
|
16565
16614
|
};
|
|
16566
16615
|
const wake = (result) => finish(result);
|
|
16567
16616
|
deadline = setTimeout(() => finish("timeout"), remaining);
|
|
@@ -16583,12 +16632,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16583
16632
|
/**
|
|
16584
16633
|
* Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
|
|
16585
16634
|
*
|
|
16586
|
-
*
|
|
16587
|
-
*
|
|
16635
|
+
* Without this the station is prompted once on connect, and a negotiation that settles without a key
|
|
16636
|
+
* leaves {@link awaitLevel2Key} answering `false` at once for the rest of that connection. That is the
|
|
16588
16637
|
* right answer for best-effort media, which proceeds at level-1 — but an operation whose ONLY wire is
|
|
16589
16638
|
* level-2 is then refused for the whole life of that connection, while a fresh session over the same
|
|
16590
16639
|
* device negotiates a key normally. Measured: a session that had settled refused every such operation
|
|
16591
|
-
* until it was rebuilt, at which point the station answered with a cipher id straight away.
|
|
16640
|
+
* until it was rebuilt, at which point the station answered with a cipher id straight away. The reply to
|
|
16641
|
+
* this ask runs the negotiation again and settles the re-opened wait either way.
|
|
16592
16642
|
*
|
|
16593
16643
|
* Bounded to one extra ask per connection, so a burst of such operations cannot turn a silent station into a
|
|
16594
16644
|
* flood, and answers whether it asked — `false` when a key is already held, when nothing can negotiate one,
|
|
@@ -16609,6 +16659,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16609
16659
|
/** Record that the level-2 negotiation has finished, with or without a key, and wake every waiter. */
|
|
16610
16660
|
settleLevel2(reason = "terminal") {
|
|
16611
16661
|
this.level2Pending = false;
|
|
16662
|
+
this.level2Negotiating = false;
|
|
16612
16663
|
const outcome = this.level2Key ? "key" : reason;
|
|
16613
16664
|
for (const wake of this.level2Waiters.splice(0))
|
|
16614
16665
|
wake(outcome);
|
|
@@ -16618,13 +16669,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16618
16669
|
* `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
|
|
16619
16670
|
* `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
|
|
16620
16671
|
* ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
|
|
16621
|
-
* key.
|
|
16672
|
+
* key. Emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
|
|
16622
16673
|
* `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` when the ECC unwrap
|
|
16623
16674
|
* fails; an RSA unwrap failure is only traced (non-fatal either way — level-1 traffic keeps working).
|
|
16624
16675
|
*
|
|
16625
|
-
* Every outcome settles the wait in {@link awaitLevel2Key},
|
|
16626
|
-
*
|
|
16627
|
-
*
|
|
16676
|
+
* Every outcome settles the wait in {@link awaitLevel2Key}, so a later command is told at once rather than
|
|
16677
|
+
* left to time out against a key that is not coming. Settling also re-opens the negotiation, so a later
|
|
16678
|
+
* GATEWAYINFO reply in the same connection generation runs it again; a lookup that completes after its
|
|
16679
|
+
* generation ended settles nothing.
|
|
16628
16680
|
*/
|
|
16629
16681
|
negotiateLevel2Key(gwPayload) {
|
|
16630
16682
|
this.level2Negotiating = true;
|
|
@@ -16746,14 +16798,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16746
16798
|
this.socket = socket;
|
|
16747
16799
|
socket.on("message", (msg, rinfo) => this.onMessage(msg, rinfo, socket));
|
|
16748
16800
|
socket.on("error", (e) => this.emit("error", e));
|
|
16749
|
-
await new Promise((
|
|
16801
|
+
await new Promise((resolve2) => {
|
|
16750
16802
|
socket.bind(() => {
|
|
16751
16803
|
try {
|
|
16752
16804
|
socket.setBroadcast(true);
|
|
16753
16805
|
} catch {
|
|
16754
16806
|
}
|
|
16755
16807
|
this.requestReceiveBuffer(socket);
|
|
16756
|
-
|
|
16808
|
+
resolve2();
|
|
16757
16809
|
});
|
|
16758
16810
|
});
|
|
16759
16811
|
void _P2PSession.detectLocalIp().then((host) => {
|
|
@@ -16816,7 +16868,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16816
16868
|
*/
|
|
16817
16869
|
static detectLocalIp() {
|
|
16818
16870
|
if (!_P2PSession.localIpPromise) {
|
|
16819
|
-
_P2PSession.localIpPromise = new Promise((
|
|
16871
|
+
_P2PSession.localIpPromise = new Promise((resolve2) => {
|
|
16820
16872
|
const probe = dgram.createSocket("udp4");
|
|
16821
16873
|
let settled = false;
|
|
16822
16874
|
const finish = (result) => {
|
|
@@ -16828,7 +16880,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16828
16880
|
probe.close();
|
|
16829
16881
|
} catch {
|
|
16830
16882
|
}
|
|
16831
|
-
|
|
16883
|
+
resolve2(result);
|
|
16832
16884
|
};
|
|
16833
16885
|
const timer = setTimeout(() => finish(void 0), 2e3);
|
|
16834
16886
|
probe.connect(53, "8.8.8.8", () => {
|
|
@@ -16980,9 +17032,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16980
17032
|
} else if (hasHeader(msg, ResponseMessageType.LOOKUP_ADDR)) {
|
|
16981
17033
|
if (!this.connected) {
|
|
16982
17034
|
const addr = parseLookupAddr(msg);
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
17035
|
+
if (addr) {
|
|
17036
|
+
this.logger.debug(`[p2p] ${this.cfg.stationSn} LOOKUP_ADDR -> ${addr.host}:${addr.port}`);
|
|
17037
|
+
if (addr.host !== "0.0.0.0")
|
|
17038
|
+
this.beginCheckCam(addr, socket);
|
|
17039
|
+
}
|
|
16986
17040
|
}
|
|
16987
17041
|
} else if (hasHeader(msg, ResponseMessageType.RLY_TO)) {
|
|
16988
17042
|
const to = parseRelayTo(msg);
|
|
@@ -17036,7 +17090,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17036
17090
|
probe.on("error", (error) => this.logger.warn("[p2p] lookup socket error", error.message));
|
|
17037
17091
|
probe.on("message", (msg, rinfo) => this.onMessage(msg, rinfo, probe));
|
|
17038
17092
|
this.probeSockets.push(probe);
|
|
17039
|
-
await new Promise((
|
|
17093
|
+
await new Promise((resolve2) => {
|
|
17040
17094
|
let settled = false;
|
|
17041
17095
|
const finish = (bound) => {
|
|
17042
17096
|
if (settled)
|
|
@@ -17051,7 +17105,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17051
17105
|
} catch {
|
|
17052
17106
|
}
|
|
17053
17107
|
}
|
|
17054
|
-
|
|
17108
|
+
resolve2();
|
|
17055
17109
|
};
|
|
17056
17110
|
const onError = () => finish(false);
|
|
17057
17111
|
const onClose = () => finish(false);
|
|
@@ -17837,25 +17891,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17837
17891
|
databasePayload(value, channel) {
|
|
17838
17892
|
return !this.level2Key && this.sendsInTheClear() ? buildRawCommandPayload(Buffer.from(value, "utf-8"), channel, 0, this.nextClearCommandBytes(), 0) : buildStringCommandPayload(value, channel, this.level1Key, 1);
|
|
17839
17893
|
}
|
|
17840
|
-
/**
|
|
17841
|
-
* Standard "give me the whole table" query params the eufy app uses for a direct
|
|
17842
|
-
* `CMD_DATABASE` read (inner `cmd 10000`). `count` bounds the row count.
|
|
17843
|
-
*/
|
|
17844
|
-
fullTableQuery(count2 = 2e3) {
|
|
17845
|
-
return {
|
|
17846
|
-
count: count2,
|
|
17847
|
-
start_date: "",
|
|
17848
|
-
end_date: "",
|
|
17849
|
-
start_id: 0,
|
|
17850
|
-
end_id: 1,
|
|
17851
|
-
flag: 0,
|
|
17852
|
-
need_ai: 1,
|
|
17853
|
-
res_unzip: 1,
|
|
17854
|
-
update_time: "0",
|
|
17855
|
-
start_time: "0",
|
|
17856
|
-
alarm_id: ""
|
|
17857
|
-
};
|
|
17858
|
-
}
|
|
17859
17894
|
/**
|
|
17860
17895
|
* Request the on-device edge-AI **face roster** over P2P — the phone-free path.
|
|
17861
17896
|
*
|
|
@@ -17872,7 +17907,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17872
17907
|
accountId: opts.accountId,
|
|
17873
17908
|
channel: opts.channel,
|
|
17874
17909
|
innerCmd: DB_QUERY.FULL_TABLE,
|
|
17875
|
-
query:
|
|
17910
|
+
query: fullTableQuery()
|
|
17876
17911
|
});
|
|
17877
17912
|
}
|
|
17878
17913
|
/**
|
|
@@ -17921,11 +17956,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17921
17956
|
this.queryDatabase(table, {
|
|
17922
17957
|
accountId: opts.accountId,
|
|
17923
17958
|
innerCmd: opts.innerCmd,
|
|
17924
|
-
query: opts.query ??
|
|
17959
|
+
query: opts.query ?? fullTableQuery()
|
|
17925
17960
|
});
|
|
17926
17961
|
this.dbReadInFlight = true;
|
|
17927
17962
|
try {
|
|
17928
|
-
return await new Promise((
|
|
17963
|
+
return await new Promise((resolve2, reject) => {
|
|
17929
17964
|
let text2 = "";
|
|
17930
17965
|
const onChunk = (chunk) => {
|
|
17931
17966
|
text2 += chunk.text;
|
|
@@ -17933,7 +17968,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17933
17968
|
if (result?.error !== void 0)
|
|
17934
17969
|
settle(() => reject(new Error(`readDatabase: ${table} refused: ${result.error}`)));
|
|
17935
17970
|
else if (result?.rows)
|
|
17936
|
-
settle(() =>
|
|
17971
|
+
settle(() => resolve2(result.rows));
|
|
17937
17972
|
};
|
|
17938
17973
|
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
17939
17974
|
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
@@ -17963,7 +17998,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17963
17998
|
accountId: opts.accountId,
|
|
17964
17999
|
channel: opts.channel,
|
|
17965
18000
|
innerCmd: DB_QUERY.FULL_TABLE,
|
|
17966
|
-
query:
|
|
18001
|
+
query: fullTableQuery()
|
|
17967
18002
|
});
|
|
17968
18003
|
}
|
|
17969
18004
|
/**
|
|
@@ -18027,8 +18062,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
18027
18062
|
*
|
|
18028
18063
|
* Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
|
|
18029
18064
|
* treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
|
|
18065
|
+
*
|
|
18066
|
+
* A datagram shorter than the 8-byte DATA header carries no sequence number and is dropped unacknowledged.
|
|
18030
18067
|
*/
|
|
18031
18068
|
onData(msg, addr) {
|
|
18069
|
+
if (msg.length < 8)
|
|
18070
|
+
return;
|
|
18032
18071
|
const dataTypeBuffer = msg.subarray(4, 6);
|
|
18033
18072
|
const seqNo = msg.subarray(6, 8).readUInt16BE();
|
|
18034
18073
|
const dataType = dataTypeBuffer[1];
|
|
@@ -18194,6 +18233,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
18194
18233
|
return P2PDataTypeHeader.DATA;
|
|
18195
18234
|
}
|
|
18196
18235
|
}
|
|
18236
|
+
/**
|
|
18237
|
+
* Decrypt and dispatch one inbound data frame.
|
|
18238
|
+
*
|
|
18239
|
+
* A `CMD_GATEWAYINFO` (1100) reply carries the ECIES envelope for the level-2 session key. When a
|
|
18240
|
+
* cipher-key resolver is configured, no key that seals a set-payload is held and no negotiation is in
|
|
18241
|
+
* flight, the reply starts one (async, fire-and-forget); a reply landing while one is in flight is ignored.
|
|
18242
|
+
*/
|
|
18197
18243
|
handleFrame(header, payload, dataType) {
|
|
18198
18244
|
let data = payload;
|
|
18199
18245
|
const isMedia = header.commandId === CMD_VIDEO_FRAME || header.commandId === CMD_AUDIO_FRAME;
|
|
@@ -18299,12 +18345,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
18299
18345
|
this.resetInboundSequencing();
|
|
18300
18346
|
const socket = this.socket;
|
|
18301
18347
|
if (socket && this.connectAddress) {
|
|
18302
|
-
await new Promise((
|
|
18348
|
+
await new Promise((resolve2) => this.send(this.connectAddress, RequestMessageType.END, void 0, socket, resolve2));
|
|
18303
18349
|
}
|
|
18304
|
-
await new Promise((
|
|
18350
|
+
await new Promise((resolve2) => {
|
|
18305
18351
|
if (!socket)
|
|
18306
|
-
return
|
|
18307
|
-
socket.close(() =>
|
|
18352
|
+
return resolve2();
|
|
18353
|
+
socket.close(() => resolve2());
|
|
18308
18354
|
});
|
|
18309
18355
|
this.socket = void 0;
|
|
18310
18356
|
this.emit("close");
|
|
@@ -19512,7 +19558,7 @@ async function captureSnapshotFromShared(source, opts = {}) {
|
|
|
19512
19558
|
const skip = opts.skipKeyframes ?? (source.state === "live" ? 0 : 1);
|
|
19513
19559
|
let burst;
|
|
19514
19560
|
try {
|
|
19515
|
-
burst = await new Promise((
|
|
19561
|
+
burst = await new Promise((resolve2, reject) => {
|
|
19516
19562
|
const bufs = [];
|
|
19517
19563
|
let sets = source.parameterSets;
|
|
19518
19564
|
let codec = "h264";
|
|
@@ -19535,7 +19581,7 @@ async function captureSnapshotFromShared(source, opts = {}) {
|
|
|
19535
19581
|
if (!settle)
|
|
19536
19582
|
settle = setTimeout(() => {
|
|
19537
19583
|
cleanup();
|
|
19538
|
-
|
|
19584
|
+
resolve2({ h264: Buffer.concat(bufs), codec, sets });
|
|
19539
19585
|
}, collectMs);
|
|
19540
19586
|
};
|
|
19541
19587
|
const onError = (err) => {
|
|
@@ -19579,7 +19625,7 @@ async function recordClip(session, seconds, opts = {}) {
|
|
|
19579
19625
|
const stream = await openLiveStream(session, opts);
|
|
19580
19626
|
let h264;
|
|
19581
19627
|
try {
|
|
19582
|
-
h264 = await new Promise((
|
|
19628
|
+
h264 = await new Promise((resolve2, reject) => {
|
|
19583
19629
|
const bufs = [];
|
|
19584
19630
|
let keyCount = 0, capturing = false;
|
|
19585
19631
|
let sets;
|
|
@@ -19592,7 +19638,7 @@ async function recordClip(session, seconds, opts = {}) {
|
|
|
19592
19638
|
};
|
|
19593
19639
|
const collected = () => {
|
|
19594
19640
|
detach();
|
|
19595
|
-
|
|
19641
|
+
resolve2(primeForDecode(Buffer.concat(bufs), sets, codec2));
|
|
19596
19642
|
};
|
|
19597
19643
|
const failed = (reason, cause) => {
|
|
19598
19644
|
detach();
|
|
@@ -19622,7 +19668,7 @@ async function recordClip(session, seconds, opts = {}) {
|
|
|
19622
19668
|
stream.stop();
|
|
19623
19669
|
}
|
|
19624
19670
|
const codec = annexbFfmpegFormat(h264);
|
|
19625
|
-
return new Promise((
|
|
19671
|
+
return new Promise((resolve2, reject) => {
|
|
19626
19672
|
const ff = spawnFfmpeg([
|
|
19627
19673
|
// prettier-ignore
|
|
19628
19674
|
"-f",
|
|
@@ -19645,7 +19691,7 @@ async function recordClip(session, seconds, opts = {}) {
|
|
|
19645
19691
|
ff.on("close", (code) => {
|
|
19646
19692
|
const mp4 = Buffer.concat(out);
|
|
19647
19693
|
if (mp4.length)
|
|
19648
|
-
|
|
19694
|
+
resolve2(mp4);
|
|
19649
19695
|
else
|
|
19650
19696
|
reject(new Error(`ffmpeg mux failed (code ${code}): ${Buffer.concat(err).toString().slice(0, 200)}`));
|
|
19651
19697
|
});
|
|
@@ -19684,7 +19730,7 @@ function jpegGeometry(jpeg) {
|
|
|
19684
19730
|
}
|
|
19685
19731
|
function annexbToJpeg(annexb, spawn2) {
|
|
19686
19732
|
const codec = annexbFfmpegFormat(annexb);
|
|
19687
|
-
return new Promise((
|
|
19733
|
+
return new Promise((resolve2, reject) => {
|
|
19688
19734
|
const ff = spawnFfmpeg([
|
|
19689
19735
|
// prettier-ignore
|
|
19690
19736
|
"-f",
|
|
@@ -19711,7 +19757,7 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
19711
19757
|
const encoded = jpeg.length >= 3 && jpeg.subarray(0, 3).toString("hex") === "ffd8ff";
|
|
19712
19758
|
const geometry = encoded ? jpegGeometry(jpeg) : void 0;
|
|
19713
19759
|
if (geometry)
|
|
19714
|
-
return
|
|
19760
|
+
return resolve2({ jpeg, ...geometry });
|
|
19715
19761
|
const diagnostics = Buffer.concat(err).toString().slice(0, 200).trim();
|
|
19716
19762
|
const what = code !== 0 ? `ffmpeg exited ${code}` : encoded ? "encoded image declares no frame header" : "no complete frame in the burst";
|
|
19717
19763
|
reject(new LiveSnapshotUnavailableError("undecodable-burst", diagnostics ? `${what}: ${diagnostics}` : what));
|
|
@@ -19744,12 +19790,12 @@ function homeBase2RecordingPath(channel, recording) {
|
|
|
19744
19790
|
function receiveRecording(session, request) {
|
|
19745
19791
|
let resolveFrames;
|
|
19746
19792
|
let rejectFrames;
|
|
19747
|
-
const frames = new Promise((
|
|
19748
|
-
resolveFrames =
|
|
19793
|
+
const frames = new Promise((resolve2, reject) => {
|
|
19794
|
+
resolveFrames = resolve2;
|
|
19749
19795
|
rejectFrames = reject;
|
|
19750
19796
|
});
|
|
19751
19797
|
let resolveDrained;
|
|
19752
|
-
const drained = new Promise((
|
|
19798
|
+
const drained = new Promise((resolve2) => resolveDrained = resolve2);
|
|
19753
19799
|
const limitMs = request.timeoutMs ?? DEFAULT_TRANSFER_MS;
|
|
19754
19800
|
let collected = [];
|
|
19755
19801
|
const started = Date.now();
|
|
@@ -20287,7 +20333,8 @@ var SharedLiveSource = class {
|
|
|
20287
20333
|
*
|
|
20288
20334
|
* The previous stream is dropped rather than stopped through the state machine: it speaks to a session that
|
|
20289
20335
|
* is gone, and its `stop` would be read as an upstream end. Only a warm-up that asked for a replacement
|
|
20290
|
-
* rewarms, so this is inert on a source that is streaming or has already failed.
|
|
20336
|
+
* rewarms, so this is inert on a source that is streaming or has already failed. Each call replaces the
|
|
20337
|
+
* re-issue ticker rather than adding one, so the start goes out once per interval however often it runs.
|
|
20291
20338
|
*/
|
|
20292
20339
|
rewarm() {
|
|
20293
20340
|
if (this.disposed || !this.sessionReplacementAsked || this._state !== "warming")
|
|
@@ -20299,6 +20346,8 @@ var SharedLiveSource = class {
|
|
|
20299
20346
|
this.openStream();
|
|
20300
20347
|
this.warmAttempts++;
|
|
20301
20348
|
this.logger.debug(`${this.tag} warming again on a replacement session (attempt ${this.warmAttempts})`);
|
|
20349
|
+
if (this.warmRetryTimer)
|
|
20350
|
+
clearInterval(this.warmRetryTimer);
|
|
20302
20351
|
this.warmRetryTimer = setInterval(() => this.reissueStart(), this.warmRetryMs);
|
|
20303
20352
|
}
|
|
20304
20353
|
/**
|
|
@@ -21019,6 +21068,7 @@ function openReadableFromConsumer(consumer, opts = {}) {
|
|
|
21019
21068
|
if (!readable.push(value))
|
|
21020
21069
|
consumer.pause();
|
|
21021
21070
|
});
|
|
21071
|
+
consumer.on("budget", (notice) => readable.emit("budget", notice));
|
|
21022
21072
|
consumer.on("stop", () => readable.push(null));
|
|
21023
21073
|
consumer.on("error", (err) => readable.destroy(err));
|
|
21024
21074
|
return readable;
|
|
@@ -21954,7 +22004,7 @@ var FragmentRecording = class extends EventEmitter6 {
|
|
|
21954
22004
|
throw this.failure;
|
|
21955
22005
|
if (this.ended)
|
|
21956
22006
|
return;
|
|
21957
|
-
await new Promise((
|
|
22007
|
+
await new Promise((resolve2) => this.wake = resolve2);
|
|
21958
22008
|
}
|
|
21959
22009
|
} finally {
|
|
21960
22010
|
this.stop();
|
|
@@ -22067,9 +22117,10 @@ var P2P_STATION_WAITS = {
|
|
|
22067
22117
|
};
|
|
22068
22118
|
var RTSP_URL_READ_TIMEOUT_MS = 12e3;
|
|
22069
22119
|
var TALKBACK_CODEC_WAIT_MS = 3e3;
|
|
22120
|
+
var OWN_LIVE_SESSION_MODELS = /* @__PURE__ */ new Set(["T8030"]);
|
|
22070
22121
|
var ELD_TALKBACK_MODELS = /* @__PURE__ */ new Set(["T8410C"]);
|
|
22071
22122
|
function speakerCodec(consumer, talk, timeoutMs) {
|
|
22072
|
-
return new Promise((
|
|
22123
|
+
return new Promise((resolve2) => {
|
|
22073
22124
|
let timer;
|
|
22074
22125
|
const settle = (codec) => {
|
|
22075
22126
|
clearTimeout(timer);
|
|
@@ -22077,7 +22128,7 @@ function speakerCodec(consumer, talk, timeoutMs) {
|
|
|
22077
22128
|
consumer.off("audio", onAudio);
|
|
22078
22129
|
consumer.off("stop", onStop);
|
|
22079
22130
|
talk.off("stop", onStop);
|
|
22080
|
-
|
|
22131
|
+
resolve2(codec);
|
|
22081
22132
|
};
|
|
22082
22133
|
const onVideo = () => {
|
|
22083
22134
|
timer ??= setTimeout(() => settle("aac-lc"), timeoutMs);
|
|
@@ -22129,6 +22180,18 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22129
22180
|
* {@link liveSources} is a claim in flight.
|
|
22130
22181
|
*/
|
|
22131
22182
|
liveSessionKeys = /* @__PURE__ */ new Map();
|
|
22183
|
+
/**
|
|
22184
|
+
* The live source build in flight per `${parentSn}:${channel}`, settled once its connection is opened
|
|
22185
|
+
* or refused.
|
|
22186
|
+
*
|
|
22187
|
+
* A build awaits its media session before it can file the source in {@link liveSources}, so a second
|
|
22188
|
+
* request for the same camera in that window finds nothing cached. Building a second source there
|
|
22189
|
+
* overwrites the first, and the orphan's teardown then closes the connection the cached one streams
|
|
22190
|
+
* on. A request that finds a build here waits for it, until its own signal aborts, then resolves its
|
|
22191
|
+
* session again and reuses what the build filed, or builds its own when that build failed. The entry
|
|
22192
|
+
* is removed when the build settles and its promise never rejects, so each wait ends on a later state.
|
|
22193
|
+
*/
|
|
22194
|
+
liveSourceBuilds = /* @__PURE__ */ new Map();
|
|
22132
22195
|
/**
|
|
22133
22196
|
* The open talkback per `${parentSn}:${channel}`, if any. The device plays one audio stream at a
|
|
22134
22197
|
* time and the session carries one audio sequence, so this path is exclusive where a live pull is
|
|
@@ -22308,17 +22371,30 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22308
22371
|
* so a start issued before the key arrives is dropped as `media-command-unsent`, and the stream then
|
|
22309
22372
|
* shows nothing until a later keepalive tick happens to find the key. A connected session that cannot
|
|
22310
22373
|
* carry the start is not a connected session, so this refuses rather than returning one.
|
|
22374
|
+
*
|
|
22375
|
+
* A session refused its key within the grace, or never connected, is closed and dropped from the manager at
|
|
22376
|
+
* once. Nothing else releases it: no source holds it, a wired station's session never idles out, and the
|
|
22377
|
+
* manager hands the same session to every later open of the camera, so the next open dials a fresh
|
|
22378
|
+
* connection instead. A session another open has since replaced is left to its own close.
|
|
22311
22379
|
*/
|
|
22312
22380
|
async openMediaSession(parentSn, channel, signal) {
|
|
22313
|
-
const
|
|
22314
|
-
await this.
|
|
22315
|
-
|
|
22316
|
-
|
|
22317
|
-
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), signal);
|
|
22318
|
-
|
|
22319
|
-
|
|
22320
|
-
|
|
22321
|
-
|
|
22381
|
+
const mediaKey = _P2PCommandRouter.mediaSessionKey(parentSn, channel);
|
|
22382
|
+
const session = await this.openSession(mediaKey, parentSn);
|
|
22383
|
+
try {
|
|
22384
|
+
await this.awaitConnected(session, parentSn, signal);
|
|
22385
|
+
let ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), signal);
|
|
22386
|
+
if (!ready && session.repromptLevel2Key()) {
|
|
22387
|
+
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), signal);
|
|
22388
|
+
}
|
|
22389
|
+
if (!ready)
|
|
22390
|
+
throw new StationKeyUnavailableError(parentSn);
|
|
22391
|
+
return session;
|
|
22392
|
+
} catch (error) {
|
|
22393
|
+
if (!signal?.aborted && this.manager.get(mediaKey) === session) {
|
|
22394
|
+
void this.manager.close(mediaKey).catch((e) => this.reportError(e instanceof Error ? e : new Error(String(e))));
|
|
22395
|
+
}
|
|
22396
|
+
throw error;
|
|
22397
|
+
}
|
|
22322
22398
|
}
|
|
22323
22399
|
/**
|
|
22324
22400
|
* Wait for `session` to connect, for up to the session's own connect deadline, and throw
|
|
@@ -22356,7 +22432,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22356
22432
|
throw new Error(`no P2P endpoint (p2p_did) for station ${parentSn}`);
|
|
22357
22433
|
let dskKey;
|
|
22358
22434
|
try {
|
|
22359
|
-
dskKey = (await this.deps.mega.getDskKeys([
|
|
22435
|
+
dskKey = (await this.deps.mega.getDskKeys([stationDev]))[parentSn]?.dskKey;
|
|
22360
22436
|
} catch (e) {
|
|
22361
22437
|
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
22362
22438
|
}
|
|
@@ -22458,7 +22534,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22458
22534
|
const session = await this.openSession(parentSn, parentSn);
|
|
22459
22535
|
if (session.isConnected)
|
|
22460
22536
|
return;
|
|
22461
|
-
await new Promise((
|
|
22537
|
+
await new Promise((resolve2, reject) => {
|
|
22462
22538
|
const cleanup = () => {
|
|
22463
22539
|
signal?.removeEventListener("abort", onAbort);
|
|
22464
22540
|
session.off("connect", onConnect);
|
|
@@ -22467,7 +22543,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22467
22543
|
};
|
|
22468
22544
|
const onConnect = () => {
|
|
22469
22545
|
cleanup();
|
|
22470
|
-
|
|
22546
|
+
resolve2();
|
|
22471
22547
|
};
|
|
22472
22548
|
const onError = (error) => {
|
|
22473
22549
|
cleanup();
|
|
@@ -22809,8 +22885,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22809
22885
|
* {@link releaseLingeringSiblings}. A pull with consumers is never touched. The release runs before the
|
|
22810
22886
|
* reuse branch, so a reuse frees the station as a cold start does.
|
|
22811
22887
|
*
|
|
22812
|
-
* `
|
|
22813
|
-
* connection of its own for it
|
|
22888
|
+
* `continuous` decides whether a camera that finds the station's own session claimed may open a
|
|
22889
|
+
* connection of its own for it; on an {@link OWN_LIVE_SESSION_MODELS} station it always gets one, and an
|
|
22890
|
+
* unwatched source left on the station's session is rebuilt there. Only the continuous-pull egresses pass it. A still must not: it wants one
|
|
22814
22891
|
* frame, and a socket plus a level-2 negotiation per thumbnail is a cost a tile refresh cannot justify —
|
|
22815
22892
|
* so a still asked for while a sibling is being watched contends as it always did, and the caller's
|
|
22816
22893
|
* retained image answers it. A live view still outranks a tile; what changed is that two live views no
|
|
@@ -22819,23 +22896,31 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22819
22896
|
* The session goes into a {@link HeldSession} cell, so it can be replaced under a source that stays in
|
|
22820
22897
|
* place.
|
|
22821
22898
|
*/
|
|
22822
|
-
async sharedLiveSourceFor(sn, opts = {},
|
|
22899
|
+
async sharedLiveSourceFor(sn, opts = {}, continuous = false) {
|
|
22823
22900
|
const { session, parentSn, channel, accountId, homeBaseAttached } = await this.resolveSession(sn, {
|
|
22824
22901
|
waitLevel2: "soft",
|
|
22825
22902
|
requireLevel2ForAttached: true,
|
|
22826
22903
|
signal: opts.signal
|
|
22827
22904
|
});
|
|
22828
22905
|
const key = `${parentSn}:${channel}`;
|
|
22906
|
+
const build = this.liveSourceBuilds.get(key);
|
|
22907
|
+
if (build) {
|
|
22908
|
+
await abortable(build, opts.signal);
|
|
22909
|
+
return this.sharedLiveSourceFor(sn, opts, continuous);
|
|
22910
|
+
}
|
|
22911
|
+
const ownSession = continuous && homeBaseAttached && this.ownsLiveSession(parentSn);
|
|
22829
22912
|
let source = this.liveSources.get(key);
|
|
22830
|
-
if (source && source.state === "stopped") {
|
|
22913
|
+
if (source && (source.state === "stopped" || ownSession && source.state !== "idle" && source.consumerCount === 0 && this.liveSessionKeys.get(key) === parentSn)) {
|
|
22831
22914
|
this.dropLiveSource(key);
|
|
22832
22915
|
source = void 0;
|
|
22833
22916
|
}
|
|
22834
22917
|
this.releaseLingeringSiblings(parentSn, channel);
|
|
22835
22918
|
if (!source) {
|
|
22836
22919
|
const logger = this.deps.logger ?? noopLogger;
|
|
22837
|
-
const sessionKey =
|
|
22920
|
+
const sessionKey = ownSession || continuous && homeBaseAttached && this.stationSessionInUse(parentSn, key) ? _P2PCommandRouter.mediaSessionKey(parentSn, channel) : parentSn;
|
|
22838
22921
|
this.liveSessionKeys.set(key, sessionKey);
|
|
22922
|
+
const built = Promise.withResolvers();
|
|
22923
|
+
this.liveSourceBuilds.set(key, built.promise);
|
|
22839
22924
|
let held;
|
|
22840
22925
|
try {
|
|
22841
22926
|
held = {
|
|
@@ -22844,6 +22929,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22844
22929
|
} catch (error) {
|
|
22845
22930
|
this.liveSessionKeys.delete(key);
|
|
22846
22931
|
throw error;
|
|
22932
|
+
} finally {
|
|
22933
|
+
this.liveSourceBuilds.delete(key);
|
|
22934
|
+
built.resolve();
|
|
22847
22935
|
}
|
|
22848
22936
|
source = new SharedLiveSource({
|
|
22849
22937
|
makeStream: (ctx) => new LiveStream(held.session, {
|
|
@@ -23128,11 +23216,18 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23128
23216
|
* yield CMAF fragments (init segment first, then a `moof`+`mdat` per keyframe boundary) muxed by the
|
|
23129
23217
|
* dependency-free internal fMP4 muxer. The returned recording handle exposes battery-budget notices
|
|
23130
23218
|
* and detaches its consumer on `stop`, iterator return, or iterator throw. No ffmpeg.
|
|
23219
|
+
*
|
|
23220
|
+
* It is a continuous pull only on an {@link OWN_LIVE_SESSION_MODELS} station; elsewhere it never opens a
|
|
23221
|
+
* connection of its own.
|
|
23131
23222
|
*/
|
|
23132
23223
|
recordFragments(sn, opts = {}) {
|
|
23133
|
-
const source = this.sharedLiveSourceFor(sn, opts);
|
|
23224
|
+
const source = this.sharedLiveSourceFor(sn, opts, this.ownsLiveSession(this.stationKeyOf(sn)));
|
|
23134
23225
|
return new FragmentRecording(source, opts);
|
|
23135
23226
|
}
|
|
23227
|
+
/** Whether `stationSn` is an {@link OWN_LIVE_SESSION_MODELS} station. */
|
|
23228
|
+
ownsLiveSession(stationSn) {
|
|
23229
|
+
return OWN_LIVE_SESSION_MODELS.has(this.recordFor(stationSn)?.model ?? "");
|
|
23230
|
+
}
|
|
23136
23231
|
/**
|
|
23137
23232
|
* **Generic P2P request/reply query.** Sends a `SET_PAYLOAD` (1350) wrapper carrying `subCmd` on the
|
|
23138
23233
|
* device channel, then resolves with the reply frame's `payload` — the `NOTIFY_PAYLOAD` (1351)
|
|
@@ -23141,7 +23236,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23141
23236
|
async p2pQuery(sn, subCmd, opts = {}) {
|
|
23142
23237
|
const { session, channel, accountId } = await this.resolveSession(sn, { waitLevel2: true });
|
|
23143
23238
|
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
23144
|
-
return await new Promise((
|
|
23239
|
+
return await new Promise((resolve2, reject) => {
|
|
23145
23240
|
const timer = setTimeout(() => {
|
|
23146
23241
|
cleanup();
|
|
23147
23242
|
reject(new Error(`timeout waiting for p2p query ${subCmd} reply from ${sn}`));
|
|
@@ -23150,7 +23245,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23150
23245
|
const j = f.json;
|
|
23151
23246
|
if (f.channel === channel && f.commandId === CommandType.CMD_NOTIFY_PAYLOAD && j?.cmd === subCmd && j.payload && typeof j.payload === "object") {
|
|
23152
23247
|
cleanup();
|
|
23153
|
-
|
|
23248
|
+
resolve2(j.payload);
|
|
23154
23249
|
}
|
|
23155
23250
|
};
|
|
23156
23251
|
const cleanup = () => {
|
|
@@ -23180,7 +23275,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23180
23275
|
async p2pControlQuery(sn, param, data, opts = {}) {
|
|
23181
23276
|
const { session } = await this.resolveSession(sn, { waitLevel2: false });
|
|
23182
23277
|
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
23183
|
-
return await new Promise((
|
|
23278
|
+
return await new Promise((resolve2, reject) => {
|
|
23184
23279
|
const timer = setTimeout(() => {
|
|
23185
23280
|
cleanup();
|
|
23186
23281
|
reject(new Error(`timeout waiting for p2p control query ${param} reply from ${sn}`));
|
|
@@ -23189,7 +23284,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23189
23284
|
const j = f.json;
|
|
23190
23285
|
if (j?.cmd === param && j.payload && typeof j.payload === "object") {
|
|
23191
23286
|
cleanup();
|
|
23192
|
-
|
|
23287
|
+
resolve2(j.payload);
|
|
23193
23288
|
}
|
|
23194
23289
|
};
|
|
23195
23290
|
const cleanup = () => {
|
|
@@ -23286,12 +23381,12 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23286
23381
|
* is ignored rather than resolving the wrong read.
|
|
23287
23382
|
*/
|
|
23288
23383
|
awaitRtspUrl(session, channel, signal) {
|
|
23289
|
-
return new Promise((
|
|
23384
|
+
return new Promise((resolve2, reject) => {
|
|
23290
23385
|
const onUrl = (ev) => {
|
|
23291
23386
|
if (ev.channel !== channel)
|
|
23292
23387
|
return;
|
|
23293
23388
|
cleanup();
|
|
23294
|
-
|
|
23389
|
+
resolve2(ev.url);
|
|
23295
23390
|
};
|
|
23296
23391
|
const onAbort = () => {
|
|
23297
23392
|
cleanup();
|
|
@@ -23611,7 +23706,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23611
23706
|
* wrapper has to share {@link serializeKeyed}.
|
|
23612
23707
|
*/
|
|
23613
23708
|
awaitKeyedReply(sn, session, channel, engage, send) {
|
|
23614
|
-
return new Promise((
|
|
23709
|
+
return new Promise((resolve2, reject) => {
|
|
23615
23710
|
let refused = false;
|
|
23616
23711
|
let mark;
|
|
23617
23712
|
let timer;
|
|
@@ -23631,7 +23726,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23631
23726
|
if (refused)
|
|
23632
23727
|
return;
|
|
23633
23728
|
refused = true;
|
|
23634
|
-
expire(_P2PCommandRouter.KEYED_REFUSAL_TWIN_MS, () => settle(() =>
|
|
23729
|
+
expire(_P2PCommandRouter.KEYED_REFUSAL_TWIN_MS, () => settle(() => resolve2({ code: KEYED_PAYLOAD_SEQ_ERROR, mark })));
|
|
23635
23730
|
};
|
|
23636
23731
|
const onResult = (r) => {
|
|
23637
23732
|
if (r.channel !== channel)
|
|
@@ -23639,7 +23734,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23639
23734
|
if (r.code === KEYED_PAYLOAD_SEQ_ERROR)
|
|
23640
23735
|
refuse();
|
|
23641
23736
|
else if (!refused)
|
|
23642
|
-
settle(() =>
|
|
23737
|
+
settle(() => resolve2({ code: r.code }));
|
|
23643
23738
|
};
|
|
23644
23739
|
const onData = (f) => {
|
|
23645
23740
|
if (f.channel !== channel || f.commandId !== CommandType.CMD_NOTIFY_PAYLOAD)
|
|
@@ -23651,7 +23746,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23651
23746
|
}
|
|
23652
23747
|
const code = keyedPayloadReportCode(f.json, engage);
|
|
23653
23748
|
if (code !== void 0 && !refused)
|
|
23654
|
-
settle(() =>
|
|
23749
|
+
settle(() => resolve2({ code }));
|
|
23655
23750
|
};
|
|
23656
23751
|
session.on("commandResult", onResult);
|
|
23657
23752
|
session.on("data", onData);
|
|
@@ -23756,7 +23851,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23756
23851
|
clearTimeout(timer);
|
|
23757
23852
|
session.off("data", onData);
|
|
23758
23853
|
};
|
|
23759
|
-
const getReplyPromise = new Promise((
|
|
23854
|
+
const getReplyPromise = new Promise((resolve2) => {
|
|
23760
23855
|
onData = (f) => {
|
|
23761
23856
|
const j = f.json;
|
|
23762
23857
|
if (j?.cmd !== CMD_TRANSFER_PAYLOAD || !j.payload)
|
|
@@ -23769,11 +23864,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23769
23864
|
if (keyTime === void 0 || keyTime !== query.time)
|
|
23770
23865
|
return;
|
|
23771
23866
|
cleanup();
|
|
23772
|
-
|
|
23867
|
+
resolve2({ lockPayload, keyTime });
|
|
23773
23868
|
};
|
|
23774
23869
|
timer = setTimeout(() => {
|
|
23775
23870
|
cleanup();
|
|
23776
|
-
|
|
23871
|
+
resolve2(void 0);
|
|
23777
23872
|
}, _P2PCommandRouter.FF09_SETTINGS_GET_TIMEOUT_MS);
|
|
23778
23873
|
session.on("data", onData);
|
|
23779
23874
|
});
|
|
@@ -23902,7 +23997,9 @@ var PORTAL_KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a
|
|
|
23902
23997
|
var PORTAL_KEEPALIVE_MS = 29e3;
|
|
23903
23998
|
|
|
23904
23999
|
// dist/transport/rtc/portal-packet.js
|
|
24000
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
23905
24001
|
var MAGIC2 = Buffer.from("XZYH", "ascii");
|
|
24002
|
+
var ZSTD_MAGIC_LE = 4247762216;
|
|
23906
24003
|
var PORTAL_HEADER_LENGTH = 16;
|
|
23907
24004
|
var PORTAL_CMD_SET_PAYLOAD = 1350;
|
|
23908
24005
|
var PORTAL_STATION_CHANNEL = 255;
|
|
@@ -23966,11 +24063,23 @@ function parseJsonBody(buf) {
|
|
|
23966
24063
|
return text2;
|
|
23967
24064
|
}
|
|
23968
24065
|
}
|
|
24066
|
+
function unwrapZstd(buf) {
|
|
24067
|
+
if (buf.length < 4 || buf.readUInt32LE(0) !== ZSTD_MAGIC_LE)
|
|
24068
|
+
return buf;
|
|
24069
|
+
try {
|
|
24070
|
+
return zstdDecompressSync(buf);
|
|
24071
|
+
} catch {
|
|
24072
|
+
return void 0;
|
|
24073
|
+
}
|
|
24074
|
+
}
|
|
23969
24075
|
function parsePortalPacket(buf, linkType = PortalLinkType.COMMAND) {
|
|
23970
|
-
const
|
|
24076
|
+
const packet = unwrapZstd(buf);
|
|
24077
|
+
if (!packet)
|
|
24078
|
+
return void 0;
|
|
24079
|
+
const header = parsePortalHeader(packet);
|
|
23971
24080
|
if (!header)
|
|
23972
24081
|
return void 0;
|
|
23973
|
-
const body =
|
|
24082
|
+
const body = packet.subarray(PORTAL_HEADER_LENGTH, PORTAL_HEADER_LENGTH + header.paramLength);
|
|
23974
24083
|
if (linkType === PortalLinkType.NOTIFY) {
|
|
23975
24084
|
const data = parseJsonBody(body);
|
|
23976
24085
|
const cmd = typeof data === "object" && data !== null && "cmd" in data ? Number(data.cmd) : void 0;
|
|
@@ -24702,12 +24811,12 @@ var RtcPeer = class extends EventEmitter9 {
|
|
|
24702
24811
|
this.remoteMid = media ? offer.match(/^a=mid:([^\r\n]+)/m)?.[1]?.trim() ?? HUB_SDP_MID : midOf(offer, "application") ?? HUB_SDP_MID;
|
|
24703
24812
|
if (media)
|
|
24704
24813
|
this.videoMid = midOf(offer, "video");
|
|
24705
|
-
const answerWait = new Promise((
|
|
24814
|
+
const answerWait = new Promise((resolve2, reject) => {
|
|
24706
24815
|
const timer = setTimeout(() => {
|
|
24707
24816
|
this.localAnswer = void 0;
|
|
24708
24817
|
reject(new Error("timed out waiting for the local SDP answer"));
|
|
24709
24818
|
}, ANSWER_TIMEOUT_MS);
|
|
24710
|
-
this.localAnswer = { resolve, reject, timer };
|
|
24819
|
+
this.localAnswer = { resolve: resolve2, reject, timer };
|
|
24711
24820
|
});
|
|
24712
24821
|
pc.setRemoteDescription(offer, "offer");
|
|
24713
24822
|
this.remoteSet = true;
|
|
@@ -24869,7 +24978,7 @@ var RtcPeer = class extends EventEmitter9 {
|
|
|
24869
24978
|
|
|
24870
24979
|
// dist/transport/rtc/signaling.js
|
|
24871
24980
|
import { EventEmitter as EventEmitter10 } from "node:events";
|
|
24872
|
-
import { createHmac as createHmac3, randomUUID as
|
|
24981
|
+
import { createHmac as createHmac3, randomUUID as randomUUID3 } from "node:crypto";
|
|
24873
24982
|
var RTC_WS_PATH = "/v1/rtc/ws/join?reqtype=nvr";
|
|
24874
24983
|
var RTC_SIGN_PATH = "/v1/smart/nvr/ws/sign";
|
|
24875
24984
|
var PORTAL_ORIGIN = "https://security.eufy.com";
|
|
@@ -24909,7 +25018,7 @@ var RtcSignalingClient = class extends EventEmitter10 {
|
|
|
24909
25018
|
this.createSocket = opts.createSocket ?? defaultSocket;
|
|
24910
25019
|
this.logger = opts.logger ?? noopLogger;
|
|
24911
25020
|
this.now = opts.now ?? Date.now;
|
|
24912
|
-
this.makeMsgId = opts.makeMsgId ?? (() =>
|
|
25021
|
+
this.makeMsgId = opts.makeMsgId ?? (() => randomUUID3().replace(/-/g, ""));
|
|
24913
25022
|
}
|
|
24914
25023
|
get isOpen() {
|
|
24915
25024
|
return this.ws?.readyState === WS_OPEN;
|
|
@@ -24953,7 +25062,7 @@ var RtcSignalingClient = class extends EventEmitter10 {
|
|
|
24953
25062
|
};
|
|
24954
25063
|
const protocols = ["v1", Buffer.from(JSON.stringify(auth)).toString("base64url")];
|
|
24955
25064
|
this.logger.debug(`[rtc] ${this.opts.stationSn} signalling connect ${this.wsUrl}`);
|
|
24956
|
-
await new Promise((
|
|
25065
|
+
await new Promise((resolve2, reject) => {
|
|
24957
25066
|
const timer = setTimeout(() => {
|
|
24958
25067
|
reject(new Error(`RTC signalling connect timeout after ${SIGNALING_CONNECT_TIMEOUT_MS}ms`));
|
|
24959
25068
|
this.close();
|
|
@@ -24967,7 +25076,7 @@ var RtcSignalingClient = class extends EventEmitter10 {
|
|
|
24967
25076
|
this.sendAuth(sign);
|
|
24968
25077
|
this.startKeepalive();
|
|
24969
25078
|
this.emit("open");
|
|
24970
|
-
|
|
25079
|
+
resolve2();
|
|
24971
25080
|
});
|
|
24972
25081
|
ws.addEventListener("message", (ev) => {
|
|
24973
25082
|
void this.handleWireMessage(ev.data);
|
|
@@ -25240,12 +25349,12 @@ var RtcSession = class extends EventEmitter11 {
|
|
|
25240
25349
|
waitForAuth() {
|
|
25241
25350
|
if (this.authOk)
|
|
25242
25351
|
return Promise.resolve();
|
|
25243
|
-
return new Promise((
|
|
25352
|
+
return new Promise((resolve2, reject) => {
|
|
25244
25353
|
const timer = setTimeout(() => {
|
|
25245
25354
|
this.authWaiter = void 0;
|
|
25246
25355
|
reject(new Error("RTC signalling auth timeout"));
|
|
25247
25356
|
}, AUTH_TIMEOUT_MS);
|
|
25248
|
-
this.authWaiter = { resolve, reject, timer };
|
|
25357
|
+
this.authWaiter = { resolve: resolve2, reject, timer };
|
|
25249
25358
|
});
|
|
25250
25359
|
}
|
|
25251
25360
|
/** Settle a pending auth wait: resolve it, or reject it with `err`; either way its timer stops. */
|
|
@@ -25372,6 +25481,7 @@ var RtcCommandRouter = class {
|
|
|
25372
25481
|
lives = /* @__PURE__ */ new Map();
|
|
25373
25482
|
warm = /* @__PURE__ */ new Map();
|
|
25374
25483
|
callSources = /* @__PURE__ */ new Map();
|
|
25484
|
+
lastTransaction = 0;
|
|
25375
25485
|
constructor(deps) {
|
|
25376
25486
|
this.deps = deps;
|
|
25377
25487
|
}
|
|
@@ -25381,21 +25491,80 @@ var RtcCommandRouter = class {
|
|
|
25381
25491
|
}
|
|
25382
25492
|
if (route.mediaTrack)
|
|
25383
25493
|
throw new Error(`rtc: ${route.stationSn} serves media only and takes no portal command`);
|
|
25384
|
-
const identity = this.deps.identity();
|
|
25385
|
-
if (!identity)
|
|
25386
|
-
throw new Error(`rtc: not logged in, cannot drive ${route.stationSn}`);
|
|
25387
|
-
const { stationSn } = route;
|
|
25388
|
-
const adminUserId = route.adminUserId || identity.userId;
|
|
25389
25494
|
const channel = route.attached ? cmd.channel : PORTAL_STATION_CHANNEL;
|
|
25390
|
-
|
|
25391
|
-
|
|
25392
|
-
|
|
25393
|
-
|
|
25394
|
-
|
|
25395
|
-
|
|
25396
|
-
|
|
25397
|
-
|
|
25398
|
-
|
|
25495
|
+
if (cmd.kind === "set-json") {
|
|
25496
|
+
return this.sendQueued(route, PORTAL_CMD_CONTROL_PAYLOAD, cmd.param, channel, {
|
|
25497
|
+
cmd: cmd.param,
|
|
25498
|
+
commandType: cmd.param,
|
|
25499
|
+
data: cmd.data
|
|
25500
|
+
});
|
|
25501
|
+
}
|
|
25502
|
+
return this.sendQueued(route, PORTAL_CMD_SET_PAYLOAD, cmd.cmd, channel, {
|
|
25503
|
+
cmd: cmd.cmd,
|
|
25504
|
+
mValue3: cmd.mValue3 ?? 0,
|
|
25505
|
+
payload: cmd.payload
|
|
25506
|
+
});
|
|
25507
|
+
}
|
|
25508
|
+
/**
|
|
25509
|
+
* Query one on-station table over the station's control channel and answer its rows.
|
|
25510
|
+
*
|
|
25511
|
+
* Sends the `1350` envelope with inner `{cmd: 1306, payload: {cmd: 10000, table, payload, transaction}}`,
|
|
25512
|
+
* the whole-table query, on the station channel. The hub ACKs on the command link and answers with one notify frame whose body
|
|
25513
|
+
* carries `{cmd, table, data, mIntRet, msg, transaction, …}`, echoing the read's `transaction`; that echo
|
|
25514
|
+
* is the only thing a reply is attributed by, and every read sends a `transaction` no other read on this
|
|
25515
|
+
* router has sent. Resolves with the reply's `data` rows. Rejects with the hub's `msg` on a non-zero
|
|
25516
|
+
* `mIntRet`, on an ACK refusing the envelope, at once when `signal` aborts (queued or sent), when the
|
|
25517
|
+
* session closes, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no reply.
|
|
25518
|
+
*
|
|
25519
|
+
* One read or command at a time per station: the read holds the station's send queue until it settles.
|
|
25520
|
+
*/
|
|
25521
|
+
readTable(route, table, opts = {}) {
|
|
25522
|
+
const transaction = this.nextTransaction();
|
|
25523
|
+
const inner = {
|
|
25524
|
+
cmd: DB_QUERY.FULL_TABLE,
|
|
25525
|
+
table,
|
|
25526
|
+
payload: fullTableQuery(),
|
|
25527
|
+
transaction
|
|
25528
|
+
};
|
|
25529
|
+
return this.sendQueued(route, PORTAL_CMD_SET_PAYLOAD, CMD_DATABASE, PORTAL_STATION_CHANNEL, { cmd: CMD_DATABASE, mValue3: 0, payload: inner }, {
|
|
25530
|
+
timeoutMs: opts.timeoutMs,
|
|
25531
|
+
signal: opts.signal,
|
|
25532
|
+
pick: (body) => {
|
|
25533
|
+
if (String(body.transaction) !== transaction)
|
|
25534
|
+
return void 0;
|
|
25535
|
+
if (typeof body.mIntRet === "number" && body.mIntRet !== 0) {
|
|
25536
|
+
throw new Error(`rtc: ${route.stationSn} refused ${table}: ${body.msg ?? body.mIntRet} (${body.mIntRet})`);
|
|
25537
|
+
}
|
|
25538
|
+
return Array.isArray(body.data) ? body.data : void 0;
|
|
25539
|
+
}
|
|
25540
|
+
});
|
|
25541
|
+
}
|
|
25542
|
+
/**
|
|
25543
|
+
* Fetch one stored image by its on-station path over the station's control channel.
|
|
25544
|
+
*
|
|
25545
|
+
* Sends the `1350` envelope with inner `{cmd: 1308, payload: [{file}]}` on the station channel. The hub
|
|
25546
|
+
* ACKs on the command link and answers with one notify frame whose body is `{file, content}`, `content`
|
|
25547
|
+
* the image in base64, and no `transaction`; the reply is the one whose `file` equals `filePath`. A reply
|
|
25548
|
+
* arriving after an abandoned read of the same path carries that path's bytes, which are the bytes a
|
|
25549
|
+
* later read of it asks for. Resolves with the decoded bytes. Rejects on an ACK refusing the envelope,
|
|
25550
|
+
* at once when `signal` aborts (queued or sent), when the session closes, and when `timeoutMs` (default
|
|
25551
|
+
* {@link DB_TABLE_TIMEOUT_MS}) elapses with no reply.
|
|
25552
|
+
*
|
|
25553
|
+
* One read or command at a time per station: the read holds the station's send queue until it settles.
|
|
25554
|
+
*/
|
|
25555
|
+
readImage(route, filePath, opts = {}) {
|
|
25556
|
+
return this.sendQueued(route, PORTAL_CMD_SET_PAYLOAD, CMD_DATABASE_IMAGE, PORTAL_STATION_CHANNEL, { cmd: CMD_DATABASE_IMAGE, mValue3: 0, payload: [{ file: filePath }] }, {
|
|
25557
|
+
...opts,
|
|
25558
|
+
pick: (body) => body.file === filePath && typeof body.content === "string" ? Buffer.from(body.content, "base64") : void 0
|
|
25559
|
+
});
|
|
25560
|
+
}
|
|
25561
|
+
/**
|
|
25562
|
+
* A `transaction` stamp in the vendor app's shape, a decimal epoch-in-milliseconds string, strictly
|
|
25563
|
+
* greater than the last one this router issued, so two reads in the same millisecond never share one.
|
|
25564
|
+
*/
|
|
25565
|
+
nextTransaction() {
|
|
25566
|
+
this.lastTransaction = Math.max(Date.now(), this.lastTransaction + 1);
|
|
25567
|
+
return String(this.lastTransaction);
|
|
25399
25568
|
}
|
|
25400
25569
|
/**
|
|
25401
25570
|
* The media of a camera on a T9000: live video, a readable feed and a live still over the station's
|
|
@@ -25571,45 +25740,109 @@ var RtcCommandRouter = class {
|
|
|
25571
25740
|
return source;
|
|
25572
25741
|
}
|
|
25573
25742
|
/**
|
|
25574
|
-
*
|
|
25575
|
-
*
|
|
25576
|
-
*
|
|
25743
|
+
* Send one envelope on the station's session, queued behind its other sends, and settle as
|
|
25744
|
+
* {@link sendAwaitAck} does. `body` is the inner payload; the station session, the `account_id` it is
|
|
25745
|
+
* stamped with and the packet are resolved here, so a command and a read reach the wire through the same
|
|
25746
|
+
* steps. With `reply.signal`, the caller's wait rejects the moment it aborts, also while the send is still
|
|
25747
|
+
* queued; a send not yet made by then is skipped.
|
|
25577
25748
|
*/
|
|
25578
|
-
|
|
25579
|
-
|
|
25749
|
+
async sendQueued(route, outerCmd, innerCmd, channel, body, reply) {
|
|
25750
|
+
reply?.signal?.throwIfAborted();
|
|
25751
|
+
const identity = this.deps.identity();
|
|
25752
|
+
if (!identity)
|
|
25753
|
+
throw new Error(`rtc: not logged in, cannot drive ${route.stationSn}`);
|
|
25754
|
+
const adminUserId = route.adminUserId || identity.userId;
|
|
25755
|
+
const st = await abortable(this.stationSession(route, adminUserId, identity), reply?.signal);
|
|
25756
|
+
const segment = st.seg.next();
|
|
25757
|
+
const packet = buildPortalPacket({
|
|
25758
|
+
commandId: outerCmd,
|
|
25759
|
+
channel,
|
|
25760
|
+
segment,
|
|
25761
|
+
payload: { account_id: adminUserId, ...body }
|
|
25762
|
+
});
|
|
25763
|
+
const run = st.queue.then(() => this.sendAwaitAck(route.stationSn, st, packet, outerCmd, innerCmd, channel, segment, reply));
|
|
25764
|
+
st.queue = run.catch(() => void 0);
|
|
25765
|
+
return abortable(run, reply?.signal);
|
|
25766
|
+
}
|
|
25767
|
+
/**
|
|
25768
|
+
* Without `reply`, completes on the command ACK or a correlated control-result notification. The
|
|
25769
|
+
* notification must match the request channel, parameter and segment and contain a structured payload.
|
|
25770
|
+
* Receipt does not establish physical actuation. Bounded by {@link ACK_TIMEOUT_MS}.
|
|
25771
|
+
*
|
|
25772
|
+
* With `reply`, a zero-result ACK is not the answer: the first notify body (an object) for which
|
|
25773
|
+
* `reply.pick` answers a value resolves with it, one `pick` throws on rejects with that error, and every
|
|
25774
|
+
* other notify frame is left alone. Bounded by `reply.timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}),
|
|
25775
|
+
* and ended by `reply.signal`; one already aborted is not sent.
|
|
25776
|
+
*
|
|
25777
|
+
* Either way an ACK with a non-zero result code rejects, and the command is sent once.
|
|
25778
|
+
*/
|
|
25779
|
+
sendAwaitAck(sn, st, packet, outerCmd, innerCmd, channel, segment, reply) {
|
|
25780
|
+
const timeoutMs = reply ? reply.timeoutMs ?? DB_TABLE_TIMEOUT_MS : ACK_TIMEOUT_MS;
|
|
25781
|
+
const awaited = reply ? "reply" : "ACK";
|
|
25782
|
+
return new Promise((resolve2, reject) => {
|
|
25580
25783
|
const onData = (frame, linkType) => {
|
|
25581
25784
|
const p = parsePortalPacket(frame, linkType);
|
|
25582
|
-
if (!p
|
|
25785
|
+
if (!p)
|
|
25786
|
+
return;
|
|
25787
|
+
if (reply && linkType === PortalLinkType.NOTIFY && typeof p.data === "object" && p.data !== null) {
|
|
25788
|
+
let picked;
|
|
25789
|
+
try {
|
|
25790
|
+
picked = reply.pick(p.data);
|
|
25791
|
+
} catch (e) {
|
|
25792
|
+
cleanup();
|
|
25793
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
25794
|
+
return;
|
|
25795
|
+
}
|
|
25796
|
+
if (picked === void 0)
|
|
25797
|
+
return;
|
|
25798
|
+
cleanup();
|
|
25799
|
+
this.deps.logger?.debug?.(`[rtc] ${sn} cmd ${innerCmd} answered`);
|
|
25800
|
+
resolve2(picked);
|
|
25801
|
+
return;
|
|
25802
|
+
}
|
|
25803
|
+
if (p.segment !== segment)
|
|
25583
25804
|
return;
|
|
25584
25805
|
const acknowledgement = linkType === PortalLinkType.COMMAND && p.commandId === outerCmd && !!p.isResponse;
|
|
25585
25806
|
const d = p.data;
|
|
25586
25807
|
const notification = outerCmd === PORTAL_CMD_CONTROL_PAYLOAD && linkType === PortalLinkType.NOTIFY && p.commandId === PORTAL_CMD_NOTIFY_PAYLOAD && p.isResponse === 0 && p.channel === channel && p.cmd === innerCmd && typeof d?.payload === "object" && d.payload !== null;
|
|
25587
25808
|
if (!acknowledgement && !notification)
|
|
25588
25809
|
return;
|
|
25810
|
+
if (reply && p.errCode === 0)
|
|
25811
|
+
return;
|
|
25589
25812
|
cleanup();
|
|
25590
25813
|
if (acknowledgement && p.errCode !== 0) {
|
|
25591
25814
|
reject(new Error(`rtc: ${sn} rejected cmd ${innerCmd} (err ${p.errCode})`));
|
|
25592
25815
|
} else {
|
|
25593
25816
|
this.deps.logger?.debug?.(`[rtc] ${sn} cmd ${innerCmd} acked`);
|
|
25594
|
-
|
|
25817
|
+
resolve2(void 0);
|
|
25595
25818
|
}
|
|
25596
25819
|
};
|
|
25597
25820
|
const onClose = () => {
|
|
25598
25821
|
cleanup();
|
|
25599
|
-
reject(new Error(`rtc: ${sn} session closed while waiting for cmd ${innerCmd}
|
|
25822
|
+
reject(new Error(`rtc: ${sn} session closed while waiting for cmd ${innerCmd} ${awaited}`));
|
|
25823
|
+
};
|
|
25824
|
+
const onAbort = () => {
|
|
25825
|
+
cleanup();
|
|
25826
|
+
reject(reply?.signal?.reason);
|
|
25600
25827
|
};
|
|
25601
25828
|
const timer = setTimeout(() => {
|
|
25602
25829
|
cleanup();
|
|
25603
|
-
reject(new Error(`rtc: ${sn} cmd ${innerCmd}
|
|
25604
|
-
},
|
|
25830
|
+
reject(new Error(`rtc: ${sn} cmd ${innerCmd} ${awaited} timed out after ${timeoutMs}ms`));
|
|
25831
|
+
}, timeoutMs);
|
|
25605
25832
|
const cleanup = () => {
|
|
25606
25833
|
clearTimeout(timer);
|
|
25607
25834
|
st.session.off("commandData", onData);
|
|
25608
25835
|
st.session.off("close", onClose);
|
|
25836
|
+
reply?.signal?.removeEventListener("abort", onAbort);
|
|
25609
25837
|
this.touch(sn, st);
|
|
25610
25838
|
};
|
|
25839
|
+
if (reply?.signal?.aborted) {
|
|
25840
|
+
onAbort();
|
|
25841
|
+
return;
|
|
25842
|
+
}
|
|
25611
25843
|
st.session.on("commandData", onData);
|
|
25612
25844
|
st.session.on("close", onClose);
|
|
25845
|
+
reply?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
25613
25846
|
if (!st.session.sendCommand(packet)) {
|
|
25614
25847
|
cleanup();
|
|
25615
25848
|
reject(new Error(`rtc: ${sn} command channel not open, cmd ${innerCmd} not sent`));
|
|
@@ -25665,7 +25898,7 @@ var RtcCommandRouter = class {
|
|
|
25665
25898
|
* promise, so an early failure cannot leave a second one rejecting unheard.
|
|
25666
25899
|
*/
|
|
25667
25900
|
bringUp(sn, session) {
|
|
25668
|
-
return new Promise((
|
|
25901
|
+
return new Promise((resolve2, reject) => {
|
|
25669
25902
|
let settled = false;
|
|
25670
25903
|
const finish = (err) => {
|
|
25671
25904
|
if (settled)
|
|
@@ -25682,7 +25915,7 @@ var RtcCommandRouter = class {
|
|
|
25682
25915
|
reject(err);
|
|
25683
25916
|
} else {
|
|
25684
25917
|
this.deps.logger?.info?.(`[rtc] ${sn} command channel up`);
|
|
25685
|
-
|
|
25918
|
+
resolve2();
|
|
25686
25919
|
}
|
|
25687
25920
|
};
|
|
25688
25921
|
const onConnected = () => finish();
|
|
@@ -26111,10 +26344,10 @@ function dpColorFields(spec) {
|
|
|
26111
26344
|
}
|
|
26112
26345
|
|
|
26113
26346
|
// dist/transport/mqtt/clean-codec.js
|
|
26114
|
-
import { randomUUID as
|
|
26347
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
26115
26348
|
function buildCleanDpEnvelope(accountId, deviceSn, dp, value, opts) {
|
|
26116
26349
|
const payload = JSON.stringify({
|
|
26117
|
-
t: opts?.uuid ??
|
|
26350
|
+
t: opts?.uuid ?? randomUUID4(),
|
|
26118
26351
|
protocol: 2,
|
|
26119
26352
|
account_id: accountId,
|
|
26120
26353
|
device_sn: deviceSn,
|
|
@@ -26353,13 +26586,13 @@ var MqttCommandRouter = class _MqttCommandRouter {
|
|
|
26353
26586
|
* timeout with no unhandled listener left behind either way.
|
|
26354
26587
|
*/
|
|
26355
26588
|
waitForMqttMessage(mqtt, match, timeoutMs) {
|
|
26356
|
-
return new Promise((
|
|
26589
|
+
return new Promise((resolve2) => {
|
|
26357
26590
|
const onMessage = (m) => {
|
|
26358
26591
|
const result = match(m);
|
|
26359
26592
|
if (result === void 0)
|
|
26360
26593
|
return;
|
|
26361
26594
|
cleanup();
|
|
26362
|
-
|
|
26595
|
+
resolve2(result);
|
|
26363
26596
|
};
|
|
26364
26597
|
const cleanup = () => {
|
|
26365
26598
|
clearTimeout(timer);
|
|
@@ -26367,7 +26600,7 @@ var MqttCommandRouter = class _MqttCommandRouter {
|
|
|
26367
26600
|
};
|
|
26368
26601
|
const timer = setTimeout(() => {
|
|
26369
26602
|
cleanup();
|
|
26370
|
-
|
|
26603
|
+
resolve2(void 0);
|
|
26371
26604
|
}, timeoutMs);
|
|
26372
26605
|
timer.unref?.();
|
|
26373
26606
|
mqtt.on("message", onMessage);
|
|
@@ -26887,7 +27120,7 @@ function decryptReply(key, resultB64) {
|
|
|
26887
27120
|
}
|
|
26888
27121
|
|
|
26889
27122
|
// dist/transport/tuya/request.js
|
|
26890
|
-
import { randomUUID as
|
|
27123
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
26891
27124
|
var TUYA_API_ENDPOINT = "https://a1.tuyaeu.com/api.json";
|
|
26892
27125
|
var TUYA_APP_KEY = "w8x4ppqkdxvqnd73ahj9";
|
|
26893
27126
|
var TUYA_APP_SECRET = "pt585qhmt75hwcynchnps9dnxh9suhwd";
|
|
@@ -26919,7 +27152,7 @@ function buildApiParams(action, opts) {
|
|
|
26919
27152
|
a: action.a,
|
|
26920
27153
|
v: action.v,
|
|
26921
27154
|
time: String(opts.time ?? Math.floor(Date.now() / 1e3)),
|
|
26922
|
-
requestId: opts.requestId ??
|
|
27155
|
+
requestId: opts.requestId ?? randomUUID5(),
|
|
26923
27156
|
sid: opts.session.sid,
|
|
26924
27157
|
deviceId: opts.session.deviceId,
|
|
26925
27158
|
chKey: opts.session.chKey,
|
|
@@ -28258,8 +28491,6 @@ var FcmRegistrar = class {
|
|
|
28258
28491
|
};
|
|
28259
28492
|
|
|
28260
28493
|
// dist/transport/push/store.js
|
|
28261
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "node:fs";
|
|
28262
|
-
import { dirname as dirname2 } from "node:path";
|
|
28263
28494
|
var MemoryFcmStore = class {
|
|
28264
28495
|
p = null;
|
|
28265
28496
|
load() {
|
|
@@ -28272,31 +28503,7 @@ var MemoryFcmStore = class {
|
|
|
28272
28503
|
this.p = null;
|
|
28273
28504
|
}
|
|
28274
28505
|
};
|
|
28275
|
-
var FileFcmStore = class {
|
|
28276
|
-
path;
|
|
28277
|
-
constructor(path) {
|
|
28278
|
-
this.path = path;
|
|
28279
|
-
}
|
|
28280
|
-
load() {
|
|
28281
|
-
try {
|
|
28282
|
-
return JSON.parse(readFileSync2(this.path, "utf-8"));
|
|
28283
|
-
} catch {
|
|
28284
|
-
return null;
|
|
28285
|
-
}
|
|
28286
|
-
}
|
|
28287
|
-
save(p) {
|
|
28288
|
-
try {
|
|
28289
|
-
mkdirSync2(dirname2(this.path), { recursive: true });
|
|
28290
|
-
} catch {
|
|
28291
|
-
}
|
|
28292
|
-
writeFileSync2(this.path, JSON.stringify(p, null, 2), { mode: 384 });
|
|
28293
|
-
}
|
|
28294
|
-
clear() {
|
|
28295
|
-
try {
|
|
28296
|
-
rmSync2(this.path);
|
|
28297
|
-
} catch {
|
|
28298
|
-
}
|
|
28299
|
-
}
|
|
28506
|
+
var FileFcmStore = class extends FileSessionStore {
|
|
28300
28507
|
};
|
|
28301
28508
|
|
|
28302
28509
|
// dist/transport/media-failure.js
|
|
@@ -28325,6 +28532,12 @@ var StoredImageCache = class {
|
|
|
28325
28532
|
activeDevices = /* @__PURE__ */ new Set();
|
|
28326
28533
|
activeDownloads = 0;
|
|
28327
28534
|
generation = 0;
|
|
28535
|
+
/**
|
|
28536
|
+
* Bumped by {@link clearLifecycleErrors}. A lifecycle failure from a download started under an older
|
|
28537
|
+
* count is not recorded; the candidate is queued again, unless a newer one is already queued, so the
|
|
28538
|
+
* current session fetches it.
|
|
28539
|
+
*/
|
|
28540
|
+
lifecycle = 0;
|
|
28328
28541
|
constructor(downloader, logger, clock = Date.now, isLifecycleError = () => false) {
|
|
28329
28542
|
this.downloader = downloader;
|
|
28330
28543
|
this.logger = logger;
|
|
@@ -28380,6 +28593,18 @@ var StoredImageCache = class {
|
|
|
28380
28593
|
return Promise.resolve(state.retained.image);
|
|
28381
28594
|
return Promise.reject(new StoredSnapshotUnavailableError(reason, "No stored snapshot is available"));
|
|
28382
28595
|
}
|
|
28596
|
+
/**
|
|
28597
|
+
* Forget every device's recorded lifecycle failure, keeping retained bytes and candidate state.
|
|
28598
|
+
*
|
|
28599
|
+
* A lifecycle failure describes the session that was current when the download ran, so it no longer
|
|
28600
|
+
* holds once a login succeeds again; {@link snapshotStored} then answers from the retained image, or
|
|
28601
|
+
* with the reason a stored image is absent. A download in flight across the call cannot record one.
|
|
28602
|
+
*/
|
|
28603
|
+
clearLifecycleErrors() {
|
|
28604
|
+
this.lifecycle += 1;
|
|
28605
|
+
for (const state of this.devices.values())
|
|
28606
|
+
state.lifecycleError = void 0;
|
|
28607
|
+
}
|
|
28383
28608
|
/** Invalidate all retained and candidate state. */
|
|
28384
28609
|
clear() {
|
|
28385
28610
|
this.generation += 1;
|
|
@@ -28395,18 +28620,23 @@ var StoredImageCache = class {
|
|
|
28395
28620
|
state.inFlight = candidate;
|
|
28396
28621
|
this.activeDownloads += 1;
|
|
28397
28622
|
this.activeDevices.add(candidate.deviceKey);
|
|
28398
|
-
|
|
28623
|
+
const lifecycle = this.lifecycle;
|
|
28624
|
+
void this.downloader(candidate.url, candidate.deviceKey).then((image) => this.complete(candidate, lifecycle, image), (error) => this.complete(candidate, lifecycle, void 0, error));
|
|
28399
28625
|
}
|
|
28400
28626
|
}
|
|
28401
|
-
complete(candidate, image, error) {
|
|
28627
|
+
complete(candidate, lifecycle, image, error) {
|
|
28402
28628
|
this.activeDownloads -= 1;
|
|
28403
28629
|
this.activeDevices.delete(candidate.deviceKey);
|
|
28404
28630
|
const state = this.devices.get(candidate.deviceKey);
|
|
28405
28631
|
if (candidate.generation === this.generation && state?.inFlight === candidate) {
|
|
28406
28632
|
state.inFlight = void 0;
|
|
28407
28633
|
if (error !== void 0 && this.isLifecycleError(error)) {
|
|
28408
|
-
|
|
28409
|
-
|
|
28634
|
+
if (lifecycle === this.lifecycle) {
|
|
28635
|
+
state.lifecycleError = error;
|
|
28636
|
+
state.reason = void 0;
|
|
28637
|
+
} else {
|
|
28638
|
+
state.queued ??= candidate;
|
|
28639
|
+
}
|
|
28410
28640
|
} else if (image === void 0 && this.retryLater(state, candidate, error)) {
|
|
28411
28641
|
state.lifecycleError = void 0;
|
|
28412
28642
|
} else if (image === void 0) {
|
|
@@ -28898,8 +29128,8 @@ var DeviceRegistry = class {
|
|
|
28898
29128
|
const waiters = this.stateWaiters.get(sn);
|
|
28899
29129
|
if (waiters) {
|
|
28900
29130
|
this.stateWaiters.delete(sn);
|
|
28901
|
-
for (const
|
|
28902
|
-
|
|
29131
|
+
for (const resolve2 of waiters)
|
|
29132
|
+
resolve2();
|
|
28903
29133
|
}
|
|
28904
29134
|
}
|
|
28905
29135
|
/**
|
|
@@ -28944,15 +29174,15 @@ var DeviceRegistry = class {
|
|
|
28944
29174
|
awaitRealtimeState(sn, timeoutMs) {
|
|
28945
29175
|
if (this.hasRealtimeState(sn) || timeoutMs <= 0)
|
|
28946
29176
|
return Promise.resolve();
|
|
28947
|
-
return new Promise((
|
|
29177
|
+
return new Promise((resolve2) => {
|
|
28948
29178
|
const timer = setTimeout(() => {
|
|
28949
29179
|
this.stateWaiters.get(sn)?.delete(done);
|
|
28950
|
-
|
|
29180
|
+
resolve2();
|
|
28951
29181
|
}, timeoutMs);
|
|
28952
29182
|
timer.unref?.();
|
|
28953
29183
|
const done = () => {
|
|
28954
29184
|
clearTimeout(timer);
|
|
28955
|
-
|
|
29185
|
+
resolve2();
|
|
28956
29186
|
};
|
|
28957
29187
|
const set = this.stateWaiters.get(sn) ?? /* @__PURE__ */ new Set();
|
|
28958
29188
|
set.add(done);
|
|
@@ -29656,7 +29886,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29656
29886
|
return true;
|
|
29657
29887
|
const delay = Math.min(500, deadline - Date.now());
|
|
29658
29888
|
if (delay > 0)
|
|
29659
|
-
await new Promise((
|
|
29889
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
29660
29890
|
if (refresh.expected !== void 0 && settled())
|
|
29661
29891
|
return true;
|
|
29662
29892
|
}
|
|
@@ -29705,7 +29935,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29705
29935
|
* manager's own log, since by then nothing is waiting to receive it.
|
|
29706
29936
|
*/
|
|
29707
29937
|
recycleStandaloneSession(sn) {
|
|
29708
|
-
const releaseWrite = new Promise((
|
|
29938
|
+
const releaseWrite = new Promise((resolve2) => void setTimeout(resolve2, SESSION_RECYCLE_WAIT_MS).unref?.());
|
|
29709
29939
|
return Promise.race([
|
|
29710
29940
|
this.p2p.resetStandaloneSession(sn).finally(() => this.reopenWiredStation(sn)),
|
|
29711
29941
|
releaseWrite.then(() => this.opts.logger?.debug(`[session ${sn}] recycle still waiting on an attached viewer \u2014 releasing the write that asked for it`))
|
|
@@ -29742,11 +29972,11 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29742
29972
|
}
|
|
29743
29973
|
/** Limit waiting on an unabortable dependency operation to the remaining semantic-event refresh window. */
|
|
29744
29974
|
beforeDeadline(operation, timeoutMs) {
|
|
29745
|
-
return new Promise((
|
|
29975
|
+
return new Promise((resolve2, reject) => {
|
|
29746
29976
|
const timeout = setTimeout(() => reject(new RefreshWindowClosedError("refresh window closed")), Math.max(0, timeoutMs));
|
|
29747
29977
|
operation.then((value) => {
|
|
29748
29978
|
clearTimeout(timeout);
|
|
29749
|
-
|
|
29979
|
+
resolve2(value);
|
|
29750
29980
|
}, (error) => {
|
|
29751
29981
|
clearTimeout(timeout);
|
|
29752
29982
|
reject(error);
|
|
@@ -29851,12 +30081,16 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29851
30081
|
/**
|
|
29852
30082
|
* On a successful login, kick off auto-realtime (unless `autoRealtime:false`). Fire-and-forget so
|
|
29853
30083
|
* `login()` returns as soon as the session is ready — realtime channels come up in the background and
|
|
29854
|
-
* surface failures via `error`. Idempotent through the retained generation promise.
|
|
30084
|
+
* surface failures via `error`. Idempotent through the retained generation promise. A login for a
|
|
30085
|
+
* different account drops every stored image; one for the same account drops only the lifecycle
|
|
30086
|
+
* failures recorded against the session it replaces.
|
|
29855
30087
|
*/
|
|
29856
30088
|
afterLogin(result) {
|
|
29857
30089
|
if (result.status === LoginStatus.Ok) {
|
|
29858
30090
|
if (this.storedImageAccount && this.storedImageAccount !== result.session.userId)
|
|
29859
30091
|
this.storedImages?.clear();
|
|
30092
|
+
else
|
|
30093
|
+
this.storedImages?.clearLifecycleErrors();
|
|
29860
30094
|
this.storedImageAccount = result.session.userId;
|
|
29861
30095
|
const auth = this.mega.auth;
|
|
29862
30096
|
if (auth?.userId)
|
|
@@ -29893,12 +30127,12 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29893
30127
|
if (options.timeoutMs === void 0)
|
|
29894
30128
|
return generation.promise;
|
|
29895
30129
|
const timeoutMs = Math.max(0, options.timeoutMs);
|
|
29896
|
-
return new Promise((
|
|
29897
|
-
const timer = setTimeout(() =>
|
|
30130
|
+
return new Promise((resolve2) => {
|
|
30131
|
+
const timer = setTimeout(() => resolve2(readinessSnapshot(generation.readiness, "timed-out")), timeoutMs);
|
|
29898
30132
|
timer.unref?.();
|
|
29899
30133
|
void generation.promise.then((result) => {
|
|
29900
30134
|
clearTimeout(timer);
|
|
29901
|
-
|
|
30135
|
+
resolve2(result);
|
|
29902
30136
|
});
|
|
29903
30137
|
});
|
|
29904
30138
|
}
|
|
@@ -29939,10 +30173,10 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29939
30173
|
let acknowledge;
|
|
29940
30174
|
let reject;
|
|
29941
30175
|
let acknowledged = false;
|
|
29942
|
-
const acknowledgement = new Promise((
|
|
30176
|
+
const acknowledgement = new Promise((resolve2, rejectPromise) => {
|
|
29943
30177
|
acknowledge = () => {
|
|
29944
30178
|
acknowledged = true;
|
|
29945
|
-
|
|
30179
|
+
resolve2();
|
|
29946
30180
|
};
|
|
29947
30181
|
reject = rejectPromise;
|
|
29948
30182
|
});
|
|
@@ -30309,8 +30543,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30309
30543
|
if (this.realtimeGeneration?.epoch === this.realtimeEpoch)
|
|
30310
30544
|
return this.realtimeGeneration;
|
|
30311
30545
|
const epoch = this.realtimeEpoch;
|
|
30312
|
-
let
|
|
30313
|
-
const promise = new Promise((done) =>
|
|
30546
|
+
let resolve2;
|
|
30547
|
+
const promise = new Promise((done) => resolve2 = done);
|
|
30314
30548
|
const generation = {
|
|
30315
30549
|
epoch,
|
|
30316
30550
|
readiness: {
|
|
@@ -30320,7 +30554,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30320
30554
|
wiredP2p: createPlaneReadiness()
|
|
30321
30555
|
},
|
|
30322
30556
|
promise,
|
|
30323
|
-
resolve,
|
|
30557
|
+
resolve: resolve2,
|
|
30324
30558
|
abort: new AbortController(),
|
|
30325
30559
|
settled: false
|
|
30326
30560
|
};
|
|
@@ -30471,6 +30705,9 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30471
30705
|
* device absent from the previous pass is skipped — first sight is discovery, not a transition;
|
|
30472
30706
|
* {@link deviceState} answers an initial reading.
|
|
30473
30707
|
*
|
|
30708
|
+
* A device with changed params is re-resolved by {@link widenCapabilities} once per pass, however many
|
|
30709
|
+
* of its params moved, since each re-resolution is a cloud param-list read.
|
|
30710
|
+
*
|
|
30474
30711
|
* Then re-reads every Tuya clean-line robot from the Tuya cloud ({@link readTuyaDevices}): its state
|
|
30475
30712
|
* is not in the cloud record this pass diffs and no live channel carries it, so the poll is where it
|
|
30476
30713
|
* stays fresh. That read runs after the `try`, not inside it, so a cloud error on the list diff does
|
|
@@ -30489,8 +30726,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30489
30726
|
this.emitSemantic(out.event, out.payload, { refresh: out.refresh });
|
|
30490
30727
|
for (const dev of diff.reported)
|
|
30491
30728
|
this.emit("deviceState", this.stateOf(dev));
|
|
30492
|
-
for (const
|
|
30493
|
-
await this.widenCapabilities(
|
|
30729
|
+
for (const sn of new Set(diff.params.map((c) => c.deviceSn)))
|
|
30730
|
+
await this.widenCapabilities(sn);
|
|
30494
30731
|
} catch (e) {
|
|
30495
30732
|
this.reportError(e);
|
|
30496
30733
|
}
|
|
@@ -30876,31 +31113,72 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30876
31113
|
return this.p2p.getSessions();
|
|
30877
31114
|
}
|
|
30878
31115
|
/**
|
|
30879
|
-
* The people enrolled on a station, read from its own `person_basic_info` table
|
|
31116
|
+
* The people enrolled on a station, read from its own `person_basic_info` table through
|
|
31117
|
+
* {@link readStationTable}.
|
|
30880
31118
|
*
|
|
30881
31119
|
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
30882
31120
|
* station, because they were never uploaded — the station holds them.
|
|
30883
31121
|
*
|
|
30884
|
-
* Resolves with the rows the station returned
|
|
30885
|
-
*
|
|
30886
|
-
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
31122
|
+
* Resolves with the rows the station returned that are objects. Rejects as {@link readStationTable}
|
|
31123
|
+
* does.
|
|
30887
31124
|
*
|
|
30888
31125
|
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
30889
31126
|
* returned as they arrive; a caller that wants only named people filters them.
|
|
30890
31127
|
*/
|
|
30891
31128
|
async getStationFaces(stationSn, opts = {}) {
|
|
31129
|
+
const rows = await this.readStationTable(stationSn, "person_basic_info", opts);
|
|
31130
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
31131
|
+
}
|
|
31132
|
+
/**
|
|
31133
|
+
* The rows of one of a station's own tables (`history_record_info`, `person_basic_info`, …), read over
|
|
31134
|
+
* the transport the station speaks: the control channel of a HomeBase S1 Pro (T9000), P2P for any
|
|
31135
|
+
* other station. The query is the table's whole content; the rows are the station's, shaped by it.
|
|
31136
|
+
*
|
|
31137
|
+
* Resolves with the rows the station returned. Rejects on a serial not in the device list, on a record
|
|
31138
|
+
* stating no `admin_user_id`, on a station that cannot be reached, on a query the station refuses, and
|
|
31139
|
+
* on the reply not arriving in time. Assembling the reply is the transport's; this resolves which
|
|
31140
|
+
* station, which transport and which account id.
|
|
31141
|
+
*/
|
|
31142
|
+
async readStationTable(stationSn, table, opts = {}) {
|
|
31143
|
+
const { station, accountId, route } = await this.stationReadTarget("readStationTable", stationSn);
|
|
31144
|
+
if (route)
|
|
31145
|
+
return this.rtc.readTable(route, table, opts);
|
|
31146
|
+
await this.p2p.ensureStation(station, opts.signal);
|
|
31147
|
+
const session = this.p2p.getSessions().get(station);
|
|
31148
|
+
if (!session)
|
|
31149
|
+
throw new Error(`readStationTable: no P2P session for ${station}`);
|
|
31150
|
+
return session.readDatabase(table, { accountId, ...opts });
|
|
31151
|
+
}
|
|
31152
|
+
/**
|
|
31153
|
+
* One image stored on a HomeBase S1 Pro (T9000), by the on-station path a table row names
|
|
31154
|
+
* (`thumb_path`, …), read over the station's control channel.
|
|
31155
|
+
*
|
|
31156
|
+
* Resolves with the image bytes as the station holds them. Rejects on a serial not in the device list,
|
|
31157
|
+
* on a record stating no `admin_user_id`, on a station other than a T9000, on a station that cannot be
|
|
31158
|
+
* reached, and on the reply not arriving in time. Fetching the image is the transport's; this resolves
|
|
31159
|
+
* which station and which account id.
|
|
31160
|
+
*/
|
|
31161
|
+
async readStationImage(stationSn, filePath, opts = {}) {
|
|
31162
|
+
const { station, route } = await this.stationReadTarget("readStationImage", stationSn);
|
|
31163
|
+
if (!route)
|
|
31164
|
+
throw new Error(`readStationImage: ${station} is not a T9000; its stored images are not read here`);
|
|
31165
|
+
return this.rtc.readImage(route, filePath, opts);
|
|
31166
|
+
}
|
|
31167
|
+
/**
|
|
31168
|
+
* The station a database read goes to, its `admin_user_id` and, for a T9000, its RTC route. Throws,
|
|
31169
|
+
* prefixed with `op`, on a serial the device list does not carry and on a record stating no admin id.
|
|
31170
|
+
*/
|
|
31171
|
+
async stationReadTarget(op, stationSn) {
|
|
30892
31172
|
if (!this.registry.list().length)
|
|
30893
31173
|
await this.getDevices();
|
|
30894
31174
|
const station = this.p2p.stationKeyOf(stationSn);
|
|
31175
|
+
const found = this.stationOf(station);
|
|
31176
|
+
if (!found.station)
|
|
31177
|
+
throw new Error(`${op}: ${station} is not in the device list`);
|
|
30895
31178
|
const accountId = this.adminUserIdOf(station);
|
|
30896
31179
|
if (!accountId)
|
|
30897
|
-
throw new Error(
|
|
30898
|
-
|
|
30899
|
-
const session = this.p2p.getSessions().get(station);
|
|
30900
|
-
if (!session)
|
|
30901
|
-
throw new Error(`getStationFaces: no P2P session for ${station}`);
|
|
30902
|
-
const rows = await session.readDatabase("person_basic_info", { accountId, ...opts });
|
|
30903
|
-
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
31180
|
+
throw new Error(`${op}: no admin_user_id on record for ${station}`);
|
|
31181
|
+
return { station, accountId, route: found.t9000 ? this.rtcRouteFor(station) : void 0 };
|
|
30904
31182
|
}
|
|
30905
31183
|
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
30906
31184
|
adminUserIdOf(stationSn) {
|
|
@@ -31127,6 +31405,9 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31127
31405
|
* Returns the connected client rather than installing it. Registration can outlive a `disconnect()`,
|
|
31128
31406
|
* and {@link ensureRealtime} owns the decision of whether a finished bring-up is still the current
|
|
31129
31407
|
* one — so this never overwrites a channel a later login already brought up.
|
|
31408
|
+
*
|
|
31409
|
+
* Persisting the seen ids after each push reports a failed `save` through `error` rather than throwing:
|
|
31410
|
+
* the push handler runs inside the MCS socket's `data` callback, where a throw is an uncaught exception.
|
|
31130
31411
|
*/
|
|
31131
31412
|
async startPush(signal) {
|
|
31132
31413
|
if (!this.mega.auth)
|
|
@@ -31173,9 +31454,13 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31173
31454
|
if (dsn)
|
|
31174
31455
|
this.prewarmForEvent(out.event, dsn);
|
|
31175
31456
|
}
|
|
31176
|
-
|
|
31457
|
+
try {
|
|
31458
|
+
store.save({ creds: persistedCreds, persistentIds: client.getPersistentIds() });
|
|
31459
|
+
} catch (e) {
|
|
31460
|
+
this.reportError(e);
|
|
31461
|
+
}
|
|
31177
31462
|
});
|
|
31178
|
-
await new Promise((
|
|
31463
|
+
await new Promise((resolve2, reject) => {
|
|
31179
31464
|
const cleanup = () => {
|
|
31180
31465
|
signal?.removeEventListener("abort", onAbort);
|
|
31181
31466
|
client.off("connect", onConnect);
|
|
@@ -31183,7 +31468,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31183
31468
|
};
|
|
31184
31469
|
const onConnect = () => {
|
|
31185
31470
|
cleanup();
|
|
31186
|
-
|
|
31471
|
+
resolve2();
|
|
31187
31472
|
};
|
|
31188
31473
|
const onError = (error) => {
|
|
31189
31474
|
cleanup();
|