@mega-yfue/eufy-sdk 0.4.0-beta.14 → 0.4.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.
@@ -552,6 +552,7 @@ export declare const CAMERA_MEMBERS: {
552
552
  } | undefined) => Promise<Buffer<ArrayBufferLike>>>;
553
553
  readonly openReadable: import("./members.js").ProvidedMember<"media", ((opts?: Parameters<NonNullable<MediaProvider["openReadable"]>>[0]) => Promise<import("node:stream").Readable>) | undefined>;
554
554
  readonly recordFragments: import("./members.js").ProvidedMember<"media", ((opts?: Parameters<NonNullable<MediaProvider["recordFragments"]>>[0]) => import("../../core/contracts.js").FragmentRecordingHandle) | undefined>;
555
+ readonly downloadRecording: import("./members.js").ProvidedMember<"media", ((opts: Parameters<NonNullable<MediaProvider["downloadRecording"]>>[0]) => Promise<import("../../core/contracts.js").RecordingDownload>) | undefined>;
555
556
  /**
556
557
  * Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
557
558
  * not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
@@ -59,6 +59,12 @@ export interface AdtsHeader {
59
59
  * nothing about whether the frame's payload has arrived yet; that is the scanner's job.
60
60
  */
61
61
  export declare function parseAdtsHeader(buf: Buffer, offset?: number): AdtsHeader | undefined;
62
+ /**
63
+ * The 7-byte ADTS header (no CRC, buffer fullness "variable") that frames one raw AAC-LC, 16 kHz, mono
64
+ * access unit of `payloadLength` bytes, so that header and payload together read back through
65
+ * {@link parseAdtsHeader} as a supported frame.
66
+ */
67
+ export declare function buildAdtsHeader(payloadLength: number): Buffer;
62
68
  /**
63
69
  * Whether a header describes the audio parameters the device's path is fixed at — AAC-LC, 16 kHz,
64
70
  * mono. A stream at any other rate or channel count is rejected rather than resampled: the device has
@@ -22,6 +22,11 @@ export declare const ResponseMessageType: {
22
22
  readonly LOOKUP_ADDR: Buffer<ArrayBuffer>;
23
23
  readonly LOOKUP_ADDR2: Buffer<ArrayBuffer>;
24
24
  readonly CAM_ID: Buffer<ArrayBuffer>;
25
+ /**
26
+ * The device's own address record, sent in answer to CHECK_CAM ahead of CAM_ID: its 20-byte device id, one
27
+ * record in `encodeSelfAddress`'s shape naming the address it answers from, then 8 zero bytes (44-byte payload).
28
+ */
29
+ readonly CAM_ADDR: Buffer<ArrayBuffer>;
25
30
  readonly TURN_SERVER_CAM_ID: Buffer<ArrayBuffer>;
26
31
  readonly PING: Buffer<ArrayBuffer>;
27
32
  readonly PONG: Buffer<ArrayBuffer>;
