@mega-yfue/eufy-sdk 0.6.0-beta.30 → 0.6.0-beta.31

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.
@@ -314,24 +314,28 @@ export declare class P2PCommandRouter {
314
314
  */
315
315
  mediaProviderFor(sn: string): MediaProvider;
316
316
  /**
317
- * Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
318
- * and frame formats are confirmed.
317
+ * The {@link RecordingStation} of the station `sn` is attached to, when `sn` is a camera attached to a
318
+ * station with an entry in {@link RECORDING_STATIONS}. A standalone device, or a camera on any other
319
+ * station, answers `undefined`.
319
320
  */
320
- private downloadsRecordings;
321
+ private recordingStationFor;
321
322
  /**
322
- * Download one recording a HomeBase 2 holds for an attached camera, then decode it
323
- * ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
324
- * on the camera's channel, so a download starts only once the previous one has drained
325
- * ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
326
- * while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
327
- * turn is held until the transfer drains.
323
+ * Download one recording `station` holds for the attached camera `sn`, then decode it
324
+ * ({@link decodeRecording}). `station` is the entry {@link mediaProviderFor} bound. Downloads queue per
325
+ * station: the station streams one recording at a time on the camera's channel, so a download starts only
326
+ * once the previous one has drained ({@link RecordingTransfer.drained}), even when that one's caller already
327
+ * has its answer. An abort while waiting rejects without sending anything; an abort mid-transfer rejects at
328
+ * once and the station's turn is held until the transfer drains.
328
329
  */
329
330
  private downloadRecording;
330
331
  /** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
331
332
  private waitTurn;
332
333
  /**
333
334
  * Start one recording download, once the station's previous download has drained: the decoded recording,
334
- * and when the station is done sending it.
335
+ * and when the station is done sending it. The path is `station.path(channel, opts.recording)`, and a name
336
+ * it refuses rejects `invalid-recording` before anything is sent. On a sealed station the cipher's ECC key
337
+ * is looked up, and its absence rejects `key-unavailable` before anything is sent. A station in the clear
338
+ * reads no cipher, and `opts.cipherId` is not read.
335
339
  */
336
340
  private startRecordingDownload;
337
341
  /**
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Download of a recording held on a HomeBase 2 (T8010), over the station's P2P session.
2
+ * Download of a recording held on a HomeBase, over the station's P2P session. {@link RECORDING_STATIONS}
3
+ * holds every way the station models differ; the rest is shared.
3
4
  *
4
5
  * The request is `CMD_DOWNLOAD_VIDEO` (1024) in the string-pair shape
5
6
  * ({@link buildStringPairCommandPayload}) on the camera's channel. The station answers on
6
7
  * {@link RECORDING_DATA_TYPE}, tagged with that channel, with the recording's `CMD_VIDEO_FRAME` (1300) and
7
8
  * `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
+ * data type, tagged with the station's {@link RecordingStation.finishChannel} whatever the camera's channel.
9
10
  *
10
- * Frame layouts:
11
+ * Frame layouts on a station that seals its recordings (HomeBase 2):
11
12
  *
12
13
  * ```
13
14
  * video, keyframe: the live E2E layout ({@link VideoFrameDecoder}), length = 179 + len exactly
@@ -18,10 +19,17 @@
18
19
  * plaintext is a whole ADTS frame, header included
19
20
  * ```
20
21
  *
22
+ * Frame layouts on a station that stores them in the clear (HomeBase 3):
23
+ *
24
+ * ```
25
+ * video, every frame: the same 22B header, then len bytes of Annex-B H.264 or H.265, length = 22 + len
26
+ * audio: the same 16B header, then len bytes of ADTS AAC
27
+ * ```
28
+ *
21
29
  * @module p2p/recording-download
22
30
  */
23
31
  import { type RecordingDownload } from "../../core/contracts.js";
24
- import type { P2PSession } from "./p2p-session.js";
32
+ import { type P2PSession } from "./p2p-session.js";
25
33
  /** Data type a station sends a recording's frames on: the binary channel (`0xd1 0x03`), not live video's. */
26
34
  export declare const RECORDING_DATA_TYPE = 3;
27
35
  /**
@@ -36,12 +44,56 @@ export interface RecordingFrame {
36
44
  raw: Buffer;
37
45
  }
38
46
  /**
39
- * The path a HomeBase 2 stores a camera's recording under: the channel as two digits, then the recording's
40
- * name as `yyyyMMddHHmmss` or `h265_yyyyMMddHHmmss`. Answers `undefined` for any other name, and for a channel
41
- * outside `0`–`99`. Both forms are digits behind one fixed prefix, so the anchored test lets no `..`, slash or
42
- * other character that could reach another path on the station through.
47
+ * How one station model stores its attached cameras' recordings and serves them over `CMD_DOWNLOAD_VIDEO`
48
+ * (1024): where a recording lives, whether it is sealed, which channel the finish frame is tagged with, and
49
+ * where a split picture continues. A model without an entry in {@link RECORDING_STATIONS} offers no download.
50
+ */
51
+ export interface RecordingStation {
52
+ /**
53
+ * The path the station stores the recording named `recording` of the camera on station channel `channel`
54
+ * under. Answers `undefined` for a name the station could not hold. The SDK builds every path from these two
55
+ * values, so a request never reaches outside the camera's own folder.
56
+ */
57
+ readonly path: (channel: number, recording: string) => string | undefined;
58
+ /**
59
+ * Whether the station stores a recording sealed under its event's cipher, which opens with that cipher's ECC
60
+ * private key, or in the clear, which reads no key. The frame layouts of each are in this module's
61
+ * documentation.
62
+ */
63
+ readonly sealed: boolean;
64
+ /** The channel the station tags `CMD_DOWNLOAD_FINISH` (1304) with. The camera's own channel is taken too. */
65
+ readonly finishChannel: number;
66
+ /**
67
+ * The payload size the station fills a `CMD_VIDEO_FRAME` to when an access unit continues in the next frame
68
+ * ({@link AccessUnitAssembler}).
69
+ */
70
+ readonly chunkBytes: number;
71
+ }
72
+ /**
73
+ * The path a HomeBase 2 stores a camera's recording under: its {@link cameraFolder}, then the recording's name
74
+ * as `yyyyMMddHHmmss` or `h265_yyyyMMddHHmmss`. Answers `undefined` for any other name, and for a channel
75
+ * {@link cameraFolder} refuses. Both forms are digits behind one fixed prefix, so the anchored test lets no
76
+ * `..`, slash or other character that could reach another path on the station through.
43
77
  */
44
78
  export declare function homeBase2RecordingPath(channel: number, recording: string): string | undefined;
79
+ /**
80
+ * The path a HomeBase 3 stores a camera's recording under:
81
+ * `/zx/emmcdata/<cameraFolder>/<yyyyMM>/<name>/<name>.zxvideo`, where the name is the recording's start as
82
+ * `yyyyMMddHHmmss`, whatever the codec, and `<yyyyMM>` is its first six digits. Answers `undefined` for any
83
+ * other name (a codec prefix, an extension or a path included), and for a channel {@link cameraFolder}
84
+ * refuses. The anchored test lets no character but digits through.
85
+ */
86
+ export declare function homeBase3RecordingPath(channel: number, recording: string): string | undefined;
87
+ /**
88
+ * The stations whose recording download is confirmed, by station model.
89
+ *
90
+ * `T8010`, HomeBase 2: `/media/mmcblk0p1/CameraNN/<name>.dat`, sealed under the event's cipher, finish tagged
91
+ * with channel 0, split pictures filled to {@link STATION_CHUNK_BYTES}.
92
+ *
93
+ * `T8030`, HomeBase 3: `/zx/emmcdata/CameraNN/<yyyyMM>/<name>/<name>.zxvideo`, in the clear, finish tagged
94
+ * with {@link STATION_CHANNEL}, a keyframe over 400000 bytes split at exactly 400000.
95
+ */
96
+ export declare const RECORDING_STATIONS: ReadonlyMap<string, RecordingStation>;
45
97
  /** One recording transfer on a station session. */
46
98
  export interface RecordingTransfer {
47
99
  /** The recording's frames, in arrival order. Rejects as {@link receiveRecording} describes. */
@@ -52,7 +104,8 @@ export interface RecordingTransfer {
52
104
  /**
53
105
  * Request one recording and collect its frames until `CMD_DOWNLOAD_FINISH`. Only frames on
54
106
  * {@link RECORDING_DATA_TYPE} tagged with the camera's channel belong to it, so a live stream open on the same
55
- * station is never mixed in. The finish frame is taken on the camera's channel or channel 0.
107
+ * station is never mixed in. The finish frame is taken on the camera's channel or on `finishChannel`
108
+ * ({@link RecordingStation.finishChannel}).
56
109
  *
57
110
  * `frames` rejects with {@link RecordingDownloadError}: `no-data` when nothing arrives within
58
111
  * {@link FIRST_FRAME_WAIT_MS}, and `incomplete` when the transfer ends without its finish frame, on a
@@ -64,14 +117,22 @@ export declare function receiveRecording(session: P2PSession, request: {
64
117
  path: string;
65
118
  accountId: string;
66
119
  channel: number;
120
+ finishChannel: number;
67
121
  timeoutMs?: number;
68
122
  signal?: AbortSignal;
69
123
  }): RecordingTransfer;
70
124
  /**
71
- * Decode a recording's frames, in arrival order, into elementary streams. Split pictures are assembled
72
- * before they enter the output; an orphan continuation cannot become its own picture. Keyframes go
73
- * through one {@link VideoFrameDecoder}, whose media key then opens the audio; a keyframe that does not
74
- * open under `eccPrivateKeyHex`, and every audio frame before the first media key, are dropped. Rejects with
75
- * {@link RecordingDownloadError} `undecodable` when no video frame decodes.
125
+ * Decode a recording's frames, in arrival order, into elementary streams. `station` sets where a split
126
+ * picture continues ({@link RecordingStation.chunkBytes}) and whether the frames are sealed
127
+ * ({@link RecordingStation.sealed}). On a sealed station a keyframe goes through one {@link VideoFrameDecoder}
128
+ * under `eccPrivateKeyHex`, and audio opens under that decoder's current media key; a keyframe that does not
129
+ * open, and audio before the first media key, are dropped. On a station in the clear every video payload is read
130
+ * as it is, and audio is everything after its 16-byte header; a keyframe that is in fact sealed begins with an
131
+ * ECIES point rather than a start code, so the assembler drops it. Split pictures are assembled before they
132
+ * enter the output, and an orphan continuation cannot become its own picture. Audio frames whose codec byte is
133
+ * not AAC-LC are dropped.
134
+ *
135
+ * Rejects with {@link RecordingDownloadError} `undecodable` when no video frame decodes, and on a sealed
136
+ * station whose key is absent or not a P-256 key.
76
137
  */
77
- export declare function decodeRecording(frames: readonly RecordingFrame[], eccPrivateKeyHex: string): RecordingDownload;
138
+ export declare function decodeRecording(frames: readonly RecordingFrame[], station: Pick<RecordingStation, "sealed" | "chunkBytes">, eccPrivateKeyHex?: string): RecordingDownload;
@@ -122,23 +122,27 @@ export declare const STATION_CHUNK_BYTES = 64000;
122
122
  *
123
123
  * Two consequences worth stating. A unit that is exactly the threshold long, or an exact multiple of it,
124
124
  * ends on a full frame and is dropped as truncated — one frame in ~64000, reported, and the alternative is
125
- * handing a decoder bytes that may genuinely be short. And a station that fills to some OTHER size is not
126
- * recognised: its units stay exactly as they arrive rather than being mis-joined.
125
+ * handing a decoder bytes that may genuinely be short. And a station that fills to some OTHER size is
126
+ * recognised only by an assembler constructed with that size: elsewhere its units stay exactly as they arrive
127
+ * rather than being mis-joined.
127
128
  */
128
129
  export declare class AccessUnitAssembler {
129
130
  private readonly onDropped?;
131
+ private readonly chunkBytes;
130
132
  private open?;
131
133
  private droppedUnits;
132
134
  /**
133
135
  * @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
134
136
  * arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
135
137
  * its own.
138
+ * @param chunkBytes the payload size the station fills a frame to before continuing a unit in the next. A
139
+ * unit is held while its latest frame declares exactly this. {@link STATION_CHUNK_BYTES} when omitted.
136
140
  */
137
141
  constructor(onDropped?: ((drop: {
138
142
  carried: number;
139
143
  chunks: number;
140
144
  count: number;
141
- }) => void) | undefined);
145
+ }) => void) | undefined, chunkBytes?: number);
142
146
  /**
143
147
  * Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
144
148
  * still being filled, one in the ordinary case.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.6.0-beta.30",
3
+ "version": "0.6.0-beta.31",
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",