@mega-yfue/eufy-sdk 0.5.0-beta.44 → 0.5.0-beta.46
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 +138 -9
- 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
|
@@ -620,7 +620,8 @@ function scanEntropy(tail, subsampling, extraTables) {
|
|
|
620
620
|
// dist/transport/http/decodeImageV2.js
|
|
621
621
|
import { decode as jpegDecode } from "jpeg-js";
|
|
622
622
|
var V2_PREFIX = "v2_eufysecurity:";
|
|
623
|
-
var
|
|
623
|
+
var DRI_SEGMENT = Buffer.from([255, 221, 0, 4]);
|
|
624
|
+
var SOS_HEADER = Buffer.from([255, 218, 0, 12, 3]);
|
|
624
625
|
var ZIGZAG = [
|
|
625
626
|
0,
|
|
626
627
|
1,
|
|
@@ -821,6 +822,9 @@ var QUANT_CHROMA = [
|
|
|
821
822
|
];
|
|
822
823
|
var DHT_DC_LUMA = Buffer.from("ffc4001f0000010501010101010100000000000000000102030405060708090a0b", "hex");
|
|
823
824
|
var DHT_AC_LUMA = Buffer.from("ffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9fa", "hex");
|
|
825
|
+
var DHT_DC_CHROMA = Buffer.from("ffc4001f0100030101010101010101010000000000000102030405060708090a0b", "hex");
|
|
826
|
+
var DC_CHROMA = DHT_DC_CHROMA.subarray(0, 5);
|
|
827
|
+
var DHT_AC_CHROMA = Buffer.from("ffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9fa", "hex");
|
|
824
828
|
var SOI = Buffer.from([255, 216]);
|
|
825
829
|
var APP0 = Buffer.from("ffe000104a46494600010100000100010000", "hex");
|
|
826
830
|
var MCU = { 0: [8, 8], 1: [16, 8], 2: [16, 16] };
|
|
@@ -973,16 +977,31 @@ function findGeometry(tail) {
|
|
|
973
977
|
}
|
|
974
978
|
return best;
|
|
975
979
|
}
|
|
980
|
+
function findScanCut(data) {
|
|
981
|
+
const sos = data.lastIndexOf(SOS_HEADER);
|
|
982
|
+
if (sos < 0)
|
|
983
|
+
return -1;
|
|
984
|
+
const dri = sos - DRI_SEGMENT.length - 2;
|
|
985
|
+
return dri >= 0 && data.subarray(dri, dri + DRI_SEGMENT.length).equals(DRI_SEGMENT) ? dri : -1;
|
|
986
|
+
}
|
|
987
|
+
function plaintextTail(data) {
|
|
988
|
+
const cut = data.indexOf(DC_CHROMA);
|
|
989
|
+
if (cut >= 0)
|
|
990
|
+
return data.subarray(cut);
|
|
991
|
+
const scan = findScanCut(data);
|
|
992
|
+
if (scan < 0)
|
|
993
|
+
return null;
|
|
994
|
+
return Buffer.concat([DHT_DC_CHROMA, DHT_AC_CHROMA, data.subarray(scan)]);
|
|
995
|
+
}
|
|
976
996
|
function isV2Image(data) {
|
|
977
997
|
return data.length >= V2_PREFIX.length && data.subarray(0, V2_PREFIX.length).toString("latin1") === V2_PREFIX;
|
|
978
998
|
}
|
|
979
999
|
function decodeImageV2(data) {
|
|
980
1000
|
if (!isV2Image(data))
|
|
981
1001
|
return null;
|
|
982
|
-
const
|
|
983
|
-
if (
|
|
1002
|
+
const tail = plaintextTail(data);
|
|
1003
|
+
if (!tail)
|
|
984
1004
|
return null;
|
|
985
|
-
const tail = data.subarray(cut);
|
|
986
1005
|
const geometry = findGeometry(tail);
|
|
987
1006
|
if (!geometry)
|
|
988
1007
|
return null;
|
|
@@ -22705,6 +22724,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22705
22724
|
// dist/transport/rtc/live.js
|
|
22706
22725
|
import { EventEmitter as EventEmitter7 } from "node:events";
|
|
22707
22726
|
|
|
22727
|
+
// dist/transport/rtc/portal-keepalive.js
|
|
22728
|
+
var PORTAL_KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a5948730400000000000000000002", "hex");
|
|
22729
|
+
var PORTAL_KEEPALIVE_MS = 29e3;
|
|
22730
|
+
|
|
22708
22731
|
// dist/transport/rtc/portal-packet.js
|
|
22709
22732
|
var MAGIC2 = Buffer.from("XZYH", "ascii");
|
|
22710
22733
|
var PORTAL_HEADER_LENGTH = 16;
|
|
@@ -22814,8 +22837,6 @@ var LIVE_STOP = 1004;
|
|
|
22814
22837
|
var LIVE_MEDIA = 1300;
|
|
22815
22838
|
var LIVE_MEDIA_CHANNEL = 101;
|
|
22816
22839
|
var LIVE_STREAM_ID = 1;
|
|
22817
|
-
var KEEPALIVE_MS = 29e3;
|
|
22818
|
-
var KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a5948730400000000000000000002", "hex");
|
|
22819
22840
|
var MEDIA_HEADER_LENGTH = 22;
|
|
22820
22841
|
var STREAM_TYPE_HEVC = 1;
|
|
22821
22842
|
function parseMediaBody(body) {
|
|
@@ -22885,9 +22906,9 @@ var RtcLiveStream = class extends EventEmitter7 {
|
|
|
22885
22906
|
return this;
|
|
22886
22907
|
}
|
|
22887
22908
|
this.keepalive = setInterval(() => {
|
|
22888
|
-
if (!session.sendRaw(
|
|
22909
|
+
if (!session.sendRaw(PORTAL_KEEPALIVE))
|
|
22889
22910
|
this.opts.logger?.warn?.(`[rtc] ${this.tag} keepalive not sent`);
|
|
22890
|
-
},
|
|
22911
|
+
}, PORTAL_KEEPALIVE_MS);
|
|
22891
22912
|
this.keepalive.unref?.();
|
|
22892
22913
|
this.opts.logger?.debug?.(`[rtc] ${this.tag} live start sent`);
|
|
22893
22914
|
return this;
|
|
@@ -23421,7 +23442,8 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23421
23442
|
}
|
|
23422
23443
|
/**
|
|
23423
23444
|
* Wire one data channel. Every channel feeds the framer once the command channel has opened it; a
|
|
23424
|
-
* message before that has nothing to reassemble into and is dropped.
|
|
23445
|
+
* message before that has nothing to reassemble into and is dropped. The hub's echo of the portal
|
|
23446
|
+
* keepalive on the command channel is unframed and surfaces as `keepaliveEcho` instead.
|
|
23425
23447
|
*/
|
|
23426
23448
|
wireChannel(label, dc) {
|
|
23427
23449
|
if (this.channels.has(label))
|
|
@@ -23459,6 +23481,10 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23459
23481
|
if (n <= 3 || n % 100 === 0) {
|
|
23460
23482
|
this.logger.debug(`[rtc] wire ${label} #${n} ${buf.length}B ${buf.subarray(0, 8).toString("hex")}`);
|
|
23461
23483
|
}
|
|
23484
|
+
if (label === COMMAND_CHANNEL && buf.equals(PORTAL_KEEPALIVE)) {
|
|
23485
|
+
this.emit("keepaliveEcho");
|
|
23486
|
+
return;
|
|
23487
|
+
}
|
|
23462
23488
|
this.framer?.recvPacket(buf);
|
|
23463
23489
|
});
|
|
23464
23490
|
}
|
|
@@ -23770,6 +23796,7 @@ var RtcSession = class extends EventEmitter10 {
|
|
|
23770
23796
|
}
|
|
23771
23797
|
});
|
|
23772
23798
|
this.peer.on("error", (e) => this.emit("error", e));
|
|
23799
|
+
this.peer.on("keepaliveEcho", () => this.emit("keepaliveEcho"));
|
|
23773
23800
|
this.peer.on("data", (frame, linkType) => {
|
|
23774
23801
|
if (linkType === PortalLinkType.LIVE)
|
|
23775
23802
|
this.emit("mediaData", frame);
|
|
@@ -23965,6 +23992,7 @@ var RtcCommandRouter = class {
|
|
|
23965
23992
|
deps;
|
|
23966
23993
|
sessions = /* @__PURE__ */ new Map();
|
|
23967
23994
|
lives = /* @__PURE__ */ new Map();
|
|
23995
|
+
warm = /* @__PURE__ */ new Map();
|
|
23968
23996
|
constructor(deps) {
|
|
23969
23997
|
this.deps = deps;
|
|
23970
23998
|
}
|
|
@@ -24214,6 +24242,96 @@ var RtcCommandRouter = class {
|
|
|
24214
24242
|
Promise.resolve().then(() => session.connect()).catch((e) => finish(e instanceof Error ? e : new Error(String(e))));
|
|
24215
24243
|
});
|
|
24216
24244
|
}
|
|
24245
|
+
/**
|
|
24246
|
+
* Hold a station's command session open: one hold per station, its idle-close ({@link IDLE_CLOSE_MS})
|
|
24247
|
+
* suppressed, {@link PORTAL_KEEPALIVE} sent every {@link PORTAL_KEEPALIVE_MS}, and the session dropped
|
|
24248
|
+
* on a tick that finds the previous keepalive unechoed. A closed session ends the hold.
|
|
24249
|
+
*
|
|
24250
|
+
* Answers an idempotent disposer that releases the hold. Throws when no account is logged in.
|
|
24251
|
+
*/
|
|
24252
|
+
keepWarm(route) {
|
|
24253
|
+
const sn = route.stationSn;
|
|
24254
|
+
const existing = this.warm.get(sn);
|
|
24255
|
+
if (existing)
|
|
24256
|
+
return existing.dispose;
|
|
24257
|
+
const identity = this.deps.identity();
|
|
24258
|
+
if (!identity)
|
|
24259
|
+
throw new Error(`rtc: not logged in, cannot warm ${sn}`);
|
|
24260
|
+
const hold = {
|
|
24261
|
+
awaitingEcho: false,
|
|
24262
|
+
dispose: () => {
|
|
24263
|
+
if (this.warm.get(sn) !== hold)
|
|
24264
|
+
return;
|
|
24265
|
+
this.warm.delete(sn);
|
|
24266
|
+
const st = hold.watch?.st;
|
|
24267
|
+
this.unwatch(hold);
|
|
24268
|
+
if (st) {
|
|
24269
|
+
st.leases = Math.max(0, st.leases - 1);
|
|
24270
|
+
this.touch(sn, st);
|
|
24271
|
+
}
|
|
24272
|
+
}
|
|
24273
|
+
};
|
|
24274
|
+
this.warm.set(sn, hold);
|
|
24275
|
+
void this.warmUp(sn, route, identity, hold);
|
|
24276
|
+
return hold.dispose;
|
|
24277
|
+
}
|
|
24278
|
+
/**
|
|
24279
|
+
* Bring a station session up for a {@link keepWarm} hold and start its keepalive. A bring-up that fails,
|
|
24280
|
+
* or a hold disposed before it completes, clears the hold and arms no timer. Each tick first checks
|
|
24281
|
+
* that the hub echoed the previous keepalive, and drops the session when it did not. The keepalive
|
|
24282
|
+
* stops when the session closes, which also clears the hold so the next command or `keepWarm`
|
|
24283
|
+
* rebuilds cleanly.
|
|
24284
|
+
*/
|
|
24285
|
+
async warmUp(sn, route, identity, hold) {
|
|
24286
|
+
const adminUserId = route.adminUserId || identity.userId;
|
|
24287
|
+
let st;
|
|
24288
|
+
try {
|
|
24289
|
+
st = await this.stationSession(route, adminUserId, identity);
|
|
24290
|
+
} catch (e) {
|
|
24291
|
+
if (this.warm.get(sn) === hold)
|
|
24292
|
+
this.warm.delete(sn);
|
|
24293
|
+
this.deps.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
24294
|
+
return;
|
|
24295
|
+
}
|
|
24296
|
+
if (this.warm.get(sn) !== hold) {
|
|
24297
|
+
this.touch(sn, st);
|
|
24298
|
+
return;
|
|
24299
|
+
}
|
|
24300
|
+
st.leases++;
|
|
24301
|
+
this.touch(sn, st);
|
|
24302
|
+
const onClose = () => {
|
|
24303
|
+
this.unwatch(hold);
|
|
24304
|
+
if (this.warm.get(sn) === hold)
|
|
24305
|
+
this.warm.delete(sn);
|
|
24306
|
+
};
|
|
24307
|
+
const onEcho = () => {
|
|
24308
|
+
hold.awaitingEcho = false;
|
|
24309
|
+
};
|
|
24310
|
+
hold.watch = { st, onClose, onEcho };
|
|
24311
|
+
st.session.once("close", onClose);
|
|
24312
|
+
st.session.on("keepaliveEcho", onEcho);
|
|
24313
|
+
hold.keepalive = setInterval(() => {
|
|
24314
|
+
if (hold.awaitingEcho) {
|
|
24315
|
+
this.deps.logger?.warn?.(`[rtc] ${sn} warm keepalive went unechoed, dropping the session`);
|
|
24316
|
+
this.drop(sn, st);
|
|
24317
|
+
return;
|
|
24318
|
+
}
|
|
24319
|
+
hold.awaitingEcho = true;
|
|
24320
|
+
st.session.sendRaw(PORTAL_KEEPALIVE);
|
|
24321
|
+
}, PORTAL_KEEPALIVE_MS);
|
|
24322
|
+
hold.keepalive.unref?.();
|
|
24323
|
+
}
|
|
24324
|
+
/** Stop a hold's keepalive and remove its listeners from the session it watches. */
|
|
24325
|
+
unwatch(hold) {
|
|
24326
|
+
if (hold.keepalive)
|
|
24327
|
+
clearInterval(hold.keepalive);
|
|
24328
|
+
hold.keepalive = void 0;
|
|
24329
|
+
if (hold.watch) {
|
|
24330
|
+
hold.watch.st.session.off("close", hold.watch.onClose);
|
|
24331
|
+
hold.watch.st.session.off("keepaliveEcho", hold.watch.onEcho);
|
|
24332
|
+
}
|
|
24333
|
+
hold.watch = void 0;
|
|
24334
|
+
}
|
|
24217
24335
|
touch(sn, st) {
|
|
24218
24336
|
if (st.idle)
|
|
24219
24337
|
clearTimeout(st.idle);
|
|
@@ -28974,6 +29092,17 @@ var EufyMega = class extends EventEmitter13 {
|
|
|
28974
29092
|
await this.getDevices();
|
|
28975
29093
|
await this.p2p.ensureStation(this.p2p.stationKeyOf(deviceSn), signal);
|
|
28976
29094
|
}
|
|
29095
|
+
/**
|
|
29096
|
+
* Keep a T9000 station's RTC command session open and warm, so a command fires without a cold reconnect.
|
|
29097
|
+
* Answers the hold's disposer; throws for any other station, and while logged out.
|
|
29098
|
+
*/
|
|
29099
|
+
keepStationWarm(stationSn) {
|
|
29100
|
+
const { t9000 } = this.stationOf(stationSn);
|
|
29101
|
+
const route = this.rtcRouteFor(stationSn);
|
|
29102
|
+
if (!t9000 || !route)
|
|
29103
|
+
throw new Error(`rtc: ${stationSn} has no warm command session`);
|
|
29104
|
+
return this.rtc.keepWarm(route);
|
|
29105
|
+
}
|
|
28977
29106
|
/** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
|
|
28978
29107
|
* stations stay closed until an on-demand open. Best-effort per station. */
|
|
28979
29108
|
async warmWiredP2P(readiness = createPlaneReadiness(), signal) {
|