@mega-yfue/eufy-sdk 0.6.0-beta.30 → 0.6.0-beta.32
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/core/contracts.d.ts +12 -9
- package/dist/index.js +171 -67
- package/dist/index.js.map +3 -3
- package/dist/transport/http/mega-client.d.ts +15 -3
- package/dist/transport/p2p/command-router.d.ts +14 -10
- package/dist/transport/p2p/recording-download.d.ts +76 -15
- package/dist/transport/p2p/video.d.ts +7 -3
- package/dist/transport/rtc/command-router.d.ts +19 -3
- package/package.json +1 -1
|
@@ -212,6 +212,8 @@ export declare class MegaHttpClient {
|
|
|
212
212
|
private region;
|
|
213
213
|
/** The host `estimate_domain` answered, persisted with the session. */
|
|
214
214
|
private estimatedDomain;
|
|
215
|
+
/** The in-flight (then settled, so cached) RTC-sign shard request from {@link portalShard}. */
|
|
216
|
+
private portalShardInFlight?;
|
|
215
217
|
private bootstrapDomain?;
|
|
216
218
|
private sessionKey?;
|
|
217
219
|
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
@@ -283,11 +285,19 @@ export declare class MegaHttpClient {
|
|
|
283
285
|
/** The active region shard (e.g. `"eu-pr"`, `"us-pr"`), set after {@link login} or a region override. */
|
|
284
286
|
get regionShard(): RegionShard;
|
|
285
287
|
/**
|
|
286
|
-
* The shard the
|
|
287
|
-
*
|
|
288
|
-
* like `ie-pr`, whose accounts need the IE sign host and cluster.
|
|
288
|
+
* The shard the estimated domain names (`…-ie-…`/`…-ie.` → `ie-pr`), else {@link regionShard}. The
|
|
289
|
+
* eu/us classification cannot name shards like `ie-pr`.
|
|
289
290
|
*/
|
|
290
291
|
get rtcShard(): string;
|
|
292
|
+
/**
|
|
293
|
+
* The shard the RTC signalling signs on, from `estimate_domain` asked with the portal's WEB identity
|
|
294
|
+
* (`Model-Type: WEB`, `App-Name: eufy_mega`) — the identity the sign itself is sent with, which can name
|
|
295
|
+
* a different shard than the PHONE identity does for one account. The in-flight request is shared by
|
|
296
|
+
* concurrent first callers, and once it settles to a shard the resolved promise is the cache; a call
|
|
297
|
+
* that fails or names no shard answers {@link rtcShard}, clears the field and is asked again next time.
|
|
298
|
+
*/
|
|
299
|
+
portalShard(): Promise<string>;
|
|
300
|
+
private fetchPortalShard;
|
|
291
301
|
/**
|
|
292
302
|
* The credentials the RTC signalling needs; `undefined` while logged out. The sign is refused
|
|
293
303
|
* ("gtoken not equal userid") unless its gtoken is the one every authed HTTP call carries.
|
|
@@ -334,6 +344,8 @@ export declare class MegaHttpClient {
|
|
|
334
344
|
* raw text. Never throws on HTTP status (the callers classify the envelope themselves). 20s timeout.
|
|
335
345
|
*/
|
|
336
346
|
private httpPost;
|
|
347
|
+
/** POST `estimate_domain` with the given identity headers. */
|
|
348
|
+
private estimateDomainRequest;
|
|
337
349
|
/** Step 1: discover the account's region shard (and the bootstrap host). */
|
|
338
350
|
estimateDomain(): Promise<void>;
|
|
339
351
|
/**
|
|
@@ -314,24 +314,28 @@ export declare class P2PCommandRouter {
|
|
|
314
314
|
*/
|
|
315
315
|
mediaProviderFor(sn: string): MediaProvider;
|
|
316
316
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
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
|
|
321
|
+
private recordingStationFor;
|
|
321
322
|
/**
|
|
322
|
-
* Download one recording
|
|
323
|
-
* ({@link decodeRecording}).
|
|
324
|
-
* on the camera's channel, so a download starts only
|
|
325
|
-
* ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
326
|
-
* while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
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
|
+
* 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
|
|
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
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
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
|
|
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.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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[],
|
|
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
|
|
126
|
-
* recognised: its units stay exactly as they arrive
|
|
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.
|
|
@@ -61,8 +61,8 @@ export interface RtcLiveRoute extends RtcRoute {
|
|
|
61
61
|
export interface RtcCommandRouterDeps {
|
|
62
62
|
/** The logged-in session's credentials; `undefined` while logged out. */
|
|
63
63
|
identity: () => RtcIdentity | undefined;
|
|
64
|
-
/** The
|
|
65
|
-
shard: () => string
|
|
64
|
+
/** The signing shard for the route (`"ie-pr"`, `"eu-pr"`, `"us-pr"`); picks the smart host. */
|
|
65
|
+
shard: (route: RtcRoute) => string | Promise<string>;
|
|
66
66
|
/** ISO country sent on the sign request (default `US`). */
|
|
67
67
|
country?: string;
|
|
68
68
|
logger?: Logger;
|
|
@@ -78,6 +78,8 @@ export declare class RtcCommandRouter {
|
|
|
78
78
|
private readonly warm;
|
|
79
79
|
private readonly callSources;
|
|
80
80
|
private lastTransaction;
|
|
81
|
+
/** The latest shard each `call` source resolved, read by its `makeStream` (see {@link callMediaProviderFor}). */
|
|
82
|
+
private readonly callShards;
|
|
81
83
|
constructor(deps: RtcCommandRouterDeps);
|
|
82
84
|
dispatchCommand(route: RtcRoute, cmd: Command): Promise<void>;
|
|
83
85
|
/**
|
|
@@ -132,7 +134,13 @@ export declare class RtcCommandRouter {
|
|
|
132
134
|
trackMediaProviderFor(route: RtcRoute): MediaProvider;
|
|
133
135
|
/** NVR media uses a dedicated call per camera, independent of station commands. */
|
|
134
136
|
nvrMediaProviderFor(route: RtcLiveRoute, views: readonly number[]): MediaProvider;
|
|
135
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Share a dedicated-call source and its consumer operations under one route key.
|
|
139
|
+
*
|
|
140
|
+
* `SharedLiveSource` calls `makeStream` synchronously, so the shard cannot be awaited inside it. Each
|
|
141
|
+
* async entry point resolves the shard and records it in {@link callShards} before touching the source;
|
|
142
|
+
* `makeStream` reads the latest recorded value.
|
|
143
|
+
*/
|
|
136
144
|
private callMediaProviderFor;
|
|
137
145
|
/**
|
|
138
146
|
* Tear down every station session (logout / shutdown). Each live source is disposed before its session
|
|
@@ -174,6 +182,14 @@ export declare class RtcCommandRouter {
|
|
|
174
182
|
*/
|
|
175
183
|
private sendAwaitAck;
|
|
176
184
|
private newSession;
|
|
185
|
+
/** A connected session for `sn` to reuse, or undefined (dropping a dead one). */
|
|
186
|
+
private reusable;
|
|
187
|
+
/**
|
|
188
|
+
* Get or open the station's session. A connected session is reused before the shard is awaited, so a
|
|
189
|
+
* command on an already-connected station never waits on the unauthenticated `estimate_domain` round
|
|
190
|
+
* trip — only the create path awaits it. After the await the map is re-checked, with no `await` between
|
|
191
|
+
* the lookup and `this.sessions.set`, so concurrent creators still share one session.
|
|
192
|
+
*/
|
|
177
193
|
private stationSession;
|
|
178
194
|
/**
|
|
179
195
|
* One bounded bring-up: the session is up when its command channel opens, and it fails, with the
|
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.32",
|
|
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",
|