@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.
@@ -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
@@ -154,6 +154,14 @@ export declare function buildStringCommandPayload(value: string, channel?: numbe
154
154
  * length prefix ({@link stringWithLength}).
155
155
  */
156
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;
157
165
  /**
158
166
  * Build a command body around an ALREADY-encrypted (or plaintext) `data` buffer with an explicit
159
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
  *
@@ -413,6 +413,8 @@ export declare class P2PSession extends EventEmitter {
413
413
  * `strValue` (admin `account_id`). See {@link buildIntStringCommandPayload}. Fire-and-forget.
414
414
  */
415
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;
416
418
  /**
417
419
  * Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
418
420
  * target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
@@ -564,6 +566,10 @@ export declare class P2PSession extends EventEmitter {
564
566
  * The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
565
567
  * level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
566
568
  * Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
569
+ *
570
+ * Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
571
+ * frames tie no chunk to its request, so a second query's reply would be assembled into the first
572
+ * one's buffer and answered as its rows.
567
573
  */
568
574
  queryDatabase(table: string, opts?: {
569
575
  accountId?: string;
@@ -591,6 +597,25 @@ export declare class P2PSession extends EventEmitter {
591
597
  accountId?: string;
592
598
  channel?: number;
593
599
  }): void;
600
+ /**
601
+ * Query one on-station table and answer its rows, once the reply is whole.
602
+ *
603
+ * The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
604
+ * several frames whose decrypted text is a fragment of one document, so the fragments are
605
+ * accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
606
+ * `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
607
+ * complete reply.
608
+ *
609
+ * One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
610
+ * session throws, so a second reply cannot land in this buffer.
611
+ */
612
+ readDatabase(table: string, opts?: {
613
+ accountId?: string;
614
+ timeoutMs?: number;
615
+ signal?: AbortSignal;
616
+ }): Promise<unknown[]>;
617
+ /** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
618
+ private dbReadInFlight;
594
619
  /**
595
620
  * Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
596
621
  * carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
@@ -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.15",
3
+ "version": "0.4.0-beta.17",
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",