@mega-yfue/eufy-sdk 0.5.0-beta.2 → 0.5.0-beta.20
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 +24 -2
- package/dist/client/eufy-mega.d.ts +20 -6
- package/dist/client/types.d.ts +1 -1
- package/dist/core/contracts.d.ts +46 -23
- package/dist/core/store.d.ts +2 -0
- package/dist/index.js +1581 -84
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/camera.d.ts +24 -10
- package/dist/model/capabilities/light.d.ts +3 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +19 -13
- package/dist/model/device-family.d.ts +2 -0
- package/dist/model/registry.d.ts +3 -2
- package/dist/model/types.d.ts +6 -0
- package/dist/transport/http/mega-client.d.ts +26 -0
- package/dist/transport/p2p/command-router.d.ts +13 -5
- package/dist/transport/p2p/fmp4.d.ts +6 -1
- package/dist/transport/p2p/live-stream.d.ts +3 -0
- package/dist/transport/p2p/p2p-session.d.ts +7 -2
- package/dist/transport/p2p/talkback.d.ts +21 -10
- package/dist/transport/p2p/video.d.ts +3 -2
- package/dist/transport/rtc/command-router.d.ts +72 -0
- package/dist/transport/rtc/peer.d.ts +93 -0
- package/dist/transport/rtc/portal-packet.d.ts +66 -0
- package/dist/transport/rtc/ptcs-framer.d.ts +85 -0
- package/dist/transport/rtc/scall-sdp.d.ts +42 -0
- package/dist/transport/rtc/session.d.ts +78 -0
- package/dist/transport/rtc/signaling.d.ts +137 -0
- package/package.json +9 -1
|
@@ -8,6 +8,8 @@ import { type Command, type MediaProvider } from "../../core/contracts.js";
|
|
|
8
8
|
* `transport/p2p/commands.ts`). Each entry notes its app `CommandType` name.
|
|
9
9
|
*/
|
|
10
10
|
export declare const CAMERA_CMD: {
|
|
11
|
+
/** Camera-reported Wi-Fi signal (app `CMD_GET_WIFI_RSSI`). */
|
|
12
|
+
readonly WIFI_RSSI: 1142;
|
|
11
13
|
/** Camera on/off. Read and write polarity depend on the family and topology. */
|
|
12
14
|
readonly CAMERA_ENABLE: 1035;
|
|
13
15
|
/** Wrapped T8410 power switch on HomeBase 3. */
|
|
@@ -271,6 +273,14 @@ export type CameraActions = Surface<typeof CAMERA_MEMBERS> & {
|
|
|
271
273
|
* @internal
|
|
272
274
|
*/
|
|
273
275
|
export declare const CAMERA_MEMBERS: {
|
|
276
|
+
/** Reads the latest cached report and exposes no setter. */
|
|
277
|
+
readonly wifiRssi: {
|
|
278
|
+
readonly param: 1142;
|
|
279
|
+
readonly type: "number";
|
|
280
|
+
readonly kind: "scalar";
|
|
281
|
+
readonly provenance: "apk";
|
|
282
|
+
readonly description: "Camera-reported Wi-Fi signal; physical units and a quality scale are unspecified.";
|
|
283
|
+
};
|
|
274
284
|
/**
|
|
275
285
|
* The default READ is the *disable*-bit convention (1035 "0" ⇒ ON, 2001 direct); the WRITE polarity is
|
|
276
286
|
* family-dependent — see `powerValue` / `isEnableBitPolarity`. Battery/solo cams report the state under
|
|
@@ -435,25 +445,29 @@ export declare const CAMERA_MEMBERS: {
|
|
|
435
445
|
* Live-view quality, the pair to {@link CAMERA_MEMBERS.recordingQuality} and a different setting on a
|
|
436
446
|
* different wire: changing one leaves the other's parameter untouched.
|
|
437
447
|
*
|
|
438
|
-
* The READ is 1020, which reports the resolved tier as a plain integer
|
|
439
|
-
*
|
|
440
|
-
*
|
|
448
|
+
* The READ is 1020, which reports the resolved tier as a plain integer, while the WRITE rides 2730's
|
|
449
|
+
* `1350` payload. They are two ids for one setting because 2730 carries a per-mode config and 1020
|
|
450
|
+
* carries the tier in force; the tier is what this property means. 1020 is also the name the param
|
|
451
|
+
* dictionary already gives that id, and `Device` keys state by NAME — a member claiming 2730 under
|
|
452
|
+
* this name would collide with it on a device reporting both.
|
|
441
453
|
*
|
|
442
|
-
* `
|
|
443
|
-
*
|
|
444
|
-
* 2731 write on the same envelope and the same session does land. The one difference the capture
|
|
445
|
-
* shows is that the app's frame carries no `mChannel`/`mValue3`, which this envelope always emits.
|
|
446
|
-
* Until a send is confirmed, the setter is absent — a control that silently does nothing is worse
|
|
447
|
-
* than none — and the read ships ahead of it, which verification per direction allows.
|
|
454
|
+
* `transaction` is part of the frame that was captured, so it is sent. The recording sibling's own
|
|
455
|
+
* confirmed frame carries no such field; neither is inferred from the other.
|
|
448
456
|
*/
|
|
449
457
|
readonly streamingQuality: {
|
|
458
|
+
readonly accepts: StreamingQualityName;
|
|
450
459
|
readonly param: 1020;
|
|
451
460
|
readonly type: "number";
|
|
452
461
|
readonly kind: "enum";
|
|
453
462
|
readonly enumValues: Readonly<Record<number, string>>;
|
|
454
463
|
readonly provenance: "verified";
|
|
455
|
-
readonly
|
|
464
|
+
readonly args: readonly [{
|
|
465
|
+
readonly name: "quality";
|
|
466
|
+
readonly kind: "enum";
|
|
467
|
+
readonly description: "A tier, or the name it maps to. 0 is Auto.";
|
|
468
|
+
}];
|
|
456
469
|
readonly description: string;
|
|
470
|
+
readonly write: (v: string | number | boolean, ctx: CommandContext) => Command | undefined;
|
|
457
471
|
};
|
|
458
472
|
/**
|
|
459
473
|
* `type` is how the value is STORED, and 2731 stores the whole config — the ACTIVE tier is lifted out
|
|
@@ -160,6 +160,9 @@ export declare const LIGHT_MEMBERS: {
|
|
|
160
160
|
* This is the switch a user changes and expects to STAY changed. {@link isOn} is a different fact:
|
|
161
161
|
* the lamp being lit right now, driven by whichever
|
|
162
162
|
* client is streaming — the vendor app lights it for a live view and drops it on quitting.
|
|
163
|
+
*
|
|
164
|
+
* The T8423 write takes the session-chosen level; level-1 int+string was verified live on a standalone
|
|
165
|
+
* unit.
|
|
163
166
|
*/
|
|
164
167
|
readonly spotlightEnabled: {
|
|
165
168
|
readonly param: 1403;
|
|
@@ -169,12 +169,11 @@ export declare const TUYA_CLEAN_TYPES: readonly ["Sweep", "SweepMop", "Mop"];
|
|
|
169
169
|
/** @internal */
|
|
170
170
|
export type TuyaCleanType = (typeof TUYA_CLEAN_TYPES)[number];
|
|
171
171
|
/**
|
|
172
|
-
* `ModeCtrlRequest.method` values for DP 152. Live-verified on T2351:
|
|
173
|
-
*
|
|
172
|
+
* `ModeCtrlRequest.method` values for DP 152. Live-verified on T2351: START_GOHOME → 6,
|
|
173
|
+
* PAUSE_TASK → 13. An auto clean is `START_AUTO_CLEAN` (0) with a payload — see
|
|
174
|
+
* {@link ModeCtrlParamMethod}.
|
|
174
175
|
*/
|
|
175
176
|
export declare const ModeCtrlMethod: {
|
|
176
|
-
/** Live-verified on a T2351. Zero, so it is omitted from the wire per the proto3 default rule. */
|
|
177
|
-
readonly START_AUTO_CLEAN: 0;
|
|
178
177
|
/** Live-verified on a T2351. */
|
|
179
178
|
readonly START_GOHOME: 6;
|
|
180
179
|
/** Live-verified on a T2351. */
|
|
@@ -202,9 +201,11 @@ export declare const ModeCtrlMethod: {
|
|
|
202
201
|
* its argument in the signature: {@link VACUUM_CLEAN_MEMBERS.startScene},
|
|
203
202
|
* {@link VACUUM_CLEAN_MEMBERS.cleanRooms} and {@link VACUUM_CLEAN_MEMBERS.cleanZones}.
|
|
204
203
|
*
|
|
205
|
-
* The outer frame these ride in is byte-verified on a live T2351, and `SCENE`, `SELECT_ROOMS`
|
|
206
|
-
* `SELECT_ZONES` have each since been RUN on a T2351 and did what they name — so their numbers rest
|
|
207
|
-
* observed behaviour rather than on the vendor's definition alone.
|
|
204
|
+
* The outer frame these ride in is byte-verified on a live T2351, and `AUTO`, `SCENE`, `SELECT_ROOMS`
|
|
205
|
+
* and `SELECT_ZONES` have each since been RUN on a T2351 and did what they name — so their numbers rest
|
|
206
|
+
* on observed behaviour rather than on the vendor's definition alone. `AUTO` is RUN with its payload:
|
|
207
|
+
* the vendor's definition calls `AutoClean.clean_times` valid only when non-zero, and the T2351 did
|
|
208
|
+
* not start on method 0 sent without it.
|
|
208
209
|
*
|
|
209
210
|
* That distinction is the whole point of checking, and this is the one place it is argued: an AIoT
|
|
210
211
|
* data-point write is fire-and-forget, so a wrong number would be a different command arriving and
|
|
@@ -215,6 +216,11 @@ export declare const ModeCtrlMethod: {
|
|
|
215
216
|
* scene id and a map id both arrive on DP 180.
|
|
216
217
|
*/
|
|
217
218
|
export declare const ModeCtrlParamMethod: {
|
|
219
|
+
/** `START_AUTO_CLEAN` with an `AutoClean` payload — clean the whole floor. */
|
|
220
|
+
readonly AUTO: {
|
|
221
|
+
readonly method: 0;
|
|
222
|
+
readonly param: 3;
|
|
223
|
+
};
|
|
218
224
|
/** `START_SELECT_ROOMS_CLEAN` — clean the named rooms of a named map. */
|
|
219
225
|
readonly SELECT_ROOMS: {
|
|
220
226
|
readonly method: 1;
|
|
@@ -274,17 +280,17 @@ export declare function encodeSelectZonesClean(mapId: number, zones: readonly Va
|
|
|
274
280
|
* @internal
|
|
275
281
|
*/
|
|
276
282
|
export declare function encodeSceneClean(sceneId: number): string;
|
|
283
|
+
/**
|
|
284
|
+
* Build a whole-floor auto clean carrying an `AutoClean` payload of one pass.
|
|
285
|
+
* @internal
|
|
286
|
+
*/
|
|
287
|
+
export declare function encodeAutoClean(): string;
|
|
277
288
|
/**
|
|
278
289
|
* Encode a `ModeCtrlRequest` protobuf (DP 152) as a DP value: `varint(bodyLen) ++ {method:1, seq:2}`.
|
|
279
290
|
*
|
|
280
291
|
* Built on {@link RawDpWriter} rather than hand-rolled bytes. The frame is unchanged and the existing
|
|
281
292
|
* byte-level test is what proves it — that test was written against a live T2351 capture, so it holds
|
|
282
293
|
* the writer to the wire rather than to this function's own idea of the wire.
|
|
283
|
-
*
|
|
284
|
-
* Method 0 (START_AUTO_CLEAN) is omitted rather than written as an explicit zero, per the proto3
|
|
285
|
-
* default-field rule and confirmed on that same capture. The writer deliberately does not apply that
|
|
286
|
-
* rule itself: whether an explicit zero and an absent field mean the same thing is the
|
|
287
|
-
* message's business, not the encoder's.
|
|
288
294
|
* @internal
|
|
289
295
|
*/
|
|
290
296
|
export declare function encodeModeCtrl(method: number, seq: number): string;
|
|
@@ -1955,7 +1961,7 @@ export declare const VACUUM_CLEAN_MEMBERS: {
|
|
|
1955
1961
|
readonly provenance: "mega";
|
|
1956
1962
|
readonly description: "Stop smart-follow mode (ModeCtrlRequest method 18 over DP 152). Method number not captured — unverified.";
|
|
1957
1963
|
};
|
|
1958
|
-
/** Start an auto-clean run via ModeCtrlRequest method 0 (DP 152). AIoT only — Tuya write unverified. */
|
|
1964
|
+
/** Start an auto-clean run via ModeCtrlRequest method 0 with one `AutoClean` pass (DP 152). AIoT only — Tuya write unverified. */
|
|
1959
1965
|
readonly startCleaning: import("./members.js").MethodMember<() => Promise<void>> & {
|
|
1960
1966
|
available: (ctx: import("./types.js").CommandContext) => boolean;
|
|
1961
1967
|
};
|
|
@@ -43,6 +43,8 @@ export declare const isOutdoorPanTilt: (ctx: FamilyContext) => boolean;
|
|
|
43
43
|
export declare const isFloodLight: (ctx: FamilyContext) => boolean;
|
|
44
44
|
/** HomeBase-family hub (has a speaker/alarm) — a station EXCLUDING the NVRs, per `HOMEBASE_TYPES`. */
|
|
45
45
|
export declare const isHomeBase: (ctx: FamilyContext) => boolean;
|
|
46
|
+
/** HomeBase S1 Pro (`STATION_9000`), the station the app drives over its WebRTC control channel. */
|
|
47
|
+
export declare const isStation9000: (ctx: FamilyContext) => boolean;
|
|
46
48
|
/** Wired doorbell (DeviceType.DOORBELL). */
|
|
47
49
|
export declare const isWiredDoorbell: (ctx: FamilyContext) => boolean;
|
|
48
50
|
/**
|
package/dist/model/registry.d.ts
CHANGED
|
@@ -48,7 +48,8 @@ export declare function resolveDevice(rec: CloudRecord): ResolvedDevice;
|
|
|
48
48
|
* The property manifest for a device, from its capabilities and the record's facts. The single place
|
|
49
49
|
* an {@link AvailabilityContext} is built — so a family-gate (`available`) and a per-model enum
|
|
50
50
|
* (`enumValuesFor`) are decided from the same truthful, session-free view on every path (initial
|
|
51
|
-
* resolve and {@link Device.reresolve}). Populated only from what a record carries
|
|
52
|
-
*
|
|
51
|
+
* resolve and {@link Device.reresolve}). Populated only from what a record carries: its cloud params and
|
|
52
|
+
* the params its device reported over realtime, the same evidence a bound getter is gated on. Never
|
|
53
|
+
* transport fields a live session hasn't produced.
|
|
53
54
|
*/
|
|
54
55
|
export declare function resolveProperties(rec: CloudRecord, codec: Codec, capabilities: Capability[]): PropertySpec[];
|
package/dist/model/types.d.ts
CHANGED
|
@@ -286,6 +286,12 @@ export interface CloudRecord {
|
|
|
286
286
|
parentSn?: string;
|
|
287
287
|
/** Reported param_type → raw value. Presence of a param is a capability signal. */
|
|
288
288
|
params?: Record<number, string>;
|
|
289
|
+
/**
|
|
290
|
+
* Params the device reported over its realtime channel, keyed in the device's own namespace. A line
|
|
291
|
+
* whose state exists only on that channel carries none of it in {@link params}, so this is the
|
|
292
|
+
* evidence an availability gate on such a param is decided from.
|
|
293
|
+
*/
|
|
294
|
+
dpParams?: Record<number, string>;
|
|
289
295
|
}
|
|
290
296
|
/** The fully-resolved device shape produced by the 3-tier resolver. */
|
|
291
297
|
export interface ResolvedDevice {
|
|
@@ -209,6 +209,8 @@ export interface SignedRetry {
|
|
|
209
209
|
export declare class MegaHttpClient {
|
|
210
210
|
private readonly cfg;
|
|
211
211
|
private region;
|
|
212
|
+
/** The host `estimate_domain` answered, persisted with the session. */
|
|
213
|
+
private estimatedDomain;
|
|
212
214
|
private bootstrapDomain?;
|
|
213
215
|
private sessionKey?;
|
|
214
216
|
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
@@ -275,6 +277,21 @@ export declare class MegaHttpClient {
|
|
|
275
277
|
} | undefined;
|
|
276
278
|
/** The active region shard (e.g. `"eu-pr"`, `"us-pr"`), set after {@link login} or a region override. */
|
|
277
279
|
get regionShard(): RegionShard;
|
|
280
|
+
/**
|
|
281
|
+
* The shard the RTC signalling signs on: the regional prefix of the estimated domain
|
|
282
|
+
* (`…-ie-…`/`…-ie.` → `ie-pr`), else {@link regionShard}. The eu/us classification cannot name shards
|
|
283
|
+
* like `ie-pr`, whose accounts need the IE sign host and cluster.
|
|
284
|
+
*/
|
|
285
|
+
get rtcShard(): string;
|
|
286
|
+
/**
|
|
287
|
+
* The credentials the RTC signalling needs; `undefined` while logged out. The sign is refused
|
|
288
|
+
* ("gtoken not equal userid") unless its gtoken is the one every authed HTTP call carries.
|
|
289
|
+
*/
|
|
290
|
+
rtcIdentity(): {
|
|
291
|
+
authToken: string;
|
|
292
|
+
userId: string;
|
|
293
|
+
gtoken: string;
|
|
294
|
+
} | undefined;
|
|
278
295
|
/**
|
|
279
296
|
* The name commands attribute themselves to — {@link MegaClientConfig.accountName} when the config
|
|
280
297
|
* pins one (trimmed; blank counts as unset), otherwise the logged-in account's display name, which
|
|
@@ -367,6 +384,15 @@ export declare class MegaHttpClient {
|
|
|
367
384
|
* params instead, which also carry `{param_type, param_value, update_time}` and are not owner-gated.
|
|
368
385
|
*/
|
|
369
386
|
getDeviceParamList<T = unknown>(deviceSn: string): Promise<T>;
|
|
387
|
+
/**
|
|
388
|
+
* Fetch the account's devices as the `devicerelation` service lists them, `{attribute: 7, house_id: ""}`.
|
|
389
|
+
*
|
|
390
|
+
* Each entry is `{device: {device_sn, dps, …}}`, and `dps` is the cloud's last-known value of every
|
|
391
|
+
* data point that device has reported, keyed by DP number. Answered for an account a device is shared
|
|
392
|
+
* with as well as for its owner. Sent with `category: eufy_home`, without which the service answers
|
|
393
|
+
* `10000`.
|
|
394
|
+
*/
|
|
395
|
+
getDeviceRelationList<T = unknown>(): Promise<T>;
|
|
370
396
|
/**
|
|
371
397
|
* Fetch one page of a device's **cleaning history** from the mega `clean` service.
|
|
372
398
|
*
|
|
@@ -258,11 +258,14 @@ export declare class P2PCommandRouter {
|
|
|
258
258
|
*/
|
|
259
259
|
dispatchCommand(sn: string, cmd: Command): Promise<void>;
|
|
260
260
|
/**
|
|
261
|
-
* Restart
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
261
|
+
* Restart the station that owns `sn`'s P2P session. `RESTART_HUB` (1034) is a station-scalar on the
|
|
262
|
+
* broadcast channel 255 whose body is `[u32 value][account_id padded]`, value `0`. ✅ Wire-confirmed
|
|
263
|
+
* byte-exact as a level-2 frame from a capture of the app's own HomeBase Restart (2026-08-03).
|
|
264
|
+
*
|
|
265
|
+
* The seal follows the session, as {@link sendBySessionLevel} defines it: a keyed session takes the
|
|
266
|
+
* level-2 frame, a keyless one — a standalone camera, which never negotiates a key — the same body
|
|
267
|
+
* sealed level-1. Both are replayed {@link DIRECT_CMD_SENDS}× at 200ms, as every other unacknowledged
|
|
268
|
+
* control here is.
|
|
266
269
|
*/
|
|
267
270
|
rebootStation(sn: string): Promise<void>;
|
|
268
271
|
/**
|
|
@@ -308,6 +311,11 @@ export declare class P2PCommandRouter {
|
|
|
308
311
|
* stream costs nothing extra and a talkback on an otherwise idle camera opens the session it needs
|
|
309
312
|
* instead of playing into silence.
|
|
310
313
|
*
|
|
314
|
+
* **On a model in {@link ELD_TALKBACK_MODELS}, the speaker plays the codec the camera sends.** So the first
|
|
315
|
+
* audio frame of the media session this talkback holds decides its codec ({@link speakerCodec}), within
|
|
316
|
+
* {@link TALKBACK_CODEC_WAIT_MS} of the session's first frame, and the claim is re-checked after that wait as
|
|
317
|
+
* after the warm-up. Every other model opens `aac-lc` at once.
|
|
318
|
+
*
|
|
311
319
|
* The level-2 key is waited for softly: only the HomeBase-attached path requires it, and
|
|
312
320
|
* {@link Talkback.start} reports that failure precisely, so a hard wait here would reject an
|
|
313
321
|
* own-session camera that legitimately never negotiates one.
|
|
@@ -20,6 +20,8 @@ export declare class Fmp4Muxer {
|
|
|
20
20
|
private lastVideoTimestampMs?;
|
|
21
21
|
private readonly audioRequested;
|
|
22
22
|
private audioCodec?;
|
|
23
|
+
/** Decoder config of an AAC-ELD track, taken from its frames; absent for AAC-LC, whose `esds` config is fixed. */
|
|
24
|
+
private audioConfig?;
|
|
23
25
|
private audioDisabled;
|
|
24
26
|
private audioSamples;
|
|
25
27
|
private audioBaseDecodeTime;
|
|
@@ -37,7 +39,10 @@ export declare class Fmp4Muxer {
|
|
|
37
39
|
* (e.g. delta frames before the first keyframe).
|
|
38
40
|
*/
|
|
39
41
|
push(frame: LiveVideoFrame, timestampMs?: number): MediaFragment | undefined;
|
|
40
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Add one station-declared AAC access unit to the audio track: an ADTS frame for AAC-LC, a raw access unit
|
|
44
|
+
* described by its frame's config for AAC-ELD. G.711 remains available via `live()`.
|
|
45
|
+
*/
|
|
41
46
|
pushAudio(frame: LiveAudioFrame, timestampMs?: number): MediaFragment | undefined;
|
|
42
47
|
/** Permanently omit incompatible audio while allowing the video recording to continue. */
|
|
43
48
|
private disableAudio;
|
|
@@ -103,6 +103,9 @@ export declare class LiveStream extends EventEmitter {
|
|
|
103
103
|
private lastCodec;
|
|
104
104
|
/** Rebuilds an access unit the station split across several frames — see {@link AccessUnitAssembler}. */
|
|
105
105
|
private readonly units;
|
|
106
|
+
/** Whether a missing video datagram or incomplete unit invalidated the current reference chain. */
|
|
107
|
+
private awaitingKeyframeAfterGap;
|
|
108
|
+
private readonly videoGapHandler;
|
|
106
109
|
/** The channel inbound media must be tagged with, once {@link acceptsMedia} trusts the station's tag. */
|
|
107
110
|
private mediaChannel?;
|
|
108
111
|
private tracedFirstVideoCommand;
|
|
@@ -277,8 +277,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
277
277
|
private decryptLevel2;
|
|
278
278
|
get isConnected(): boolean;
|
|
279
279
|
/**
|
|
280
|
-
* Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and
|
|
281
|
-
* less or refuses.
|
|
280
|
+
* Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and log once per session when it grants
|
|
281
|
+
* less (debug) or refuses (warn).
|
|
282
282
|
*
|
|
283
283
|
* The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
|
|
284
284
|
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
@@ -435,6 +435,11 @@ export declare class P2PSession extends EventEmitter {
|
|
|
435
435
|
* "direct" commands (e.g. CAMERA_SWITCH 1035) carry a binary struct, not JSON.
|
|
436
436
|
*/
|
|
437
437
|
sendRawLevel2Bytes(payload: Buffer, channel: number, outerCmd?: number, signCode?: number): boolean;
|
|
438
|
+
/**
|
|
439
|
+
* The level-1 twin of {@link sendRawLevel2Bytes}: the plaintext `payload` sealed AES-128-ECB under the
|
|
440
|
+
* level-1 key, signCode 1 — the seal of the level-1 media start. Returns `false` when not connected.
|
|
441
|
+
*/
|
|
442
|
+
sendRawLevel1Bytes(payload: Buffer, channel: number, outerCmd: number): boolean;
|
|
438
443
|
/**
|
|
439
444
|
* Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
|
|
440
445
|
* its runtime topology takes, as {@link startLiveMedia} selects its start:
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Managed talkback session over P2P — the send-side twin of `live-stream.ts`. Takes
|
|
3
|
-
*
|
|
4
|
-
* bracketed by the device's
|
|
2
|
+
* Managed talkback session over P2P — the send-side twin of `live-stream.ts`. Takes audio from a host in
|
|
3
|
+
* the codec the camera's speaker plays (AAC-LC ADTS, or raw AAC-ELD access units), recovers frame
|
|
4
|
+
* boundaries, and paces the frames onto the wire at their own playback rate, bracketed by the device's
|
|
5
|
+
* start/stop control frames.
|
|
5
6
|
*
|
|
6
7
|
* Pacing is the reason this exists rather than a bare `sendAudioFrame` loop. Each AAC-LC frame at
|
|
7
|
-
* 16 kHz is 1024 samples — exactly 64 ms of audio — and
|
|
8
|
+
* 16 kHz is 1024 samples — exactly 64 ms of audio — and each AAC-ELD access unit is 512 samples, 32 ms.
|
|
9
|
+
* The device plays what arrives when it arrives.
|
|
8
10
|
* A host piping a file would otherwise deliver a minute of audio in a few hundred milliseconds and
|
|
9
11
|
* lose all but the tail. A live source paces itself and simply keeps the queue near-empty.
|
|
10
12
|
*
|
|
@@ -17,7 +19,7 @@
|
|
|
17
19
|
*/
|
|
18
20
|
import { EventEmitter } from "node:events";
|
|
19
21
|
import { Writable } from "node:stream";
|
|
20
|
-
import type { AacEncoder, StreamBudgetNotice, TalkbackHandle } from "../../core/contracts.js";
|
|
22
|
+
import type { AacEncoder, StreamBudgetNotice, TalkbackCodec, TalkbackHandle } from "../../core/contracts.js";
|
|
21
23
|
import { type Logger } from "../../core/logger.js";
|
|
22
24
|
/**
|
|
23
25
|
* The subset of the session this needs — start/stop the path, push one frame, and report a frame the
|
|
@@ -66,7 +68,7 @@ export interface TalkbackOptions {
|
|
|
66
68
|
export declare const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
67
69
|
/**
|
|
68
70
|
* A talkback session bound to one camera channel. Opens the device's path on construction via
|
|
69
|
-
* {@link start}, then drains queued frames on a
|
|
71
|
+
* {@link start}, then drains queued frames on a tick of one frame's duration until {@link stop}.
|
|
70
72
|
*/
|
|
71
73
|
export declare class Talkback extends EventEmitter implements TalkbackHandle {
|
|
72
74
|
private readonly sink;
|
|
@@ -74,7 +76,8 @@ export declare class Talkback extends EventEmitter implements TalkbackHandle {
|
|
|
74
76
|
private readonly reader;
|
|
75
77
|
private readonly queue;
|
|
76
78
|
private readonly logger;
|
|
77
|
-
|
|
79
|
+
/** Playback duration of one queued frame, the pacer's step: 64 ms for `aac-lc`, 32 ms for `aac-eld`. */
|
|
80
|
+
private frameMs;
|
|
78
81
|
private readonly idleTimeoutMs;
|
|
79
82
|
private timer?;
|
|
80
83
|
private started;
|
|
@@ -94,17 +97,24 @@ export declare class Talkback extends EventEmitter implements TalkbackHandle {
|
|
|
94
97
|
private chain;
|
|
95
98
|
private readonly onAudioGap;
|
|
96
99
|
constructor(sink: TalkbackSink, opts: TalkbackOptions);
|
|
100
|
+
/** Frames the pacing queue holds before a writable applies backpressure: {@link HIGH_WATER_MS} of this codec. */
|
|
101
|
+
private get highWater();
|
|
102
|
+
/** The codec this talkback sends, set by {@link start}. */
|
|
103
|
+
codec: TalkbackCodec;
|
|
97
104
|
/** Frames still queued for the wire. */
|
|
98
105
|
get pending(): number;
|
|
99
106
|
/**
|
|
100
107
|
* Open the device's talkback path and begin the pacing tick. Throws when the topology's control
|
|
101
108
|
* frame could not be sent — for a HomeBase-attached camera that means the level-2 key was never
|
|
102
109
|
* negotiated, which would otherwise leave a silent session that accepts audio nobody hears.
|
|
110
|
+
*
|
|
111
|
+
* `codec` is the codec the camera's speaker plays; it fixes the input this talkback accepts and the pacing step.
|
|
103
112
|
*/
|
|
104
|
-
start(): this;
|
|
113
|
+
start(codec: TalkbackCodec): this;
|
|
105
114
|
/**
|
|
106
|
-
* Queue audio.
|
|
107
|
-
*
|
|
115
|
+
* Queue audio in the codec this talkback sends. For `aac-lc` the bytes are ADTS, or PCM when an encoder was
|
|
116
|
+
* supplied, read as a stream, so a chunk carrying part of a frame is held until the rest arrives. For `aac-eld`
|
|
117
|
+
* each call is one whole access unit; a unit over the device's length limit is dropped with an `error`.
|
|
108
118
|
*
|
|
109
119
|
* Audio written after {@link end} is dropped with an `error` rather than queued: `finished` has
|
|
110
120
|
* either already fired or is owed on the frames written before it, so a late chunk would either play
|
|
@@ -160,6 +170,7 @@ export declare class Talkback extends EventEmitter implements TalkbackHandle {
|
|
|
160
170
|
/**
|
|
161
171
|
* A Writable over {@link write} that withholds its callback while the queue is at the high-water
|
|
162
172
|
* mark — which is what turns `pipe()` from a file into playback at speed rather than a memory spike.
|
|
173
|
+
* For `aac-eld` it is object-mode: each write is one access unit.
|
|
163
174
|
*/
|
|
164
175
|
writable(): Writable;
|
|
165
176
|
/** Drain an encoder's partial trailing block, so the last syllable is not lost. */
|
|
@@ -144,9 +144,10 @@ export declare class AccessUnitAssembler {
|
|
|
144
144
|
* `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
|
|
145
145
|
* frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
|
|
146
146
|
* of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
|
|
147
|
-
* carries).
|
|
147
|
+
* carries). A continuation without its matching open unit has no Annex-B start code and cannot be emitted
|
|
148
|
+
* as a complete unit, even if its repeated header says keyframe.
|
|
148
149
|
*/
|
|
149
150
|
push(payload: Buffer, decode: (payload: Buffer) => Buffer | undefined): AssembledAccessUnit[];
|
|
150
151
|
/** Forget an incomplete unit, counting and reporting it. */
|
|
151
|
-
|
|
152
|
+
discard(): void;
|
|
152
153
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTC command router: drives a **HomeBase S1 Pro (T9000)** over the portal's WebRTC data channel instead
|
|
3
|
+
* of P2P.
|
|
4
|
+
*
|
|
5
|
+
* The T9000 answers no P2P lookup, but it accepts the portal's control channel: sign → WS join → scall →
|
|
6
|
+
* SDP answer → ICE over the relay the hub grants → DTLS → SCTP data channels, with commands riding
|
|
7
|
+
* `WebrtcDataChannel` as portal packets (see {@link buildPortalPacket}).
|
|
8
|
+
*
|
|
9
|
+
* `set-payload` rides the `1350` SET_PAYLOAD envelope, inner `{account_id, cmd, mValue3, payload}`;
|
|
10
|
+
* `set-json` rides the `1700` CONTROL_PAYLOAD envelope, inner `{account_id, cmd, commandType, data}`.
|
|
11
|
+
* A command to the station goes on the station channel `255`; an attached device's command shares the
|
|
12
|
+
* station's session and keeps the device's channel. Other command kinds are refused.
|
|
13
|
+
*
|
|
14
|
+
* One session per station, reused across commands and closed after {@link IDLE_CLOSE_MS} without one.
|
|
15
|
+
* Sends on a session are serialised; an ACK is correlated by its outer command and segment, since it
|
|
16
|
+
* carries no inner command. An ACK is not an observation of the resulting state.
|
|
17
|
+
*/
|
|
18
|
+
import type { Command } from "../../core/contracts.js";
|
|
19
|
+
import type { Logger } from "../../core/logger.js";
|
|
20
|
+
import { RtcSession, type RtcSessionOptions } from "./session.js";
|
|
21
|
+
export interface RtcIdentity {
|
|
22
|
+
authToken: string;
|
|
23
|
+
userId: string;
|
|
24
|
+
/** The `gtoken` header value the mega session's authed HTTP calls carry. */
|
|
25
|
+
gtoken: string;
|
|
26
|
+
}
|
|
27
|
+
/** Where a command goes: the station's session, and whether it addresses the station or a device on it. */
|
|
28
|
+
export interface RtcRoute {
|
|
29
|
+
stationSn: string;
|
|
30
|
+
/**
|
|
31
|
+
* The station's `member.admin_user_id`, the account the session and the payload name; the logged-in
|
|
32
|
+
* user when the station's record carries none.
|
|
33
|
+
*/
|
|
34
|
+
adminUserId?: string;
|
|
35
|
+
/** True for a device attached to the station, which keeps the command's own channel. */
|
|
36
|
+
attached: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface RtcCommandRouterDeps {
|
|
39
|
+
/** The logged-in session's credentials; `undefined` while logged out. */
|
|
40
|
+
identity: () => RtcIdentity | undefined;
|
|
41
|
+
/** The mega shard the account signs on (`"ie-pr"`, `"eu-pr"`, `"us-pr"`); picks the smart host. */
|
|
42
|
+
shard: () => string;
|
|
43
|
+
/** ISO country sent on the sign request (default `US`). */
|
|
44
|
+
country?: string;
|
|
45
|
+
logger?: Logger;
|
|
46
|
+
onError?: (e: Error) => void;
|
|
47
|
+
createSession?: (opts: RtcSessionOptions) => RtcSession;
|
|
48
|
+
}
|
|
49
|
+
export declare class RtcCommandRouter {
|
|
50
|
+
private readonly deps;
|
|
51
|
+
private readonly sessions;
|
|
52
|
+
constructor(deps: RtcCommandRouterDeps);
|
|
53
|
+
dispatchCommand(route: RtcRoute, cmd: Command): Promise<void>;
|
|
54
|
+
/** Tear down every station session (logout / shutdown). */
|
|
55
|
+
close(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Completes on the command ACK or a correlated control-result notification. The notification must
|
|
58
|
+
* match the request channel, parameter and segment and contain a structured payload. Receipt does
|
|
59
|
+
* not establish physical actuation. The command is sent once, bounded by {@link ACK_TIMEOUT_MS}.
|
|
60
|
+
*/
|
|
61
|
+
private sendAwaitAck;
|
|
62
|
+
private stationSession;
|
|
63
|
+
/**
|
|
64
|
+
* One bounded bring-up: the session is up when its command channel opens, and it fails, with the
|
|
65
|
+
* session closed and every listener gone, when `connect()` throws, the session closes first, or
|
|
66
|
+
* {@link CONNECT_TIMEOUT_MS} passes. A single promise, so an early failure cannot leave a second one
|
|
67
|
+
* rejecting unheard.
|
|
68
|
+
*/
|
|
69
|
+
private bringUp;
|
|
70
|
+
private touch;
|
|
71
|
+
private drop;
|
|
72
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebRTC peer for a T9000 session: libdatachannel through `node-datachannel`, driven the way the
|
|
3
|
+
* portal drives a browser's RTCPeerConnection.
|
|
4
|
+
*
|
|
5
|
+
* The hub is the offerer: it sends its SDP once it has granted the session, and the peer answers. Six
|
|
6
|
+
* data channels are declared in the portal's order (`WebrtcDataChannel` first, the command channel,
|
|
7
|
+
* then audio, idr, video, notify, download). Every one carries PTCS-framed portal packets; which logical
|
|
8
|
+
* channel a reassembled frame belongs to is the PTCS header's channel, not the data channel it rode on.
|
|
9
|
+
*
|
|
10
|
+
* ICE is relay-only. The hub grants a TURN allocation in its `scall 100` reply and completes DTLS only
|
|
11
|
+
* over the relay pair; on a host pair ICE connects and the DTLS handshake never completes. The peer
|
|
12
|
+
* plays the DTLS client (`active`) and opens its channels on the client's even SCTP stream ids, which
|
|
13
|
+
* is what the hub pairs with.
|
|
14
|
+
*
|
|
15
|
+
* The native peer is injectable.
|
|
16
|
+
*/
|
|
17
|
+
import { EventEmitter } from "node:events";
|
|
18
|
+
import type { PeerConnection, RtcConfig } from "node-datachannel";
|
|
19
|
+
import { type Logger } from "../../core/logger.js";
|
|
20
|
+
export interface TurnConfig {
|
|
21
|
+
turn_addr: string;
|
|
22
|
+
turn_port: number;
|
|
23
|
+
turn_user: string;
|
|
24
|
+
turn_password: string;
|
|
25
|
+
alt_turn_addr?: string;
|
|
26
|
+
alt_turn_port?: number;
|
|
27
|
+
}
|
|
28
|
+
export type NativePeerFactory = (name: string, config: RtcConfig) => PeerConnection;
|
|
29
|
+
export interface RtcPeerOptions {
|
|
30
|
+
createPeer?: NativePeerFactory;
|
|
31
|
+
logger?: Logger;
|
|
32
|
+
}
|
|
33
|
+
export interface RtcPeerEvents {
|
|
34
|
+
commandChannelOpen: [];
|
|
35
|
+
/** The command channel went away while the peer itself may still be up. */
|
|
36
|
+
commandChannelClosed: [];
|
|
37
|
+
data: [frame: Buffer, linkType: number];
|
|
38
|
+
iceCandidate: [candidate: string];
|
|
39
|
+
iceGatheringComplete: [];
|
|
40
|
+
connectionState: [state: string];
|
|
41
|
+
error: [err: Error];
|
|
42
|
+
}
|
|
43
|
+
/** The portal's channel list; index 0 is the command channel. */
|
|
44
|
+
export declare const DATA_CHANNEL_LABELS: readonly ["WebrtcDataChannel", "audio", "idr", "video", "notify", "download"];
|
|
45
|
+
export declare const COMMAND_CHANNEL: "WebrtcDataChannel";
|
|
46
|
+
export declare class RtcPeer extends EventEmitter<RtcPeerEvents> {
|
|
47
|
+
private readonly opts;
|
|
48
|
+
private pc?;
|
|
49
|
+
private readonly channels;
|
|
50
|
+
private framer?;
|
|
51
|
+
private readonly wireTally;
|
|
52
|
+
private commandOpen;
|
|
53
|
+
private remoteSet;
|
|
54
|
+
private handlingOffer;
|
|
55
|
+
private channelsCreated;
|
|
56
|
+
private gatheringDone;
|
|
57
|
+
/** Set by the framer's wire callback when the native channel refused a packet mid-send. */
|
|
58
|
+
private wireSendFailed;
|
|
59
|
+
private readonly pending;
|
|
60
|
+
private localAnswer?;
|
|
61
|
+
private readonly logger;
|
|
62
|
+
constructor(opts?: RtcPeerOptions);
|
|
63
|
+
/** Create the native peer on the relay the hub granted in `scall 100`. */
|
|
64
|
+
init(turn: TurnConfig): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* The hub offered: apply the offer, declare the channels, return the answer SDP to signal back.
|
|
67
|
+
*
|
|
68
|
+
* The offer is pinned to `passive` before it is applied, so the native peer has one legal answer,
|
|
69
|
+
* `active`: the role is fixed where it is decided, since rewriting the answer afterwards would change
|
|
70
|
+
* only what is announced. The offer goes in before the channels are declared: `createDataChannel` on
|
|
71
|
+
* a peer without a remote description starts its own negotiation and leaves it in `have-local-offer`.
|
|
72
|
+
* The data channels need no m-line of their own; the offer's SCTP m-line carries them. Only an ANSWER
|
|
73
|
+
* is taken as the local description: one read in `have-local-offer` is an offer.
|
|
74
|
+
*/
|
|
75
|
+
handleRemoteOffer(offerSdp: string): Promise<string>;
|
|
76
|
+
addRemoteCandidate(candidate: string): void;
|
|
77
|
+
/**
|
|
78
|
+
* Send one portal packet on the command channel, through the framer. False when the channel is not
|
|
79
|
+
* usable or the native send refused a wire packet, so a dropped frame does not read as success.
|
|
80
|
+
*/
|
|
81
|
+
sendCommand(portalPacket: Buffer): boolean;
|
|
82
|
+
/** Tear the peer down. A pending answer is rejected, so a `handleRemoteOffer` in flight settles. */
|
|
83
|
+
close(): void;
|
|
84
|
+
private acceptsCandidate;
|
|
85
|
+
private createChannels;
|
|
86
|
+
/**
|
|
87
|
+
* Wire one data channel. Every channel feeds the framer once the command channel has opened it; a
|
|
88
|
+
* message before that has nothing to reassemble into and is dropped.
|
|
89
|
+
*/
|
|
90
|
+
private wireChannel;
|
|
91
|
+
private addNow;
|
|
92
|
+
private flushPending;
|
|
93
|
+
}
|