@mega-yfue/eufy-sdk 0.5.0-beta.65 → 0.5.0-beta.67
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 +10 -7
- package/dist/index.js +281 -42
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +2 -0
- package/dist/model/device-family.d.ts +2 -0
- package/dist/transport/rtc/call-live.d.ts +41 -0
- package/dist/transport/rtc/command-router.d.ts +13 -0
- package/dist/transport/rtc/peer.d.ts +19 -1
- package/dist/transport/rtc/scall-sdp.d.ts +8 -0
- package/dist/transport/rtc/session.d.ts +5 -0
- package/package.json +1 -1
|
@@ -416,7 +416,7 @@ export declare class EufyMega extends EventEmitter {
|
|
|
416
416
|
/**
|
|
417
417
|
* The station a device's commands go through: the device itself, or the HomeBase the registry links it
|
|
418
418
|
* to. Answers the target record, the station record and serial, the station's `device_type`, and the
|
|
419
|
-
* `t9000` / `t8n00` family flags.
|
|
419
|
+
* `t9000` / `t8n00` / `t8223` family flags.
|
|
420
420
|
*/
|
|
421
421
|
private stationOf;
|
|
422
422
|
/** The vendor DeviceType of the station a device hangs off; `undefined` for a station itself. */
|
|
@@ -443,18 +443,21 @@ export declare class EufyMega extends EventEmitter {
|
|
|
443
443
|
* kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
|
|
444
444
|
* station and the devices attached to it go over RTC, with an attached device's command refused
|
|
445
445
|
* unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
|
|
446
|
-
* relay candidates; T9000 retains compact SDP and relay-only candidates.
|
|
446
|
+
* relay candidates; T9000 retains compact SDP and relay-only candidates. A T8223 doorbell's route serves
|
|
447
|
+
* media only, and the RTC router refuses a command on it. Everything else is P2P.
|
|
447
448
|
*/
|
|
448
449
|
private routeCommand;
|
|
449
450
|
/**
|
|
450
|
-
* The RTC route for a T9000 or T8N00 station or a device attached to one,
|
|
451
|
-
* classification; `undefined` for any other device. A T8N00
|
|
452
|
-
* `
|
|
451
|
+
* The RTC route for a T9000 or T8N00 station or a device attached to one, or a T8223 doorbell that is its
|
|
452
|
+
* own station, decided by the station's model classification; `undefined` for any other device. A T8N00
|
|
453
|
+
* route carries its native `call` signalling; a T8223 route does too, and streams its video on a media
|
|
454
|
+
* track. The admin id is the station's `member.admin_user_id`, else the logged-in user.
|
|
453
455
|
*/
|
|
454
456
|
private rtcRouteFor;
|
|
455
457
|
/**
|
|
456
|
-
* The RTC media provider for a
|
|
457
|
-
* the station; `undefined` otherwise, including a camera behind
|
|
458
|
+
* The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
|
|
459
|
+
* `device_channel` resolves back to it on the station; `undefined` otherwise, including a camera behind
|
|
460
|
+
* a T8N00, whose route carries commands only.
|
|
458
461
|
*/
|
|
459
462
|
private rtcMediaFor;
|
|
460
463
|
/**
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// dist/client/eufy-mega.js
|
|
8
|
-
import { EventEmitter as
|
|
8
|
+
import { EventEmitter as EventEmitter14 } from "node:events";
|
|
9
9
|
|
|
10
10
|
// dist/transport/http/mega-client.js
|
|
11
11
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -3655,6 +3655,7 @@ var isIndoorPanTiltS350 = (ctx) => has(INDOOR_PT_S350_TYPES, ctx.deviceType);
|
|
|
3655
3655
|
var isHomeBase = (ctx) => has(HOMEBASE_TYPES, ctx.deviceType);
|
|
3656
3656
|
var isStation9000 = (ctx) => ctx.deviceType === DeviceType.STATION_9000;
|
|
3657
3657
|
var isNvrT8N00 = (ctx) => ctx.model === "T8N00";
|
|
3658
|
+
var isDoorbellT8223 = (ctx) => ctx.model === "T8223";
|
|
3658
3659
|
var isAiotVacuum = (ctx) => ctx.category !== "eufy_home_tuya";
|
|
3659
3660
|
var isTuyaVacuum = (ctx) => ctx.category === "eufy_home_tuya";
|
|
3660
3661
|
|
|
@@ -7756,6 +7757,8 @@ var ARMING_CMD = {
|
|
|
7756
7757
|
* The ENCRYPTION LEVEL is the session's to pick (`"auto"`), not this command's: the T8030 the envelope
|
|
7757
7758
|
* was captured on holds a level-2 key and seals it level-2, while an own-session camera that never
|
|
7758
7759
|
* negotiates one carries the same envelope level-1. See `armingCommand`.
|
|
7760
|
+
*
|
|
7761
|
+
* A T8410C takes the mode as a direct frame instead of this envelope; see {@link DIRECT_ARMING_MODELS}.
|
|
7759
7762
|
*/
|
|
7760
7763
|
SET_ARMING: 1224,
|
|
7761
7764
|
/**
|
|
@@ -7794,6 +7797,8 @@ var ARMING_CMD = {
|
|
|
7794
7797
|
};
|
|
7795
7798
|
var DEVICE_NOTIFY_FLAG = 8;
|
|
7796
7799
|
var STANDALONE_ACTION_MODELS = /* @__PURE__ */ new Set(["T8170", "T8171"]);
|
|
7800
|
+
var DIRECT_ARMING_MODELS = /* @__PURE__ */ new Set(["T8410C"]);
|
|
7801
|
+
var DIRECT_ARMING_MODES = /* @__PURE__ */ new Set(["away", "home", "disarmed"]);
|
|
7797
7802
|
var STANDALONE_READ_MODELS = /* @__PURE__ */ new Set([...STANDALONE_ACTION_MODELS, "T8400", "T8442", "T8410"]);
|
|
7798
7803
|
function ownAction(read, mode) {
|
|
7799
7804
|
const value = Number(read(`${mode}Action`)?.value);
|
|
@@ -7826,6 +7831,12 @@ function armingCommand(mode, ctx) {
|
|
|
7826
7831
|
if (!ctx.accountName) {
|
|
7827
7832
|
throw new Error(`arming: missing account identity (user_name) [${describeDevice(ctx)}]`);
|
|
7828
7833
|
}
|
|
7834
|
+
if (DIRECT_ARMING_MODELS.has(ctx.model ?? "")) {
|
|
7835
|
+
if (!DIRECT_ARMING_MODES.has(mode)) {
|
|
7836
|
+
throw new Error(`arming: mode ${mode} is unconfirmed on the direct 1224 frame [${describeDevice(ctx)}]`);
|
|
7837
|
+
}
|
|
7838
|
+
return setStationScalar(ARMING_CMD.SET_ARMING, ARMING_MODE_WIRE[mode], ctx.channel);
|
|
7839
|
+
}
|
|
7829
7840
|
return setPayload(ARMING_CMD.SET_ARMING, { mode_type: ARMING_MODE_WIRE[mode], user_name: ctx.accountName }, ctx, 0, void 0, "auto");
|
|
7830
7841
|
}
|
|
7831
7842
|
function alarmDelayCommand(mode, config, ctx) {
|
|
@@ -7880,7 +7891,7 @@ var ARMING_MEMBERS = {
|
|
|
7880
7891
|
kind: "enum",
|
|
7881
7892
|
enumValues: ARMING_MODE_LABELS,
|
|
7882
7893
|
provenance: "verified",
|
|
7883
|
-
description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads and SETS all 9 modes the app defines \u2014 away/home/schedule/custom1/custom2/custom3/off/geo/disarmed. Three are byte-captured writes and six are live-confirmed (each sent and observed to report its own MODE_SWITCH); see ARMING_MODE_WIRE in arming.ts for the per-value evidence.",
|
|
7894
|
+
description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads and SETS all 9 modes the app defines \u2014 away/home/schedule/custom1/custom2/custom3/off/geo/disarmed. Three are byte-captured writes and six are live-confirmed (each sent and observed to report its own MODE_SWITCH); see ARMING_MODE_WIRE in arming.ts for the per-value evidence. On a T8410C only away, home and disarmed are writable.",
|
|
7884
7895
|
observation: {
|
|
7885
7896
|
event: "armingModeChanged",
|
|
7886
7897
|
reflects: (value) => ({ param: ARMING_CMD.SET_ARMING, expected: ARMING_MODE_WIRE[armingModeOf(value)] }),
|
|
@@ -23356,11 +23367,139 @@ var RtcLiveStream = class extends EventEmitter7 {
|
|
|
23356
23367
|
}
|
|
23357
23368
|
};
|
|
23358
23369
|
|
|
23370
|
+
// dist/transport/rtc/call-live.js
|
|
23371
|
+
import { EventEmitter as EventEmitter8 } from "node:events";
|
|
23372
|
+
var START_CODE = Buffer.from([0, 0, 0, 1]);
|
|
23373
|
+
var H264Depacketizer = class {
|
|
23374
|
+
onUnit;
|
|
23375
|
+
nals = [];
|
|
23376
|
+
fu;
|
|
23377
|
+
lastSeq = -1;
|
|
23378
|
+
broken = false;
|
|
23379
|
+
constructor(onUnit) {
|
|
23380
|
+
this.onUnit = onUnit;
|
|
23381
|
+
}
|
|
23382
|
+
push(packet) {
|
|
23383
|
+
if (packet.length < 12 || packet[0] >> 6 !== 2)
|
|
23384
|
+
return;
|
|
23385
|
+
const pt = packet[1] & 127;
|
|
23386
|
+
if (pt >= 72 && pt <= 79)
|
|
23387
|
+
return;
|
|
23388
|
+
const marker = (packet[1] & 128) !== 0;
|
|
23389
|
+
const seq = packet.readUInt16BE(2);
|
|
23390
|
+
const timestamp = packet.readUInt32BE(4);
|
|
23391
|
+
if (this.lastSeq >= 0 && seq !== (this.lastSeq + 1 & 65535))
|
|
23392
|
+
this.broken = true;
|
|
23393
|
+
this.lastSeq = seq;
|
|
23394
|
+
const cc = packet[0] & 15;
|
|
23395
|
+
let off = 12 + cc * 4;
|
|
23396
|
+
if (packet[0] & 16) {
|
|
23397
|
+
if (packet.length < off + 4)
|
|
23398
|
+
return;
|
|
23399
|
+
off += 4 + packet.readUInt16BE(off + 2) * 4;
|
|
23400
|
+
}
|
|
23401
|
+
let end = packet.length;
|
|
23402
|
+
if (packet[0] & 32)
|
|
23403
|
+
end -= packet[packet.length - 1];
|
|
23404
|
+
if (end <= off)
|
|
23405
|
+
return;
|
|
23406
|
+
const p = packet.subarray(off, end);
|
|
23407
|
+
const type = p[0] & 31;
|
|
23408
|
+
if (type >= 1 && type <= 23) {
|
|
23409
|
+
this.nals.push(Buffer.from(p));
|
|
23410
|
+
} else if (type === 24) {
|
|
23411
|
+
let o = 1;
|
|
23412
|
+
while (o + 2 <= p.length) {
|
|
23413
|
+
const len = p.readUInt16BE(o);
|
|
23414
|
+
if (o + 2 + len > p.length)
|
|
23415
|
+
break;
|
|
23416
|
+
this.nals.push(Buffer.from(p.subarray(o + 2, o + 2 + len)));
|
|
23417
|
+
o += 2 + len;
|
|
23418
|
+
}
|
|
23419
|
+
} else if (type === 28 && p.length > 2) {
|
|
23420
|
+
const start = (p[1] & 128) !== 0;
|
|
23421
|
+
const stop = (p[1] & 64) !== 0;
|
|
23422
|
+
if (start)
|
|
23423
|
+
this.fu = [Buffer.from([p[0] & 224 | p[1] & 31])];
|
|
23424
|
+
if (this.fu)
|
|
23425
|
+
this.fu.push(Buffer.from(p.subarray(2)));
|
|
23426
|
+
else
|
|
23427
|
+
this.broken = true;
|
|
23428
|
+
if (stop && this.fu) {
|
|
23429
|
+
this.nals.push(Buffer.concat(this.fu));
|
|
23430
|
+
this.fu = void 0;
|
|
23431
|
+
}
|
|
23432
|
+
}
|
|
23433
|
+
if (marker) {
|
|
23434
|
+
if (!this.broken && !this.fu && this.nals.length) {
|
|
23435
|
+
const parts = [];
|
|
23436
|
+
for (const n of this.nals)
|
|
23437
|
+
parts.push(START_CODE, n);
|
|
23438
|
+
this.onUnit(Buffer.concat(parts), timestamp);
|
|
23439
|
+
}
|
|
23440
|
+
this.nals = [];
|
|
23441
|
+
this.fu = void 0;
|
|
23442
|
+
this.broken = false;
|
|
23443
|
+
}
|
|
23444
|
+
}
|
|
23445
|
+
};
|
|
23446
|
+
var RtcCallLiveStream = class extends EventEmitter8 {
|
|
23447
|
+
session;
|
|
23448
|
+
logger;
|
|
23449
|
+
running = false;
|
|
23450
|
+
depacketizer = new H264Depacketizer((unit, ts) => this.onUnit(unit, ts));
|
|
23451
|
+
/** `session` is a session on a {@link RtcPeerOptions.mediaTrack} peer, not yet connected. */
|
|
23452
|
+
constructor(session, logger) {
|
|
23453
|
+
super();
|
|
23454
|
+
this.session = session;
|
|
23455
|
+
this.logger = logger ?? noopLogger;
|
|
23456
|
+
}
|
|
23457
|
+
start() {
|
|
23458
|
+
if (this.running)
|
|
23459
|
+
return this;
|
|
23460
|
+
this.running = true;
|
|
23461
|
+
const { session } = this;
|
|
23462
|
+
session.on("rtp", (packet) => this.depacketizer.push(packet));
|
|
23463
|
+
session.on("close", () => this.fail(new Error("rtc call: session closed")));
|
|
23464
|
+
session.on("error", (e) => this.fail(e));
|
|
23465
|
+
session.connect().catch((e) => this.fail(e instanceof Error ? e : new Error(String(e))));
|
|
23466
|
+
return this;
|
|
23467
|
+
}
|
|
23468
|
+
stop() {
|
|
23469
|
+
if (!this.running)
|
|
23470
|
+
return;
|
|
23471
|
+
this.teardown();
|
|
23472
|
+
this.emit("stop");
|
|
23473
|
+
}
|
|
23474
|
+
onUnit(unit, timestamp) {
|
|
23475
|
+
const frame = {
|
|
23476
|
+
codec: "h264",
|
|
23477
|
+
data: unit,
|
|
23478
|
+
keyframe: hasIdr(unit, "h264"),
|
|
23479
|
+
width: 0,
|
|
23480
|
+
height: 0,
|
|
23481
|
+
sourceTimestamp: timestamp
|
|
23482
|
+
};
|
|
23483
|
+
this.emit("video", frame);
|
|
23484
|
+
}
|
|
23485
|
+
fail(err) {
|
|
23486
|
+
if (!this.running)
|
|
23487
|
+
return;
|
|
23488
|
+
this.logger.warn(`[rtc-call] ${err.message}`);
|
|
23489
|
+
this.teardown();
|
|
23490
|
+
this.emit("error", err);
|
|
23491
|
+
}
|
|
23492
|
+
teardown() {
|
|
23493
|
+
this.running = false;
|
|
23494
|
+
this.session.close();
|
|
23495
|
+
}
|
|
23496
|
+
};
|
|
23497
|
+
|
|
23359
23498
|
// dist/transport/rtc/session.js
|
|
23360
|
-
import { EventEmitter as
|
|
23499
|
+
import { EventEmitter as EventEmitter11 } from "node:events";
|
|
23361
23500
|
|
|
23362
23501
|
// dist/transport/rtc/peer.js
|
|
23363
|
-
import { EventEmitter as
|
|
23502
|
+
import { EventEmitter as EventEmitter9 } from "node:events";
|
|
23364
23503
|
|
|
23365
23504
|
// dist/transport/rtc/ptcs-framer.js
|
|
23366
23505
|
var MAGIC3 = Buffer.from("PTCS", "ascii");
|
|
@@ -23611,6 +23750,17 @@ function forceDtlsRole(sdp, role) {
|
|
|
23611
23750
|
function toWireCandidate(candidate) {
|
|
23612
23751
|
return candidate.trim().replace(/^a=/, "");
|
|
23613
23752
|
}
|
|
23753
|
+
function midOf(sdp, kind) {
|
|
23754
|
+
const section = sdp.split(/(?=^m=)/m).find((s) => s.startsWith(`m=${kind} `));
|
|
23755
|
+
return section?.match(/^a=mid:([^\r\n]+)/m)?.[1]?.trim();
|
|
23756
|
+
}
|
|
23757
|
+
function rejectDataSection(sdp) {
|
|
23758
|
+
const appMid = midOf(sdp, "application");
|
|
23759
|
+
const out = sdp.replace(/^m=application \d+/m, "m=application 0");
|
|
23760
|
+
if (appMid === void 0)
|
|
23761
|
+
return out;
|
|
23762
|
+
return out.replace(/^(a=group:BUNDLE)([^\r\n]*)/m, (_, head, mids) => [head, ...mids.split(" ").filter((t) => t && t !== appMid)].join(" "));
|
|
23763
|
+
}
|
|
23614
23764
|
|
|
23615
23765
|
// dist/transport/rtc/peer.js
|
|
23616
23766
|
var DATA_CHANNEL_LABELS = ["WebrtcDataChannel", "audio", "idr", "video", "notify", "download"];
|
|
@@ -23645,7 +23795,7 @@ async function loadNativePeerFactory() {
|
|
|
23645
23795
|
return (name, config) => new ndc.PeerConnection(name, config);
|
|
23646
23796
|
}
|
|
23647
23797
|
var ANSWER_TIMEOUT_MS = 15e3;
|
|
23648
|
-
var RtcPeer = class extends
|
|
23798
|
+
var RtcPeer = class extends EventEmitter9 {
|
|
23649
23799
|
opts;
|
|
23650
23800
|
pc;
|
|
23651
23801
|
channels = /* @__PURE__ */ new Map();
|
|
@@ -23656,6 +23806,8 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23656
23806
|
remoteMid = HUB_SDP_MID;
|
|
23657
23807
|
handlingOffer = false;
|
|
23658
23808
|
channelsCreated = false;
|
|
23809
|
+
videoMid;
|
|
23810
|
+
videoTrack;
|
|
23659
23811
|
gatheringDone = false;
|
|
23660
23812
|
/** Set by the framer's wire callback when the native channel refused a packet mid-send. */
|
|
23661
23813
|
wireSendFailed = false;
|
|
@@ -23705,6 +23857,8 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23705
23857
|
this.emit("connectionState", state);
|
|
23706
23858
|
});
|
|
23707
23859
|
pc.onDataChannel((dc) => this.wireChannel(dc.getLabel(), dc));
|
|
23860
|
+
if (this.opts.mediaTrack)
|
|
23861
|
+
pc.onTrack((track) => this.wireVideoTrack(track));
|
|
23708
23862
|
}
|
|
23709
23863
|
/**
|
|
23710
23864
|
* The hub offered: apply the offer, declare the channels, return the answer SDP to signal back.
|
|
@@ -23714,7 +23868,9 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23714
23868
|
* only what is announced. The offer goes in before the channels are declared: `createDataChannel` on
|
|
23715
23869
|
* a peer without a remote description starts its own negotiation and leaves it in `have-local-offer`.
|
|
23716
23870
|
* The data channels need no m-line of their own; the offer's SCTP m-line carries them. Only an ANSWER
|
|
23717
|
-
* is taken as the local description: one read in `have-local-offer` is an offer.
|
|
23871
|
+
* is taken as the local description: one read in `have-local-offer` is an offer. A
|
|
23872
|
+
* {@link RtcPeerOptions.mediaTrack} peer applies the offer with its application section rejected,
|
|
23873
|
+
* declares no channels, and adds remote candidates on the offer's first (bundle-tagged) mid.
|
|
23718
23874
|
*/
|
|
23719
23875
|
async handleRemoteOffer(offerSdp) {
|
|
23720
23876
|
const pc = this.pc;
|
|
@@ -23724,9 +23880,12 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23724
23880
|
throw new Error("RTC peer already handling an offer");
|
|
23725
23881
|
this.handlingOffer = true;
|
|
23726
23882
|
try {
|
|
23727
|
-
const
|
|
23728
|
-
const
|
|
23729
|
-
|
|
23883
|
+
const media = this.opts.mediaTrack === true;
|
|
23884
|
+
const pinned = forceDtlsRole(offerSdp, "passive");
|
|
23885
|
+
const offer = media ? rejectDataSection(pinned) : pinned;
|
|
23886
|
+
this.remoteMid = media ? offer.match(/^a=mid:([^\r\n]+)/m)?.[1]?.trim() ?? HUB_SDP_MID : midOf(offer, "application") ?? HUB_SDP_MID;
|
|
23887
|
+
if (media)
|
|
23888
|
+
this.videoMid = midOf(offer, "video");
|
|
23730
23889
|
const answerWait = new Promise((resolve, reject) => {
|
|
23731
23890
|
const timer = setTimeout(() => {
|
|
23732
23891
|
this.localAnswer = void 0;
|
|
@@ -23736,7 +23895,8 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23736
23895
|
});
|
|
23737
23896
|
pc.setRemoteDescription(offer, "offer");
|
|
23738
23897
|
this.remoteSet = true;
|
|
23739
|
-
|
|
23898
|
+
if (!media)
|
|
23899
|
+
this.createChannels();
|
|
23740
23900
|
const local = pc.localDescription();
|
|
23741
23901
|
let answer = String(local?.type ?? "").toLowerCase() === "answer" ? local?.sdp ?? "" : "";
|
|
23742
23902
|
if (!answer)
|
|
@@ -23805,6 +23965,7 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23805
23965
|
this.pc = void 0;
|
|
23806
23966
|
this.channels.clear();
|
|
23807
23967
|
this.channelsCreated = false;
|
|
23968
|
+
this.videoTrack = void 0;
|
|
23808
23969
|
this.remoteSet = false;
|
|
23809
23970
|
this.pending.length = 0;
|
|
23810
23971
|
}
|
|
@@ -23868,6 +24029,14 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23868
24029
|
this.framer?.recvPacket(buf);
|
|
23869
24030
|
});
|
|
23870
24031
|
}
|
|
24032
|
+
/** Take the offer's video m-line as the one track whose RTP is emitted; any other track is left alone. */
|
|
24033
|
+
wireVideoTrack(track) {
|
|
24034
|
+
if (this.videoTrack || this.videoMid === void 0 || track.mid() !== this.videoMid)
|
|
24035
|
+
return;
|
|
24036
|
+
this.videoTrack = track;
|
|
24037
|
+
this.logger.debug(`[rtc] video track ${this.videoMid}`);
|
|
24038
|
+
track.onMessage((msg) => this.emit("rtp", Buffer.isBuffer(msg) ? msg : Buffer.from(msg)));
|
|
24039
|
+
}
|
|
23871
24040
|
addNow(candidate) {
|
|
23872
24041
|
try {
|
|
23873
24042
|
this.pc?.addRemoteCandidate(candidate, this.remoteMid);
|
|
@@ -23883,7 +24052,7 @@ var RtcPeer = class extends EventEmitter8 {
|
|
|
23883
24052
|
};
|
|
23884
24053
|
|
|
23885
24054
|
// dist/transport/rtc/signaling.js
|
|
23886
|
-
import { EventEmitter as
|
|
24055
|
+
import { EventEmitter as EventEmitter10 } from "node:events";
|
|
23887
24056
|
import { createHmac as createHmac3, randomUUID as randomUUID2 } from "node:crypto";
|
|
23888
24057
|
var RTC_WS_PATH = "/v1/rtc/ws/join?reqtype=nvr";
|
|
23889
24058
|
var RTC_SIGN_PATH = "/v1/smart/nvr/ws/sign";
|
|
@@ -23899,7 +24068,7 @@ var defaultSocket = (url, init) => new WebSocket(url, {
|
|
|
23899
24068
|
protocols: init.protocols,
|
|
23900
24069
|
headers: { Origin: init.origin }
|
|
23901
24070
|
});
|
|
23902
|
-
var RtcSignalingClient = class extends
|
|
24071
|
+
var RtcSignalingClient = class extends EventEmitter10 {
|
|
23903
24072
|
opts;
|
|
23904
24073
|
ws;
|
|
23905
24074
|
sign;
|
|
@@ -24124,7 +24293,7 @@ var RtcSignalingClient = class extends EventEmitter9 {
|
|
|
24124
24293
|
|
|
24125
24294
|
// dist/transport/rtc/session.js
|
|
24126
24295
|
var AUTH_TIMEOUT_MS = 15e3;
|
|
24127
|
-
var RtcSession = class extends
|
|
24296
|
+
var RtcSession = class extends EventEmitter11 {
|
|
24128
24297
|
opts;
|
|
24129
24298
|
signaling;
|
|
24130
24299
|
peer;
|
|
@@ -24143,6 +24312,7 @@ var RtcSession = class extends EventEmitter10 {
|
|
|
24143
24312
|
this.logger = opts.logger ?? noopLogger;
|
|
24144
24313
|
this.signaling = (opts.createSignaling ?? ((o) => new RtcSignalingClient(o)))(opts);
|
|
24145
24314
|
this.peer = (opts.createPeer ?? ((o) => new RtcPeer(o)))({ logger: this.logger, ...opts.peer });
|
|
24315
|
+
const mediaTrack = opts.peer?.mediaTrack === true;
|
|
24146
24316
|
this.signaling.on("message", (inner) => {
|
|
24147
24317
|
this.chain = this.chain.then(() => this.onSignaling(inner)).catch((e) => {
|
|
24148
24318
|
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
@@ -24170,13 +24340,14 @@ var RtcSession = class extends EventEmitter10 {
|
|
|
24170
24340
|
this.announceClose();
|
|
24171
24341
|
});
|
|
24172
24342
|
this.peer.on("connectionState", (state) => {
|
|
24173
|
-
if ((state === "failed" || state === "closed") && this.connected) {
|
|
24343
|
+
if ((state === "failed" || state === "closed") && (this.connected || mediaTrack)) {
|
|
24174
24344
|
this.connected = false;
|
|
24175
24345
|
this.announceClose();
|
|
24176
24346
|
}
|
|
24177
24347
|
});
|
|
24178
24348
|
this.peer.on("error", (e) => this.emit("error", e));
|
|
24179
24349
|
this.peer.on("keepaliveEcho", () => this.emit("keepaliveEcho"));
|
|
24350
|
+
this.peer.on("rtp", (packet) => this.emit("rtp", packet));
|
|
24180
24351
|
this.peer.on("data", (frame, linkType) => {
|
|
24181
24352
|
if (linkType === PortalLinkType.LIVE)
|
|
24182
24353
|
this.emit("mediaData", frame);
|
|
@@ -24373,6 +24544,7 @@ var RtcCommandRouter = class {
|
|
|
24373
24544
|
sessions = /* @__PURE__ */ new Map();
|
|
24374
24545
|
lives = /* @__PURE__ */ new Map();
|
|
24375
24546
|
warm = /* @__PURE__ */ new Map();
|
|
24547
|
+
callSources = /* @__PURE__ */ new Map();
|
|
24376
24548
|
constructor(deps) {
|
|
24377
24549
|
this.deps = deps;
|
|
24378
24550
|
}
|
|
@@ -24380,6 +24552,8 @@ var RtcCommandRouter = class {
|
|
|
24380
24552
|
if (cmd.kind !== "set-payload" && cmd.kind !== "set-json") {
|
|
24381
24553
|
throw new Error(`rtc: ${cmd.kind} is not routable over the RTC control channel (only set-payload or set-json)`);
|
|
24382
24554
|
}
|
|
24555
|
+
if (route.mediaTrack)
|
|
24556
|
+
throw new Error(`rtc: ${route.stationSn} serves media only and takes no portal command`);
|
|
24383
24557
|
const identity = this.deps.identity();
|
|
24384
24558
|
if (!identity)
|
|
24385
24559
|
throw new Error(`rtc: not logged in, cannot drive ${route.stationSn}`);
|
|
@@ -24414,11 +24588,56 @@ var RtcCommandRouter = class {
|
|
|
24414
24588
|
}
|
|
24415
24589
|
};
|
|
24416
24590
|
}
|
|
24591
|
+
/**
|
|
24592
|
+
* Live view of a station on a {@link RtcRoute.mediaTrack} route: a live feed, a readable feed and a live
|
|
24593
|
+
* still, from one shared source per station whose every warm-up places a fresh `call` on its own
|
|
24594
|
+
* session. Recording has no wire here and is refused.
|
|
24595
|
+
*/
|
|
24596
|
+
trackMediaProviderFor(route) {
|
|
24597
|
+
const sn = route.stationSn;
|
|
24598
|
+
const sourceFor = (hints = {}) => {
|
|
24599
|
+
let source = this.callSources.get(sn);
|
|
24600
|
+
if (!source) {
|
|
24601
|
+
source = new SharedLiveSource({
|
|
24602
|
+
makeStream: () => {
|
|
24603
|
+
const identity = this.deps.identity();
|
|
24604
|
+
if (!identity)
|
|
24605
|
+
throw new Error(`rtc: not logged in, cannot call ${sn}`);
|
|
24606
|
+
const session = this.newSession(route, route.adminUserId || identity.userId, identity);
|
|
24607
|
+
return new RtcCallLiveStream(session, this.deps.logger);
|
|
24608
|
+
},
|
|
24609
|
+
label: sn,
|
|
24610
|
+
logger: this.deps.logger,
|
|
24611
|
+
powered: hints.powered,
|
|
24612
|
+
preBufferSeconds: hints.preBufferSeconds,
|
|
24613
|
+
warmTimeoutMs: 45e3
|
|
24614
|
+
});
|
|
24615
|
+
this.callSources.set(sn, source);
|
|
24616
|
+
}
|
|
24617
|
+
return source;
|
|
24618
|
+
};
|
|
24619
|
+
return {
|
|
24620
|
+
live: async (opts) => attachUnlessAborted(sourceFor(opts), opts?.signal),
|
|
24621
|
+
openReadable: async (opts) => openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts),
|
|
24622
|
+
snapshotLive: (opts) => captureSnapshotFromShared(sourceFor(opts), {
|
|
24623
|
+
...opts,
|
|
24624
|
+
logger: this.deps.logger,
|
|
24625
|
+
ffmpegPath: this.deps.ffmpegPath
|
|
24626
|
+
}),
|
|
24627
|
+
record: async () => {
|
|
24628
|
+
throw new Error(`record is not available for ${sn} over a portal call`);
|
|
24629
|
+
}
|
|
24630
|
+
};
|
|
24631
|
+
}
|
|
24417
24632
|
/**
|
|
24418
24633
|
* Tear down every station session (logout / shutdown). Each live source is disposed before its session
|
|
24419
24634
|
* closes, so its consumers end cleanly and its stop still rides the session.
|
|
24420
24635
|
*/
|
|
24421
24636
|
close() {
|
|
24637
|
+
for (const [sn, src] of this.callSources) {
|
|
24638
|
+
this.callSources.delete(sn);
|
|
24639
|
+
src.dispose();
|
|
24640
|
+
}
|
|
24422
24641
|
for (const [sn, live] of this.lives) {
|
|
24423
24642
|
this.lives.delete(sn);
|
|
24424
24643
|
live.source.dispose();
|
|
@@ -24548,6 +24767,19 @@ var RtcCommandRouter = class {
|
|
|
24548
24767
|
}
|
|
24549
24768
|
});
|
|
24550
24769
|
}
|
|
24770
|
+
newSession(route, adminUserId, identity) {
|
|
24771
|
+
return (this.deps.createSession ?? ((o) => new RtcSession(o)))({
|
|
24772
|
+
authToken: identity.authToken,
|
|
24773
|
+
gtoken: identity.gtoken,
|
|
24774
|
+
stationSn: route.stationSn,
|
|
24775
|
+
adminUserId,
|
|
24776
|
+
shard: this.deps.shard(),
|
|
24777
|
+
country: this.deps.country ?? "US",
|
|
24778
|
+
logger: this.deps.logger,
|
|
24779
|
+
signalingMode: route.signalingMode,
|
|
24780
|
+
peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy, mediaTrack: route.mediaTrack }
|
|
24781
|
+
});
|
|
24782
|
+
}
|
|
24551
24783
|
async stationSession(route, adminUserId, identity) {
|
|
24552
24784
|
const sn = route.stationSn;
|
|
24553
24785
|
const existing = this.sessions.get(sn);
|
|
@@ -24559,17 +24791,7 @@ var RtcCommandRouter = class {
|
|
|
24559
24791
|
}
|
|
24560
24792
|
this.drop(sn, existing);
|
|
24561
24793
|
}
|
|
24562
|
-
const session =
|
|
24563
|
-
authToken: identity.authToken,
|
|
24564
|
-
gtoken: identity.gtoken,
|
|
24565
|
-
stationSn: sn,
|
|
24566
|
-
adminUserId,
|
|
24567
|
-
shard: this.deps.shard(),
|
|
24568
|
-
country: this.deps.country ?? "US",
|
|
24569
|
-
logger: this.deps.logger,
|
|
24570
|
-
signalingMode: route.signalingMode,
|
|
24571
|
-
peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy }
|
|
24572
|
-
});
|
|
24794
|
+
const session = this.newSession(route, adminUserId, identity);
|
|
24573
24795
|
const ready = this.bringUp(sn, session);
|
|
24574
24796
|
const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
|
|
24575
24797
|
session.on("error", (e) => this.deps.onError?.(e));
|
|
@@ -26380,7 +26602,7 @@ async function resolveLightEffect(mega, lightId) {
|
|
|
26380
26602
|
}
|
|
26381
26603
|
|
|
26382
26604
|
// dist/transport/push/push-client.js
|
|
26383
|
-
import { EventEmitter as
|
|
26605
|
+
import { EventEmitter as EventEmitter13 } from "node:events";
|
|
26384
26606
|
import tls2 from "node:tls";
|
|
26385
26607
|
|
|
26386
26608
|
// dist/transport/push/proto.js
|
|
@@ -26635,7 +26857,7 @@ var MessageTag;
|
|
|
26635
26857
|
})(MessageTag || (MessageTag = {}));
|
|
26636
26858
|
|
|
26637
26859
|
// dist/transport/push/parser.js
|
|
26638
|
-
import { EventEmitter as
|
|
26860
|
+
import { EventEmitter as EventEmitter12 } from "node:events";
|
|
26639
26861
|
var State;
|
|
26640
26862
|
(function(State2) {
|
|
26641
26863
|
State2[State2["VERSION_TAG_AND_SIZE"] = 0] = "VERSION_TAG_AND_SIZE";
|
|
@@ -26653,7 +26875,7 @@ var TAG_TO_TYPE = {
|
|
|
26653
26875
|
[MessageTag.DataMessageStanza]: "mcs_proto.DataMessageStanza",
|
|
26654
26876
|
[MessageTag.StreamErrorStanza]: "mcs_proto.StreamErrorStanza"
|
|
26655
26877
|
};
|
|
26656
|
-
var McsParser = class extends
|
|
26878
|
+
var McsParser = class extends EventEmitter12 {
|
|
26657
26879
|
data = Buffer.alloc(0);
|
|
26658
26880
|
state = State.VERSION_TAG_AND_SIZE;
|
|
26659
26881
|
messageTag = 0;
|
|
@@ -26831,7 +27053,7 @@ function normalizePushEvent(raw) {
|
|
|
26831
27053
|
raw
|
|
26832
27054
|
};
|
|
26833
27055
|
}
|
|
26834
|
-
var PushClient = class _PushClient extends
|
|
27056
|
+
var PushClient = class _PushClient extends EventEmitter13 {
|
|
26835
27057
|
creds;
|
|
26836
27058
|
logger;
|
|
26837
27059
|
/** Consecutive MCS login rejections tolerated (self-healing propagation) before surfacing an error. */
|
|
@@ -28038,7 +28260,7 @@ function readinessSnapshot(readiness, state = readiness.state) {
|
|
|
28038
28260
|
wiredP2p: Object.freeze({ ...readiness.wiredP2p })
|
|
28039
28261
|
});
|
|
28040
28262
|
}
|
|
28041
|
-
var EufyMega = class extends
|
|
28263
|
+
var EufyMega = class extends EventEmitter14 {
|
|
28042
28264
|
opts;
|
|
28043
28265
|
mega;
|
|
28044
28266
|
/**
|
|
@@ -28905,7 +29127,7 @@ var EufyMega = class extends EventEmitter13 {
|
|
|
28905
29127
|
/**
|
|
28906
29128
|
* The station a device's commands go through: the device itself, or the HomeBase the registry links it
|
|
28907
29129
|
* to. Answers the target record, the station record and serial, the station's `device_type`, and the
|
|
28908
|
-
* `t9000` / `t8n00` family flags.
|
|
29130
|
+
* `t9000` / `t8n00` / `t8223` family flags.
|
|
28909
29131
|
*/
|
|
28910
29132
|
stationOf(sn) {
|
|
28911
29133
|
const devices = this.registry.list();
|
|
@@ -28915,7 +29137,16 @@ var EufyMega = class extends EventEmitter13 {
|
|
|
28915
29137
|
const raw = station?.raw ?? {};
|
|
28916
29138
|
const deviceType = typeof raw.device_type === "number" ? raw.device_type : void 0;
|
|
28917
29139
|
const family = { deviceType, model: station?.model };
|
|
28918
|
-
return {
|
|
29140
|
+
return {
|
|
29141
|
+
target,
|
|
29142
|
+
station,
|
|
29143
|
+
stationSn,
|
|
29144
|
+
raw,
|
|
29145
|
+
deviceType,
|
|
29146
|
+
t9000: isStation9000(family),
|
|
29147
|
+
t8n00: isNvrT8N00(family),
|
|
29148
|
+
t8223: isDoorbellT8223(family)
|
|
29149
|
+
};
|
|
28919
29150
|
}
|
|
28920
29151
|
/** The vendor DeviceType of the station a device hangs off; `undefined` for a station itself. */
|
|
28921
29152
|
stationDeviceTypeOf(sn) {
|
|
@@ -28974,7 +29205,8 @@ var EufyMega = class extends EventEmitter13 {
|
|
|
28974
29205
|
* kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
|
|
28975
29206
|
* station and the devices attached to it go over RTC, with an attached device's command refused
|
|
28976
29207
|
* unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
|
|
28977
|
-
* relay candidates; T9000 retains compact SDP and relay-only candidates.
|
|
29208
|
+
* relay candidates; T9000 retains compact SDP and relay-only candidates. A T8223 doorbell's route serves
|
|
29209
|
+
* media only, and the RTC router refuses a command on it. Everything else is P2P.
|
|
28978
29210
|
*/
|
|
28979
29211
|
routeCommand(sn, cmd) {
|
|
28980
29212
|
if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
|
|
@@ -29003,25 +29235,32 @@ var EufyMega = class extends EventEmitter13 {
|
|
|
29003
29235
|
return this.p2p.dispatchCommand(sn, cmd);
|
|
29004
29236
|
}
|
|
29005
29237
|
/**
|
|
29006
|
-
* The RTC route for a T9000 or T8N00 station or a device attached to one,
|
|
29007
|
-
* classification; `undefined` for any other device. A T8N00
|
|
29008
|
-
* `
|
|
29238
|
+
* The RTC route for a T9000 or T8N00 station or a device attached to one, or a T8223 doorbell that is its
|
|
29239
|
+
* own station, decided by the station's model classification; `undefined` for any other device. A T8N00
|
|
29240
|
+
* route carries its native `call` signalling; a T8223 route does too, and streams its video on a media
|
|
29241
|
+
* track. The admin id is the station's `member.admin_user_id`, else the logged-in user.
|
|
29009
29242
|
*/
|
|
29010
29243
|
rtcRouteFor(sn) {
|
|
29011
|
-
const { target, station, stationSn, raw, t9000, t8n00: native } = this.stationOf(sn);
|
|
29012
|
-
|
|
29244
|
+
const { target, station, stationSn, raw, t9000, t8n00: native, t8223 } = this.stationOf(sn);
|
|
29245
|
+
const doorbell = t8223 && stationSn === sn;
|
|
29246
|
+
if (!target || !station || !(t9000 || native || doorbell))
|
|
29013
29247
|
return void 0;
|
|
29014
29248
|
const member = raw.member?.admin_user_id;
|
|
29015
29249
|
const adminUserId = typeof member === "string" && member ? member : void 0;
|
|
29016
29250
|
const base = { stationSn, adminUserId, attached: stationSn !== sn };
|
|
29251
|
+
if (doorbell)
|
|
29252
|
+
return { ...base, signalingMode: "call", iceTransportPolicy: "all", mediaTrack: true };
|
|
29017
29253
|
return native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base;
|
|
29018
29254
|
}
|
|
29019
29255
|
/**
|
|
29020
|
-
* The RTC media provider for a
|
|
29021
|
-
* the station; `undefined` otherwise, including a camera behind
|
|
29256
|
+
* The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
|
|
29257
|
+
* `device_channel` resolves back to it on the station; `undefined` otherwise, including a camera behind
|
|
29258
|
+
* a T8N00, whose route carries commands only.
|
|
29022
29259
|
*/
|
|
29023
29260
|
rtcMediaFor(sn) {
|
|
29024
29261
|
const route = this.rtcRouteFor(sn);
|
|
29262
|
+
if (route?.mediaTrack)
|
|
29263
|
+
return this.rtc.trackMediaProviderFor(route);
|
|
29025
29264
|
if (!route?.attached || route.signalingMode === "call")
|
|
29026
29265
|
return void 0;
|
|
29027
29266
|
const raw = this.registry.list().find((d) => d.sn === sn)?.raw ?? {};
|
|
@@ -30662,7 +30901,7 @@ var SolixClient = class _SolixClient {
|
|
|
30662
30901
|
};
|
|
30663
30902
|
|
|
30664
30903
|
// dist/transport/mqtt/solix-mqtt.js
|
|
30665
|
-
import { EventEmitter as
|
|
30904
|
+
import { EventEmitter as EventEmitter15 } from "node:events";
|
|
30666
30905
|
var SOLIX_METER_FIELD_NAMES = {
|
|
30667
30906
|
168: "meterPowerL1",
|
|
30668
30907
|
// Anker Modbus: primary_phase_1_active_power
|
|
@@ -30840,7 +31079,7 @@ function addSolarbankScalars(frame, out) {
|
|
|
30840
31079
|
out.gridExportLimit = df.readUInt16LE(5);
|
|
30841
31080
|
}
|
|
30842
31081
|
}
|
|
30843
|
-
var SolixMqtt = class extends
|
|
31082
|
+
var SolixMqtt = class extends EventEmitter15 {
|
|
30844
31083
|
transport;
|
|
30845
31084
|
appName;
|
|
30846
31085
|
userId;
|