@mega-yfue/eufy-sdk 0.6.0-beta.30 → 0.6.0-beta.32
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/core/contracts.d.ts +12 -9
- package/dist/index.js +171 -67
- package/dist/index.js.map +3 -3
- package/dist/transport/http/mega-client.d.ts +15 -3
- package/dist/transport/p2p/command-router.d.ts +14 -10
- package/dist/transport/p2p/recording-download.d.ts +76 -15
- package/dist/transport/p2p/video.d.ts +7 -3
- package/dist/transport/rtc/command-router.d.ts +19 -3
- package/package.json +1 -1
package/dist/core/contracts.d.ts
CHANGED
|
@@ -153,8 +153,9 @@ export declare class DeviceChannelUnresolvedError extends Error {
|
|
|
153
153
|
/**
|
|
154
154
|
* Why {@link MediaProvider.downloadRecording} returned no recording.
|
|
155
155
|
*
|
|
156
|
-
* - `invalid-recording` — the recording name is not one the station could hold.
|
|
157
|
-
* - `key-unavailable` — the
|
|
156
|
+
* - `invalid-recording` — the recording name is not one the camera's station could hold.
|
|
157
|
+
* - `key-unavailable` — the station stores the recording sealed and the key it was sealed under could not be
|
|
158
|
+
* obtained.
|
|
158
159
|
* - `no-data` — the station sent nothing within the wait.
|
|
159
160
|
* - `incomplete` — the transfer ended before the station's finish frame.
|
|
160
161
|
* - `undecodable` — data arrived but no video frame could be decoded from it.
|
|
@@ -170,10 +171,10 @@ export declare class RecordingDownloadError extends Error {
|
|
|
170
171
|
/**
|
|
171
172
|
* A recording downloaded from the station, as elementary streams.
|
|
172
173
|
*
|
|
173
|
-
* `video` is Annex-B H.264 in decode order, `audio` AAC-LC 16 kHz mono in ADTS framing (absent
|
|
174
|
-
* recording carried none that could be decoded). `frames` counts the distinct video sequences
|
|
175
|
-
* `missingFrames` counts gaps between received sequences and received sequences that yielded no
|
|
176
|
-
* picture, including at either end. `durationMs` spans the first to the last emitted picture by the
|
|
174
|
+
* `video` is Annex-B H.264 or H.265 in decode order, `audio` AAC-LC 16 kHz mono in ADTS framing (absent
|
|
175
|
+
* when the recording carried none that could be decoded). `frames` counts the distinct video sequences
|
|
176
|
+
* emitted. `missingFrames` counts gaps between received sequences and received sequences that yielded no
|
|
177
|
+
* usable picture, including at either end. `durationMs` spans the first to the last emitted picture by the
|
|
177
178
|
* camera's own timestamps; `fps` is derived from those stamps.
|
|
178
179
|
*/
|
|
179
180
|
export interface RecordingDownload {
|
|
@@ -884,9 +885,11 @@ export interface MediaProvider {
|
|
|
884
885
|
encoder?: AacEncoder;
|
|
885
886
|
} & SharedSourceHints): Promise<TalkbackHandle>;
|
|
886
887
|
/**
|
|
887
|
-
* Download one recording the camera's station stored
|
|
888
|
-
* the
|
|
889
|
-
*
|
|
888
|
+
* Download one recording the camera's station stored. `recording` names it: the file name the station stores
|
|
889
|
+
* it under, without the folder or the extension. `cipherId` is the cipher the station names for it, and is not
|
|
890
|
+
* read on a station that stores its recordings in the clear. One download runs at a time per station. Rejects with
|
|
891
|
+
* {@link RecordingDownloadError}. Optional, and absent on a camera whose station's recording download is
|
|
892
|
+
* unverified.
|
|
890
893
|
*/
|
|
891
894
|
downloadRecording?(opts: {
|
|
892
895
|
recording: string;
|
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 ?? {};
|
|
@@ -18951,15 +18980,19 @@ var VideoFrameDecoder = class {
|
|
|
18951
18980
|
var STATION_CHUNK_BYTES = 64e3;
|
|
18952
18981
|
var AccessUnitAssembler = class {
|
|
18953
18982
|
onDropped;
|
|
18983
|
+
chunkBytes;
|
|
18954
18984
|
open;
|
|
18955
18985
|
droppedUnits = 0;
|
|
18956
18986
|
/**
|
|
18957
18987
|
* @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
|
|
18958
18988
|
* arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
|
|
18959
18989
|
* its own.
|
|
18990
|
+
* @param chunkBytes the payload size the station fills a frame to before continuing a unit in the next. A
|
|
18991
|
+
* unit is held while its latest frame declares exactly this. {@link STATION_CHUNK_BYTES} when omitted.
|
|
18960
18992
|
*/
|
|
18961
|
-
constructor(onDropped) {
|
|
18993
|
+
constructor(onDropped, chunkBytes = STATION_CHUNK_BYTES) {
|
|
18962
18994
|
this.onDropped = onDropped;
|
|
18995
|
+
this.chunkBytes = chunkBytes;
|
|
18963
18996
|
}
|
|
18964
18997
|
/**
|
|
18965
18998
|
* Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
|
|
@@ -18978,7 +19011,7 @@ var AccessUnitAssembler = class {
|
|
|
18978
19011
|
this.discard();
|
|
18979
19012
|
return [];
|
|
18980
19013
|
}
|
|
18981
|
-
const full = header.payloadLength ===
|
|
19014
|
+
const full = header.payloadLength === this.chunkBytes;
|
|
18982
19015
|
const open2 = this.open;
|
|
18983
19016
|
if (open2 && sameUnit(open2.header, header) && !beginsAccessUnit(body)) {
|
|
18984
19017
|
open2.chunks.push(body);
|
|
@@ -19992,16 +20025,31 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
19992
20025
|
var RECORDING_DATA_TYPE = 3;
|
|
19993
20026
|
var VIDEO_HEADER_LEN2 = 22;
|
|
19994
20027
|
var AUDIO_CODEC_AAC_LC = 0;
|
|
19995
|
-
var FINISH_CHANNEL = 0;
|
|
19996
20028
|
var FIRST_FRAME_WAIT_MS = 2e4;
|
|
19997
20029
|
var IDLE_END_MS = 15e3;
|
|
19998
20030
|
var DEFAULT_TRANSFER_MS = 12e4;
|
|
19999
20031
|
var MAX_TRANSFER_BYTES = 32 * 1024 * 1024;
|
|
20032
|
+
function cameraFolder(channel) {
|
|
20033
|
+
if (!Number.isInteger(channel) || channel < 0 || channel > 99)
|
|
20034
|
+
return void 0;
|
|
20035
|
+
return `Camera${String(channel).padStart(2, "0")}`;
|
|
20036
|
+
}
|
|
20000
20037
|
function homeBase2RecordingPath(channel, recording) {
|
|
20001
|
-
|
|
20038
|
+
const folder = cameraFolder(channel);
|
|
20039
|
+
if (!folder || !/^(?:h265_)?\d{14}$/.test(recording))
|
|
20040
|
+
return void 0;
|
|
20041
|
+
return `/media/mmcblk0p1/${folder}/${recording}.dat`;
|
|
20042
|
+
}
|
|
20043
|
+
function homeBase3RecordingPath(channel, recording) {
|
|
20044
|
+
const folder = cameraFolder(channel);
|
|
20045
|
+
if (!folder || !/^\d{14}$/.test(recording))
|
|
20002
20046
|
return void 0;
|
|
20003
|
-
return `/
|
|
20047
|
+
return `/zx/emmcdata/${folder}/${recording.slice(0, 6)}/${recording}/${recording}.zxvideo`;
|
|
20004
20048
|
}
|
|
20049
|
+
var RECORDING_STATIONS = /* @__PURE__ */ new Map([
|
|
20050
|
+
["T8010", { path: homeBase2RecordingPath, sealed: true, finishChannel: 0, chunkBytes: STATION_CHUNK_BYTES }],
|
|
20051
|
+
["T8030", { path: homeBase3RecordingPath, sealed: false, finishChannel: STATION_CHANNEL3, chunkBytes: 4e5 }]
|
|
20052
|
+
]);
|
|
20005
20053
|
function receiveRecording(session, request) {
|
|
20006
20054
|
let resolveFrames;
|
|
20007
20055
|
let rejectFrames;
|
|
@@ -20045,7 +20093,7 @@ function receiveRecording(session, request) {
|
|
|
20045
20093
|
};
|
|
20046
20094
|
const onData = (frame) => {
|
|
20047
20095
|
if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
|
|
20048
|
-
if (frame.channel !== request.channel && frame.channel !==
|
|
20096
|
+
if (frame.channel !== request.channel && frame.channel !== request.finishChannel)
|
|
20049
20097
|
return;
|
|
20050
20098
|
if (draining)
|
|
20051
20099
|
incomplete(draining);
|
|
@@ -20102,12 +20150,17 @@ function receiveRecording(session, request) {
|
|
|
20102
20150
|
}
|
|
20103
20151
|
return { frames, drained };
|
|
20104
20152
|
}
|
|
20105
|
-
function
|
|
20106
|
-
const
|
|
20107
|
-
|
|
20153
|
+
function clearVideoPayload(raw, header) {
|
|
20154
|
+
const end = VIDEO_HEADER_LEN2 + header.payloadLength;
|
|
20155
|
+
return raw.length >= end ? raw.subarray(VIDEO_HEADER_LEN2, end) : void 0;
|
|
20156
|
+
}
|
|
20157
|
+
function decodeRecording(frames, station, eccPrivateKeyHex) {
|
|
20158
|
+
const eccPrivateKey = station.sealed ? Buffer.from(eccPrivateKeyHex ?? "", "hex") : void 0;
|
|
20159
|
+
if (eccPrivateKey && eccPrivateKey.length !== 32) {
|
|
20108
20160
|
throw new RecordingDownloadError("undecodable", "the cipher key is not a P-256 key");
|
|
20109
|
-
|
|
20110
|
-
const
|
|
20161
|
+
}
|
|
20162
|
+
const decoder = eccPrivateKey && new VideoFrameDecoder(eccPrivateKey);
|
|
20163
|
+
const assembler = new AccessUnitAssembler(void 0, station.chunkBytes);
|
|
20111
20164
|
const video = [];
|
|
20112
20165
|
const audio = [];
|
|
20113
20166
|
const numbers = /* @__PURE__ */ new Set();
|
|
@@ -20120,7 +20173,7 @@ function decodeRecording(frames, eccPrivateKeyHex) {
|
|
|
20120
20173
|
if (!header)
|
|
20121
20174
|
continue;
|
|
20122
20175
|
receivedNumbers.add(header.sequence);
|
|
20123
|
-
const units = assembler.push(raw, (payload) => header.keyframe ? decoder.decodeFrame(payload)?.h264 : payload
|
|
20176
|
+
const units = assembler.push(raw, (payload) => decoder && header.keyframe ? decoder.decodeFrame(payload)?.h264 : clearVideoPayload(payload, header));
|
|
20124
20177
|
for (const unit of units) {
|
|
20125
20178
|
video.push(unit.data);
|
|
20126
20179
|
numbers.add(header.sequence);
|
|
@@ -20129,10 +20182,10 @@ function decodeRecording(frames, eccPrivateKeyHex) {
|
|
|
20129
20182
|
lastStamp = stamp;
|
|
20130
20183
|
}
|
|
20131
20184
|
} else if (commandId === CommandType.CMD_AUDIO_FRAME) {
|
|
20132
|
-
|
|
20133
|
-
if (!mediaKey || raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
20185
|
+
if (raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
20134
20186
|
continue;
|
|
20135
|
-
const
|
|
20187
|
+
const mediaKey = decoder?.currentMediaKey;
|
|
20188
|
+
const aac = decoder ? mediaKey && openSealedAudio(raw, mediaKey) : raw.subarray(AUDIO_HEADER_LEN);
|
|
20136
20189
|
if (aac?.length)
|
|
20137
20190
|
audio.push(aac);
|
|
20138
20191
|
}
|
|
@@ -22864,7 +22917,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22864
22917
|
* start has no level-1 form for.
|
|
22865
22918
|
*/
|
|
22866
22919
|
mediaProviderFor(sn) {
|
|
22867
|
-
const
|
|
22920
|
+
const station = this.recordingStationFor(sn);
|
|
22921
|
+
const recordings = station ? { downloadRecording: (opts) => this.downloadRecording(sn, station, opts) } : {};
|
|
22868
22922
|
return {
|
|
22869
22923
|
...recordings,
|
|
22870
22924
|
snapshotLive: async (opts) => {
|
|
@@ -22906,29 +22960,32 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22906
22960
|
};
|
|
22907
22961
|
}
|
|
22908
22962
|
/**
|
|
22909
|
-
*
|
|
22910
|
-
*
|
|
22963
|
+
* The {@link RecordingStation} of the station `sn` is attached to, when `sn` is a camera attached to a
|
|
22964
|
+
* station with an entry in {@link RECORDING_STATIONS}. A standalone device, or a camera on any other
|
|
22965
|
+
* station, answers `undefined`.
|
|
22911
22966
|
*/
|
|
22912
|
-
|
|
22967
|
+
recordingStationFor(sn) {
|
|
22913
22968
|
const dev = this.recordFor(sn);
|
|
22914
22969
|
if (!dev)
|
|
22915
|
-
return
|
|
22970
|
+
return void 0;
|
|
22916
22971
|
const parentSn = stationOf(dev);
|
|
22917
|
-
|
|
22972
|
+
if (parentSn === sn)
|
|
22973
|
+
return void 0;
|
|
22974
|
+
return RECORDING_STATIONS.get(this.recordFor(parentSn)?.model ?? "");
|
|
22918
22975
|
}
|
|
22919
22976
|
/**
|
|
22920
|
-
* Download one recording
|
|
22921
|
-
* ({@link decodeRecording}).
|
|
22922
|
-
* on the camera's channel, so a download starts only
|
|
22923
|
-
* ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
22924
|
-
* while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
22925
|
-
* turn is held until the transfer drains.
|
|
22977
|
+
* Download one recording `station` holds for the attached camera `sn`, then decode it
|
|
22978
|
+
* ({@link decodeRecording}). `station` is the entry {@link mediaProviderFor} bound. Downloads queue per
|
|
22979
|
+
* station: the station streams one recording at a time on the camera's channel, so a download starts only
|
|
22980
|
+
* once the previous one has drained ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
22981
|
+
* has its answer. An abort while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
22982
|
+
* once and the station's turn is held until the transfer drains.
|
|
22926
22983
|
*/
|
|
22927
|
-
async downloadRecording(sn, opts) {
|
|
22984
|
+
async downloadRecording(sn, station, opts) {
|
|
22928
22985
|
opts.signal?.throwIfAborted();
|
|
22929
22986
|
const parentSn = this.stationKeyOf(sn);
|
|
22930
22987
|
const previous = this.recordingDownloads.get(parentSn) ?? Promise.resolve();
|
|
22931
|
-
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, opts));
|
|
22988
|
+
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, station, opts));
|
|
22932
22989
|
const turn = Promise.allSettled([previous, started.then((t) => t.drained)]).then(() => {
|
|
22933
22990
|
if (this.recordingDownloads.get(parentSn) === turn)
|
|
22934
22991
|
this.recordingDownloads.delete(parentSn);
|
|
@@ -22942,25 +22999,32 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22942
22999
|
}
|
|
22943
23000
|
/**
|
|
22944
23001
|
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
22945
|
-
* and when the station is done sending it.
|
|
23002
|
+
* and when the station is done sending it. The path is `station.path(channel, opts.recording)`, and a name
|
|
23003
|
+
* it refuses rejects `invalid-recording` before anything is sent. On a sealed station the cipher's ECC key
|
|
23004
|
+
* is looked up, and its absence rejects `key-unavailable` before anything is sent. A station in the clear
|
|
23005
|
+
* reads no cipher, and `opts.cipherId` is not read.
|
|
22946
23006
|
*/
|
|
22947
|
-
async startRecordingDownload(sn, opts) {
|
|
23007
|
+
async startRecordingDownload(sn, station, opts) {
|
|
22948
23008
|
const { session, parentSn, channel, accountId } = await this.resolveSession(sn, { signal: opts.signal });
|
|
22949
|
-
const path =
|
|
23009
|
+
const path = station.path(channel, opts.recording);
|
|
22950
23010
|
if (!path)
|
|
22951
23011
|
throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
|
|
22952
|
-
const eccKey = (await this.cipherKeysFor(session, opts.cipherId, accountId, parentSn))?.ecc;
|
|
22953
|
-
if (!eccKey) {
|
|
23012
|
+
const eccKey = station.sealed ? (await this.cipherKeysFor(session, opts.cipherId, accountId, parentSn))?.ecc : void 0;
|
|
23013
|
+
if (station.sealed && !eccKey) {
|
|
22954
23014
|
throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
|
|
22955
23015
|
}
|
|
22956
23016
|
const transfer = receiveRecording(session, {
|
|
22957
23017
|
path,
|
|
22958
23018
|
accountId,
|
|
22959
23019
|
channel,
|
|
23020
|
+
finishChannel: station.finishChannel,
|
|
22960
23021
|
timeoutMs: opts.timeoutMs,
|
|
22961
23022
|
signal: opts.signal
|
|
22962
23023
|
});
|
|
22963
|
-
return {
|
|
23024
|
+
return {
|
|
23025
|
+
recording: transfer.frames.then((frames) => decodeRecording(frames, station, eccKey)),
|
|
23026
|
+
drained: transfer.drained
|
|
23027
|
+
};
|
|
22964
23028
|
}
|
|
22965
23029
|
/**
|
|
22966
23030
|
* Open a {@link Talkback} on a device's camera channel.
|
|
@@ -25692,6 +25756,8 @@ var RtcCommandRouter = class {
|
|
|
25692
25756
|
warm = /* @__PURE__ */ new Map();
|
|
25693
25757
|
callSources = /* @__PURE__ */ new Map();
|
|
25694
25758
|
lastTransaction = 0;
|
|
25759
|
+
/** The latest shard each `call` source resolved, read by its `makeStream` (see {@link callMediaProviderFor}). */
|
|
25760
|
+
callShards = /* @__PURE__ */ new Map();
|
|
25695
25761
|
constructor(deps) {
|
|
25696
25762
|
this.deps = deps;
|
|
25697
25763
|
}
|
|
@@ -25818,7 +25884,13 @@ var RtcCommandRouter = class {
|
|
|
25818
25884
|
views
|
|
25819
25885
|
};
|
|
25820
25886
|
}
|
|
25821
|
-
/**
|
|
25887
|
+
/**
|
|
25888
|
+
* Share a dedicated-call source and its consumer operations under one route key.
|
|
25889
|
+
*
|
|
25890
|
+
* `SharedLiveSource` calls `makeStream` synchronously, so the shard cannot be awaited inside it. Each
|
|
25891
|
+
* async entry point resolves the shard and records it in {@link callShards} before touching the source;
|
|
25892
|
+
* `makeStream` reads the latest recorded value.
|
|
25893
|
+
*/
|
|
25822
25894
|
callMediaProviderFor(route, key, makeStream, recordError, warmTimeoutMs) {
|
|
25823
25895
|
const sourceFor = (opts = {}) => {
|
|
25824
25896
|
opts.signal?.throwIfAborted();
|
|
@@ -25832,7 +25904,7 @@ var RtcCommandRouter = class {
|
|
|
25832
25904
|
if (!identity)
|
|
25833
25905
|
throw new Error(`rtc: not logged in, cannot call ${route.stationSn}`);
|
|
25834
25906
|
const accountId = route.adminUserId || identity.userId;
|
|
25835
|
-
return makeStream(this.newSession(route, accountId, identity), accountId, view);
|
|
25907
|
+
return makeStream(this.newSession(route, accountId, identity, this.callShards.get(key)), accountId, view);
|
|
25836
25908
|
},
|
|
25837
25909
|
label: sourceKey,
|
|
25838
25910
|
logger: this.deps.logger,
|
|
@@ -25844,14 +25916,26 @@ var RtcCommandRouter = class {
|
|
|
25844
25916
|
}
|
|
25845
25917
|
return source;
|
|
25846
25918
|
};
|
|
25919
|
+
const resolveShard = async () => {
|
|
25920
|
+
this.callShards.set(key, await this.deps.shard(route));
|
|
25921
|
+
};
|
|
25847
25922
|
return {
|
|
25848
|
-
live: async (opts) =>
|
|
25849
|
-
|
|
25850
|
-
|
|
25851
|
-
|
|
25852
|
-
|
|
25853
|
-
|
|
25854
|
-
|
|
25923
|
+
live: async (opts) => {
|
|
25924
|
+
await resolveShard();
|
|
25925
|
+
return attachUnlessAborted(sourceFor(opts), opts?.signal);
|
|
25926
|
+
},
|
|
25927
|
+
openReadable: async (opts) => {
|
|
25928
|
+
await resolveShard();
|
|
25929
|
+
return openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts);
|
|
25930
|
+
},
|
|
25931
|
+
snapshotLive: async (opts) => {
|
|
25932
|
+
await resolveShard();
|
|
25933
|
+
return captureSnapshotFromShared(sourceFor(opts), {
|
|
25934
|
+
...opts,
|
|
25935
|
+
logger: this.deps.logger,
|
|
25936
|
+
ffmpegPath: this.deps.ffmpegPath
|
|
25937
|
+
});
|
|
25938
|
+
},
|
|
25855
25939
|
record: async () => {
|
|
25856
25940
|
throw new Error(recordError);
|
|
25857
25941
|
}
|
|
@@ -25866,6 +25950,7 @@ var RtcCommandRouter = class {
|
|
|
25866
25950
|
this.callSources.delete(sn);
|
|
25867
25951
|
src.dispose();
|
|
25868
25952
|
}
|
|
25953
|
+
this.callShards.clear();
|
|
25869
25954
|
for (const [sn, live] of this.lives) {
|
|
25870
25955
|
this.lives.delete(sn);
|
|
25871
25956
|
live.source.dispose();
|
|
@@ -26059,31 +26144,50 @@ var RtcCommandRouter = class {
|
|
|
26059
26144
|
}
|
|
26060
26145
|
});
|
|
26061
26146
|
}
|
|
26062
|
-
newSession(route, adminUserId, identity) {
|
|
26147
|
+
newSession(route, adminUserId, identity, shard) {
|
|
26063
26148
|
return (this.deps.createSession ?? ((o) => new RtcSession(o)))({
|
|
26064
26149
|
authToken: identity.authToken,
|
|
26065
26150
|
gtoken: identity.gtoken,
|
|
26066
26151
|
stationSn: route.stationSn,
|
|
26067
26152
|
adminUserId,
|
|
26068
|
-
shard
|
|
26153
|
+
shard,
|
|
26069
26154
|
country: this.deps.country ?? "US",
|
|
26070
26155
|
logger: this.deps.logger,
|
|
26071
26156
|
signalingMode: route.signalingMode,
|
|
26072
26157
|
peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy, mediaTrack: route.mediaTrack }
|
|
26073
26158
|
});
|
|
26074
26159
|
}
|
|
26160
|
+
/** A connected session for `sn` to reuse, or undefined (dropping a dead one). */
|
|
26161
|
+
async reusable(sn) {
|
|
26162
|
+
const st = this.sessions.get(sn);
|
|
26163
|
+
if (!st)
|
|
26164
|
+
return void 0;
|
|
26165
|
+
await st.ready;
|
|
26166
|
+
if (st.session.isConnected) {
|
|
26167
|
+
this.touch(sn, st);
|
|
26168
|
+
return st;
|
|
26169
|
+
}
|
|
26170
|
+
this.drop(sn, st);
|
|
26171
|
+
return void 0;
|
|
26172
|
+
}
|
|
26173
|
+
/**
|
|
26174
|
+
* Get or open the station's session. A connected session is reused before the shard is awaited, so a
|
|
26175
|
+
* command on an already-connected station never waits on the unauthenticated `estimate_domain` round
|
|
26176
|
+
* trip — only the create path awaits it. After the await the map is re-checked, with no `await` between
|
|
26177
|
+
* the lookup and `this.sessions.set`, so concurrent creators still share one session.
|
|
26178
|
+
*/
|
|
26075
26179
|
async stationSession(route, adminUserId, identity) {
|
|
26076
26180
|
const sn = route.stationSn;
|
|
26077
|
-
const
|
|
26078
|
-
if (
|
|
26079
|
-
|
|
26080
|
-
|
|
26081
|
-
|
|
26082
|
-
|
|
26083
|
-
|
|
26084
|
-
|
|
26085
|
-
}
|
|
26086
|
-
const session = this.newSession(route, adminUserId, identity);
|
|
26181
|
+
const reused = await this.reusable(sn);
|
|
26182
|
+
if (reused)
|
|
26183
|
+
return reused;
|
|
26184
|
+
const shard = await this.deps.shard(route);
|
|
26185
|
+
if (this.sessions.has(sn)) {
|
|
26186
|
+
const again = await this.reusable(sn);
|
|
26187
|
+
if (again)
|
|
26188
|
+
return again;
|
|
26189
|
+
}
|
|
26190
|
+
const session = this.newSession(route, adminUserId, identity, shard);
|
|
26087
26191
|
const ready = this.bringUp(sn, session);
|
|
26088
26192
|
const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
|
|
26089
26193
|
session.on("error", (e) => this.deps.onError?.(e));
|
|
@@ -29733,7 +29837,7 @@ var EufyMega = class _EufyMega extends EventEmitter14 {
|
|
|
29733
29837
|
});
|
|
29734
29838
|
this.rtc = new RtcCommandRouter({
|
|
29735
29839
|
identity: () => this.mega.rtcIdentity(),
|
|
29736
|
-
shard: () => this.mega.rtcShard,
|
|
29840
|
+
shard: (route) => route.mediaTrack ? this.mega.rtcShard : this.mega.portalShard(),
|
|
29737
29841
|
country: opts.countryCode,
|
|
29738
29842
|
logger: opts.logger,
|
|
29739
29843
|
ffmpegPath: opts.ffmpegPath,
|