@mega-yfue/eufy-sdk 0.5.0-beta.45 → 0.5.0-beta.47
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 +5 -0
- package/dist/index.js +118 -6
- package/dist/index.js.map +3 -3
- package/dist/transport/rtc/command-router.d.ts +19 -0
- package/dist/transport/rtc/peer.d.ts +4 -1
- package/dist/transport/rtc/portal-keepalive.d.ts +11 -0
- package/dist/transport/rtc/session.d.ts +2 -0
- package/package.json +1 -1
|
@@ -711,6 +711,11 @@ export declare class EufyMega extends EventEmitter {
|
|
|
711
711
|
* — an already-open session resolves immediately.
|
|
712
712
|
*/
|
|
713
713
|
connectStation(deviceSn: string, signal?: AbortSignal): Promise<void>;
|
|
714
|
+
/**
|
|
715
|
+
* Keep a T9000 station's RTC command session open and warm, so a command fires without a cold reconnect.
|
|
716
|
+
* Answers the hold's disposer; throws for any other station, and while logged out.
|
|
717
|
+
*/
|
|
718
|
+
keepStationWarm(stationSn: string): () => void;
|
|
714
719
|
/** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
|
|
715
720
|
* stations stay closed until an on-demand open. Best-effort per station. */
|
|
716
721
|
private warmWiredP2P;
|
package/dist/index.js
CHANGED
|
@@ -22012,8 +22012,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22012
22012
|
tearDownStation(stationSn) {
|
|
22013
22013
|
this.manager.remove(stationSn);
|
|
22014
22014
|
for (const key of [...this.liveSources.keys()]) {
|
|
22015
|
-
|
|
22015
|
+
const owner = this.liveSessionKeys.get(key);
|
|
22016
|
+
if (key.startsWith(`${stationSn}:`) && !(owner && _P2PCommandRouter.isMediaSessionKey(owner))) {
|
|
22016
22017
|
this.dropLiveSource(key);
|
|
22018
|
+
}
|
|
22017
22019
|
}
|
|
22018
22020
|
for (const [key, talk] of this.talkbacks) {
|
|
22019
22021
|
if (key.startsWith(`${stationSn}:`)) {
|
|
@@ -22724,6 +22726,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22724
22726
|
// dist/transport/rtc/live.js
|
|
22725
22727
|
import { EventEmitter as EventEmitter7 } from "node:events";
|
|
22726
22728
|
|
|
22729
|
+
// dist/transport/rtc/portal-keepalive.js
|
|
22730
|
+
var PORTAL_KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a5948730400000000000000000002", "hex");
|
|
22731
|
+
var PORTAL_KEEPALIVE_MS = 29e3;
|
|
22732
|
+
|
|
22727
22733
|
// dist/transport/rtc/portal-packet.js
|
|
22728
22734
|
var MAGIC2 = Buffer.from("XZYH", "ascii");
|
|
22729
22735
|
var PORTAL_HEADER_LENGTH = 16;
|
|
@@ -22833,8 +22839,6 @@ var LIVE_STOP = 1004;
|
|
|
22833
22839
|
var LIVE_MEDIA = 1300;
|
|
22834
22840
|
var LIVE_MEDIA_CHANNEL = 101;
|
|
22835
22841
|
var LIVE_STREAM_ID = 1;
|
|
22836
|
-
var KEEPALIVE_MS = 29e3;
|
|
22837
|
-
var KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a5948730400000000000000000002", "hex");
|
|
22838
22842
|
var MEDIA_HEADER_LENGTH = 22;
|
|
22839
22843
|
var STREAM_TYPE_HEVC = 1;
|
|
22840
22844
|
function parseMediaBody(body) {
|
|
@@ -22904,9 +22908,9 @@ var RtcLiveStream = class extends EventEmitter7 {
|
|
|
22904
22908
|
return this;
|
|
22905
22909
|
}
|
|
22906
22910
|
this.keepalive = setInterval(() => {
|
|
22907
|
-
if (!session.sendRaw(
|
|
22911
|
+
if (!session.sendRaw(PORTAL_KEEPALIVE))
|
|
22908
22912
|
this.opts.logger?.warn?.(`[rtc] ${this.tag} keepalive not sent`);
|
|
22909
|
-
},
|
|
22913
|
+
}, PORTAL_KEEPALIVE_MS);
|
|
22910
22914
|
this.keepalive.unref?.();
|
|
22911
22915
|
this.opts.logger?.debug?.(`[rtc] ${this.tag} live start sent`);
|
|
22912
22916
|
return this;
|
|
@@ -23440,7 +23444,8 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23440
23444
|
}
|
|
23441
23445
|
/**
|
|
23442
23446
|
* Wire one data channel. Every channel feeds the framer once the command channel has opened it; a
|
|
23443
|
-
* message before that has nothing to reassemble into and is dropped.
|
|
23447
|
+
* message before that has nothing to reassemble into and is dropped. The hub's echo of the portal
|
|
23448
|
+
* keepalive on the command channel is unframed and surfaces as `keepaliveEcho` instead.
|
|
23444
23449
|
*/
|
|
23445
23450
|
wireChannel(label, dc) {
|
|
23446
23451
|
if (this.channels.has(label))
|
|
@@ -23478,6 +23483,10 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23478
23483
|
if (n <= 3 || n % 100 === 0) {
|
|
23479
23484
|
this.logger.debug(`[rtc] wire ${label} #${n} ${buf.length}B ${buf.subarray(0, 8).toString("hex")}`);
|
|
23480
23485
|
}
|
|
23486
|
+
if (label === COMMAND_CHANNEL && buf.equals(PORTAL_KEEPALIVE)) {
|
|
23487
|
+
this.emit("keepaliveEcho");
|
|
23488
|
+
return;
|
|
23489
|
+
}
|
|
23481
23490
|
this.framer?.recvPacket(buf);
|
|
23482
23491
|
});
|
|
23483
23492
|
}
|
|
@@ -23789,6 +23798,7 @@ var RtcSession = class extends EventEmitter10 {
|
|
|
23789
23798
|
}
|
|
23790
23799
|
});
|
|
23791
23800
|
this.peer.on("error", (e) => this.emit("error", e));
|
|
23801
|
+
this.peer.on("keepaliveEcho", () => this.emit("keepaliveEcho"));
|
|
23792
23802
|
this.peer.on("data", (frame, linkType) => {
|
|
23793
23803
|
if (linkType === PortalLinkType.LIVE)
|
|
23794
23804
|
this.emit("mediaData", frame);
|
|
@@ -23984,6 +23994,7 @@ var RtcCommandRouter = class {
|
|
|
23984
23994
|
deps;
|
|
23985
23995
|
sessions = /* @__PURE__ */ new Map();
|
|
23986
23996
|
lives = /* @__PURE__ */ new Map();
|
|
23997
|
+
warm = /* @__PURE__ */ new Map();
|
|
23987
23998
|
constructor(deps) {
|
|
23988
23999
|
this.deps = deps;
|
|
23989
24000
|
}
|
|
@@ -24233,6 +24244,96 @@ var RtcCommandRouter = class {
|
|
|
24233
24244
|
Promise.resolve().then(() => session.connect()).catch((e) => finish(e instanceof Error ? e : new Error(String(e))));
|
|
24234
24245
|
});
|
|
24235
24246
|
}
|
|
24247
|
+
/**
|
|
24248
|
+
* Hold a station's command session open: one hold per station, its idle-close ({@link IDLE_CLOSE_MS})
|
|
24249
|
+
* suppressed, {@link PORTAL_KEEPALIVE} sent every {@link PORTAL_KEEPALIVE_MS}, and the session dropped
|
|
24250
|
+
* on a tick that finds the previous keepalive unechoed. A closed session ends the hold.
|
|
24251
|
+
*
|
|
24252
|
+
* Answers an idempotent disposer that releases the hold. Throws when no account is logged in.
|
|
24253
|
+
*/
|
|
24254
|
+
keepWarm(route) {
|
|
24255
|
+
const sn = route.stationSn;
|
|
24256
|
+
const existing = this.warm.get(sn);
|
|
24257
|
+
if (existing)
|
|
24258
|
+
return existing.dispose;
|
|
24259
|
+
const identity = this.deps.identity();
|
|
24260
|
+
if (!identity)
|
|
24261
|
+
throw new Error(`rtc: not logged in, cannot warm ${sn}`);
|
|
24262
|
+
const hold = {
|
|
24263
|
+
awaitingEcho: false,
|
|
24264
|
+
dispose: () => {
|
|
24265
|
+
if (this.warm.get(sn) !== hold)
|
|
24266
|
+
return;
|
|
24267
|
+
this.warm.delete(sn);
|
|
24268
|
+
const st = hold.watch?.st;
|
|
24269
|
+
this.unwatch(hold);
|
|
24270
|
+
if (st) {
|
|
24271
|
+
st.leases = Math.max(0, st.leases - 1);
|
|
24272
|
+
this.touch(sn, st);
|
|
24273
|
+
}
|
|
24274
|
+
}
|
|
24275
|
+
};
|
|
24276
|
+
this.warm.set(sn, hold);
|
|
24277
|
+
void this.warmUp(sn, route, identity, hold);
|
|
24278
|
+
return hold.dispose;
|
|
24279
|
+
}
|
|
24280
|
+
/**
|
|
24281
|
+
* Bring a station session up for a {@link keepWarm} hold and start its keepalive. A bring-up that fails,
|
|
24282
|
+
* or a hold disposed before it completes, clears the hold and arms no timer. Each tick first checks
|
|
24283
|
+
* that the hub echoed the previous keepalive, and drops the session when it did not. The keepalive
|
|
24284
|
+
* stops when the session closes, which also clears the hold so the next command or `keepWarm`
|
|
24285
|
+
* rebuilds cleanly.
|
|
24286
|
+
*/
|
|
24287
|
+
async warmUp(sn, route, identity, hold) {
|
|
24288
|
+
const adminUserId = route.adminUserId || identity.userId;
|
|
24289
|
+
let st;
|
|
24290
|
+
try {
|
|
24291
|
+
st = await this.stationSession(route, adminUserId, identity);
|
|
24292
|
+
} catch (e) {
|
|
24293
|
+
if (this.warm.get(sn) === hold)
|
|
24294
|
+
this.warm.delete(sn);
|
|
24295
|
+
this.deps.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
24296
|
+
return;
|
|
24297
|
+
}
|
|
24298
|
+
if (this.warm.get(sn) !== hold) {
|
|
24299
|
+
this.touch(sn, st);
|
|
24300
|
+
return;
|
|
24301
|
+
}
|
|
24302
|
+
st.leases++;
|
|
24303
|
+
this.touch(sn, st);
|
|
24304
|
+
const onClose = () => {
|
|
24305
|
+
this.unwatch(hold);
|
|
24306
|
+
if (this.warm.get(sn) === hold)
|
|
24307
|
+
this.warm.delete(sn);
|
|
24308
|
+
};
|
|
24309
|
+
const onEcho = () => {
|
|
24310
|
+
hold.awaitingEcho = false;
|
|
24311
|
+
};
|
|
24312
|
+
hold.watch = { st, onClose, onEcho };
|
|
24313
|
+
st.session.once("close", onClose);
|
|
24314
|
+
st.session.on("keepaliveEcho", onEcho);
|
|
24315
|
+
hold.keepalive = setInterval(() => {
|
|
24316
|
+
if (hold.awaitingEcho) {
|
|
24317
|
+
this.deps.logger?.warn?.(`[rtc] ${sn} warm keepalive went unechoed, dropping the session`);
|
|
24318
|
+
this.drop(sn, st);
|
|
24319
|
+
return;
|
|
24320
|
+
}
|
|
24321
|
+
hold.awaitingEcho = true;
|
|
24322
|
+
st.session.sendRaw(PORTAL_KEEPALIVE);
|
|
24323
|
+
}, PORTAL_KEEPALIVE_MS);
|
|
24324
|
+
hold.keepalive.unref?.();
|
|
24325
|
+
}
|
|
24326
|
+
/** Stop a hold's keepalive and remove its listeners from the session it watches. */
|
|
24327
|
+
unwatch(hold) {
|
|
24328
|
+
if (hold.keepalive)
|
|
24329
|
+
clearInterval(hold.keepalive);
|
|
24330
|
+
hold.keepalive = void 0;
|
|
24331
|
+
if (hold.watch) {
|
|
24332
|
+
hold.watch.st.session.off("close", hold.watch.onClose);
|
|
24333
|
+
hold.watch.st.session.off("keepaliveEcho", hold.watch.onEcho);
|
|
24334
|
+
}
|
|
24335
|
+
hold.watch = void 0;
|
|
24336
|
+
}
|
|
24236
24337
|
touch(sn, st) {
|
|
24237
24338
|
if (st.idle)
|
|
24238
24339
|
clearTimeout(st.idle);
|
|
@@ -28993,6 +29094,17 @@ var EufyMega = class extends EventEmitter13 {
|
|
|
28993
29094
|
await this.getDevices();
|
|
28994
29095
|
await this.p2p.ensureStation(this.p2p.stationKeyOf(deviceSn), signal);
|
|
28995
29096
|
}
|
|
29097
|
+
/**
|
|
29098
|
+
* Keep a T9000 station's RTC command session open and warm, so a command fires without a cold reconnect.
|
|
29099
|
+
* Answers the hold's disposer; throws for any other station, and while logged out.
|
|
29100
|
+
*/
|
|
29101
|
+
keepStationWarm(stationSn) {
|
|
29102
|
+
const { t9000 } = this.stationOf(stationSn);
|
|
29103
|
+
const route = this.rtcRouteFor(stationSn);
|
|
29104
|
+
if (!t9000 || !route)
|
|
29105
|
+
throw new Error(`rtc: ${stationSn} has no warm command session`);
|
|
29106
|
+
return this.rtc.keepWarm(route);
|
|
29107
|
+
}
|
|
28996
29108
|
/** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
|
|
28997
29109
|
* stations stay closed until an on-demand open. Best-effort per station. */
|
|
28998
29110
|
async warmWiredP2P(readiness = createPlaneReadiness(), signal) {
|