@mega-yfue/eufy-sdk 0.6.0-beta.15 → 0.6.0-beta.16
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 -0
- package/dist/index.js +215 -45
- package/dist/index.js.map +3 -3
- package/dist/transport/p2p/audio-frame.d.ts +13 -0
- package/dist/transport/p2p/live-stream.d.ts +33 -7
- package/dist/transport/p2p/live-trace.d.ts +2 -0
- package/dist/transport/p2p/p2p-session.d.ts +55 -1
- package/dist/transport/p2p/recording-download.d.ts +22 -0
- package/dist/transport/p2p/video.d.ts +2 -0
- package/dist/transport/rtc/session.d.ts +4 -2
- package/package.json +1 -1
|
@@ -12,3 +12,16 @@ export declare const AUDIO_TYPE_OFFSET = 5;
|
|
|
12
12
|
* itself when a stream produces no audio at all, to feed its decoder silence.
|
|
13
13
|
*/
|
|
14
14
|
export declare const AUDIO_CODECS: Readonly<Record<number, AudioCodec>>;
|
|
15
|
+
/**
|
|
16
|
+
* Whether an audio frame is sealed: after its header, a 16-byte GCM tag and a 12-byte nonce precede the
|
|
17
|
+
* `u32@0` bytes its header declares, where a frame in the clear carries those bytes straight after the header.
|
|
18
|
+
* A stream whose keyframes wrap their media key in an ECIES envelope seals its audio this way, measured on
|
|
19
|
+
* HomeBase recordings and on HomeBase 3 live streams answering a P-256 start.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isSealedAudio(frame: Buffer): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The payload of a sealed audio frame ({@link isSealedAudio}), opened with AES-256-GCM under `mediaKey`, the
|
|
24
|
+
* media key of the keyframe before it, with the video frames' AAD. Answers `undefined` for a frame that is not
|
|
25
|
+
* sealed or does not authenticate under that key.
|
|
26
|
+
*/
|
|
27
|
+
export declare function openSealedAudio(frame: Buffer, mediaKey: Buffer): Buffer | undefined;
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* - **Plaintext** (`signCode 0`, e.g. HomeBase-attached cameras): a fixed 22-byte frame header
|
|
8
8
|
* (`parseVideoFrameHeader`) followed directly by Annex-B NAL units — verified live (960×540,
|
|
9
9
|
* keyframe = header flag bit0). We strip the 22-byte header.
|
|
10
|
-
* - **Encrypted
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - **Encrypted keyframes**: either the AES-256-GCM body under a media key wrapped in an ECIES envelope
|
|
11
|
+
* for an offered P-256 key ({@link VideoFrameDecoder}), or an AES-ECB head under a media key wrapped for
|
|
12
|
+
* the session's RSA key ({@link P2PSession.decodeVideoFrame}). An attached camera's start on a station
|
|
13
|
+
* with a {@link P2PSession.mediaEccPrivateKey} offers that P-256 key first, so its stream decodes with
|
|
14
|
+
* that pair unless `eccPrivateKey` names another.
|
|
13
15
|
*
|
|
14
16
|
* Emits: `video` ({@link LiveVideoFrame} — Annex-B), `audio` ({@link LiveAudioFrame}), `start`, `stop`,
|
|
15
17
|
* `error`.
|
|
@@ -54,7 +56,10 @@ export interface LiveStreamOptions {
|
|
|
54
56
|
* however long the station keeps serving that sibling instead of this one.
|
|
55
57
|
*/
|
|
56
58
|
channel?: number;
|
|
57
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* P-256 private key (32B) whose public point the camera wraps each keyframe's media key for. Defaults, on an
|
|
61
|
+
* attached camera, to the session's `P2PSession.mediaEccPrivateKey`, the pair its start offers.
|
|
62
|
+
*/
|
|
58
63
|
eccPrivateKey?: Buffer;
|
|
59
64
|
/** Admin account id — required for the level-2 (`signCode 8`) media-start payload selecting a camera. */
|
|
60
65
|
accountId?: string;
|
|
@@ -112,6 +117,8 @@ export declare class LiveStream extends EventEmitter {
|
|
|
112
117
|
private tracedFirstVideoUnit;
|
|
113
118
|
private tracedFirstKeyframe;
|
|
114
119
|
private tracedDecodeFailures;
|
|
120
|
+
/** How the frame that opened the latest access unit was read; see {@link settleStartKey}. */
|
|
121
|
+
private unitOpenedBy;
|
|
115
122
|
private tracedFirstForeignFrame;
|
|
116
123
|
private stallTimer?;
|
|
117
124
|
/**
|
|
@@ -198,11 +205,30 @@ export declare class LiveStream extends EventEmitter {
|
|
|
198
205
|
*/
|
|
199
206
|
private acceptsMedia;
|
|
200
207
|
/**
|
|
201
|
-
* The Annex-B payload of a frame that starts an access unit. The
|
|
202
|
-
*
|
|
203
|
-
*
|
|
208
|
+
* The Annex-B payload of a frame that starts an access unit. The authenticated ECIES/GCM
|
|
209
|
+
* {@link VideoFrameDecoder} goes first: its HMAC and GCM checks fail closed on a frame not sealed for its
|
|
210
|
+
* key. The session's RSA path cannot: on Node ≥ 24.5, PKCS#1 v1.5 decryption of a key not wrapped for it
|
|
211
|
+
* returns pseudo-random bytes instead of throwing (implicit rejection), which would decode an ECC
|
|
212
|
+
* keyframe into noise. The session's decode, which also passes plaintext frames, and the header strip
|
|
213
|
+
* follow. A payload that opens an access unit records how it was read: by the ECIES path, or otherwise by
|
|
214
|
+
* its sign code, sealed (`"rsa"`) or in the clear (`"plain"`).
|
|
204
215
|
*/
|
|
205
216
|
private annexbOf;
|
|
217
|
+
/**
|
|
218
|
+
* Tell the session how this keyframe was opened, which answers the attached starts on this channel
|
|
219
|
+
* ({@link P2PSession.settleAttachedKey}), and re-issue the start when the session asks for one: a plaintext
|
|
220
|
+
* keyframe answering the session's P-256 offer, from a camera that did not use the point, which the next
|
|
221
|
+
* start asks with RSA. Only a delivered keyframe answers: a frame no key opens, or a continuation without the
|
|
222
|
+
* frame that opened its unit, delivers nothing.
|
|
223
|
+
*/
|
|
224
|
+
private settleStartKey;
|
|
225
|
+
/**
|
|
226
|
+
* The payload of an audio frame. On a stream with an ECIES decoder, a sealed frame ({@link isSealedAudio}) is
|
|
227
|
+
* opened under the media key of the latest keyframe, and answers `undefined` when it does not open or arrives
|
|
228
|
+
* before any keyframe gave a media key, since its bytes are ciphertext. Every other frame is the bytes after
|
|
229
|
+
* its header.
|
|
230
|
+
*/
|
|
231
|
+
private audioPayload;
|
|
206
232
|
/**
|
|
207
233
|
* Report an access unit the transport could not complete.
|
|
208
234
|
*
|
|
@@ -23,6 +23,8 @@ export type LiveTrace =
|
|
|
23
23
|
topology: "attached" | "own";
|
|
24
24
|
action: "start" | "keepalive";
|
|
25
25
|
level2: boolean;
|
|
26
|
+
/** The media-key type an attached start offered. */
|
|
27
|
+
key?: "ecc" | "rsa";
|
|
26
28
|
}
|
|
27
29
|
/** The device acknowledged a retained start, or that start was repeated / abandoned unacknowledged. */
|
|
28
30
|
| {
|
|
@@ -180,6 +180,13 @@ export declare class P2PSession extends EventEmitter {
|
|
|
180
180
|
private level2Seq;
|
|
181
181
|
private rsaPrivateKey?;
|
|
182
182
|
private rsaModulusHex?;
|
|
183
|
+
/** The P-256 pair an attached camera's media start offers; see {@link mediaEccPrivateKey}. */
|
|
184
|
+
private mediaEcc?;
|
|
185
|
+
/**
|
|
186
|
+
* Per attached channel, the media-key type its latest start offered on this connection, or that last answered,
|
|
187
|
+
* and whether a keyframe has answered it. See {@link attachedStartKey} and {@link settleAttachedKey}.
|
|
188
|
+
*/
|
|
189
|
+
private readonly attachedStartKeys;
|
|
183
190
|
/**
|
|
184
191
|
* Whether a level-2 key negotiation kicked off by a GATEWAYINFO reply is in flight. Cleared when it
|
|
185
192
|
* settles, so a later reply — the answer to {@link repromptLevel2Key} — negotiates again.
|
|
@@ -333,7 +340,15 @@ export declare class P2PSession extends EventEmitter {
|
|
|
333
340
|
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
334
341
|
*/
|
|
335
342
|
private requestReceiveBuffer;
|
|
336
|
-
/**
|
|
343
|
+
/**
|
|
344
|
+
* Open the socket and start the lookup → hole-punch handshake.
|
|
345
|
+
*
|
|
346
|
+
* A socket closed or failing while binding never invokes its bind callback, so the bind wait also
|
|
347
|
+
* settles on the socket's `close` and `error`. On `close`, a connect whose session was closed meanwhile
|
|
348
|
+
* returns without starting the handshake, so it never sends a lookup or arms a timer on a closed
|
|
349
|
+
* session. On `error`, connect closes the session and rejects with {@link StationUnreachableError}
|
|
350
|
+
* carrying the bind error as its `cause`.
|
|
351
|
+
*/
|
|
337
352
|
connect(): Promise<void>;
|
|
338
353
|
/** Cached across every `P2PSession` in this process — the local outbound IPv4 doesn't vary by
|
|
339
354
|
* station, so there's no reason to re-probe it per session (e.g. once per station in a fleet). Only
|
|
@@ -618,6 +633,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
618
633
|
* Send a CMD_SET_PAYLOAD(1350) wrapping `{account_id, cmd:<subCmd>, mChannel, mValue3:<subCmd>,
|
|
619
634
|
* payload}`, encrypted level-2 (AES-256-GCM, signCode 8) — the app's media-control path. The
|
|
620
635
|
* camera is selected by `mChannel` (= the device's `device_channel`) AND the frame-header channel.
|
|
636
|
+
* A start (HomeBase-controlled camera path, the V6 app's media-start envelope) also carries an
|
|
637
|
+
* `accountId` inside its payload and the media key `key` offers (see {@link attachedStartKey}). A start
|
|
638
|
+
* offering the P-256 point labels itself `ClientOS: "IOS"`, as every simple attached start in the app
|
|
639
|
+
* captures that carry that key form does; the RSA start keeps `"Android"`.
|
|
621
640
|
*/
|
|
622
641
|
private sendMediaPayloadLevel2;
|
|
623
642
|
/**
|
|
@@ -638,6 +657,41 @@ export declare class P2PSession extends EventEmitter {
|
|
|
638
657
|
* (T8171 2560×1440, T8210 640×480). Returns `undefined` on failure (frame is skipped).
|
|
639
658
|
*/
|
|
640
659
|
private rsaUnwrapKey;
|
|
660
|
+
/**
|
|
661
|
+
* The media-key type the next attached start sent on `channel` offers, recorded as offered: ECC, the current
|
|
662
|
+
* app's form, for the first start this connection sends there; RSA for every start after one that no keyframe
|
|
663
|
+
* answered before it was re-issued; and, once a keyframe has answered, the type that opened it, until
|
|
664
|
+
* {@link settleAttachedKey} records another. Only an opened keyframe answers: frames no key opens, and deltas,
|
|
665
|
+
* do not. A re-issue is a full restart on an attached camera, so a camera that cannot read the point is asked
|
|
666
|
+
* with RSA, the start every station answers, and is not asked with the point again while that goes unanswered.
|
|
667
|
+
* A stream decoding with the session's pair reads either answer. A station without {@link mediaEccPrivateKey}
|
|
668
|
+
* is offered RSA only. A start that cannot be sent offers nothing and records nothing.
|
|
669
|
+
*/
|
|
670
|
+
private attachedStartKey;
|
|
671
|
+
/**
|
|
672
|
+
* Record how a keyframe of `channel` was opened, which answers the starts there. A keyframe opened under a
|
|
673
|
+
* key type is an answer of that type, whichever type the latest start offered: a station can serve the
|
|
674
|
+
* P-256 start after the RSA re-issue went out. A plaintext keyframe
|
|
675
|
+
* answering an unanswered P-256 offer is a camera that did not use the point, and the next start there offers
|
|
676
|
+
* RSA; once a P-256 answer has been opened it changes nothing, and answering RSA, it settles RSA. Answers
|
|
677
|
+
* whether a new start is needed. A channel no attached start has offered a key on is left as it is.
|
|
678
|
+
*/
|
|
679
|
+
settleAttachedKey(channel: number, opened: "ecc" | "rsa" | "plain"): boolean;
|
|
680
|
+
/**
|
|
681
|
+
* The 32-byte P-256 private scalar paired with the point an attached camera's media start offers as `key`.
|
|
682
|
+
* A camera answering that start wraps each keyframe's AES-256-GCM media key in an ECIES envelope for this
|
|
683
|
+
* key ({@link VideoFrameDecoder}). Generated once per session, like the RSA pair.
|
|
684
|
+
*
|
|
685
|
+
* `undefined` unless the station is a HomeBase 3 (`T8030`): the app's P-256 start and the device runs
|
|
686
|
+
* that answered it were both observed on that station only.
|
|
687
|
+
*/
|
|
688
|
+
get mediaEccPrivateKey(): Buffer | undefined;
|
|
689
|
+
/**
|
|
690
|
+
* The session's P-256 pair: the private scalar as 32 bytes, and the public point as 128 hex characters,
|
|
691
|
+
* `x ‖ y` without the uncompressed-point prefix, the form the current app's attached-camera start carries in
|
|
692
|
+
* `key`. `ECDH.getPrivateKey()` drops a scalar's leading zero bytes, so the scalar is padded back to 32.
|
|
693
|
+
*/
|
|
694
|
+
private mediaEccPair;
|
|
641
695
|
/**
|
|
642
696
|
* The RSA-1024 public-key modulus (128-byte hex) the station uses to wrap the per-stream media
|
|
643
697
|
* key. Lazily generates a keypair; `rsaPrivateKey` decrypts the media key the station returns.
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download of a recording held on a HomeBase 2 (T8010), over the station's P2P session.
|
|
3
|
+
*
|
|
4
|
+
* The request is `CMD_DOWNLOAD_VIDEO` (1024) in the string-pair shape
|
|
5
|
+
* ({@link buildStringPairCommandPayload}) on the camera's channel. The station answers on
|
|
6
|
+
* {@link RECORDING_DATA_TYPE}, tagged with that channel, with the recording's `CMD_VIDEO_FRAME` (1300) and
|
|
7
|
+
* `CMD_AUDIO_FRAME` (1301) frames, faster than real time, then `CMD_DOWNLOAD_FINISH` (1304) on the control
|
|
8
|
+
* data type, tagged with channel 0 whatever the camera's channel.
|
|
9
|
+
*
|
|
10
|
+
* Frame layouts:
|
|
11
|
+
*
|
|
12
|
+
* ```
|
|
13
|
+
* video, keyframe: the live E2E layout ({@link VideoFrameDecoder}), length = 179 + len exactly
|
|
14
|
+
* video, other frames: the same 22B header (u32@0 = len · u16@6 = frame number · u48@0x0e = stamp in ms),
|
|
15
|
+
* then len bytes of plaintext H.264
|
|
16
|
+
* audio: 16B header (u32@0 = len · u8@5 = codec, 0 = AAC-LC), then 16B GCM tag, 12B GCM nonce and len
|
|
17
|
+
* bytes of AES-256-GCM AAC under the media key of the preceding keyframe, AAD as live video; the
|
|
18
|
+
* plaintext is a whole ADTS frame, header included
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @module p2p/recording-download
|
|
22
|
+
*/
|
|
1
23
|
import { type RecordingDownload } from "../../core/contracts.js";
|
|
2
24
|
import type { P2PSession } from "./p2p-session.js";
|
|
3
25
|
/** Data type a station sends a recording's frames on: the binary channel (`0xd1 0x03`), not live video's. */
|
|
@@ -153,3 +153,5 @@ export declare class AccessUnitAssembler {
|
|
|
153
153
|
/** Forget an incomplete unit, counting and reporting it. */
|
|
154
154
|
discard(): void;
|
|
155
155
|
}
|
|
156
|
+
/** Whether a decoded payload OPENS an access unit — a continuation begins mid-NAL, with no start code. */
|
|
157
|
+
export declare function beginsAccessUnit(body: Buffer): boolean;
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
*
|
|
16
16
|
* The command data channel is the session: `close` fires once, when the channel closes, the peer fails,
|
|
17
17
|
* the signalling socket drops or {@link RtcSession.close} is called, even while the peer connection is
|
|
18
|
-
* nominally up. A
|
|
19
|
-
*
|
|
18
|
+
* nominally up. A dropped signalling socket closes the peer with it: a hub that still sees the peer
|
|
19
|
+
* answers every later `scall` with 486 until that peer's ICE gives up. A peer that cannot be built when
|
|
20
|
+
* the hub grants the relay ends the session there: the failure is emitted as `error` and `close` fires
|
|
21
|
+
* carrying it.
|
|
20
22
|
*
|
|
21
23
|
* A session whose peer is a {@link RtcPeerOptions.mediaTrack} peer has no command channel: `close` fires
|
|
22
24
|
* when the peer fails or closes, and the video track's packets are emitted as `rtp`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mega-yfue/eufy-sdk",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.16",
|
|
4
4
|
"description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "mega-yfue",
|