@@ -149,6 +154,14 @@ export declare function buildStringCommandPayload(value: string, channel?: numbe
149
154
  * length prefix ({@link stringWithLength}).
150
155
  */
151
156
  export declare function buildIntStringCommandPayload(value: number, valueSub: number, strValue: string, channel?: number, key?: Buffer, encType?: number): Buffer;
157
+ /**
158
+ * Build a **string-pair** command body: five zero bytes, then `strValue` and `strValueSub`, each in the
159
+ * 128-byte-chunk length form ({@link stringWithLength}), AES-128-ECB encrypted (level-1) like
160
+ * {@link buildStringCommandPayload} when `key` is given. `CMD_DOWNLOAD_VIDEO` (1024) takes this shape on a
161
+ * HomeBase 2: `strValue` = the recording's path on the station, `strValueSub` = the station admin
162
+ * `account_id`, on the camera's channel.
163
+ */
164
+ export declare function buildStringPairCommandPayload(strValue: string, strValueSub: string, channel?: number, key?: Buffer, encType?: number): Buffer;
152
165
  /**
153
166
  * Build a command body around an ALREADY-encrypted (or plaintext) `data` buffer with an explicit
154
167
  * `signCode` — used for level-2 (`signCode 8`, AES-256-GCM) commands like the media-start 1350 the
@@ -121,6 +121,8 @@ export declare class P2PCommandRouter {
121
121
  private readonly talkbacks;
122
122
  /** cipher_id → ECC private key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
123
123
  private readonly cipherKeyCache;
124
+ /** The recording download in flight per station; a station serves one at a time. */
125
+ private readonly recordingDownloads;
124
126
  constructor(deps: P2PRouterDeps);
125
127
  /** Forward one P2P failure once even when both the session listener and startup waiter observe it. */
126
128
  private reportError;
@@ -237,6 +239,12 @@ export declare class P2PCommandRouter {
237
239
  * while its own session is still serving.
238
240
  */
239
241
  private makeSession;
242
+ /**
243
+ * The ECC private key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
244
+ * answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
245
+ * cached.
246
+ */
247
+ private cipherKeyFor;
240
248
  /**
241
249
  * Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
242
250
  * stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
@@ -269,6 +277,27 @@ export declare class P2PCommandRouter {
269
277
  * start has no level-1 form for.
270
278
  */
271
279
  mediaProviderFor(sn: string): MediaProvider;
280
+ /**
281
+ * Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
282
+ * and frame formats are confirmed.
283
+ */
284
+ private downloadsRecordings;
285
+ /**
286
+ * Download one recording a HomeBase 2 holds for an attached camera, then decode it
287
+ * ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
288
+ * on the camera's channel, so a download starts only once the previous one has drained
289
+ * ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
290
+ * while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
291
+ * turn is held until the transfer drains.
292
+ */
293
+ private downloadRecording;
294
+ /** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
295
+ private waitTurn;
296
+ /**
297
+ * Start one recording download, once the station's previous download has drained: the decoded recording,
298
+ * and when the station is done sending it.
299
+ */
300
+ private startRecordingDownload;
272
301
  /**
273
302
  * Open a {@link Talkback} on a device's camera channel.
274
303
  *
@@ -326,6 +326,9 @@ export declare class P2PSession extends EventEmitter {
326
326
  * straddling a level-2 wait on a camera whose failure to deliver video had a separate cause, and did not
327
327
  * recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
328
328
  * as a cause.
329
+ *
330
+ * CAM_ADDR is recognised and dropped: it names the address the device is already answering from, and the
331
+ * CAM_ID that follows it is what completes the connect.
329
332
  */
330
333
  private onMessage;
331
334
  private beginCheckCam;
@@ -410,6 +413,8 @@ export declare class P2PSession extends EventEmitter {
410
413
  * `strValue` (admin `account_id`). See {@link buildIntStringCommandPayload}. Fire-and-forget.
411
414
  */
412
415
  sendIntStringCommand(commandType: number, value: number, valueSub: number, strValue: string, channel?: number): void;
416
+ /** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
417
+ sendStringPairCommand(commandType: number, strValue: string, strValueSub: string, channel: number): void;
413
418
  /**
414
419
  * Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
415
420
  * target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
@@ -0,0 +1,54 @@
1
+ import { type RecordingDownload } from "../../core/contracts.js";
2
+ import type { P2PSession } from "./p2p-session.js";
3
+ /** Data type a station sends a recording's frames on: the binary channel (`0xd1 0x03`), not live video's. */
4
+ export declare const RECORDING_DATA_TYPE = 3;
5
+ /**
6
+ * Most frame bytes a transfer holds. The station sends faster than real time, so the time bounds alone do
7
+ * not bound memory; an event recording is far below this.
8
+ */
9
+ export declare const MAX_TRANSFER_BYTES: number;
10
+ /** One recording frame as received, in arrival order. */
11
+ export interface RecordingFrame {
12
+ commandId: number;
13
+ signCode: number;
14
+ raw: Buffer;
15
+ }
16
+ /**
17
+ * The path a HomeBase 2 stores a camera's recording under: the camera's channel, two digits, and the
18
+ * recording name the event push carries. Answers `undefined` for a name that is not the station's
19
+ * fourteen-digit `yyyyMMddHHmmss` form. The name comes from a push, so the test stays anchored and
20
+ * digits-only: no `..`, slash or other character that could reach another path on the station gets through.
21
+ */
22
+ export declare function homeBase2RecordingPath(channel: number, recording: string): string | undefined;
23
+ /** One recording transfer on a station session. */
24
+ export interface RecordingTransfer {
25
+ /** The recording's frames, in arrival order. Rejects as {@link receiveRecording} describes. */
26
+ frames: Promise<RecordingFrame[]>;
27
+ /** Resolves once the station has stopped sending this recording, which can be after `frames` rejected. */
28
+ drained: Promise<void>;
29
+ }
30
+ /**
31
+ * Request one recording and collect its frames until `CMD_DOWNLOAD_FINISH`. Only frames on
32
+ * {@link RECORDING_DATA_TYPE} tagged with the camera's channel belong to it, so a live stream open on the same
33
+ * station is never mixed in.
34
+ *
35
+ * `frames` rejects with {@link RecordingDownloadError}: `no-data` when nothing arrives within
36
+ * {@link FIRST_FRAME_WAIT_MS}, and `incomplete` when the transfer ends without its finish frame, on a
37
+ * {@link IDLE_END_MS} silence, on `timeoutMs` or on {@link MAX_TRANSFER_BYTES}. An abort rejects it at once
38
+ * with the signal's reason. After a time bound, the byte ceiling or an abort, the transfer stops collecting
39
+ * but keeps listening until the station finishes or goes quiet, and only then resolves `drained`.
40
+ */
41
+ export declare function receiveRecording(session: P2PSession, request: {
42
+ path: string;
43
+ accountId: string;
44
+ channel: number;
45
+ timeoutMs?: number;
46
+ signal?: AbortSignal;
47
+ }): RecordingTransfer;
48
+ /**
49
+ * Decode a recording's frames, in arrival order, into elementary streams. Keyframes go through one
50
+ * {@link VideoFrameDecoder}, whose media key then opens the audio; a keyframe that does not open under
51
+ * `eccPrivateKeyHex`, and every audio frame before the first media key, are dropped. Rejects with
52
+ * {@link RecordingDownloadError} `undecodable` when no video frame decodes.
53
+ */
54
+ export declare function decodeRecording(frames: readonly RecordingFrame[], eccPrivateKeyHex: string): RecordingDownload;
@@ -1,3 +1,5 @@
1
+ /** AAD used for the AES-256-GCM body cipher of every video frame, and of a recording's audio. */
2
+ export declare const VIDEO_GCM_AAD: Buffer<ArrayBuffer>;
1
3
  /** Parsed fields of a `CMD_VIDEO_FRAME` 22-byte header. */
2
4
  export interface VideoFrameHeader {
3
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.4.0-beta.14",
3
+ "version": "0.4.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",