@mega-yfue/eufy-sdk 0.6.0-beta.38 → 0.6.0-beta.39
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 +209 -170
- package/dist/index.js.map +3 -3
- package/dist/model/capabilities/battery.d.ts +4 -0
- package/dist/transport/p2p/codec.d.ts +6 -0
- package/dist/transport/p2p/command-router.d.ts +3 -0
- package/dist/transport/p2p/live-trace.d.ts +1 -1
- package/dist/transport/p2p/p2p-session.d.ts +14 -2
- package/dist/transport/p2p/video.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15335,6 +15335,7 @@ __export(codec_exports, {
|
|
|
15335
15335
|
encryptP2PData: () => encryptP2PData,
|
|
15336
15336
|
frameMessage: () => frameMessage,
|
|
15337
15337
|
gatewayInfoCipherId: () => gatewayInfoCipherId,
|
|
15338
|
+
gatewayInfoRefused: () => gatewayInfoRefused,
|
|
15338
15339
|
hasHeader: () => hasHeader,
|
|
15339
15340
|
p2pCommandEncryptionKey: () => p2pCommandEncryptionKey,
|
|
15340
15341
|
p2pDidToBuffer: () => p2pDidToBuffer,
|
|
@@ -15453,6 +15454,10 @@ function decryptP2PData(data, key) {
|
|
|
15453
15454
|
function gatewayInfoCipherId(gwPayload) {
|
|
15454
15455
|
return gwPayload.length >= 2 ? gwPayload.readUInt16LE(0) : 0;
|
|
15455
15456
|
}
|
|
15457
|
+
var GATEWAY_LEVEL2_REFUSED = -103;
|
|
15458
|
+
function gatewayInfoRefused(gwPayload) {
|
|
15459
|
+
return gwPayload.length >= 4 && gwPayload.readInt32LE(0) === GATEWAY_LEVEL2_REFUSED;
|
|
15460
|
+
}
|
|
15456
15461
|
function eufyKdf(shared, outLen) {
|
|
15457
15462
|
const hmac = (k, d) => createHmac2("sha256", k).update(d).digest();
|
|
15458
15463
|
const label = Buffer.from("ECIES");
|
|
@@ -16598,6 +16603,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16598
16603
|
liveStartedChannels = /* @__PURE__ */ new Map();
|
|
16599
16604
|
/** The `cipher_id` this connection's `CMD_GATEWAYINFO` reply named, once its negotiation has read it. */
|
|
16600
16605
|
gatewayCipherId;
|
|
16606
|
+
/** Whether this connection's latest `CMD_GATEWAYINFO` reply was a refusal — see {@link gatewayInfoRefused}. */
|
|
16607
|
+
gatewayRefused = false;
|
|
16601
16608
|
/** The `n` of the last `[0x01, n]` header this connection sent in the clear — see {@link nextClearCommandBytes}. */
|
|
16602
16609
|
clearCommandCount = 0;
|
|
16603
16610
|
unackedLiveStarts = /* @__PURE__ */ new Map();
|
|
@@ -16795,9 +16802,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16795
16802
|
* Every outcome settles the wait in {@link awaitLevel2Key}, so a later command is told at once rather than
|
|
16796
16803
|
* left to time out against a key that is not coming. Settling also re-opens the negotiation, so a later
|
|
16797
16804
|
* GATEWAYINFO reply in the same connection generation runs it again; a lookup that completes after its
|
|
16798
|
-
* generation ended settles nothing.
|
|
16805
|
+
* generation ended settles nothing. A {@link gatewayInfoRefused} reply names no cipher, so it settles at once
|
|
16806
|
+
* with no cloud lookup, and a later reply naming a cipher withdraws the refusal.
|
|
16799
16807
|
*/
|
|
16800
16808
|
negotiateLevel2Key(gwPayload) {
|
|
16809
|
+
this.gatewayRefused = gatewayInfoRefused(gwPayload);
|
|
16810
|
+
if (this.gatewayRefused) {
|
|
16811
|
+
this.logger.debug(`[p2p] ${this.cfg.stationSn} gateway info refused level-2`);
|
|
16812
|
+
this.trace({ phase: "level2-unavailable", reason: "gateway-refused" });
|
|
16813
|
+
this.settleLevel2();
|
|
16814
|
+
return;
|
|
16815
|
+
}
|
|
16801
16816
|
this.level2Negotiating = true;
|
|
16802
16817
|
const generation = this.connectionGeneration;
|
|
16803
16818
|
const cipherId = gatewayInfoCipherId(gwPayload);
|
|
@@ -16920,6 +16935,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16920
16935
|
this.level2Negotiating = false;
|
|
16921
16936
|
this.level2Reprompted = false;
|
|
16922
16937
|
this.gatewayCipherId = void 0;
|
|
16938
|
+
this.gatewayRefused = false;
|
|
16923
16939
|
this.clearCommandCount = 0;
|
|
16924
16940
|
}
|
|
16925
16941
|
const socket = dgram.createSocket("udp4");
|
|
@@ -17544,6 +17560,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17544
17560
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
17545
17561
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
17546
17562
|
}
|
|
17563
|
+
/**
|
|
17564
|
+
* Whether this connection's station answered `CMD_GATEWAYINFO` with a refusal ({@link gatewayInfoRefused}) and
|
|
17565
|
+
* no key that frames a set-payload is held.
|
|
17566
|
+
*/
|
|
17567
|
+
get refusedLevel2() {
|
|
17568
|
+
return this.gatewayRefused && !this.level2KeyServes("set-payload");
|
|
17569
|
+
}
|
|
17547
17570
|
/**
|
|
17548
17571
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
17549
17572
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -17554,10 +17577,15 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17554
17577
|
* On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
|
|
17555
17578
|
* layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
|
|
17556
17579
|
* byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
|
|
17580
|
+
*
|
|
17581
|
+
* With `clearOnRefusal` on a session that {@link refusedLevel2}, the frame goes in the clear in the vendor
|
|
17582
|
+
* app's form for such a device: header bytes `[01 00]`, signCode 0, and a body of `{cmd, payload}` alone.
|
|
17583
|
+
* Without it, a refused session answers `false` and sends nothing.
|
|
17557
17584
|
*/
|
|
17558
|
-
sendControlLevel2(cmd, channel, accountId, payload, mValue3 = cmd) {
|
|
17559
|
-
if (!this.connectAddress || !this.level2KeyServes("set-payload"))
|
|
17585
|
+
sendControlLevel2(cmd, channel, accountId, payload, mValue3 = cmd, clearOnRefusal = false) {
|
|
17586
|
+
if (!this.connectAddress || !(this.level2KeyServes("set-payload") || clearOnRefusal && this.refusedLevel2)) {
|
|
17560
17587
|
return false;
|
|
17588
|
+
}
|
|
17561
17589
|
const value = Buffer.from(JSON.stringify({ account_id: accountId, cmd, mChannel: channel, mValue3, payload }), "utf-8");
|
|
17562
17590
|
let sealed;
|
|
17563
17591
|
if (this.level2Key) {
|
|
@@ -17565,8 +17593,10 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17565
17593
|
if (!body)
|
|
17566
17594
|
return false;
|
|
17567
17595
|
sealed = buildRawCommandPayload(body, channel, 8, [8, 0], 0);
|
|
17568
|
-
} else {
|
|
17596
|
+
} else if (this.level2EcbKey) {
|
|
17569
17597
|
sealed = buildRawCommandPayload(encryptP2PData(paddingP2PData(value), this.level2EcbKey), channel, 2);
|
|
17598
|
+
} else {
|
|
17599
|
+
sealed = buildRawCommandPayload(Buffer.from(JSON.stringify({ cmd, payload }), "utf-8"), channel);
|
|
17570
17600
|
}
|
|
17571
17601
|
const data = Buffer.concat([buildCommandHeader(this.seqNumber, CMD_SET_PAYLOAD), sealed]);
|
|
17572
17602
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
@@ -18919,169 +18949,6 @@ import { EventEmitter as EventEmitter3 } from "node:events";
|
|
|
18919
18949
|
|
|
18920
18950
|
// dist/transport/p2p/video.js
|
|
18921
18951
|
import { createDecipheriv as createDecipheriv6 } from "node:crypto";
|
|
18922
|
-
var VIDEO_GCM_AAD = Buffer.from("eufy security");
|
|
18923
|
-
var HEADER_LEN = 22;
|
|
18924
|
-
var ENVELOPE_OFFSET = 22;
|
|
18925
|
-
var ENVELOPE_LEN = 129;
|
|
18926
|
-
var GCM_TAG_OFFSET = 151;
|
|
18927
|
-
var GCM_TAG_LEN = 16;
|
|
18928
|
-
var GCM_NONCE_OFFSET = 167;
|
|
18929
|
-
var GCM_NONCE_LEN = 12;
|
|
18930
|
-
var BODY_OFFSET = 179;
|
|
18931
|
-
function parseVideoFrameHeader(payload) {
|
|
18932
|
-
if (payload.length < HEADER_LEN)
|
|
18933
|
-
return void 0;
|
|
18934
|
-
const flags = payload.readUInt8(4);
|
|
18935
|
-
return {
|
|
18936
|
-
payloadLength: payload.readUInt32LE(0),
|
|
18937
|
-
keyframe: (flags & 1) === 1,
|
|
18938
|
-
flags,
|
|
18939
|
-
sequence: payload.readUInt16LE(6),
|
|
18940
|
-
width: payload.readInt16LE(10),
|
|
18941
|
-
height: payload.readInt16LE(12),
|
|
18942
|
-
timestamp: payload.readUInt32LE(14)
|
|
18943
|
-
};
|
|
18944
|
-
}
|
|
18945
|
-
var VideoFrameDecoder = class {
|
|
18946
|
-
eccPrivateKeyHex;
|
|
18947
|
-
mediaKey;
|
|
18948
|
-
/** @param eccPrivateKey the camera's 32-byte ECC private key (P-256 scalar). */
|
|
18949
|
-
constructor(eccPrivateKey) {
|
|
18950
|
-
if (eccPrivateKey.length !== 32)
|
|
18951
|
-
throw new Error("eccPrivateKey must be 32 bytes");
|
|
18952
|
-
this.eccPrivateKeyHex = eccPrivateKey.toString("hex");
|
|
18953
|
-
}
|
|
18954
|
-
/** The media key recovered from the most recent keyframe, if any. */
|
|
18955
|
-
get currentMediaKey() {
|
|
18956
|
-
return this.mediaKey ? Buffer.from(this.mediaKey) : void 0;
|
|
18957
|
-
}
|
|
18958
|
-
/**
|
|
18959
|
-
* Recover the per-stream media key from a keyframe's 129-byte ECIES envelope: ECIES unwrap
|
|
18960
|
-
* (ECDH → eufyKDF → AES-128-CBC, with the trailing HMAC verified) → 32-byte AES-256-GCM key.
|
|
18961
|
-
*/
|
|
18962
|
-
unwrapMediaKey(envelope) {
|
|
18963
|
-
const key = eciesUnwrap(envelope, this.eccPrivateKeyHex, { verifyHmac: true, pkcs7: true });
|
|
18964
|
-
return key && key.length === 32 ? key : void 0;
|
|
18965
|
-
}
|
|
18966
|
-
/**
|
|
18967
|
-
* Decode one video frame. Returns the keyframe flag and decrypted H.264 bytes, or `undefined`
|
|
18968
|
-
* if the frame is malformed, the envelope/key is wrong, or GCM authentication fails.
|
|
18969
|
-
*/
|
|
18970
|
-
decodeFrame(payload) {
|
|
18971
|
-
const header = parseVideoFrameHeader(payload);
|
|
18972
|
-
if (!header)
|
|
18973
|
-
return void 0;
|
|
18974
|
-
if (header.keyframe) {
|
|
18975
|
-
if (payload.length < ENVELOPE_OFFSET + ENVELOPE_LEN)
|
|
18976
|
-
return void 0;
|
|
18977
|
-
const envelope = payload.subarray(ENVELOPE_OFFSET, ENVELOPE_OFFSET + ENVELOPE_LEN);
|
|
18978
|
-
const key = this.unwrapMediaKey(envelope);
|
|
18979
|
-
if (!key)
|
|
18980
|
-
return void 0;
|
|
18981
|
-
this.mediaKey = key;
|
|
18982
|
-
}
|
|
18983
|
-
const mediaKey = this.mediaKey;
|
|
18984
|
-
if (!mediaKey)
|
|
18985
|
-
return void 0;
|
|
18986
|
-
if (payload.length < BODY_OFFSET)
|
|
18987
|
-
return void 0;
|
|
18988
|
-
const tag2 = payload.subarray(GCM_TAG_OFFSET, GCM_TAG_OFFSET + GCM_TAG_LEN);
|
|
18989
|
-
const nonce = payload.subarray(GCM_NONCE_OFFSET, GCM_NONCE_OFFSET + GCM_NONCE_LEN);
|
|
18990
|
-
const body = payload.subarray(BODY_OFFSET);
|
|
18991
|
-
try {
|
|
18992
|
-
const dec2 = createDecipheriv6("aes-256-gcm", mediaKey, nonce);
|
|
18993
|
-
dec2.setAAD(VIDEO_GCM_AAD);
|
|
18994
|
-
dec2.setAuthTag(tag2);
|
|
18995
|
-
const head = dec2.update(body);
|
|
18996
|
-
const tail = dec2.final();
|
|
18997
|
-
const h264 = tail.length ? Buffer.concat([head, tail]) : head;
|
|
18998
|
-
return { keyframe: header.keyframe, h264, width: header.width, height: header.height };
|
|
18999
|
-
} catch {
|
|
19000
|
-
return void 0;
|
|
19001
|
-
}
|
|
19002
|
-
}
|
|
19003
|
-
};
|
|
19004
|
-
var STATION_CHUNK_BYTES = 64e3;
|
|
19005
|
-
var AccessUnitAssembler = class {
|
|
19006
|
-
onDropped;
|
|
19007
|
-
chunkBytes;
|
|
19008
|
-
open;
|
|
19009
|
-
droppedUnits = 0;
|
|
19010
|
-
/**
|
|
19011
|
-
* @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
|
|
19012
|
-
* arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
|
|
19013
|
-
* its own.
|
|
19014
|
-
* @param chunkBytes the payload size the station fills a frame to before continuing a unit in the next. A
|
|
19015
|
-
* unit is held while its latest frame declares exactly this. {@link STATION_CHUNK_BYTES} when omitted.
|
|
19016
|
-
*/
|
|
19017
|
-
constructor(onDropped, chunkBytes = STATION_CHUNK_BYTES) {
|
|
19018
|
-
this.onDropped = onDropped;
|
|
19019
|
-
this.chunkBytes = chunkBytes;
|
|
19020
|
-
}
|
|
19021
|
-
/**
|
|
19022
|
-
* Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
|
|
19023
|
-
* still being filled, one in the ordinary case.
|
|
19024
|
-
*
|
|
19025
|
-
* `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
|
|
19026
|
-
* frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
|
|
19027
|
-
* of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
|
|
19028
|
-
* carries). A continuation without its matching open unit has no Annex-B start code and cannot be emitted
|
|
19029
|
-
* as a complete unit, even if its repeated header says keyframe.
|
|
19030
|
-
*/
|
|
19031
|
-
push(payload, decode) {
|
|
19032
|
-
const header = parseVideoFrameHeader(payload);
|
|
19033
|
-
const body = header && decode(payload);
|
|
19034
|
-
if (!header || !body?.length) {
|
|
19035
|
-
this.discard();
|
|
19036
|
-
return [];
|
|
19037
|
-
}
|
|
19038
|
-
const full = header.payloadLength === this.chunkBytes;
|
|
19039
|
-
const open2 = this.open;
|
|
19040
|
-
if (open2 && sameUnit(open2.header, header) && !beginsAccessUnit(body)) {
|
|
19041
|
-
open2.chunks.push(body);
|
|
19042
|
-
open2.carried += body.length;
|
|
19043
|
-
if (full)
|
|
19044
|
-
return [];
|
|
19045
|
-
this.open = void 0;
|
|
19046
|
-
return [unitOf(open2.header, Buffer.concat(open2.chunks))];
|
|
19047
|
-
}
|
|
19048
|
-
this.discard();
|
|
19049
|
-
if (!beginsAccessUnit(body))
|
|
19050
|
-
return [];
|
|
19051
|
-
if (full) {
|
|
19052
|
-
this.open = { header, chunks: [body], carried: body.length };
|
|
19053
|
-
return [];
|
|
19054
|
-
}
|
|
19055
|
-
return [unitOf(header, body)];
|
|
19056
|
-
}
|
|
19057
|
-
/** Forget an incomplete unit, counting and reporting it. */
|
|
19058
|
-
discard() {
|
|
19059
|
-
const open2 = this.open;
|
|
19060
|
-
if (!open2)
|
|
19061
|
-
return;
|
|
19062
|
-
this.open = void 0;
|
|
19063
|
-
this.droppedUnits++;
|
|
19064
|
-
this.onDropped?.({ carried: open2.carried, chunks: open2.chunks.length, count: this.droppedUnits });
|
|
19065
|
-
}
|
|
19066
|
-
};
|
|
19067
|
-
var SPLIT_UNIT_STAMP_SPREAD_MS = 100;
|
|
19068
|
-
function sameUnit(open2, next) {
|
|
19069
|
-
return next.sequence === open2.sequence && next.timestamp - open2.timestamp >>> 0 <= SPLIT_UNIT_STAMP_SPREAD_MS;
|
|
19070
|
-
}
|
|
19071
|
-
function beginsAccessUnit(body) {
|
|
19072
|
-
if (body.length < 4 || body[0] !== 0 || body[1] !== 0)
|
|
19073
|
-
return false;
|
|
19074
|
-
return body[2] === 1 || body[2] === 0 && body[3] === 1;
|
|
19075
|
-
}
|
|
19076
|
-
function unitOf(header, data) {
|
|
19077
|
-
return {
|
|
19078
|
-
sourceTimestamp: header.timestamp,
|
|
19079
|
-
keyframe: header.keyframe,
|
|
19080
|
-
width: header.width,
|
|
19081
|
-
height: header.height,
|
|
19082
|
-
data
|
|
19083
|
-
};
|
|
19084
|
-
}
|
|
19085
18952
|
|
|
19086
18953
|
// dist/transport/p2p/annexb.js
|
|
19087
18954
|
var ANNEXB_START = Buffer.from([0, 0, 0, 1]);
|
|
@@ -19399,6 +19266,172 @@ function h265Geometry(sps) {
|
|
|
19399
19266
|
};
|
|
19400
19267
|
}
|
|
19401
19268
|
|
|
19269
|
+
// dist/transport/p2p/video.js
|
|
19270
|
+
var VIDEO_GCM_AAD = Buffer.from("eufy security");
|
|
19271
|
+
var HEADER_LEN = 22;
|
|
19272
|
+
var ENVELOPE_OFFSET = 22;
|
|
19273
|
+
var ENVELOPE_LEN = 129;
|
|
19274
|
+
var GCM_TAG_OFFSET = 151;
|
|
19275
|
+
var GCM_TAG_LEN = 16;
|
|
19276
|
+
var GCM_NONCE_OFFSET = 167;
|
|
19277
|
+
var GCM_NONCE_LEN = 12;
|
|
19278
|
+
var BODY_OFFSET = 179;
|
|
19279
|
+
function parseVideoFrameHeader(payload) {
|
|
19280
|
+
if (payload.length < HEADER_LEN)
|
|
19281
|
+
return void 0;
|
|
19282
|
+
const flags = payload.readUInt8(4);
|
|
19283
|
+
return {
|
|
19284
|
+
payloadLength: payload.readUInt32LE(0),
|
|
19285
|
+
keyframe: (flags & 1) === 1,
|
|
19286
|
+
flags,
|
|
19287
|
+
sequence: payload.readUInt16LE(6),
|
|
19288
|
+
width: payload.readInt16LE(10),
|
|
19289
|
+
height: payload.readInt16LE(12),
|
|
19290
|
+
timestamp: payload.readUInt32LE(14)
|
|
19291
|
+
};
|
|
19292
|
+
}
|
|
19293
|
+
var VideoFrameDecoder = class {
|
|
19294
|
+
eccPrivateKeyHex;
|
|
19295
|
+
mediaKey;
|
|
19296
|
+
/** @param eccPrivateKey the camera's 32-byte ECC private key (P-256 scalar). */
|
|
19297
|
+
constructor(eccPrivateKey) {
|
|
19298
|
+
if (eccPrivateKey.length !== 32)
|
|
19299
|
+
throw new Error("eccPrivateKey must be 32 bytes");
|
|
19300
|
+
this.eccPrivateKeyHex = eccPrivateKey.toString("hex");
|
|
19301
|
+
}
|
|
19302
|
+
/** The media key recovered from the most recent keyframe, if any. */
|
|
19303
|
+
get currentMediaKey() {
|
|
19304
|
+
return this.mediaKey ? Buffer.from(this.mediaKey) : void 0;
|
|
19305
|
+
}
|
|
19306
|
+
/**
|
|
19307
|
+
* Recover the per-stream media key from a keyframe's 129-byte ECIES envelope: ECIES unwrap
|
|
19308
|
+
* (ECDH → eufyKDF → AES-128-CBC, with the trailing HMAC verified) → 32-byte AES-256-GCM key.
|
|
19309
|
+
*/
|
|
19310
|
+
unwrapMediaKey(envelope) {
|
|
19311
|
+
const key = eciesUnwrap(envelope, this.eccPrivateKeyHex, { verifyHmac: true, pkcs7: true });
|
|
19312
|
+
return key && key.length === 32 ? key : void 0;
|
|
19313
|
+
}
|
|
19314
|
+
/**
|
|
19315
|
+
* Decode one video frame. Returns the keyframe flag and decrypted H.264 bytes, or `undefined`
|
|
19316
|
+
* if the frame is malformed, the envelope/key is wrong, or GCM authentication fails.
|
|
19317
|
+
*/
|
|
19318
|
+
decodeFrame(payload) {
|
|
19319
|
+
const header = parseVideoFrameHeader(payload);
|
|
19320
|
+
if (!header)
|
|
19321
|
+
return void 0;
|
|
19322
|
+
if (header.keyframe) {
|
|
19323
|
+
if (payload.length < ENVELOPE_OFFSET + ENVELOPE_LEN)
|
|
19324
|
+
return void 0;
|
|
19325
|
+
const envelope = payload.subarray(ENVELOPE_OFFSET, ENVELOPE_OFFSET + ENVELOPE_LEN);
|
|
19326
|
+
const key = this.unwrapMediaKey(envelope);
|
|
19327
|
+
if (!key)
|
|
19328
|
+
return void 0;
|
|
19329
|
+
this.mediaKey = key;
|
|
19330
|
+
}
|
|
19331
|
+
const mediaKey = this.mediaKey;
|
|
19332
|
+
if (!mediaKey)
|
|
19333
|
+
return void 0;
|
|
19334
|
+
if (payload.length < BODY_OFFSET)
|
|
19335
|
+
return void 0;
|
|
19336
|
+
const tag2 = payload.subarray(GCM_TAG_OFFSET, GCM_TAG_OFFSET + GCM_TAG_LEN);
|
|
19337
|
+
const nonce = payload.subarray(GCM_NONCE_OFFSET, GCM_NONCE_OFFSET + GCM_NONCE_LEN);
|
|
19338
|
+
const body = payload.subarray(BODY_OFFSET);
|
|
19339
|
+
try {
|
|
19340
|
+
const dec2 = createDecipheriv6("aes-256-gcm", mediaKey, nonce);
|
|
19341
|
+
dec2.setAAD(VIDEO_GCM_AAD);
|
|
19342
|
+
dec2.setAuthTag(tag2);
|
|
19343
|
+
const head = dec2.update(body);
|
|
19344
|
+
const tail = dec2.final();
|
|
19345
|
+
const h264 = tail.length ? Buffer.concat([head, tail]) : head;
|
|
19346
|
+
return { keyframe: header.keyframe, h264, width: header.width, height: header.height };
|
|
19347
|
+
} catch {
|
|
19348
|
+
return void 0;
|
|
19349
|
+
}
|
|
19350
|
+
}
|
|
19351
|
+
};
|
|
19352
|
+
var STATION_CHUNK_BYTES = 64e3;
|
|
19353
|
+
var AccessUnitAssembler = class {
|
|
19354
|
+
onDropped;
|
|
19355
|
+
chunkBytes;
|
|
19356
|
+
open;
|
|
19357
|
+
droppedUnits = 0;
|
|
19358
|
+
/**
|
|
19359
|
+
* @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
|
|
19360
|
+
* arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
|
|
19361
|
+
* its own.
|
|
19362
|
+
* @param chunkBytes the payload size the station fills a frame to before continuing a unit in the next. A
|
|
19363
|
+
* unit is held while its latest frame declares exactly this. {@link STATION_CHUNK_BYTES} when omitted.
|
|
19364
|
+
*/
|
|
19365
|
+
constructor(onDropped, chunkBytes = STATION_CHUNK_BYTES) {
|
|
19366
|
+
this.onDropped = onDropped;
|
|
19367
|
+
this.chunkBytes = chunkBytes;
|
|
19368
|
+
}
|
|
19369
|
+
/**
|
|
19370
|
+
* Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
|
|
19371
|
+
* still being filled, one in the ordinary case.
|
|
19372
|
+
*
|
|
19373
|
+
* `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
|
|
19374
|
+
* frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
|
|
19375
|
+
* of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
|
|
19376
|
+
* carries). A continuation without its matching open unit has no Annex-B start code and cannot be emitted
|
|
19377
|
+
* as a complete unit, even if its repeated header says keyframe.
|
|
19378
|
+
*/
|
|
19379
|
+
push(payload, decode) {
|
|
19380
|
+
const header = parseVideoFrameHeader(payload);
|
|
19381
|
+
const body = header && decode(payload);
|
|
19382
|
+
if (!header || !body?.length) {
|
|
19383
|
+
this.discard();
|
|
19384
|
+
return [];
|
|
19385
|
+
}
|
|
19386
|
+
const full = header.payloadLength === this.chunkBytes;
|
|
19387
|
+
const open2 = this.open;
|
|
19388
|
+
if (open2 && sameUnit(open2.header, header) && !beginsAccessUnit(body)) {
|
|
19389
|
+
open2.chunks.push(body);
|
|
19390
|
+
open2.carried += body.length;
|
|
19391
|
+
if (full)
|
|
19392
|
+
return [];
|
|
19393
|
+
this.open = void 0;
|
|
19394
|
+
return [unitOf(open2.header, Buffer.concat(open2.chunks))];
|
|
19395
|
+
}
|
|
19396
|
+
this.discard();
|
|
19397
|
+
if (!beginsAccessUnit(body))
|
|
19398
|
+
return [];
|
|
19399
|
+
if (full) {
|
|
19400
|
+
this.open = { header, chunks: [body], carried: body.length };
|
|
19401
|
+
return [];
|
|
19402
|
+
}
|
|
19403
|
+
return [unitOf(header, body)];
|
|
19404
|
+
}
|
|
19405
|
+
/** Forget an incomplete unit, counting and reporting it. */
|
|
19406
|
+
discard() {
|
|
19407
|
+
const open2 = this.open;
|
|
19408
|
+
if (!open2)
|
|
19409
|
+
return;
|
|
19410
|
+
this.open = void 0;
|
|
19411
|
+
this.droppedUnits++;
|
|
19412
|
+
this.onDropped?.({ carried: open2.carried, chunks: open2.chunks.length, count: this.droppedUnits });
|
|
19413
|
+
}
|
|
19414
|
+
};
|
|
19415
|
+
var SPLIT_UNIT_STAMP_SPREAD_MS = 100;
|
|
19416
|
+
function sameUnit(open2, next) {
|
|
19417
|
+
return next.sequence === open2.sequence && next.timestamp - open2.timestamp >>> 0 <= SPLIT_UNIT_STAMP_SPREAD_MS;
|
|
19418
|
+
}
|
|
19419
|
+
function beginsAccessUnit(body) {
|
|
19420
|
+
if (body.length < 4 || body[0] !== 0 || body[1] !== 0)
|
|
19421
|
+
return false;
|
|
19422
|
+
return body[2] === 1 || body[2] === 0 && body[3] === 1;
|
|
19423
|
+
}
|
|
19424
|
+
function unitOf(header, data) {
|
|
19425
|
+
const codec = header.keyframe ? void 0 : sniffAnnexbCodec(data);
|
|
19426
|
+
return {
|
|
19427
|
+
sourceTimestamp: header.timestamp,
|
|
19428
|
+
keyframe: header.keyframe || codec !== void 0 && hasIdr(data, codec),
|
|
19429
|
+
width: header.width,
|
|
19430
|
+
height: header.height,
|
|
19431
|
+
data
|
|
19432
|
+
};
|
|
19433
|
+
}
|
|
19434
|
+
|
|
19402
19435
|
// dist/transport/p2p/audio-frame.js
|
|
19403
19436
|
import { createDecipheriv as createDecipheriv7 } from "node:crypto";
|
|
19404
19437
|
var CMD_AUDIO_FRAME2 = 1301;
|
|
@@ -22429,6 +22462,7 @@ var RTSP_URL_READ_TIMEOUT_MS = 12e3;
|
|
|
22429
22462
|
var TALKBACK_CODEC_WAIT_MS = 3e3;
|
|
22430
22463
|
var OWN_LIVE_SESSION_MODELS = /* @__PURE__ */ new Set(["T8030"]);
|
|
22431
22464
|
var ELD_TALKBACK_MODELS = /* @__PURE__ */ new Set(["T8410C"]);
|
|
22465
|
+
var CLEAR_FF09_MODELS = /* @__PURE__ */ new Set(["T8506"]);
|
|
22432
22466
|
function speakerCodec(consumer, talk, timeoutMs) {
|
|
22433
22467
|
return new Promise((resolve2) => {
|
|
22434
22468
|
let timer;
|
|
@@ -23774,6 +23808,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23774
23808
|
* sources on the replaced session are discarded before closing it (see {@link tearDownStation}).
|
|
23775
23809
|
* Replaced at most once per resolution, so a station whose replacement is silent too is returned rather
|
|
23776
23810
|
* than closed again.
|
|
23811
|
+
*
|
|
23812
|
+
* `clearFallback` hands over a session whose station {@link P2PSession.refusedLevel2} instead of throwing, for
|
|
23813
|
+
* a standalone device of a model in {@link CLEAR_FF09_MODELS}.
|
|
23777
23814
|
*/
|
|
23778
23815
|
async resolveSession(sn, opts = {}, rebuilt = false) {
|
|
23779
23816
|
const dev = await this.deviceFor(sn);
|
|
@@ -23825,8 +23862,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23825
23862
|
if (!ready && session.repromptLevel2Key()) {
|
|
23826
23863
|
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call", sender), opts.signal);
|
|
23827
23864
|
}
|
|
23828
|
-
|
|
23865
|
+
const sendsClear = opts.clearFallback === true && !homeBaseAttached && CLEAR_FF09_MODELS.has(this.recordFor(sn)?.model ?? "") && session.refusedLevel2;
|
|
23866
|
+
if (!ready && !sendsClear)
|
|
23829
23867
|
throw new StationKeyUnavailableError(parentSn);
|
|
23868
|
+
return { session, parentSn, channel, accountId, homeBaseAttached, sendsClear };
|
|
23830
23869
|
}
|
|
23831
23870
|
return { session, parentSn, channel, accountId, homeBaseAttached };
|
|
23832
23871
|
}
|
|
@@ -23936,7 +23975,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23936
23975
|
});
|
|
23937
23976
|
return;
|
|
23938
23977
|
}
|
|
23939
|
-
await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd), resolved ?? await this.resolveSession(sn, { waitLevel2: true, level2Sender }));
|
|
23978
|
+
await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId, sendsClear }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd, sendsClear), resolved ?? await this.resolveSession(sn, { waitLevel2: true, level2Sender }));
|
|
23940
23979
|
}
|
|
23941
23980
|
/**
|
|
23942
23981
|
* **`keyed-payload-actuate` intent** — the classic Wi-Fi lock's actuation: the sealed fields from
|
|
@@ -24103,7 +24142,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
24103
24142
|
* bytes and never the routing channel — that's re-resolved here from the device record.
|
|
24104
24143
|
*/
|
|
24105
24144
|
async sendFf09Actuate(sn, cmd) {
|
|
24106
|
-
const resolved = await this.resolveSession(sn, { waitLevel2: true });
|
|
24145
|
+
const resolved = await this.resolveSession(sn, { waitLevel2: true, clearFallback: true });
|
|
24107
24146
|
const frame = buildFf09Frame({
|
|
24108
24147
|
engage: cmd.engage,
|
|
24109
24148
|
adminUserId: cmd.adminUserId,
|