@mega-yfue/eufy-sdk 0.4.0-beta.15 → 0.4.0-beta.17
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 +18 -2
- package/dist/client/types.d.ts +25 -0
- package/dist/core/contracts.d.ts +43 -0
- package/dist/index.js +523 -112
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/camera.d.ts +1 -0
- package/dist/transport/p2p/adts.d.ts +6 -0
- package/dist/transport/p2p/codec.d.ts +8 -0
- package/dist/transport/p2p/command-router.d.ts +29 -0
- package/dist/transport/p2p/p2p-session.d.ts +25 -0
- package/dist/transport/p2p/recording-download.d.ts +54 -0
- package/dist/transport/p2p/video.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1147,6 +1147,14 @@ var DeviceChannelUnresolvedError = class extends Error {
|
|
|
1147
1147
|
this.name = "DeviceChannelUnresolvedError";
|
|
1148
1148
|
}
|
|
1149
1149
|
};
|
|
1150
|
+
var RecordingDownloadError = class extends Error {
|
|
1151
|
+
reason;
|
|
1152
|
+
constructor(reason, message, options) {
|
|
1153
|
+
super(message, options);
|
|
1154
|
+
this.reason = reason;
|
|
1155
|
+
this.name = "RecordingDownloadError";
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1150
1158
|
var LiveStreamStartError = class extends Error {
|
|
1151
1159
|
reason;
|
|
1152
1160
|
stage;
|
|
@@ -5702,6 +5710,7 @@ var CAMERA_MEMBERS = {
|
|
|
5702
5710
|
refuseWhenDisabled(ctx, read);
|
|
5703
5711
|
return m.recordFragments({ powered: poweredOf(ctx), ...opts });
|
|
5704
5712
|
}), "Continuous fragmented-MP4 (CMAF) recording."),
|
|
5713
|
+
downloadRecording: provided("media", (m) => m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 and AAC elementary streams."),
|
|
5705
5714
|
/**
|
|
5706
5715
|
* Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
|
|
5707
5716
|
* not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
|
|
@@ -14342,6 +14351,7 @@ __export(codec_exports, {
|
|
|
14342
14351
|
buildLookupWithKeyPayload2: () => buildLookupWithKeyPayload2,
|
|
14343
14352
|
buildRawCommandPayload: () => buildRawCommandPayload,
|
|
14344
14353
|
buildStringCommandPayload: () => buildStringCommandPayload,
|
|
14354
|
+
buildStringPairCommandPayload: () => buildStringPairCommandPayload,
|
|
14345
14355
|
buildVoidCommandPayload: () => buildVoidCommandPayload,
|
|
14346
14356
|
decodeP2PCloudIPs: () => decodeP2PCloudIPs,
|
|
14347
14357
|
decryptP2PData: () => decryptP2PData,
|
|
@@ -14594,6 +14604,12 @@ function buildIntStringCommandPayload(value, valueSub, strValue, channel = 0, ke
|
|
|
14594
14604
|
data
|
|
14595
14605
|
]);
|
|
14596
14606
|
}
|
|
14607
|
+
function buildStringPairCommandPayload(strValue, strValueSub, channel = 0, key, encType = 1) {
|
|
14608
|
+
const encrypted = !!key && key.length === 16;
|
|
14609
|
+
const body = Buffer.concat([Buffer.alloc(5), stringWithLength(strValue), stringWithLength(strValueSub)]);
|
|
14610
|
+
const data = encrypted ? encryptP2PData(paddingP2PData(body), key) : body;
|
|
14611
|
+
return buildRawCommandPayload(data, channel, encrypted ? encType : 0);
|
|
14612
|
+
}
|
|
14597
14613
|
function buildRawCommandPayload(data, channel = 0, signCode = 0, magic = [1, 0], streamId = 0) {
|
|
14598
14614
|
const header = Buffer.allocUnsafe(2);
|
|
14599
14615
|
header.writeUInt16LE(data.length, 0);
|
|
@@ -15250,6 +15266,49 @@ function paramReport(payload) {
|
|
|
15250
15266
|
return Object.keys(out).length ? out : void 0;
|
|
15251
15267
|
}
|
|
15252
15268
|
var traceSequence = 0;
|
|
15269
|
+
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
15270
|
+
function firstTableRows(text2) {
|
|
15271
|
+
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
15272
|
+
for (let start = decoded.indexOf("{"); start >= 0; start = decoded.indexOf("{", start + 1)) {
|
|
15273
|
+
const end = closingBrace(decoded, start);
|
|
15274
|
+
if (end < 0)
|
|
15275
|
+
return void 0;
|
|
15276
|
+
let parsed;
|
|
15277
|
+
try {
|
|
15278
|
+
parsed = JSON.parse(decoded.slice(start, end + 1));
|
|
15279
|
+
} catch {
|
|
15280
|
+
continue;
|
|
15281
|
+
}
|
|
15282
|
+
const data = parsed?.data;
|
|
15283
|
+
if (Array.isArray(data))
|
|
15284
|
+
return data;
|
|
15285
|
+
}
|
|
15286
|
+
return void 0;
|
|
15287
|
+
}
|
|
15288
|
+
function closingBrace(text2, start) {
|
|
15289
|
+
let depth = 0;
|
|
15290
|
+
let inString = false;
|
|
15291
|
+
let escaped = false;
|
|
15292
|
+
for (let i = start; i < text2.length; i++) {
|
|
15293
|
+
const c = text2[i];
|
|
15294
|
+
if (inString) {
|
|
15295
|
+
if (escaped)
|
|
15296
|
+
escaped = false;
|
|
15297
|
+
else if (c === "\\")
|
|
15298
|
+
escaped = true;
|
|
15299
|
+
else if (c === '"')
|
|
15300
|
+
inString = false;
|
|
15301
|
+
continue;
|
|
15302
|
+
}
|
|
15303
|
+
if (c === '"')
|
|
15304
|
+
inString = true;
|
|
15305
|
+
else if (c === "{")
|
|
15306
|
+
depth++;
|
|
15307
|
+
else if (c === "}" && --depth === 0)
|
|
15308
|
+
return i;
|
|
15309
|
+
}
|
|
15310
|
+
return -1;
|
|
15311
|
+
}
|
|
15253
15312
|
var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
15254
15313
|
cfg;
|
|
15255
15314
|
socket;
|
|
@@ -16066,6 +16125,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16066
16125
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16067
16126
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16068
16127
|
}
|
|
16128
|
+
/** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
|
|
16129
|
+
sendStringPairCommand(commandType, strValue, strValueSub, channel) {
|
|
16130
|
+
if (!this.connectAddress)
|
|
16131
|
+
throw new Error(`P2P session ${this.cfg.stationSn} is not connected`);
|
|
16132
|
+
const data = Buffer.concat([
|
|
16133
|
+
buildCommandHeader(this.seqNumber, commandType),
|
|
16134
|
+
buildStringPairCommandPayload(strValue, strValueSub, channel, this.level1Key, 1)
|
|
16135
|
+
]);
|
|
16136
|
+
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16137
|
+
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16138
|
+
}
|
|
16069
16139
|
/**
|
|
16070
16140
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
16071
16141
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -16468,8 +16538,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16468
16538
|
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
16469
16539
|
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
16470
16540
|
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
16541
|
+
*
|
|
16542
|
+
* Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
|
|
16543
|
+
* frames tie no chunk to its request, so a second query's reply would be assembled into the first
|
|
16544
|
+
* one's buffer and answered as its rows.
|
|
16471
16545
|
*/
|
|
16472
16546
|
queryDatabase(table, opts = {}) {
|
|
16547
|
+
if (this.dbReadInFlight)
|
|
16548
|
+
throw new Error(`queryDatabase: ${this.cfg.stationSn} is already reading a table`);
|
|
16473
16549
|
if (!this.connectAddress)
|
|
16474
16550
|
throw new Error("not connected");
|
|
16475
16551
|
const channel = opts.channel ?? STATION_CHANNEL3;
|
|
@@ -16532,6 +16608,49 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16532
16608
|
query: this.fullTableQuery()
|
|
16533
16609
|
});
|
|
16534
16610
|
}
|
|
16611
|
+
/**
|
|
16612
|
+
* Query one on-station table and answer its rows, once the reply is whole.
|
|
16613
|
+
*
|
|
16614
|
+
* The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
|
|
16615
|
+
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
16616
|
+
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
16617
|
+
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
16618
|
+
* complete reply.
|
|
16619
|
+
*
|
|
16620
|
+
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
16621
|
+
* session throws, so a second reply cannot land in this buffer.
|
|
16622
|
+
*/
|
|
16623
|
+
async readDatabase(table, opts = {}) {
|
|
16624
|
+
if (opts.signal?.aborted)
|
|
16625
|
+
throw new Error("readDatabase: aborted");
|
|
16626
|
+
this.queryDatabase(table, { accountId: opts.accountId, query: this.fullTableQuery() });
|
|
16627
|
+
this.dbReadInFlight = true;
|
|
16628
|
+
try {
|
|
16629
|
+
return await new Promise((resolve, reject) => {
|
|
16630
|
+
let text2 = "";
|
|
16631
|
+
const onChunk = (chunk) => {
|
|
16632
|
+
text2 += chunk.text;
|
|
16633
|
+
const rows = firstTableRows(text2);
|
|
16634
|
+
if (rows)
|
|
16635
|
+
settle(() => resolve(rows));
|
|
16636
|
+
};
|
|
16637
|
+
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
16638
|
+
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
16639
|
+
const settle = (finish) => {
|
|
16640
|
+
clearTimeout(timer);
|
|
16641
|
+
this.off("dbChunk", onChunk);
|
|
16642
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
16643
|
+
finish();
|
|
16644
|
+
};
|
|
16645
|
+
this.on("dbChunk", onChunk);
|
|
16646
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
16647
|
+
});
|
|
16648
|
+
} finally {
|
|
16649
|
+
this.dbReadInFlight = false;
|
|
16650
|
+
}
|
|
16651
|
+
}
|
|
16652
|
+
/** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
|
|
16653
|
+
dbReadInFlight = false;
|
|
16535
16654
|
/**
|
|
16536
16655
|
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
16537
16656
|
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
@@ -18244,6 +18363,282 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
18244
18363
|
});
|
|
18245
18364
|
}
|
|
18246
18365
|
|
|
18366
|
+
// dist/transport/p2p/recording-download.js
|
|
18367
|
+
import { createDecipheriv as createDecipheriv7 } from "node:crypto";
|
|
18368
|
+
|
|
18369
|
+
// dist/transport/p2p/adts.js
|
|
18370
|
+
var PROFILE_AAC_LC = 1;
|
|
18371
|
+
var FREQ_INDEX_16K = 8;
|
|
18372
|
+
var CHANNELS_MONO = 1;
|
|
18373
|
+
var AAC_SAMPLES_PER_FRAME = 1024;
|
|
18374
|
+
var AAC_SAMPLE_RATE = 16e3;
|
|
18375
|
+
var AAC_FRAME_MS = AAC_SAMPLES_PER_FRAME * 1e3 / AAC_SAMPLE_RATE;
|
|
18376
|
+
var MAX_AUDIO_FRAME_BYTES = 640;
|
|
18377
|
+
var HEADER_LEN_NO_CRC = 7;
|
|
18378
|
+
function parseAdtsHeader(buf, offset = 0) {
|
|
18379
|
+
if (offset + HEADER_LEN_NO_CRC > buf.length)
|
|
18380
|
+
return void 0;
|
|
18381
|
+
if (buf[offset] !== 255 || (buf[offset + 1] & 240) !== 240)
|
|
18382
|
+
return void 0;
|
|
18383
|
+
const headerLength = buf[offset + 1] & 1 ? 7 : 9;
|
|
18384
|
+
const frameLength = (buf[offset + 3] & 3) << 11 | buf[offset + 4] << 3 | (buf[offset + 5] & 224) >> 5;
|
|
18385
|
+
if (frameLength < headerLength)
|
|
18386
|
+
return void 0;
|
|
18387
|
+
return {
|
|
18388
|
+
frameLength,
|
|
18389
|
+
headerLength,
|
|
18390
|
+
profile: (buf[offset + 2] & 192) >> 6,
|
|
18391
|
+
frequencyIndex: (buf[offset + 2] & 60) >> 2,
|
|
18392
|
+
channels: (buf[offset + 2] & 1) << 2 | (buf[offset + 3] & 192) >> 6
|
|
18393
|
+
};
|
|
18394
|
+
}
|
|
18395
|
+
function buildAdtsHeader(payloadLength) {
|
|
18396
|
+
const frameLength = payloadLength + HEADER_LEN_NO_CRC;
|
|
18397
|
+
return Buffer.from([
|
|
18398
|
+
255,
|
|
18399
|
+
241,
|
|
18400
|
+
PROFILE_AAC_LC << 6 | FREQ_INDEX_16K << 2 | CHANNELS_MONO >> 2,
|
|
18401
|
+
(CHANNELS_MONO & 3) << 6 | frameLength >> 11 & 3,
|
|
18402
|
+
frameLength >> 3 & 255,
|
|
18403
|
+
(frameLength & 7) << 5 | 31,
|
|
18404
|
+
252
|
|
18405
|
+
]);
|
|
18406
|
+
}
|
|
18407
|
+
function isSupportedAdts(h) {
|
|
18408
|
+
return h.profile === PROFILE_AAC_LC && h.frequencyIndex === FREQ_INDEX_16K && h.channels === CHANNELS_MONO;
|
|
18409
|
+
}
|
|
18410
|
+
function describeAdts(h) {
|
|
18411
|
+
const rate = ADTS_SAMPLE_RATES[h.frequencyIndex] ?? `index ${h.frequencyIndex}`;
|
|
18412
|
+
const profile = h.profile === PROFILE_AAC_LC ? "AAC-LC" : `profile field ${h.profile}`;
|
|
18413
|
+
return `${profile}, ${rate} Hz, ${h.channels} channel(s)`;
|
|
18414
|
+
}
|
|
18415
|
+
var ADTS_SAMPLE_RATES = [
|
|
18416
|
+
96e3,
|
|
18417
|
+
88200,
|
|
18418
|
+
64e3,
|
|
18419
|
+
48e3,
|
|
18420
|
+
44100,
|
|
18421
|
+
32e3,
|
|
18422
|
+
24e3,
|
|
18423
|
+
22050,
|
|
18424
|
+
16e3,
|
|
18425
|
+
12e3,
|
|
18426
|
+
11025,
|
|
18427
|
+
8e3,
|
|
18428
|
+
7350
|
|
18429
|
+
];
|
|
18430
|
+
var AdtsFrameReader = class {
|
|
18431
|
+
buffered = Buffer.alloc(0);
|
|
18432
|
+
/**
|
|
18433
|
+
* Append `chunk` and return every complete frame now available, each a standalone buffer that still
|
|
18434
|
+
* carries its own ADTS header (the device expects the header on the wire).
|
|
18435
|
+
*/
|
|
18436
|
+
push(chunk) {
|
|
18437
|
+
this.buffered = this.buffered.length ? Buffer.concat([this.buffered, chunk]) : chunk;
|
|
18438
|
+
const frames = [];
|
|
18439
|
+
let at = 0;
|
|
18440
|
+
for (; ; ) {
|
|
18441
|
+
const h = parseAdtsHeader(this.buffered, at);
|
|
18442
|
+
if (!h) {
|
|
18443
|
+
if (at + HEADER_LEN_NO_CRC > this.buffered.length)
|
|
18444
|
+
break;
|
|
18445
|
+
at++;
|
|
18446
|
+
continue;
|
|
18447
|
+
}
|
|
18448
|
+
if (at + h.frameLength > this.buffered.length)
|
|
18449
|
+
break;
|
|
18450
|
+
frames.push(this.buffered.subarray(at, at + h.frameLength));
|
|
18451
|
+
at += h.frameLength;
|
|
18452
|
+
}
|
|
18453
|
+
this.buffered = at ? this.buffered.subarray(at) : this.buffered;
|
|
18454
|
+
return frames;
|
|
18455
|
+
}
|
|
18456
|
+
/** Bytes held back awaiting the rest of their frame — non-zero mid-stream, zero on a clean boundary. */
|
|
18457
|
+
get pending() {
|
|
18458
|
+
return this.buffered.length;
|
|
18459
|
+
}
|
|
18460
|
+
};
|
|
18461
|
+
|
|
18462
|
+
// dist/transport/p2p/recording-download.js
|
|
18463
|
+
var RECORDING_DATA_TYPE = 3;
|
|
18464
|
+
var VIDEO_HEADER_LEN2 = 22;
|
|
18465
|
+
var AUDIO_HEADER_LEN2 = 16;
|
|
18466
|
+
var AUDIO_NONCE_START = 32;
|
|
18467
|
+
var AUDIO_BODY_START = 44;
|
|
18468
|
+
var AUDIO_CODEC_AAC_LC = 0;
|
|
18469
|
+
var FIRST_FRAME_WAIT_MS = 2e4;
|
|
18470
|
+
var IDLE_END_MS = 15e3;
|
|
18471
|
+
var DEFAULT_TRANSFER_MS = 12e4;
|
|
18472
|
+
var MAX_TRANSFER_BYTES = 32 * 1024 * 1024;
|
|
18473
|
+
function homeBase2RecordingPath(channel, recording) {
|
|
18474
|
+
if (!/^\d{14}$/.test(recording) || !Number.isInteger(channel) || channel < 0 || channel > 99)
|
|
18475
|
+
return void 0;
|
|
18476
|
+
return `/media/mmcblk0p1/Camera${String(channel).padStart(2, "0")}/${recording}.dat`;
|
|
18477
|
+
}
|
|
18478
|
+
function receiveRecording(session, request) {
|
|
18479
|
+
let resolveFrames;
|
|
18480
|
+
let rejectFrames;
|
|
18481
|
+
const frames = new Promise((resolve, reject) => {
|
|
18482
|
+
resolveFrames = resolve;
|
|
18483
|
+
rejectFrames = reject;
|
|
18484
|
+
});
|
|
18485
|
+
let resolveDrained;
|
|
18486
|
+
const drained = new Promise((resolve) => resolveDrained = resolve);
|
|
18487
|
+
const limitMs = request.timeoutMs ?? DEFAULT_TRANSFER_MS;
|
|
18488
|
+
let collected = [];
|
|
18489
|
+
const started = Date.now();
|
|
18490
|
+
let lastFrameAt = started;
|
|
18491
|
+
let received = false;
|
|
18492
|
+
let bytes2 = 0;
|
|
18493
|
+
let draining;
|
|
18494
|
+
let delivered = false;
|
|
18495
|
+
let listening = true;
|
|
18496
|
+
const deliver = (error) => {
|
|
18497
|
+
if (delivered)
|
|
18498
|
+
return;
|
|
18499
|
+
delivered = true;
|
|
18500
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
18501
|
+
if (error)
|
|
18502
|
+
rejectFrames(error);
|
|
18503
|
+
else
|
|
18504
|
+
resolveFrames(collected);
|
|
18505
|
+
};
|
|
18506
|
+
const stop = () => {
|
|
18507
|
+
if (!listening)
|
|
18508
|
+
return;
|
|
18509
|
+
listening = false;
|
|
18510
|
+
clearInterval(tick);
|
|
18511
|
+
session.off("data", onData);
|
|
18512
|
+
resolveDrained();
|
|
18513
|
+
};
|
|
18514
|
+
const incomplete = (why) => deliver(new RecordingDownloadError("incomplete", `the recording transfer ${why} before its finish frame`));
|
|
18515
|
+
const drain = (why) => {
|
|
18516
|
+
draining ??= why;
|
|
18517
|
+
collected = [];
|
|
18518
|
+
};
|
|
18519
|
+
const onData = (frame) => {
|
|
18520
|
+
if (frame.channel !== request.channel)
|
|
18521
|
+
return;
|
|
18522
|
+
if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
|
|
18523
|
+
if (draining)
|
|
18524
|
+
incomplete(draining);
|
|
18525
|
+
else
|
|
18526
|
+
deliver();
|
|
18527
|
+
return stop();
|
|
18528
|
+
}
|
|
18529
|
+
if (frame.commandId !== CommandType.CMD_VIDEO_FRAME && frame.commandId !== CommandType.CMD_AUDIO_FRAME)
|
|
18530
|
+
return;
|
|
18531
|
+
if (frame.dataType !== RECORDING_DATA_TYPE)
|
|
18532
|
+
return;
|
|
18533
|
+
received = true;
|
|
18534
|
+
lastFrameAt = Date.now();
|
|
18535
|
+
if (draining)
|
|
18536
|
+
return;
|
|
18537
|
+
collected.push({ commandId: frame.commandId, signCode: frame.signCode, raw: Buffer.from(frame.raw) });
|
|
18538
|
+
bytes2 += frame.raw.length;
|
|
18539
|
+
if (bytes2 >= MAX_TRANSFER_BYTES)
|
|
18540
|
+
drain(`reached ${MAX_TRANSFER_BYTES} bytes`);
|
|
18541
|
+
};
|
|
18542
|
+
const onAbort = () => {
|
|
18543
|
+
deliver(request.signal?.reason instanceof Error ? request.signal.reason : new Error("aborted"));
|
|
18544
|
+
drain("was aborted");
|
|
18545
|
+
};
|
|
18546
|
+
const tick = setInterval(() => {
|
|
18547
|
+
const now2 = Date.now();
|
|
18548
|
+
if (!received && now2 - started > FIRST_FRAME_WAIT_MS) {
|
|
18549
|
+
deliver(new RecordingDownloadError("no-data", `the station sent nothing within ${FIRST_FRAME_WAIT_MS} ms`));
|
|
18550
|
+
stop();
|
|
18551
|
+
} else if (received && now2 - lastFrameAt > IDLE_END_MS) {
|
|
18552
|
+
incomplete(draining ?? `went quiet for ${IDLE_END_MS} ms`);
|
|
18553
|
+
stop();
|
|
18554
|
+
} else if (now2 - started > 2 * limitMs && draining) {
|
|
18555
|
+
incomplete(draining);
|
|
18556
|
+
stop();
|
|
18557
|
+
} else if (now2 - started > limitMs) {
|
|
18558
|
+
drain(`ran past ${limitMs} ms`);
|
|
18559
|
+
}
|
|
18560
|
+
}, 250);
|
|
18561
|
+
if (request.signal?.aborted) {
|
|
18562
|
+
onAbort();
|
|
18563
|
+
stop();
|
|
18564
|
+
return { frames, drained };
|
|
18565
|
+
}
|
|
18566
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
18567
|
+
session.on("data", onData);
|
|
18568
|
+
try {
|
|
18569
|
+
session.sendStringPairCommand(CommandType.CMD_DOWNLOAD_VIDEO, request.path, request.accountId, request.channel);
|
|
18570
|
+
} catch (error) {
|
|
18571
|
+
deliver(error instanceof Error ? error : new Error(String(error)));
|
|
18572
|
+
stop();
|
|
18573
|
+
}
|
|
18574
|
+
return { frames, drained };
|
|
18575
|
+
}
|
|
18576
|
+
function openAudio(key, nonce, tag2, body) {
|
|
18577
|
+
try {
|
|
18578
|
+
const decipher = createDecipheriv7("aes-256-gcm", key, nonce);
|
|
18579
|
+
decipher.setAAD(VIDEO_GCM_AAD);
|
|
18580
|
+
decipher.setAuthTag(tag2);
|
|
18581
|
+
return Buffer.concat([decipher.update(body), decipher.final()]);
|
|
18582
|
+
} catch {
|
|
18583
|
+
return void 0;
|
|
18584
|
+
}
|
|
18585
|
+
}
|
|
18586
|
+
function decodeRecording(frames, eccPrivateKeyHex) {
|
|
18587
|
+
const eccPrivateKey = Buffer.from(eccPrivateKeyHex, "hex");
|
|
18588
|
+
if (eccPrivateKey.length !== 32)
|
|
18589
|
+
throw new RecordingDownloadError("undecodable", "the cipher key is not a P-256 key");
|
|
18590
|
+
const decoder = new VideoFrameDecoder(eccPrivateKey);
|
|
18591
|
+
const video = [];
|
|
18592
|
+
const audio = [];
|
|
18593
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
18594
|
+
let firstStamp;
|
|
18595
|
+
let lastStamp;
|
|
18596
|
+
for (const { commandId, raw } of frames) {
|
|
18597
|
+
if (commandId === CommandType.CMD_VIDEO_FRAME) {
|
|
18598
|
+
const header = parseVideoFrameHeader(raw);
|
|
18599
|
+
if (!header)
|
|
18600
|
+
continue;
|
|
18601
|
+
const h264 = header.keyframe ? decoder.decodeFrame(raw)?.h264 : raw.length >= VIDEO_HEADER_LEN2 + header.payloadLength ? raw.subarray(VIDEO_HEADER_LEN2, VIDEO_HEADER_LEN2 + header.payloadLength) : void 0;
|
|
18602
|
+
if (!h264?.length)
|
|
18603
|
+
continue;
|
|
18604
|
+
video.push(h264);
|
|
18605
|
+
numbers.add(header.sequence);
|
|
18606
|
+
const stamp = raw.readUIntLE(14, 6);
|
|
18607
|
+
firstStamp ??= stamp;
|
|
18608
|
+
lastStamp = stamp;
|
|
18609
|
+
} else if (commandId === CommandType.CMD_AUDIO_FRAME) {
|
|
18610
|
+
const mediaKey = decoder.currentMediaKey;
|
|
18611
|
+
if (!mediaKey || raw.length < AUDIO_BODY_START || raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
18612
|
+
continue;
|
|
18613
|
+
const len = raw.readUInt32LE(0);
|
|
18614
|
+
if (raw.length < AUDIO_BODY_START + len)
|
|
18615
|
+
continue;
|
|
18616
|
+
const aac = openAudio(mediaKey, raw.subarray(AUDIO_NONCE_START, AUDIO_BODY_START), raw.subarray(AUDIO_HEADER_LEN2, AUDIO_NONCE_START), raw.subarray(AUDIO_BODY_START, AUDIO_BODY_START + len));
|
|
18617
|
+
if (aac?.length)
|
|
18618
|
+
audio.push(buildAdtsHeader(aac.length), aac);
|
|
18619
|
+
}
|
|
18620
|
+
}
|
|
18621
|
+
if (!video.length)
|
|
18622
|
+
throw new RecordingDownloadError("undecodable", "no video frame of the recording decoded");
|
|
18623
|
+
const ordered = [...numbers];
|
|
18624
|
+
let missingFrames = 0;
|
|
18625
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
18626
|
+
const step = ordered[i] - ordered[i - 1] & 65535;
|
|
18627
|
+
if (step > 1 && step < 32768)
|
|
18628
|
+
missingFrames += step - 1;
|
|
18629
|
+
}
|
|
18630
|
+
const span = ordered.length > 1 ? ordered[ordered.length - 1] - ordered[0] & 65535 : 0;
|
|
18631
|
+
const durationMs = (lastStamp ?? 0) - (firstStamp ?? 0);
|
|
18632
|
+
return {
|
|
18633
|
+
video: Buffer.concat(video),
|
|
18634
|
+
...audio.length ? { audio: Buffer.concat(audio) } : {},
|
|
18635
|
+
frames: ordered.length,
|
|
18636
|
+
missingFrames,
|
|
18637
|
+
durationMs,
|
|
18638
|
+
fps: durationMs > 0 && span > 0 ? Math.round(span * 1e5 / durationMs) / 100 : 0
|
|
18639
|
+
};
|
|
18640
|
+
}
|
|
18641
|
+
|
|
18247
18642
|
// dist/transport/p2p/shared-live-source.js
|
|
18248
18643
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
18249
18644
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
@@ -19366,89 +19761,6 @@ function openReadableFromConsumer(consumer, opts = {}) {
|
|
|
19366
19761
|
// dist/transport/p2p/talkback.js
|
|
19367
19762
|
import { EventEmitter as EventEmitter5 } from "node:events";
|
|
19368
19763
|
import { Writable } from "node:stream";
|
|
19369
|
-
|
|
19370
|
-
// dist/transport/p2p/adts.js
|
|
19371
|
-
var PROFILE_AAC_LC = 1;
|
|
19372
|
-
var FREQ_INDEX_16K = 8;
|
|
19373
|
-
var CHANNELS_MONO = 1;
|
|
19374
|
-
var AAC_SAMPLES_PER_FRAME = 1024;
|
|
19375
|
-
var AAC_SAMPLE_RATE = 16e3;
|
|
19376
|
-
var AAC_FRAME_MS = AAC_SAMPLES_PER_FRAME * 1e3 / AAC_SAMPLE_RATE;
|
|
19377
|
-
var MAX_AUDIO_FRAME_BYTES = 640;
|
|
19378
|
-
var HEADER_LEN_NO_CRC = 7;
|
|
19379
|
-
function parseAdtsHeader(buf, offset = 0) {
|
|
19380
|
-
if (offset + HEADER_LEN_NO_CRC > buf.length)
|
|
19381
|
-
return void 0;
|
|
19382
|
-
if (buf[offset] !== 255 || (buf[offset + 1] & 240) !== 240)
|
|
19383
|
-
return void 0;
|
|
19384
|
-
const headerLength = buf[offset + 1] & 1 ? 7 : 9;
|
|
19385
|
-
const frameLength = (buf[offset + 3] & 3) << 11 | buf[offset + 4] << 3 | (buf[offset + 5] & 224) >> 5;
|
|
19386
|
-
if (frameLength < headerLength)
|
|
19387
|
-
return void 0;
|
|
19388
|
-
return {
|
|
19389
|
-
frameLength,
|
|
19390
|
-
headerLength,
|
|
19391
|
-
profile: (buf[offset + 2] & 192) >> 6,
|
|
19392
|
-
frequencyIndex: (buf[offset + 2] & 60) >> 2,
|
|
19393
|
-
channels: (buf[offset + 2] & 1) << 2 | (buf[offset + 3] & 192) >> 6
|
|
19394
|
-
};
|
|
19395
|
-
}
|
|
19396
|
-
function isSupportedAdts(h) {
|
|
19397
|
-
return h.profile === PROFILE_AAC_LC && h.frequencyIndex === FREQ_INDEX_16K && h.channels === CHANNELS_MONO;
|
|
19398
|
-
}
|
|
19399
|
-
function describeAdts(h) {
|
|
19400
|
-
const rate = ADTS_SAMPLE_RATES[h.frequencyIndex] ?? `index ${h.frequencyIndex}`;
|
|
19401
|
-
const profile = h.profile === PROFILE_AAC_LC ? "AAC-LC" : `profile field ${h.profile}`;
|
|
19402
|
-
return `${profile}, ${rate} Hz, ${h.channels} channel(s)`;
|
|
19403
|
-
}
|
|
19404
|
-
var ADTS_SAMPLE_RATES = [
|
|
19405
|
-
96e3,
|
|
19406
|
-
88200,
|
|
19407
|
-
64e3,
|
|
19408
|
-
48e3,
|
|
19409
|
-
44100,
|
|
19410
|
-
32e3,
|
|
19411
|
-
24e3,
|
|
19412
|
-
22050,
|
|
19413
|
-
16e3,
|
|
19414
|
-
12e3,
|
|
19415
|
-
11025,
|
|
19416
|
-
8e3,
|
|
19417
|
-
7350
|
|
19418
|
-
];
|
|
19419
|
-
var AdtsFrameReader = class {
|
|
19420
|
-
buffered = Buffer.alloc(0);
|
|
19421
|
-
/**
|
|
19422
|
-
* Append `chunk` and return every complete frame now available, each a standalone buffer that still
|
|
19423
|
-
* carries its own ADTS header (the device expects the header on the wire).
|
|
19424
|
-
*/
|
|
19425
|
-
push(chunk) {
|
|
19426
|
-
this.buffered = this.buffered.length ? Buffer.concat([this.buffered, chunk]) : chunk;
|
|
19427
|
-
const frames = [];
|
|
19428
|
-
let at = 0;
|
|
19429
|
-
for (; ; ) {
|
|
19430
|
-
const h = parseAdtsHeader(this.buffered, at);
|
|
19431
|
-
if (!h) {
|
|
19432
|
-
if (at + HEADER_LEN_NO_CRC > this.buffered.length)
|
|
19433
|
-
break;
|
|
19434
|
-
at++;
|
|
19435
|
-
continue;
|
|
19436
|
-
}
|
|
19437
|
-
if (at + h.frameLength > this.buffered.length)
|
|
19438
|
-
break;
|
|
19439
|
-
frames.push(this.buffered.subarray(at, at + h.frameLength));
|
|
19440
|
-
at += h.frameLength;
|
|
19441
|
-
}
|
|
19442
|
-
this.buffered = at ? this.buffered.subarray(at) : this.buffered;
|
|
19443
|
-
return frames;
|
|
19444
|
-
}
|
|
19445
|
-
/** Bytes held back awaiting the rest of their frame — non-zero mid-stream, zero on a clean boundary. */
|
|
19446
|
-
get pending() {
|
|
19447
|
-
return this.buffered.length;
|
|
19448
|
-
}
|
|
19449
|
-
};
|
|
19450
|
-
|
|
19451
|
-
// dist/transport/p2p/talkback.js
|
|
19452
19764
|
var DEFAULT_HIGH_WATER_FRAMES = Math.ceil(2e3 / AAC_FRAME_MS);
|
|
19453
19765
|
var DEFAULT_IDLE_TIMEOUT_MS = 3e4;
|
|
19454
19766
|
var Talkback = class extends EventEmitter5 {
|
|
@@ -20424,6 +20736,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20424
20736
|
talkbacks = /* @__PURE__ */ new Map();
|
|
20425
20737
|
/** cipher_id → ECC private key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
20426
20738
|
cipherKeyCache = /* @__PURE__ */ new Map();
|
|
20739
|
+
/** The recording download in flight per station; a station serves one at a time. */
|
|
20740
|
+
recordingDownloads = /* @__PURE__ */ new Map();
|
|
20427
20741
|
constructor(deps) {
|
|
20428
20742
|
this.deps = deps;
|
|
20429
20743
|
this.manager = new SessionManager({
|
|
@@ -20636,28 +20950,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20636
20950
|
localAddress,
|
|
20637
20951
|
dskKey,
|
|
20638
20952
|
noBroadcast: this.deps.noBroadcast,
|
|
20639
|
-
resolveCipherKey:
|
|
20640
|
-
if (this.cipherKeyCache.has(cipherId))
|
|
20641
|
-
return this.cipherKeyCache.get(cipherId);
|
|
20642
|
-
let ecc;
|
|
20643
|
-
try {
|
|
20644
|
-
const ciphers = await this.deps.mega.getCiphers([cipherId], adminUserId, stationSn);
|
|
20645
|
-
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
20646
|
-
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
20647
|
-
ecc = ciphers[0].ecc_private_key;
|
|
20648
|
-
this.traceOnStation(session, {
|
|
20649
|
-
phase: "cipher-fallback",
|
|
20650
|
-
cipherId,
|
|
20651
|
-
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
20652
|
-
});
|
|
20653
|
-
}
|
|
20654
|
-
} catch (e) {
|
|
20655
|
-
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
20656
|
-
}
|
|
20657
|
-
if (ecc !== void 0)
|
|
20658
|
-
this.cipherKeyCache.set(cipherId, ecc);
|
|
20659
|
-
return ecc;
|
|
20660
|
-
},
|
|
20953
|
+
resolveCipherKey: (cipherId) => this.cipherKeyFor(session, cipherId, adminUserId, stationSn),
|
|
20661
20954
|
logger: this.deps.logger ?? noopLogger
|
|
20662
20955
|
});
|
|
20663
20956
|
session.on("error", (e) => this.reportError(e));
|
|
@@ -20681,6 +20974,33 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20681
20974
|
session.on("data", (f) => this.deps.onFrame(stationSn, f));
|
|
20682
20975
|
return session;
|
|
20683
20976
|
}
|
|
20977
|
+
/**
|
|
20978
|
+
* The ECC private key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
|
|
20979
|
+
* answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
|
|
20980
|
+
* cached.
|
|
20981
|
+
*/
|
|
20982
|
+
async cipherKeyFor(session, cipherId, userId, stationSn) {
|
|
20983
|
+
if (this.cipherKeyCache.has(cipherId))
|
|
20984
|
+
return this.cipherKeyCache.get(cipherId);
|
|
20985
|
+
let ecc;
|
|
20986
|
+
try {
|
|
20987
|
+
const ciphers = await this.deps.mega.getCiphers([cipherId], userId, stationSn);
|
|
20988
|
+
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
20989
|
+
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
20990
|
+
ecc = ciphers[0].ecc_private_key;
|
|
20991
|
+
this.traceOnStation(session, {
|
|
20992
|
+
phase: "cipher-fallback",
|
|
20993
|
+
cipherId,
|
|
20994
|
+
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
20995
|
+
});
|
|
20996
|
+
}
|
|
20997
|
+
} catch (e) {
|
|
20998
|
+
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
20999
|
+
}
|
|
21000
|
+
if (ecc !== void 0)
|
|
21001
|
+
this.cipherKeyCache.set(cipherId, ecc);
|
|
21002
|
+
return ecc;
|
|
21003
|
+
}
|
|
20684
21004
|
/**
|
|
20685
21005
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
20686
21006
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -20808,7 +21128,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20808
21128
|
* start has no level-1 form for.
|
|
20809
21129
|
*/
|
|
20810
21130
|
mediaProviderFor(sn) {
|
|
21131
|
+
const recordings = this.downloadsRecordings(sn) ? { downloadRecording: (opts) => this.downloadRecording(sn, opts) } : {};
|
|
20811
21132
|
return {
|
|
21133
|
+
...recordings,
|
|
20812
21134
|
snapshotLive: async (opts) => {
|
|
20813
21135
|
const source = await this.sharedLiveSourceFor(sn, opts ?? {});
|
|
20814
21136
|
return captureSnapshotFromShared(source, {
|
|
@@ -20847,6 +21169,63 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20847
21169
|
}
|
|
20848
21170
|
};
|
|
20849
21171
|
}
|
|
21172
|
+
/**
|
|
21173
|
+
* Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
|
|
21174
|
+
* and frame formats are confirmed.
|
|
21175
|
+
*/
|
|
21176
|
+
downloadsRecordings(sn) {
|
|
21177
|
+
const dev = this.recordFor(sn);
|
|
21178
|
+
if (!dev)
|
|
21179
|
+
return false;
|
|
21180
|
+
const parentSn = stationOf(dev);
|
|
21181
|
+
return parentSn !== sn && this.recordFor(parentSn)?.model === "T8010";
|
|
21182
|
+
}
|
|
21183
|
+
/**
|
|
21184
|
+
* Download one recording a HomeBase 2 holds for an attached camera, then decode it
|
|
21185
|
+
* ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
|
|
21186
|
+
* on the camera's channel, so a download starts only once the previous one has drained
|
|
21187
|
+
* ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
|
|
21188
|
+
* while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
|
|
21189
|
+
* turn is held until the transfer drains.
|
|
21190
|
+
*/
|
|
21191
|
+
async downloadRecording(sn, opts) {
|
|
21192
|
+
opts.signal?.throwIfAborted();
|
|
21193
|
+
const parentSn = this.stationKeyOf(sn);
|
|
21194
|
+
const previous = this.recordingDownloads.get(parentSn) ?? Promise.resolve();
|
|
21195
|
+
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, opts));
|
|
21196
|
+
const turn = Promise.allSettled([previous, started.then((t) => t.drained)]).then(() => {
|
|
21197
|
+
if (this.recordingDownloads.get(parentSn) === turn)
|
|
21198
|
+
this.recordingDownloads.delete(parentSn);
|
|
21199
|
+
});
|
|
21200
|
+
this.recordingDownloads.set(parentSn, turn);
|
|
21201
|
+
return started.then((t) => t.recording);
|
|
21202
|
+
}
|
|
21203
|
+
/** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
|
|
21204
|
+
waitTurn(previous, signal) {
|
|
21205
|
+
return abortable(previous.then(() => void 0, () => void 0), signal);
|
|
21206
|
+
}
|
|
21207
|
+
/**
|
|
21208
|
+
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
21209
|
+
* and when the station is done sending it.
|
|
21210
|
+
*/
|
|
21211
|
+
async startRecordingDownload(sn, opts) {
|
|
21212
|
+
const { session, parentSn, channel, accountId } = await this.resolveSession(sn, { signal: opts.signal });
|
|
21213
|
+
const path = homeBase2RecordingPath(channel, opts.recording);
|
|
21214
|
+
if (!path)
|
|
21215
|
+
throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
|
|
21216
|
+
const eccKey = await this.cipherKeyFor(session, opts.cipherId, accountId, parentSn);
|
|
21217
|
+
if (!eccKey) {
|
|
21218
|
+
throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
|
|
21219
|
+
}
|
|
21220
|
+
const transfer = receiveRecording(session, {
|
|
21221
|
+
path,
|
|
21222
|
+
accountId,
|
|
21223
|
+
channel,
|
|
21224
|
+
timeoutMs: opts.timeoutMs,
|
|
21225
|
+
signal: opts.signal
|
|
21226
|
+
});
|
|
21227
|
+
return { recording: transfer.frames.then((frames) => decodeRecording(frames, eccKey)), drained: transfer.drained };
|
|
21228
|
+
}
|
|
20850
21229
|
/**
|
|
20851
21230
|
* Open a {@link Talkback} on a device's camera channel.
|
|
20852
21231
|
*
|
|
@@ -25093,6 +25472,11 @@ function recordString(raw, key) {
|
|
|
25093
25472
|
const v = typeof nested === "string" && nested ? nested : raw[key];
|
|
25094
25473
|
return typeof v === "string" && v ? v : void 0;
|
|
25095
25474
|
}
|
|
25475
|
+
function adminUserIdFrom(raw) {
|
|
25476
|
+
const member = (raw ?? {}).member;
|
|
25477
|
+
const id = member?.admin_user_id;
|
|
25478
|
+
return typeof id === "string" && id ? id : void 0;
|
|
25479
|
+
}
|
|
25096
25480
|
function tuyaDevIdFrom(raw) {
|
|
25097
25481
|
for (const field of ["tuya_uuid", "tuya_virtual_id", "tuya_device_id", "virtualId"]) {
|
|
25098
25482
|
const v = recordString(raw, field);
|
|
@@ -25250,13 +25634,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
25250
25634
|
onError: (e) => this.reportError(e),
|
|
25251
25635
|
// The `a2` account id an `eufy_life` DP frame embeds — the owning member's `admin_user_id`,
|
|
25252
25636
|
// falling back to the logged-in account's user id (the confirmed script path does the same).
|
|
25253
|
-
resolveAccountId: (dev) =>
|
|
25254
|
-
const member = (dev.raw ?? {}).member;
|
|
25255
|
-
const adminId = member?.admin_user_id;
|
|
25256
|
-
if (typeof adminId === "string" && adminId)
|
|
25257
|
-
return adminId;
|
|
25258
|
-
return this.mega.auth?.userId ?? "";
|
|
25259
|
-
},
|
|
25637
|
+
resolveAccountId: (dev) => adminUserIdFrom(dev.raw) ?? this.mega.auth?.userId ?? "",
|
|
25260
25638
|
// Fetch + parse a gallery effect from the HTTP catalogue into the serializable spec. The catalogue
|
|
25261
25639
|
// lives in the http layer (transport/http/light-catalog); the mqtt router owns the frame bytes.
|
|
25262
25640
|
resolvePreset: (presetId) => resolveLightEffect(this.mega, presetId),
|
|
@@ -26703,6 +27081,37 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26703
27081
|
getP2pSessions() {
|
|
26704
27082
|
return this.p2p.getSessions();
|
|
26705
27083
|
}
|
|
27084
|
+
/**
|
|
27085
|
+
* The people enrolled on a station, read from its own `person_basic_info` table over P2P.
|
|
27086
|
+
*
|
|
27087
|
+
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
27088
|
+
* station, because they were never uploaded — the station holds them.
|
|
27089
|
+
*
|
|
27090
|
+
* Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
|
|
27091
|
+
* record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
|
|
27092
|
+
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
27093
|
+
*
|
|
27094
|
+
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
27095
|
+
* returned as they arrive; a caller that wants only named people filters them.
|
|
27096
|
+
*/
|
|
27097
|
+
async getStationFaces(stationSn, opts = {}) {
|
|
27098
|
+
if (!this.registry.list().length)
|
|
27099
|
+
await this.getDevices();
|
|
27100
|
+
const station = this.p2p.stationKeyOf(stationSn);
|
|
27101
|
+
const accountId = this.adminUserIdOf(station);
|
|
27102
|
+
if (!accountId)
|
|
27103
|
+
throw new Error(`getStationFaces: no admin_user_id on record for ${station}`);
|
|
27104
|
+
await this.p2p.ensureStation(station, opts.signal);
|
|
27105
|
+
const session = this.p2p.getSessions().get(station);
|
|
27106
|
+
if (!session)
|
|
27107
|
+
throw new Error(`getStationFaces: no P2P session for ${station}`);
|
|
27108
|
+
const rows = await session.readDatabase("person_basic_info", { accountId, ...opts });
|
|
27109
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
27110
|
+
}
|
|
27111
|
+
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
27112
|
+
adminUserIdOf(stationSn) {
|
|
27113
|
+
return adminUserIdFrom(this.registry.list().find((d) => d.sn === stationSn)?.raw);
|
|
27114
|
+
}
|
|
26706
27115
|
/**
|
|
26707
27116
|
* The liveness facts for one device — see {@link DeviceState}. Facts, not an `online` verdict: "how
|
|
26708
27117
|
* long is too long" is a threshold that belongs to the caller, and it differs per device class.
|
|
@@ -28197,6 +28606,7 @@ export {
|
|
|
28197
28606
|
ROOM_SUCTIONS,
|
|
28198
28607
|
RTSP_MEMBERS,
|
|
28199
28608
|
RawDpWriter,
|
|
28609
|
+
RecordingDownloadError,
|
|
28200
28610
|
RecordingQuality,
|
|
28201
28611
|
RtspRecordingMode,
|
|
28202
28612
|
SCENE_INVALID_REASONS,
|
|
@@ -28240,6 +28650,7 @@ export {
|
|
|
28240
28650
|
VACUUM_DOCK_MEMBERS,
|
|
28241
28651
|
VACUUM_SCHEDULE_ACTIONS,
|
|
28242
28652
|
VACUUM_SCHEDULE_WEEKDAYS,
|
|
28653
|
+
VIDEO_GCM_AAD,
|
|
28243
28654
|
VacuumMapStore,
|
|
28244
28655
|
VideoFrameDecoder,
|
|
28245
28656
|
WORKING_MODE_MAPS,
|