@mega-yfue/eufy-sdk 0.6.0-beta.4 → 0.6.0-beta.40
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/device-registry.d.ts +6 -0
- package/dist/client/eufy-mega.d.ts +103 -14
- package/dist/client/push-repeats.d.ts +10 -0
- package/dist/client/types.d.ts +12 -9
- package/dist/core/contracts.d.ts +28 -14
- package/dist/core/store.d.ts +12 -1
- package/dist/core/types.d.ts +4 -2
- package/dist/index.js +3332 -2431
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/access.d.ts +3 -2
- package/dist/model/capabilities/arming.d.ts +2 -1
- package/dist/model/capabilities/battery.d.ts +8 -7
- package/dist/model/capabilities/camera.d.ts +13 -2
- package/dist/model/capabilities/doorbell.d.ts +15 -3
- package/dist/model/capabilities/index.d.ts +1 -1
- package/dist/model/capabilities/members.d.ts +3 -1
- package/dist/model/capabilities/mower.d.ts +3 -2
- package/dist/model/capabilities/ptz.d.ts +11 -5
- package/dist/model/capabilities/vacuum-clean.d.ts +23 -0
- package/dist/model/device-family.d.ts +3 -1
- package/dist/model/device-types.d.ts +1 -0
- package/dist/transport/http/mega-client.d.ts +18 -5
- package/dist/transport/mqtt/biz-stream.d.ts +22 -25
- package/dist/transport/p2p/audio-frame.d.ts +13 -0
- package/dist/transport/p2p/codec.d.ts +8 -2
- package/dist/transport/p2p/command-router.d.ts +48 -13
- package/dist/transport/p2p/database-wire.d.ts +30 -0
- package/dist/transport/p2p/level2-settle.d.ts +9 -0
- package/dist/transport/p2p/live-stream.d.ts +34 -7
- package/dist/transport/p2p/live-trace.d.ts +7 -5
- package/dist/transport/p2p/p2p-session.d.ts +112 -16
- package/dist/transport/p2p/readable-egress.d.ts +3 -1
- package/dist/transport/p2p/recording-download.d.ts +109 -13
- package/dist/transport/p2p/shared-live-source.d.ts +9 -3
- package/dist/transport/p2p/video.d.ts +12 -6
- package/dist/transport/push/store.d.ts +7 -6
- package/dist/transport/rtc/command-router.d.ts +86 -9
- package/dist/transport/rtc/live.d.ts +4 -1
- package/dist/transport/rtc/portal-packet.d.ts +3 -13
- package/dist/transport/rtc/session.d.ts +19 -2
- package/dist/transport/stored-image-cache.d.ts +14 -0
- package/package.json +1 -1
|
@@ -1,5 +1,35 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
4
|
+
*
|
|
5
|
+
* The request is `CMD_DOWNLOAD_VIDEO` (1024) in the string-pair shape
|
|
6
|
+
* ({@link buildStringPairCommandPayload}) on the camera's channel. The station answers on
|
|
7
|
+
* {@link RECORDING_DATA_TYPE}, tagged with that channel, with the recording's `CMD_VIDEO_FRAME` (1300) and
|
|
8
|
+
* `CMD_AUDIO_FRAME` (1301) frames, faster than real time, then `CMD_DOWNLOAD_FINISH` (1304) on the control
|
|
9
|
+
* data type, tagged with the station's {@link RecordingStation.finishChannel} whatever the camera's channel.
|
|
10
|
+
*
|
|
11
|
+
* Frame layouts on a station that seals its recordings (HomeBase 2):
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* video, keyframe: the live E2E layout ({@link VideoFrameDecoder}), length = 179 + len exactly
|
|
15
|
+
* video, other frames: the same 22B header (u32@0 = len · u16@6 = frame number · u48@0x0e = stamp in ms),
|
|
16
|
+
* then len bytes of plaintext H.264
|
|
17
|
+
* audio: 16B header (u32@0 = len · u8@5 = codec, 0 = AAC-LC), then 16B GCM tag, 12B GCM nonce and len
|
|
18
|
+
* bytes of AES-256-GCM AAC under the media key of the preceding keyframe, AAD as live video; the
|
|
19
|
+
* plaintext is a whole ADTS frame, header included
|
|
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
|
+
*
|
|
29
|
+
* @module p2p/recording-download
|
|
30
|
+
*/
|
|
1
31
|
import { type RecordingDownload } from "../../core/contracts.js";
|
|
2
|
-
import type
|
|
32
|
+
import { type P2PSession } from "./p2p-session.js";
|
|
3
33
|
/** Data type a station sends a recording's frames on: the binary channel (`0xd1 0x03`), not live video's. */
|
|
4
34
|
export declare const RECORDING_DATA_TYPE = 3;
|
|
5
35
|
/**
|
|
@@ -14,12 +44,62 @@ export interface RecordingFrame {
|
|
|
14
44
|
raw: Buffer;
|
|
15
45
|
}
|
|
16
46
|
/**
|
|
17
|
-
*
|
|
18
|
-
* recording
|
|
19
|
-
*
|
|
20
|
-
|
|
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
|
+
* The result code that opens the station's `CMD_DOWNLOAD_VIDEO` reply on the camera's channel when it holds no
|
|
73
|
+
* recording at the path, after which it sends nothing. Absent on a station whose reply is unconfirmed.
|
|
74
|
+
*/
|
|
75
|
+
readonly missingCode?: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The path a HomeBase 2 stores a camera's recording under: its {@link cameraFolder}, then the recording's name
|
|
79
|
+
* as `yyyyMMddHHmmss` or `h265_yyyyMMddHHmmss`. Answers `undefined` for any other name, and for a channel
|
|
80
|
+
* {@link cameraFolder} refuses. Both forms are digits behind one fixed prefix, so the anchored test lets no
|
|
81
|
+
* `..`, slash or other character that could reach another path on the station through.
|
|
21
82
|
*/
|
|
22
83
|
export declare function homeBase2RecordingPath(channel: number, recording: string): string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The path a HomeBase 3 stores a camera's recording under:
|
|
86
|
+
* `/zx/emmcdata/<cameraFolder>/<yyyyMM>/<name>/<name>.zxvideo`, where the name is the recording's start as
|
|
87
|
+
* `yyyyMMddHHmmss`, whatever the codec, and `<yyyyMM>` is its first six digits. Answers `undefined` for any
|
|
88
|
+
* other name (a codec prefix, an extension or a path included), and for a channel {@link cameraFolder}
|
|
89
|
+
* refuses. The anchored test lets no character but digits through.
|
|
90
|
+
*/
|
|
91
|
+
export declare function homeBase3RecordingPath(channel: number, recording: string): string | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* The stations whose recording download is confirmed, by station model.
|
|
94
|
+
*
|
|
95
|
+
* `T8010`, HomeBase 2: `/media/mmcblk0p1/CameraNN/<name>.dat`, sealed under the event's cipher, finish tagged
|
|
96
|
+
* with channel 0, split pictures filled to {@link STATION_CHUNK_BYTES}.
|
|
97
|
+
*
|
|
98
|
+
* `T8030`, HomeBase 3: `/zx/emmcdata/CameraNN/<yyyyMM>/<name>/<name>.zxvideo`, in the clear, finish tagged
|
|
99
|
+
* with {@link STATION_CHANNEL}, a keyframe over 400000 bytes split at exactly 400000, a missing recording
|
|
100
|
+
* answered with -111.
|
|
101
|
+
*/
|
|
102
|
+
export declare const RECORDING_STATIONS: ReadonlyMap<string, RecordingStation>;
|
|
23
103
|
/** One recording transfer on a station session. */
|
|
24
104
|
export interface RecordingTransfer {
|
|
25
105
|
/** The recording's frames, in arrival order. Rejects as {@link receiveRecording} describes. */
|
|
@@ -30,9 +110,12 @@ export interface RecordingTransfer {
|
|
|
30
110
|
/**
|
|
31
111
|
* Request one recording and collect its frames until `CMD_DOWNLOAD_FINISH`. Only frames on
|
|
32
112
|
* {@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. The finish frame is taken on the camera's channel or
|
|
113
|
+
* station is never mixed in. The finish frame is taken on the camera's channel or on `finishChannel`
|
|
114
|
+
* ({@link RecordingStation.finishChannel}).
|
|
34
115
|
*
|
|
35
|
-
* `frames` rejects with {@link RecordingDownloadError}: `no-data` when
|
|
116
|
+
* `frames` rejects with {@link RecordingDownloadError}: `no-data` when the station's reply on the camera's
|
|
117
|
+
* channel carries `missingCode` ({@link RecordingStation.missingCode}) before any of the recording's frames, which
|
|
118
|
+
* also ends the transfer, or when nothing arrives within
|
|
36
119
|
* {@link FIRST_FRAME_WAIT_MS}, and `incomplete` when the transfer ends without its finish frame, on a
|
|
37
120
|
* {@link IDLE_END_MS} silence, on `timeoutMs` or on {@link MAX_TRANSFER_BYTES}. An abort rejects it at once
|
|
38
121
|
* with the signal's reason. After a time bound, the byte ceiling or an abort, the transfer stops collecting
|
|
@@ -42,14 +125,27 @@ export declare function receiveRecording(session: P2PSession, request: {
|
|
|
42
125
|
path: string;
|
|
43
126
|
accountId: string;
|
|
44
127
|
channel: number;
|
|
128
|
+
finishChannel: number;
|
|
129
|
+
missingCode?: number;
|
|
45
130
|
timeoutMs?: number;
|
|
46
131
|
signal?: AbortSignal;
|
|
47
132
|
}): RecordingTransfer;
|
|
48
133
|
/**
|
|
49
|
-
* Decode a recording's frames, in arrival order, into elementary streams.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
134
|
+
* Decode a recording's frames, in arrival order, into elementary streams. `station` sets where a split
|
|
135
|
+
* picture continues ({@link RecordingStation.chunkBytes}) and whether the frames are sealed
|
|
136
|
+
* ({@link RecordingStation.sealed}). On a sealed station a keyframe goes through one {@link VideoFrameDecoder}
|
|
137
|
+
* under `eccPrivateKeyHex`, and audio opens under that decoder's current media key; a keyframe that does not
|
|
138
|
+
* open, and audio before the first media key, are dropped. On a station in the clear every video payload is read
|
|
139
|
+
* as it is, and audio is everything after its 16-byte header; a keyframe that is in fact sealed begins with an
|
|
140
|
+
* ECIES point rather than a start code, so the assembler drops it. Split pictures are assembled before they
|
|
141
|
+
* enter the output, and an orphan continuation cannot become its own picture. Audio frames whose codec byte is
|
|
142
|
+
* not AAC-LC are dropped.
|
|
143
|
+
*
|
|
144
|
+
* Frame numbers are compared only within one frame size: a keyframe whose width or height differs from the last
|
|
145
|
+
* frame that states a size is counted as the frame after the one before it, and the frames after it follow on
|
|
146
|
+
* from there. A frame whose header states no size is passed over.
|
|
147
|
+
*
|
|
148
|
+
* Rejects with {@link RecordingDownloadError} `undecodable` when no video frame decodes, and on a sealed
|
|
149
|
+
* station whose key is absent or not a P-256 key.
|
|
54
150
|
*/
|
|
55
|
-
export declare function decodeRecording(frames: readonly RecordingFrame[],
|
|
151
|
+
export declare function decodeRecording(frames: readonly RecordingFrame[], station: Pick<RecordingStation, "sealed" | "chunkBytes">, eccPrivateKeyHex?: string): RecordingDownload;
|
|
@@ -256,7 +256,8 @@ export declare class SharedLiveSource {
|
|
|
256
256
|
*
|
|
257
257
|
* The previous stream is dropped rather than stopped through the state machine: it speaks to a session that
|
|
258
258
|
* is gone, and its `stop` would be read as an upstream end. Only a warm-up that asked for a replacement
|
|
259
|
-
* rewarms, so this is inert on a source that is streaming or has already failed.
|
|
259
|
+
* rewarms, so this is inert on a source that is streaming or has already failed. Each call replaces the
|
|
260
|
+
* re-issue ticker rather than adding one, so the start goes out once per interval however often it runs.
|
|
260
261
|
*/
|
|
261
262
|
rewarm(): void;
|
|
262
263
|
/**
|
|
@@ -376,8 +377,13 @@ export declare class SharedLiveSource {
|
|
|
376
377
|
private isKeyframe;
|
|
377
378
|
/**
|
|
378
379
|
* Handle a consumer leaving. When the last one detaches (1→0), release the station-session user via
|
|
379
|
-
* {@link SharedLiveSourceOptions.onIdle} (its own longer idle timer then arms) and
|
|
380
|
-
* linger teardown.
|
|
380
|
+
* {@link SharedLiveSourceOptions.onIdle} (its own longer idle timer then arms) and, while a stream is
|
|
381
|
+
* still running, arm its linger teardown.
|
|
382
|
+
*
|
|
383
|
+
* A source that ended its own stream ends its consumers before tearing down, and a Readable consumer
|
|
384
|
+
* detaches only once it has reached EOF — after that teardown. The source is then already `stopped`,
|
|
385
|
+
* with its session claim released, and stays so: lingering would make the owner hand it out again
|
|
386
|
+
* instead of building a source that holds a session.
|
|
381
387
|
*/
|
|
382
388
|
private onDetach;
|
|
383
389
|
/** Refcount hit zero — arm the linger teardown. A new attach in the window cancels it. */
|
|
@@ -73,7 +73,7 @@ export declare class VideoFrameDecoder {
|
|
|
73
73
|
export interface AssembledAccessUnit {
|
|
74
74
|
/** Unsigned 32-bit header timestamp from the first frame of this access unit. */
|
|
75
75
|
sourceTimestamp: number;
|
|
76
|
-
/** True when
|
|
76
|
+
/** True when header bit 0 is set, or (bit 0 clear) the payload contains an IDR — a point to begin decoding at. */
|
|
77
77
|
keyframe: boolean;
|
|
78
78
|
/** Frame width the unit's header declared. */
|
|
79
79
|
width: number;
|
|
@@ -102,8 +102,8 @@ export declare const STATION_CHUNK_BYTES = 64000;
|
|
|
102
102
|
* total length is nowhere on the wire and cannot be waited for. What the wire does carry, verified on two
|
|
103
103
|
* models:
|
|
104
104
|
*
|
|
105
|
-
* - the frames of one unit **repeat its
|
|
106
|
-
*
|
|
105
|
+
* - the frames of one unit **repeat its {@link VideoFrameHeader.sequence}**, and carry the unit's timestamp
|
|
106
|
+
* or one stamped up to `SPLIT_UNIT_STAMP_SPREAD_MS` later;
|
|
107
107
|
* - every frame of a split unit is filled to {@link STATION_CHUNK_BYTES} except the last;
|
|
108
108
|
* - a frame that STARTS a unit begins with an Annex-B start code once decoded, and a continuation begins
|
|
109
109
|
* mid-NAL, so it does not.
|
|
@@ -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.
|
|
@@ -153,3 +157,5 @@ export declare class AccessUnitAssembler {
|
|
|
153
157
|
/** Forget an incomplete unit, counting and reporting it. */
|
|
154
158
|
discard(): void;
|
|
155
159
|
}
|
|
160
|
+
/** Whether a decoded payload OPENS an access unit — a continuation begins mid-NAL, with no start code. */
|
|
161
|
+
export declare function beginsAccessUnit(body: Buffer): boolean;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for FCM credentials + seen persistent-ids, so we register the FCM
|
|
3
|
+
* token ONCE and just reconnect (and don't re-receive old pushes) across runs.
|
|
4
|
+
*/
|
|
5
|
+
import { FileSessionStore } from "../../core/store.js";
|
|
1
6
|
import type { FcmCredentials } from "./types.js";
|
|
2
7
|
export interface PersistedPush {
|
|
3
8
|
creds: FcmCredentials;
|
|
@@ -14,10 +19,6 @@ export declare class MemoryFcmStore implements FcmStore {
|
|
|
14
19
|
save(p: PersistedPush): void;
|
|
15
20
|
clear(): void;
|
|
16
21
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
constructor(path: string);
|
|
20
|
-
load(): PersistedPush | null;
|
|
21
|
-
save(p: PersistedPush): void;
|
|
22
|
-
clear(): void;
|
|
22
|
+
/** JSON-file FCM store: the {@link FileSessionStore} file format and write guarantees, on the push record. */
|
|
23
|
+
export declare class FileFcmStore extends FileSessionStore<PersistedPush> implements FcmStore {
|
|
23
24
|
}
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
* a live pull holds it open. Sends on a session are serialised; an ACK is correlated by its outer command
|
|
15
15
|
* and segment, since it carries no inner command. An ACK is not an observation of the resulting state.
|
|
16
16
|
*
|
|
17
|
+
* The station's own database is read over the same channel: a `set-payload` with inner `1306`
|
|
18
|
+
* (CMD_DATABASE) answers a table and inner `1308` (CMD_DATABASE_IMAGE) a stored image, each as one
|
|
19
|
+
* notify frame carrying the whole document, zstd-compressed when large. The notify link carries
|
|
20
|
+
* unrelated station reports between them, so a reply is matched by its content, never by arrival order.
|
|
21
|
+
*
|
|
17
22
|
* Live view of an attached camera rides the same session (the hub accepts one RTC session per account),
|
|
18
23
|
* through one shared live source per station: the station serves one camera at a time, so a second
|
|
19
24
|
* camera is refused until the first one's view ends.
|
|
20
25
|
*/
|
|
21
|
-
import { type Command, type MediaProvider } from "../../core/contracts.js";
|
|
26
|
+
import { type Command, type MediaOutputResolution, type MediaProvider } from "../../core/contracts.js";
|
|
22
27
|
import type { Logger } from "../../core/logger.js";
|
|
23
28
|
import { RtcSession, type RtcSessionOptions } from "./session.js";
|
|
24
29
|
export interface RtcIdentity {
|
|
@@ -56,8 +61,8 @@ export interface RtcLiveRoute extends RtcRoute {
|
|
|
56
61
|
export interface RtcCommandRouterDeps {
|
|
57
62
|
/** The logged-in session's credentials; `undefined` while logged out. */
|
|
58
63
|
identity: () => RtcIdentity | undefined;
|
|
59
|
-
/** The
|
|
60
|
-
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>;
|
|
61
66
|
/** ISO country sent on the sign request (default `US`). */
|
|
62
67
|
country?: string;
|
|
63
68
|
logger?: Logger;
|
|
@@ -72,8 +77,50 @@ export declare class RtcCommandRouter {
|
|
|
72
77
|
private readonly lives;
|
|
73
78
|
private readonly warm;
|
|
74
79
|
private readonly callSources;
|
|
80
|
+
private lastTransaction;
|
|
81
|
+
/** The latest shard each `call` source resolved, read by its `makeStream` (see {@link callMediaProviderFor}). */
|
|
82
|
+
private readonly callShards;
|
|
75
83
|
constructor(deps: RtcCommandRouterDeps);
|
|
76
84
|
dispatchCommand(route: RtcRoute, cmd: Command): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Query one on-station table over the station's control channel and answer its rows.
|
|
87
|
+
*
|
|
88
|
+
* Sends the `1350` envelope with inner `{cmd: 1306, payload: {cmd: 10000, table, payload, transaction}}`,
|
|
89
|
+
* the whole-table query, on the station channel. The hub ACKs on the command link and answers with one notify frame whose body
|
|
90
|
+
* carries `{cmd, table, data, mIntRet, msg, transaction, …}`, echoing the read's `transaction`; that echo
|
|
91
|
+
* is the only thing a reply is attributed by, and every read sends a `transaction` no other read on this
|
|
92
|
+
* router has sent. Resolves with the reply's `data` rows. Rejects with the hub's `msg` on a non-zero
|
|
93
|
+
* `mIntRet`, on an ACK refusing the envelope, at once when `signal` aborts (queued or sent), when the
|
|
94
|
+
* session closes, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no reply.
|
|
95
|
+
*
|
|
96
|
+
* One read or command at a time per station: the read holds the station's send queue until it settles.
|
|
97
|
+
*/
|
|
98
|
+
readTable(route: RtcRoute, table: string, opts?: {
|
|
99
|
+
timeoutMs?: number;
|
|
100
|
+
signal?: AbortSignal;
|
|
101
|
+
}): Promise<unknown[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Fetch one stored image by its on-station path over the station's control channel.
|
|
104
|
+
*
|
|
105
|
+
* Sends the `1350` envelope with inner `{cmd: 1308, payload: [{file}]}` on the station channel. The hub
|
|
106
|
+
* ACKs on the command link and answers with one notify frame whose body is `{file, content}`, `content`
|
|
107
|
+
* the image in base64, and no `transaction`; the reply is the one whose `file` equals `filePath`. A reply
|
|
108
|
+
* arriving after an abandoned read of the same path carries that path's bytes, which are the bytes a
|
|
109
|
+
* later read of it asks for. Resolves with the decoded bytes. Rejects on an ACK refusing the envelope,
|
|
110
|
+
* at once when `signal` aborts (queued or sent), when the session closes, and when `timeoutMs` (default
|
|
111
|
+
* {@link DB_TABLE_TIMEOUT_MS}) elapses with no reply.
|
|
112
|
+
*
|
|
113
|
+
* One read or command at a time per station: the read holds the station's send queue until it settles.
|
|
114
|
+
*/
|
|
115
|
+
readImage(route: RtcRoute, filePath: string, opts?: {
|
|
116
|
+
timeoutMs?: number;
|
|
117
|
+
signal?: AbortSignal;
|
|
118
|
+
}): Promise<Buffer>;
|
|
119
|
+
/**
|
|
120
|
+
* A `transaction` stamp in the vendor app's shape, a decimal epoch-in-milliseconds string, strictly
|
|
121
|
+
* greater than the last one this router issued, so two reads in the same millisecond never share one.
|
|
122
|
+
*/
|
|
123
|
+
private nextTransaction;
|
|
77
124
|
/**
|
|
78
125
|
* The media of a camera on a T9000: live video, a readable feed and a live still over the station's
|
|
79
126
|
* session. Recording has no wire here and is refused; the optional members it has no wire for are absent.
|
|
@@ -86,8 +133,14 @@ export declare class RtcCommandRouter {
|
|
|
86
133
|
*/
|
|
87
134
|
trackMediaProviderFor(route: RtcRoute): MediaProvider;
|
|
88
135
|
/** NVR media uses a dedicated call per camera, independent of station commands. */
|
|
89
|
-
nvrMediaProviderFor(route: RtcLiveRoute, views: readonly number[]): MediaProvider;
|
|
90
|
-
/**
|
|
136
|
+
nvrMediaProviderFor(route: RtcLiveRoute, views: readonly number[], outputResolutions?: readonly MediaOutputResolution[]): MediaProvider;
|
|
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
|
+
*/
|
|
91
144
|
private callMediaProviderFor;
|
|
92
145
|
/**
|
|
93
146
|
* Tear down every station session (logout / shutdown). Each live source is disposed before its session
|
|
@@ -108,17 +161,41 @@ export declare class RtcCommandRouter {
|
|
|
108
161
|
private refuseIfWatched;
|
|
109
162
|
private newLiveSource;
|
|
110
163
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
164
|
+
* Send one envelope on the station's session, queued behind its other sends, and settle as
|
|
165
|
+
* {@link sendAwaitAck} does. `body` is the inner payload; the station session, the `account_id` it is
|
|
166
|
+
* stamped with and the packet are resolved here, so a command and a read reach the wire through the same
|
|
167
|
+
* steps. With `reply.signal`, the caller's wait rejects the moment it aborts, also while the send is still
|
|
168
|
+
* queued; a send not yet made by then is skipped.
|
|
169
|
+
*/
|
|
170
|
+
private sendQueued;
|
|
171
|
+
/**
|
|
172
|
+
* Without `reply`, completes on the command ACK or a correlated control-result notification. The
|
|
173
|
+
* notification must match the request channel, parameter and segment and contain a structured payload.
|
|
174
|
+
* Receipt does not establish physical actuation. Bounded by {@link ACK_TIMEOUT_MS}.
|
|
175
|
+
*
|
|
176
|
+
* With `reply`, a zero-result ACK is not the answer: the first notify body (an object) for which
|
|
177
|
+
* `reply.pick` answers a value resolves with it, one `pick` throws on rejects with that error, and every
|
|
178
|
+
* other notify frame is left alone. Bounded by `reply.timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}),
|
|
179
|
+
* and ended by `reply.signal`; one already aborted is not sent.
|
|
180
|
+
*
|
|
181
|
+
* Either way an ACK with a non-zero result code rejects, and the command is sent once.
|
|
114
182
|
*/
|
|
115
183
|
private sendAwaitAck;
|
|
116
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
|
+
*/
|
|
117
193
|
private stationSession;
|
|
118
194
|
/**
|
|
119
195
|
* One bounded bring-up: the session is up when its command channel opens, and it fails, with the
|
|
120
196
|
* session closed and every listener gone, when `connect()` throws, the session closes first (rejecting
|
|
121
|
-
* with the failure that closed it, when it carries one), or {@link CONNECT_TIMEOUT_MS} passes
|
|
197
|
+
* with the failure that closed it, when it carries one), or {@link CONNECT_TIMEOUT_MS} passes, naming the
|
|
198
|
+
* stage the session reached and its peer's last state. A single
|
|
122
199
|
* promise, so an early failure cannot leave a second one rejecting unheard.
|
|
123
200
|
*/
|
|
124
201
|
private bringUp;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Start is inner `cmd 1003` in a `1350` SET_PAYLOAD on the station channel `255`. The header byte the
|
|
7
7
|
* codec names `isResponse` carries the portal's `streamId`, `1` for a live view, and `chn_list` is an
|
|
8
8
|
* array of `{index, chn, sensor, isUps, isClicked}` objects. Stop is inner `cmd 1004`, the same way.
|
|
9
|
+
* - The NVR start may carry `stitch_resolution: {width, height}` to set output geometry; omission keeps the NVR default.
|
|
9
10
|
* - Video arrives on the live link as portal packets `1300` on channel `100 + play id`; a single-camera
|
|
10
11
|
* view is play id `1`, whatever the camera's own channel. Each body is a fixed 22-byte media header,
|
|
11
12
|
* then the Annex B video it declares. See {@link parseMediaBody}.
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
* shared live source's.
|
|
18
19
|
*/
|
|
19
20
|
import { EventEmitter } from "node:events";
|
|
20
|
-
import type { LiveStreamHandle } from "../../core/contracts.js";
|
|
21
|
+
import type { LiveStreamHandle, MediaOutputResolution } from "../../core/contracts.js";
|
|
21
22
|
import type { Logger } from "../../core/logger.js";
|
|
22
23
|
import type { RtcSession } from "./session.js";
|
|
23
24
|
import { type SegmentCounter } from "./portal-packet.js";
|
|
@@ -46,6 +47,8 @@ export interface RtcLiveOptions {
|
|
|
46
47
|
nvr?: boolean;
|
|
47
48
|
/** Optical view carried by the NVR start request; defaults to 0. */
|
|
48
49
|
view?: number;
|
|
50
|
+
/** NVR live-start output geometry; absence sends no size override. */
|
|
51
|
+
outputResolution?: MediaOutputResolution;
|
|
49
52
|
}
|
|
50
53
|
/**
|
|
51
54
|
* One camera's live pull. `start()` sends the start; a non-zero start ACK or a closed session ends it with
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The command framing a HomeBase S1 Pro (T9000) speaks over its WebRTC data channel — the wire the
|
|
3
|
-
* security.eufy.com web client builds, reproduced byte-exact.
|
|
4
|
-
*
|
|
5
|
-
* A packet is a 16-byte header behind the ASCII magic `XZYH`, then a JSON body, sent in the clear (the
|
|
6
|
-
* DTLS channel is the encryption). Replies come back with `isResponse = 1`, the same `segment` the
|
|
7
|
-
* request carried, and a body whose first four bytes are a little-endian result code.
|
|
8
|
-
*
|
|
9
|
-
* Every offset is the portal bundle's header builder and parser (`Gr()` / `Qr()`) and its
|
|
10
|
-
* `worker_sctp_send`.
|
|
11
|
-
*/
|
|
12
1
|
export declare const PORTAL_HEADER_LENGTH = 16;
|
|
13
2
|
/** The SET_PAYLOAD envelope. */
|
|
14
3
|
export declare const PORTAL_CMD_SET_PAYLOAD = 1350;
|
|
@@ -61,8 +50,9 @@ export declare function isPortalPacket(buf: Buffer): boolean;
|
|
|
61
50
|
/** A complete request packet: header + body. */
|
|
62
51
|
export declare function buildPortalPacket(req: PortalRequest): Buffer;
|
|
63
52
|
/**
|
|
64
|
-
* Parse an inbound packet. On the command channel the body opens
|
|
65
|
-
* carry JSON after it; on the notify channel the whole body is JSON
|
|
53
|
+
* Parse an inbound packet, opening a zstd-compressed one first. On the command channel the body opens
|
|
54
|
+
* with the int32 result code and may carry JSON after it; on the notify channel the whole body is JSON
|
|
55
|
+
* with a nested `cmd`. `undefined` for a frame that is neither a packet nor a stream holding one.
|
|
66
56
|
*/
|
|
67
57
|
export declare function parsePortalPacket(buf: Buffer, linkType?: number): PortalResponse | undefined;
|
|
68
58
|
/** A 1..255 segment counter that never yields 0 (the portal reserves it). */
|
|
@@ -15,8 +15,12 @@
|
|
|
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. Before the command channel opens, a peer that fails or closes also ends the session, and so
|
|
22
|
+
* does a hub `hangup` on a `scall` session, the exchange above; {@link RtcSession.stage} names how far
|
|
23
|
+
* the bring-up got. A `call` session's pre-open hangup is not evidenced as terminal and stays logged only.
|
|
20
24
|
*
|
|
21
25
|
* A session whose peer is a {@link RtcPeerOptions.mediaTrack} peer has no command channel: `close` fires
|
|
22
26
|
* when the peer fails or closes, and the video track's packets are emitted as `rtp`.
|
|
@@ -45,6 +49,11 @@ export interface RtcSessionEvents {
|
|
|
45
49
|
/** One packet of the video track, in a {@link RtcPeerOptions.mediaTrack} session. */
|
|
46
50
|
rtp: [packet: Buffer];
|
|
47
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* How far a session's bring-up got, in order: the sign fetch, the signalling socket, its auth, the placed
|
|
54
|
+
* call, the hub's relay grant, our answer to its offer, and the open command channel.
|
|
55
|
+
*/
|
|
56
|
+
type RtcBringUpStage = "signing" | "connecting" | "authenticating" | "calling" | "relay-granted" | "answered" | "connected";
|
|
48
57
|
export declare class RtcSession extends EventEmitter<RtcSessionEvents> {
|
|
49
58
|
private readonly opts;
|
|
50
59
|
private readonly signaling;
|
|
@@ -58,8 +67,15 @@ export declare class RtcSession extends EventEmitter<RtcSessionEvents> {
|
|
|
58
67
|
private closeAnnounced;
|
|
59
68
|
private sdpHandled;
|
|
60
69
|
private chain;
|
|
70
|
+
private stageReached;
|
|
71
|
+
private lastPeerState?;
|
|
61
72
|
constructor(opts: RtcSessionOptions);
|
|
73
|
+
private get mediaTrack();
|
|
62
74
|
get isConnected(): boolean;
|
|
75
|
+
/** The last bring-up stage this session reached. */
|
|
76
|
+
get stage(): RtcBringUpStage;
|
|
77
|
+
/** The peer connection's last reported state; `undefined` before it reports one. */
|
|
78
|
+
get peerState(): string | undefined;
|
|
63
79
|
/**
|
|
64
80
|
* Start the sequence; resolves once the call is sent. `connected` fires when the channel opens. A session
|
|
65
81
|
* closed by the time the sign fetch or the auth completes rejects instead of opening the socket or
|
|
@@ -97,3 +113,4 @@ export declare class RtcSession extends EventEmitter<RtcSessionEvents> {
|
|
|
97
113
|
*/
|
|
98
114
|
private onInfo;
|
|
99
115
|
}
|
|
116
|
+
export {};
|
|
@@ -9,6 +9,12 @@ export declare class StoredImageCache {
|
|
|
9
9
|
private readonly activeDevices;
|
|
10
10
|
private activeDownloads;
|
|
11
11
|
private generation;
|
|
12
|
+
/**
|
|
13
|
+
* Bumped by {@link clearLifecycleErrors}. A lifecycle failure from a download started under an older
|
|
14
|
+
* count is not recorded; the candidate is queued again, unless a newer one is already queued, so the
|
|
15
|
+
* current session fetches it.
|
|
16
|
+
*/
|
|
17
|
+
private lifecycle;
|
|
12
18
|
constructor(downloader: (url: string, deviceKey: string) => Promise<Buffer>, logger: Logger, clock?: () => number, isLifecycleError?: (error: unknown) => boolean);
|
|
13
19
|
/**
|
|
14
20
|
* Observe a normalized thumbnail URL and start acquisition eagerly.
|
|
@@ -20,6 +26,14 @@ export declare class StoredImageCache {
|
|
|
20
26
|
observe(deviceKey: string, url: string): void;
|
|
21
27
|
/** Return retained bytes without starting or awaiting network work. */
|
|
22
28
|
snapshotStored(deviceKey: string): Promise<Buffer>;
|
|
29
|
+
/**
|
|
30
|
+
* Forget every device's recorded lifecycle failure, keeping retained bytes and candidate state.
|
|
31
|
+
*
|
|
32
|
+
* A lifecycle failure describes the session that was current when the download ran, so it no longer
|
|
33
|
+
* holds once a login succeeds again; {@link snapshotStored} then answers from the retained image, or
|
|
34
|
+
* with the reason a stored image is absent. A download in flight across the call cannot record one.
|
|
35
|
+
*/
|
|
36
|
+
clearLifecycleErrors(): void;
|
|
23
37
|
/** Invalidate all retained and candidate state. */
|
|
24
38
|
clear(): void;
|
|
25
39
|
private pump;
|
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.40",
|
|
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",
|