@mega-yfue/eufy-sdk 0.6.0-beta.31 → 0.6.0-beta.33
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/index.js
CHANGED
|
@@ -1609,11 +1609,17 @@ var CONTENDED_SESSION_HINT = "the token was rejected again soon after it was las
|
|
|
1609
1609
|
function tokenRejected(code, msg) {
|
|
1610
1610
|
return code === EufyCloudErrorCode.SESSION_KICKED || /user_id is empty|invalid[^,]*\btoken\b|\btoken\b[^,]*(expired|error|not exist)|kicked|(?:\btoken\b|\bsession\b)[^,]*does not exist|unauthor/i.test(msg ?? "");
|
|
1611
1611
|
}
|
|
1612
|
+
function shardOfDomain(domain) {
|
|
1613
|
+
const m = /-([a-z]{2})(?:[.-]|$)/i.exec(domain);
|
|
1614
|
+
return m ? `${m[1].toLowerCase()}-pr` : void 0;
|
|
1615
|
+
}
|
|
1612
1616
|
var MegaHttpClient = class {
|
|
1613
1617
|
cfg;
|
|
1614
1618
|
region;
|
|
1615
1619
|
/** The host `estimate_domain` answered, persisted with the session. */
|
|
1616
1620
|
estimatedDomain = "";
|
|
1621
|
+
/** The in-flight (then settled, so cached) RTC-sign shard request from {@link portalShard}. */
|
|
1622
|
+
portalShardInFlight;
|
|
1617
1623
|
bootstrapDomain;
|
|
1618
1624
|
sessionKey;
|
|
1619
1625
|
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
@@ -1730,13 +1736,36 @@ var MegaHttpClient = class {
|
|
|
1730
1736
|
return this.region;
|
|
1731
1737
|
}
|
|
1732
1738
|
/**
|
|
1733
|
-
* The shard the
|
|
1734
|
-
*
|
|
1735
|
-
* like `ie-pr`, whose accounts need the IE sign host and cluster.
|
|
1739
|
+
* The shard the estimated domain names (`…-ie-…`/`…-ie.` → `ie-pr`), else {@link regionShard}. The
|
|
1740
|
+
* eu/us classification cannot name shards like `ie-pr`.
|
|
1736
1741
|
*/
|
|
1737
1742
|
get rtcShard() {
|
|
1738
|
-
|
|
1739
|
-
|
|
1743
|
+
return shardOfDomain(this.estimatedDomain) ?? this.region;
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* The shard the RTC signalling signs on, from `estimate_domain` asked with the portal's WEB identity
|
|
1747
|
+
* (`Model-Type: WEB`, `App-Name: eufy_mega`) — the identity the sign itself is sent with, which can name
|
|
1748
|
+
* a different shard than the PHONE identity does for one account. The in-flight request is shared by
|
|
1749
|
+
* concurrent first callers, and once it settles to a shard the resolved promise is the cache; a call
|
|
1750
|
+
* that fails or names no shard answers {@link rtcShard}, clears the field and is asked again next time.
|
|
1751
|
+
*/
|
|
1752
|
+
async portalShard() {
|
|
1753
|
+
return this.portalShardInFlight ??= this.fetchPortalShard();
|
|
1754
|
+
}
|
|
1755
|
+
async fetchPortalShard() {
|
|
1756
|
+
try {
|
|
1757
|
+
const res = await this.estimateDomainRequest({ "Model-Type": "WEB", "App-Name": "eufy_mega" });
|
|
1758
|
+
const domain = res.data?.data?.domain;
|
|
1759
|
+
const shard = typeof domain === "string" ? shardOfDomain(domain) : void 0;
|
|
1760
|
+
if (shard) {
|
|
1761
|
+
this.logger.debug("[mega] portal shard:", shard);
|
|
1762
|
+
return shard;
|
|
1763
|
+
}
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
this.logger.debug("[mega] portal estimate_domain failed:", e instanceof Error ? e.message : String(e));
|
|
1766
|
+
}
|
|
1767
|
+
this.portalShardInFlight = void 0;
|
|
1768
|
+
return this.rtcShard;
|
|
1740
1769
|
}
|
|
1741
1770
|
/**
|
|
1742
1771
|
* The credentials the RTC signalling needs; `undefined` while logged out. The sign is refused
|
|
@@ -1833,13 +1862,13 @@ var MegaHttpClient = class {
|
|
|
1833
1862
|
});
|
|
1834
1863
|
return { status: res.status, data: parseMaybeJson(await res.text()) };
|
|
1835
1864
|
}
|
|
1865
|
+
/** POST `estimate_domain` with the given identity headers. */
|
|
1866
|
+
estimateDomainRequest(headers) {
|
|
1867
|
+
return this.httpPost(`https://mega-${this.region}.eufy.com/passport/estimate_domain`, { ab: this.cfg.countryCode.toLowerCase(), mode: 1 }, { ...headers, "content-type": "application/json" });
|
|
1868
|
+
}
|
|
1836
1869
|
/** Step 1: discover the account's region shard (and the bootstrap host). */
|
|
1837
1870
|
async estimateDomain() {
|
|
1838
|
-
const
|
|
1839
|
-
const res = await this.httpPost(url, { ab: this.cfg.countryCode.toLowerCase(), mode: 1 }, {
|
|
1840
|
-
...this.baseHeaders(),
|
|
1841
|
-
"content-type": "application/json"
|
|
1842
|
-
});
|
|
1871
|
+
const res = await this.estimateDomainRequest(this.baseHeaders());
|
|
1843
1872
|
this.logger.debug("[mega] estimate_domain raw:", JSON.stringify(res.data).slice(0, 400));
|
|
1844
1873
|
const env = res.data;
|
|
1845
1874
|
const d = env?.data ?? {};
|
|
@@ -5332,7 +5361,7 @@ var AUDIO_MEMBERS = {
|
|
|
5332
5361
|
kind: "percent",
|
|
5333
5362
|
writeOnly: true,
|
|
5334
5363
|
provenance: "verified",
|
|
5335
|
-
available: (ctx) => hasCapability(ctx, "doorbell"),
|
|
5364
|
+
available: (ctx) => hasCapability(ctx, "doorbell") && !isCallDoorbell(ctx),
|
|
5336
5365
|
min: 0,
|
|
5337
5366
|
max: 100,
|
|
5338
5367
|
description: "Doorbell ring/chime volume 0..100 (1708 DOORBELL_RINGTONE_VOLUME). Wire verified live on T8214.",
|
|
@@ -5390,8 +5419,11 @@ var DOORBELL_TYPES = [
|
|
|
5390
5419
|
DeviceType.BATTERY_DOORBELL_2,
|
|
5391
5420
|
DeviceType.BATTERY_DOORBELL_PLUS,
|
|
5392
5421
|
DeviceType.DOORBELL_SOLO,
|
|
5393
|
-
DeviceType.BATTERY_DOORBELL_PLUS_E340
|
|
5422
|
+
DeviceType.BATTERY_DOORBELL_PLUS_E340,
|
|
5423
|
+
DeviceType.BATTERY_DOORBELL_C30,
|
|
5424
|
+
DeviceType.BATTERY_DOORBELL_C31
|
|
5394
5425
|
];
|
|
5426
|
+
var writable = (ctx) => !isCallDoorbell(ctx);
|
|
5395
5427
|
var DOORBELL_CMD = {
|
|
5396
5428
|
/**
|
|
5397
5429
|
* Doorbell "quick response" — make the doorbell PLAY a stored voice reply. Same 136-byte
|
|
@@ -5528,6 +5560,7 @@ var DOORBELL_MEMBERS = {
|
|
|
5528
5560
|
*/
|
|
5529
5561
|
chimeSwitch: {
|
|
5530
5562
|
param: DOORBELL_CMD.CHIME_SWITCH,
|
|
5563
|
+
available: writable,
|
|
5531
5564
|
type: "bool",
|
|
5532
5565
|
kind: "boolean",
|
|
5533
5566
|
provenance: "verified",
|
|
@@ -5540,6 +5573,7 @@ var DOORBELL_MEMBERS = {
|
|
|
5540
5573
|
*/
|
|
5541
5574
|
mechanicalChimeSwitch: {
|
|
5542
5575
|
param: DOORBELL_CMD.MECHANICAL_CHIME_SWITCH,
|
|
5576
|
+
available: writable,
|
|
5543
5577
|
type: "bool",
|
|
5544
5578
|
kind: "boolean",
|
|
5545
5579
|
provenance: "verified",
|
|
@@ -5549,6 +5583,7 @@ var DOORBELL_MEMBERS = {
|
|
|
5549
5583
|
/** Same reasoning and the same capture session as {@link DOORBELL_MEMBERS.mechanicalChimeSwitch}. */
|
|
5550
5584
|
wdrSwitch: {
|
|
5551
5585
|
param: DOORBELL_CMD.WDR_SWITCH,
|
|
5586
|
+
available: writable,
|
|
5552
5587
|
type: "bool",
|
|
5553
5588
|
kind: "boolean",
|
|
5554
5589
|
provenance: "verified",
|
|
@@ -5579,6 +5614,7 @@ var DOORBELL_MEMBERS = {
|
|
|
5579
5614
|
*/
|
|
5580
5615
|
dingdongVolume: {
|
|
5581
5616
|
param: DOORBELL_CMD.DINGDONG_VOLUME,
|
|
5617
|
+
available: writable,
|
|
5582
5618
|
type: "number",
|
|
5583
5619
|
unit: "%",
|
|
5584
5620
|
kind: "percent",
|
|
@@ -5596,6 +5632,7 @@ var DOORBELL_MEMBERS = {
|
|
|
5596
5632
|
*/
|
|
5597
5633
|
dingdongRingtone: {
|
|
5598
5634
|
param: DOORBELL_CMD.DINGDONG_RINGTONE,
|
|
5635
|
+
available: writable,
|
|
5599
5636
|
type: "number",
|
|
5600
5637
|
kind: "enum",
|
|
5601
5638
|
enumValues: enumLabels(DoorbellRingtone),
|
|
@@ -5661,7 +5698,7 @@ var DOORBELL_MEMBERS = {
|
|
|
5661
5698
|
throw new Error(`videoQuality: ${String(quality)} is not one of ${enumLabels(DoorbellVideoQuality)}`);
|
|
5662
5699
|
}
|
|
5663
5700
|
await sink.dispatch(setScalar(DOORBELL_CMD.VIDEO_QUALITY, q + (asBool(highCompression) ? 5 : 0), ctx, "direct-binary"));
|
|
5664
|
-
}, "Set the live-view quality and the video encoding format, which share one wire."),
|
|
5701
|
+
}, "Set the live-view quality and the video encoding format, which share one wire.", writable),
|
|
5665
5702
|
/**
|
|
5666
5703
|
* A config blob the device reports WHOLE, so it is typed `string` and handed back unparsed — three
|
|
5667
5704
|
* settings live inside it (motion notifications, ring notifications, notification style) and the SDK
|
|
@@ -5709,7 +5746,7 @@ var DOORBELL_MEMBERS = {
|
|
|
5709
5746
|
} catch {
|
|
5710
5747
|
}
|
|
5711
5748
|
}
|
|
5712
|
-
}, "Play a quick response at the visitor."),
|
|
5749
|
+
}, "Play a quick response at the visitor.", writable),
|
|
5713
5750
|
/**
|
|
5714
5751
|
* Fetch the doorbell's quick-response list — a P2P request/reply query that returns data, so it
|
|
5715
5752
|
* exists only on a device bound to a media provider that offers one.
|
|
@@ -6019,8 +6056,9 @@ function powerCommand(on, ctx) {
|
|
|
6019
6056
|
function privacyCommand(enabled, channel) {
|
|
6020
6057
|
return { kind: "p2p-privacy-burst", enabled, channel };
|
|
6021
6058
|
}
|
|
6059
|
+
var doorbellLed = (ctx) => hasCapability(ctx, "doorbell") && !isCallDoorbell(ctx);
|
|
6022
6060
|
function statusLedCommand(on, ctx) {
|
|
6023
|
-
if (
|
|
6061
|
+
if (doorbellLed(ctx))
|
|
6024
6062
|
return setPayload(CAMERA_CMD.DOORBELL_LED, { light_enable: on ? 1 : 0 }, ctx);
|
|
6025
6063
|
return setScalar(CAMERA_CMD.DEV_LED_SWITCH, on ? 1 : 0, ctx, "int-string");
|
|
6026
6064
|
}
|
|
@@ -6331,8 +6369,8 @@ var CAMERA_MEMBERS = {
|
|
|
6331
6369
|
kind: "boolean",
|
|
6332
6370
|
provenance: "verified",
|
|
6333
6371
|
available: isCameraCodec,
|
|
6334
|
-
readAvailable: (ctx) => !
|
|
6335
|
-
readAliases: [{ paramType: CAMERA_CMD.DOORBELL_LED, available:
|
|
6372
|
+
readAvailable: (ctx) => !doorbellLed(ctx),
|
|
6373
|
+
readAliases: [{ paramType: CAMERA_CMD.DOORBELL_LED, available: doorbellLed }],
|
|
6336
6374
|
requiresRead: true,
|
|
6337
6375
|
description: "Camera status LED. Video doorbells report the same state under their button-ring LED parameter.",
|
|
6338
6376
|
write: (v, ctx) => statusLedCommand(asBool(v), ctx)
|
|
@@ -25727,6 +25765,8 @@ var RtcCommandRouter = class {
|
|
|
25727
25765
|
warm = /* @__PURE__ */ new Map();
|
|
25728
25766
|
callSources = /* @__PURE__ */ new Map();
|
|
25729
25767
|
lastTransaction = 0;
|
|
25768
|
+
/** The latest shard each `call` source resolved, read by its `makeStream` (see {@link callMediaProviderFor}). */
|
|
25769
|
+
callShards = /* @__PURE__ */ new Map();
|
|
25730
25770
|
constructor(deps) {
|
|
25731
25771
|
this.deps = deps;
|
|
25732
25772
|
}
|
|
@@ -25853,7 +25893,13 @@ var RtcCommandRouter = class {
|
|
|
25853
25893
|
views
|
|
25854
25894
|
};
|
|
25855
25895
|
}
|
|
25856
|
-
/**
|
|
25896
|
+
/**
|
|
25897
|
+
* Share a dedicated-call source and its consumer operations under one route key.
|
|
25898
|
+
*
|
|
25899
|
+
* `SharedLiveSource` calls `makeStream` synchronously, so the shard cannot be awaited inside it. Each
|
|
25900
|
+
* async entry point resolves the shard and records it in {@link callShards} before touching the source;
|
|
25901
|
+
* `makeStream` reads the latest recorded value.
|
|
25902
|
+
*/
|
|
25857
25903
|
callMediaProviderFor(route, key, makeStream, recordError, warmTimeoutMs) {
|
|
25858
25904
|
const sourceFor = (opts = {}) => {
|
|
25859
25905
|
opts.signal?.throwIfAborted();
|
|
@@ -25867,7 +25913,7 @@ var RtcCommandRouter = class {
|
|
|
25867
25913
|
if (!identity)
|
|
25868
25914
|
throw new Error(`rtc: not logged in, cannot call ${route.stationSn}`);
|
|
25869
25915
|
const accountId = route.adminUserId || identity.userId;
|
|
25870
|
-
return makeStream(this.newSession(route, accountId, identity), accountId, view);
|
|
25916
|
+
return makeStream(this.newSession(route, accountId, identity, this.callShards.get(key)), accountId, view);
|
|
25871
25917
|
},
|
|
25872
25918
|
label: sourceKey,
|
|
25873
25919
|
logger: this.deps.logger,
|
|
@@ -25879,14 +25925,26 @@ var RtcCommandRouter = class {
|
|
|
25879
25925
|
}
|
|
25880
25926
|
return source;
|
|
25881
25927
|
};
|
|
25928
|
+
const resolveShard = async () => {
|
|
25929
|
+
this.callShards.set(key, await this.deps.shard(route));
|
|
25930
|
+
};
|
|
25882
25931
|
return {
|
|
25883
|
-
live: async (opts) =>
|
|
25884
|
-
|
|
25885
|
-
|
|
25886
|
-
|
|
25887
|
-
|
|
25888
|
-
|
|
25889
|
-
|
|
25932
|
+
live: async (opts) => {
|
|
25933
|
+
await resolveShard();
|
|
25934
|
+
return attachUnlessAborted(sourceFor(opts), opts?.signal);
|
|
25935
|
+
},
|
|
25936
|
+
openReadable: async (opts) => {
|
|
25937
|
+
await resolveShard();
|
|
25938
|
+
return openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts);
|
|
25939
|
+
},
|
|
25940
|
+
snapshotLive: async (opts) => {
|
|
25941
|
+
await resolveShard();
|
|
25942
|
+
return captureSnapshotFromShared(sourceFor(opts), {
|
|
25943
|
+
...opts,
|
|
25944
|
+
logger: this.deps.logger,
|
|
25945
|
+
ffmpegPath: this.deps.ffmpegPath
|
|
25946
|
+
});
|
|
25947
|
+
},
|
|
25890
25948
|
record: async () => {
|
|
25891
25949
|
throw new Error(recordError);
|
|
25892
25950
|
}
|
|
@@ -25901,6 +25959,7 @@ var RtcCommandRouter = class {
|
|
|
25901
25959
|
this.callSources.delete(sn);
|
|
25902
25960
|
src.dispose();
|
|
25903
25961
|
}
|
|
25962
|
+
this.callShards.clear();
|
|
25904
25963
|
for (const [sn, live] of this.lives) {
|
|
25905
25964
|
this.lives.delete(sn);
|
|
25906
25965
|
live.source.dispose();
|
|
@@ -26094,31 +26153,50 @@ var RtcCommandRouter = class {
|
|
|
26094
26153
|
}
|
|
26095
26154
|
});
|
|
26096
26155
|
}
|
|
26097
|
-
newSession(route, adminUserId, identity) {
|
|
26156
|
+
newSession(route, adminUserId, identity, shard) {
|
|
26098
26157
|
return (this.deps.createSession ?? ((o) => new RtcSession(o)))({
|
|
26099
26158
|
authToken: identity.authToken,
|
|
26100
26159
|
gtoken: identity.gtoken,
|
|
26101
26160
|
stationSn: route.stationSn,
|
|
26102
26161
|
adminUserId,
|
|
26103
|
-
shard
|
|
26162
|
+
shard,
|
|
26104
26163
|
country: this.deps.country ?? "US",
|
|
26105
26164
|
logger: this.deps.logger,
|
|
26106
26165
|
signalingMode: route.signalingMode,
|
|
26107
26166
|
peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy, mediaTrack: route.mediaTrack }
|
|
26108
26167
|
});
|
|
26109
26168
|
}
|
|
26169
|
+
/** A connected session for `sn` to reuse, or undefined (dropping a dead one). */
|
|
26170
|
+
async reusable(sn) {
|
|
26171
|
+
const st = this.sessions.get(sn);
|
|
26172
|
+
if (!st)
|
|
26173
|
+
return void 0;
|
|
26174
|
+
await st.ready;
|
|
26175
|
+
if (st.session.isConnected) {
|
|
26176
|
+
this.touch(sn, st);
|
|
26177
|
+
return st;
|
|
26178
|
+
}
|
|
26179
|
+
this.drop(sn, st);
|
|
26180
|
+
return void 0;
|
|
26181
|
+
}
|
|
26182
|
+
/**
|
|
26183
|
+
* Get or open the station's session. A connected session is reused before the shard is awaited, so a
|
|
26184
|
+
* command on an already-connected station never waits on the unauthenticated `estimate_domain` round
|
|
26185
|
+
* trip — only the create path awaits it. After the await the map is re-checked, with no `await` between
|
|
26186
|
+
* the lookup and `this.sessions.set`, so concurrent creators still share one session.
|
|
26187
|
+
*/
|
|
26110
26188
|
async stationSession(route, adminUserId, identity) {
|
|
26111
26189
|
const sn = route.stationSn;
|
|
26112
|
-
const
|
|
26113
|
-
if (
|
|
26114
|
-
|
|
26115
|
-
|
|
26116
|
-
|
|
26117
|
-
|
|
26118
|
-
|
|
26119
|
-
|
|
26120
|
-
}
|
|
26121
|
-
const session = this.newSession(route, adminUserId, identity);
|
|
26190
|
+
const reused = await this.reusable(sn);
|
|
26191
|
+
if (reused)
|
|
26192
|
+
return reused;
|
|
26193
|
+
const shard = await this.deps.shard(route);
|
|
26194
|
+
if (this.sessions.has(sn)) {
|
|
26195
|
+
const again = await this.reusable(sn);
|
|
26196
|
+
if (again)
|
|
26197
|
+
return again;
|
|
26198
|
+
}
|
|
26199
|
+
const session = this.newSession(route, adminUserId, identity, shard);
|
|
26122
26200
|
const ready = this.bringUp(sn, session);
|
|
26123
26201
|
const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
|
|
26124
26202
|
session.on("error", (e) => this.deps.onError?.(e));
|
|
@@ -29768,7 +29846,7 @@ var EufyMega = class _EufyMega extends EventEmitter14 {
|
|
|
29768
29846
|
});
|
|
29769
29847
|
this.rtc = new RtcCommandRouter({
|
|
29770
29848
|
identity: () => this.mega.rtcIdentity(),
|
|
29771
|
-
shard: () => this.mega.rtcShard,
|
|
29849
|
+
shard: (route) => route.mediaTrack ? this.mega.rtcShard : this.mega.portalShard(),
|
|
29772
29850
|
country: opts.countryCode,
|
|
29773
29851
|
logger: opts.logger,
|
|
29774
29852
|
ffmpegPath: opts.ffmpegPath,
|