@mega-yfue/eufy-sdk 0.5.0-beta.7 → 0.5.0-beta.71
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 +83 -10
- package/dist/client/types.d.ts +1 -1
- package/dist/core/contracts.d.ts +71 -30
- package/dist/core/store.d.ts +4 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/index.js +3611 -603
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +39 -4
- package/dist/model/capabilities/camera.d.ts +24 -10
- package/dist/model/capabilities/dp-catalog.d.ts +4 -3
- package/dist/model/capabilities/index.d.ts +16 -3
- package/dist/model/capabilities/light.d.ts +3 -0
- package/dist/model/capabilities/lock.d.ts +21 -7
- package/dist/model/capabilities/motion.d.ts +5 -0
- package/dist/model/capabilities/mower.d.ts +230 -0
- package/dist/model/capabilities/rtsp.d.ts +16 -3
- package/dist/model/capabilities/smart-drop.d.ts +20 -0
- package/dist/model/capabilities/suction.d.ts +5 -5
- package/dist/model/capabilities/types.d.ts +10 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +19 -13
- package/dist/model/classify.d.ts +1 -2
- package/dist/model/device-family.d.ts +8 -0
- package/dist/model/param-dictionary.d.ts +6 -0
- package/dist/model/param-namespace.d.ts +1 -1
- package/dist/model/registry.d.ts +3 -2
- package/dist/model/types.d.ts +26 -12
- package/dist/transport/abortable.d.ts +8 -0
- package/dist/transport/http/mega-client.d.ts +34 -2
- package/dist/transport/mqtt/broker-discovery.d.ts +2 -1
- package/dist/transport/mqtt/command-router.d.ts +3 -2
- package/dist/transport/p2p/codec.d.ts +16 -0
- package/dist/transport/p2p/command-router.d.ts +27 -35
- package/dist/transport/p2p/fmp4.d.ts +12 -2
- package/dist/transport/p2p/live-trace.d.ts +11 -2
- package/dist/transport/p2p/p2p-session.d.ts +59 -12
- package/dist/transport/p2p/readable-egress.d.ts +4 -0
- package/dist/transport/p2p/recording-download.d.ts +4 -3
- package/dist/transport/p2p/shared-live-source.d.ts +27 -13
- package/dist/transport/p2p/talkback.d.ts +21 -10
- package/dist/transport/p2p/video.d.ts +2 -0
- package/dist/transport/rtc/call-live.d.ts +41 -0
- package/dist/transport/rtc/command-router.d.ts +141 -0
- package/dist/transport/rtc/live.d.ts +69 -0
- package/dist/transport/rtc/peer.d.ts +122 -0
- package/dist/transport/rtc/portal-keepalive.d.ts +11 -0
- package/dist/transport/rtc/portal-packet.d.ts +72 -0
- package/dist/transport/rtc/ptcs-framer.d.ts +86 -0
- package/dist/transport/rtc/scall-sdp.d.ts +50 -0
- package/dist/transport/rtc/session.d.ts +95 -0
- package/dist/transport/rtc/signaling.d.ts +139 -0
- package/dist/transport/token-echo.d.ts +8 -0
- package/dist/transport/tuya/account.d.ts +8 -5
- package/dist/transport/tuya/client.d.ts +19 -5
- package/dist/transport/tuya/command-router.d.ts +10 -14
- package/dist/transport/tuya/dp-codec.d.ts +2 -23
- package/dist/transport/tuya/et3.d.ts +22 -0
- package/dist/transport/tuya/request.d.ts +18 -8
- package/dist/transport/tuya/sign.d.ts +7 -0
- package/package.json +2 -1
|
@@ -46,12 +46,18 @@ export interface P2PSessionConfig {
|
|
|
46
46
|
/** Disable UDP broadcast local lookup (e.g. cloud-only). Default false. */
|
|
47
47
|
noBroadcast?: boolean;
|
|
48
48
|
/**
|
|
49
|
-
* Resolve a station `cipher_id` → its
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
* Resolve a station `cipher_id` → its private keys (from cloud `get_ciphers`): `ecc`, the ECC private
|
|
50
|
+
* key hex (`ecc_private_key`), and `rsa`, the RSA private key (`private_key`). A plain string is the ECC key. Called once per
|
|
51
|
+
* negotiation. When provided, the session auto-negotiates the **level-2** session key on connect: it
|
|
52
|
+
* reads the `cipher_id` + envelope from the `CMD_GATEWAYINFO` reply and derives the key. With an ECC
|
|
53
|
+
* key that is the ECIES unwrap and `setLevel2Key()` — so signCode 2/8 frames (camera info, event DB,
|
|
54
|
+
* event images) decrypt live. A cipher with no ECC key carries the key RSA-encrypted instead, and only
|
|
55
|
+
* {@link P2PSession.sendControlLevel2} seals with it.
|
|
56
|
+
*/
|
|
57
|
+
resolveCipherKey?: (cipherId: number) => Promise<string | {
|
|
58
|
+
ecc?: string;
|
|
59
|
+
rsa?: string;
|
|
60
|
+
} | undefined>;
|
|
55
61
|
/** Diagnostics sink. Omit for silence. */
|
|
56
62
|
logger?: Logger;
|
|
57
63
|
}
|
|
@@ -149,6 +155,12 @@ export declare class P2PSession extends EventEmitter {
|
|
|
149
155
|
private readonly level1Key;
|
|
150
156
|
/** Negotiated 32-byte level-2/gateway key (AES-256-GCM). Set via setLevel2Key once known. */
|
|
151
157
|
private level2Key?;
|
|
158
|
+
/**
|
|
159
|
+
* Negotiated 16-byte level-2 key of a cipher with no ECC key (RSA-encrypted in `CMD_GATEWAYINFO`). Only
|
|
160
|
+
* {@link sendControlLevel2} seals with it, AES-128-ECB in the level-1 layout at encryption type 2; never
|
|
161
|
+
* held beside {@link level2Key}.
|
|
162
|
+
*/
|
|
163
|
+
private level2EcbKey?;
|
|
152
164
|
private level2Seq;
|
|
153
165
|
private rsaPrivateKey?;
|
|
154
166
|
private rsaModulusHex?;
|
|
@@ -204,10 +216,18 @@ export declare class P2PSession extends EventEmitter {
|
|
|
204
216
|
private trace;
|
|
205
217
|
/** Provide the negotiated 32-byte session key so level-2 (signCode 2/8) frames can be decrypted. */
|
|
206
218
|
setLevel2Key(key: Buffer): void;
|
|
207
|
-
/** Whether the level-2 session key has been negotiated/set. */
|
|
219
|
+
/** Whether the AES-256-GCM level-2 session key has been negotiated/set. */
|
|
208
220
|
get hasLevel2Key(): boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Whether the held level-2 key can frame for `sender`: every level-2 sender needs the AES-256-GCM key,
|
|
223
|
+
* and `"set-payload"` ({@link sendControlLevel2}) also takes the AES-128-ECB key of an RSA cipher.
|
|
224
|
+
*/
|
|
225
|
+
private level2KeyServes;
|
|
209
226
|
/**
|
|
210
227
|
* Resolve with whether a level-2 key is available, waiting only while waiting can still change that.
|
|
228
|
+
* `sender` names who will frame with it: by default every level-2 sender, which needs the AES-256-GCM
|
|
229
|
+
* key; `"set-payload"` is {@link sendControlLevel2} alone, which also takes the AES-128-ECB key of an RSA
|
|
230
|
+
* cipher, so `true` for it does not imply {@link hasLevel2Key}.
|
|
211
231
|
*
|
|
212
232
|
* A `"session"` grace is measured once, not restarted per call. Best-effort media uses it because every
|
|
213
233
|
* egress asks this same session and can proceed without the key on an own-session camera; a per-call
|
|
@@ -232,7 +252,7 @@ export declare class P2PSession extends EventEmitter {
|
|
|
232
252
|
* `terminal` outcome is the one already stated where the negotiation concluded, since that is where the
|
|
233
253
|
* cipher and the cause are known, and re-stating it here would double every settled negotiation.
|
|
234
254
|
*/
|
|
235
|
-
awaitLevel2Key(graceMs: number, graceFrom?: "call" | "session"): Promise<boolean>;
|
|
255
|
+
awaitLevel2Key(graceMs: number, graceFrom?: "call" | "session", sender?: "every" | "set-payload"): Promise<boolean>;
|
|
236
256
|
/**
|
|
237
257
|
* Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
|
|
238
258
|
*
|
|
@@ -253,9 +273,12 @@ export declare class P2PSession extends EventEmitter {
|
|
|
253
273
|
private settleLevel2;
|
|
254
274
|
/**
|
|
255
275
|
* Negotiate the level-2 session key from the decrypted CMD_GATEWAYINFO payload: read its
|
|
256
|
-
* `cipher_id`, resolve that cipher's
|
|
257
|
-
* `resolveCipherKey`), run the ECIES unwrap, and `setLevel2Key()`.
|
|
258
|
-
*
|
|
276
|
+
* `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
|
|
277
|
+
* `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
|
|
278
|
+
* ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
|
|
279
|
+
* key. One-shot; emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
|
|
280
|
+
* `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` on failure
|
|
281
|
+
* (non-fatal — level-1 traffic keeps working regardless).
|
|
259
282
|
*
|
|
260
283
|
* Every outcome settles the wait in {@link awaitLevel2Key}, because being one-shot is what makes a
|
|
261
284
|
* failure final for this connection generation: nothing will retry it there, so a later command must be
|
|
@@ -421,6 +444,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
421
444
|
* mechanism proven for camera selection in media start. Use for control commands routed through a
|
|
422
445
|
* HomeBase (where level-1 ECB is rejected). Returns `false` if the level-2 key isn't negotiated
|
|
423
446
|
* yet (caller can fall back to {@link sendStringPayloadCommand}).
|
|
447
|
+
*
|
|
448
|
+
* On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
|
|
449
|
+
* layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
|
|
450
|
+
* byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
|
|
424
451
|
*/
|
|
425
452
|
sendControlLevel2(cmd: number, channel: number, accountId: string, payload: Record<string, unknown>, mValue3?: number): boolean;
|
|
426
453
|
/**
|
|
@@ -435,6 +462,11 @@ export declare class P2PSession extends EventEmitter {
|
|
|
435
462
|
* "direct" commands (e.g. CAMERA_SWITCH 1035) carry a binary struct, not JSON.
|
|
436
463
|
*/
|
|
437
464
|
sendRawLevel2Bytes(payload: Buffer, channel: number, outerCmd?: number, signCode?: number): boolean;
|
|
465
|
+
/**
|
|
466
|
+
* The level-1 twin of {@link sendRawLevel2Bytes}: the plaintext `payload` sealed AES-128-ECB under the
|
|
467
|
+
* level-1 key, signCode 1 — the seal of the level-1 media start. Returns `false` when not connected.
|
|
468
|
+
*/
|
|
469
|
+
sendRawLevel1Bytes(payload: Buffer, channel: number, outerCmd: number): boolean;
|
|
438
470
|
/**
|
|
439
471
|
* Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
|
|
440
472
|
* its runtime topology takes, as {@link startLiveMedia} selects its start:
|
|
@@ -606,6 +638,18 @@ export declare class P2PSession extends EventEmitter {
|
|
|
606
638
|
accountId?: string;
|
|
607
639
|
channel?: number;
|
|
608
640
|
}): void;
|
|
641
|
+
/**
|
|
642
|
+
* Read a date-ranged page of `history_record_info` from the station. The reply's `data` array groups
|
|
643
|
+
* recording rows with companion tables; each group retains its `table_name` and `payload`.
|
|
644
|
+
* `startTime` is `"0"` for the newest page or the oldest row's `yyyyMMddHHmmss` cursor for older rows.
|
|
645
|
+
*/
|
|
646
|
+
queryRecordPage(opts: {
|
|
647
|
+
accountId?: string;
|
|
648
|
+
startDate: string;
|
|
649
|
+
endDate: string;
|
|
650
|
+
startTime?: string;
|
|
651
|
+
count?: number;
|
|
652
|
+
}): Promise<unknown[]>;
|
|
609
653
|
/**
|
|
610
654
|
* Query one on-station table and answer its rows, once the reply is whole.
|
|
611
655
|
*
|
|
@@ -613,13 +657,15 @@ export declare class P2PSession extends EventEmitter {
|
|
|
613
657
|
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
614
658
|
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
615
659
|
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
616
|
-
* complete reply
|
|
660
|
+
* complete reply, and when the station answers a non-zero `mIntRet`.
|
|
617
661
|
*
|
|
618
662
|
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
619
663
|
* session throws, so a second reply cannot land in this buffer.
|
|
620
664
|
*/
|
|
621
665
|
readDatabase(table: string, opts?: {
|
|
622
666
|
accountId?: string;
|
|
667
|
+
innerCmd?: number;
|
|
668
|
+
query?: Record<string, unknown>;
|
|
623
669
|
timeoutMs?: number;
|
|
624
670
|
signal?: AbortSignal;
|
|
625
671
|
}): Promise<unknown[]>;
|
|
@@ -699,6 +745,7 @@ export declare class P2PSession extends EventEmitter {
|
|
|
699
745
|
* Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
|
|
700
746
|
* of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
|
|
701
747
|
* it would lose that frame and every frame after it until a datagram happened to begin on a header.
|
|
748
|
+
* Payload fragments retain their original slices and are copied once, when the complete frame is delivered.
|
|
702
749
|
*/
|
|
703
750
|
private reassemble;
|
|
704
751
|
/**
|
|
@@ -17,11 +17,15 @@ import type { Consumer } from "./shared-live-source.js";
|
|
|
17
17
|
export interface ReadableEgressOptions {
|
|
18
18
|
/** Emit {@link LiveVideoFrame} objects instead of raw Annex-B bytes (default false = bytes). */
|
|
19
19
|
objectMode?: boolean;
|
|
20
|
+
/** Yield ReceivedVideoFrame envelopes with original source-arrival times; ignored in raw mode. */
|
|
21
|
+
arrivalTime?: boolean;
|
|
20
22
|
/** Readable highWaterMark (bytes, or object count in objectMode). */
|
|
21
23
|
highWaterMark?: number;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Wrap a shared-source {@link Consumer} in a fresh Readable. The consumer is detached when the
|
|
25
27
|
* Readable is destroyed/ended, so callers own the lifetime by owning the stream.
|
|
28
|
+
* Frames arrive through onMedia; an empty video listener releases the keyframe-prime
|
|
29
|
+
* because ConsumerImpl replays it when the first video listener is added.
|
|
26
30
|
*/
|
|
27
31
|
export declare function openReadableFromConsumer(consumer: Consumer, opts?: ReadableEgressOptions): Readable;
|
|
@@ -46,9 +46,10 @@ export declare function receiveRecording(session: P2PSession, request: {
|
|
|
46
46
|
signal?: AbortSignal;
|
|
47
47
|
}): RecordingTransfer;
|
|
48
48
|
/**
|
|
49
|
-
* Decode a recording's frames, in arrival order, into elementary streams.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* Decode a recording's frames, in arrival order, into elementary streams. Split pictures are assembled
|
|
50
|
+
* before they enter the output; an orphan continuation cannot become its own picture. Keyframes go
|
|
51
|
+
* through one {@link VideoFrameDecoder}, whose media key then opens the audio; a keyframe that does not
|
|
52
|
+
* open under `eccPrivateKeyHex`, and every audio frame before the first media key, are dropped. Rejects with
|
|
52
53
|
* {@link RecordingDownloadError} `undecodable` when no video frame decodes.
|
|
53
54
|
*/
|
|
54
55
|
export declare function decodeRecording(frames: readonly RecordingFrame[], eccPrivateKeyHex: string): RecordingDownload;
|
|
@@ -83,12 +83,13 @@ export interface SharedLiveSourceOptions {
|
|
|
83
83
|
*/
|
|
84
84
|
onStopped?: () => void;
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
86
|
+
* This session is not serving the warm-up: a media start was abandoned unacknowledged, or forced starts went
|
|
87
|
+
* unanswered by any media, before anything was delivered. The owner answers whether it is replacing the
|
|
88
|
+
* session, and calls {@link SharedLiveSource.rewarm} once it has the replacement.
|
|
88
89
|
*
|
|
89
|
-
* Asked at most once per warm-up: further
|
|
90
|
+
* Asked at most once per warm-up: further evidence is the same session saying the same thing.
|
|
90
91
|
*/
|
|
91
|
-
onSessionUnreachable?: () =>
|
|
92
|
+
onSessionUnreachable?: () => boolean;
|
|
92
93
|
}
|
|
93
94
|
/**
|
|
94
95
|
* A single consumer of a {@link SharedLiveSource}. A {@link LiveStreamConsumer} (so `live()` can hand it
|
|
@@ -251,18 +252,17 @@ export declare class SharedLiveSource {
|
|
|
251
252
|
*/
|
|
252
253
|
private warm;
|
|
253
254
|
/**
|
|
254
|
-
*
|
|
255
|
+
* Ask the owner for a replacement session where nothing has been delivered yet, answering whether one is coming.
|
|
255
256
|
*
|
|
256
|
-
*
|
|
257
|
-
* 4–37 ms awake and 238 ms waking, so it is the session that is not being heard rather than a slow device
|
|
258
|
-
*
|
|
259
|
-
*
|
|
257
|
+
* An unacknowledged start is roughly twenty byte-identical sends with no reply, against acknowledgement latencies
|
|
258
|
+
* of 4–37 ms awake and 238 ms waking, so it is the session that is not being heard rather than a slow device: the
|
|
259
|
+
* camera was never told to stream. Where media has already flowed the abandonment means something else and this
|
|
260
|
+
* does nothing. The DEADLINE is left running: the window belongs to the attempt, not to the session it started on.
|
|
260
261
|
*
|
|
261
|
-
* The retry ticker is stopped
|
|
262
|
-
*
|
|
263
|
-
* started on.
|
|
262
|
+
* The retry ticker is stopped only once the owner has said it is replacing the session: an owner that cannot
|
|
263
|
+
* (an attached camera shares its HomeBase session) leaves the source re-issuing as it always did.
|
|
264
264
|
*/
|
|
265
|
-
private
|
|
265
|
+
private askForFreshSession;
|
|
266
266
|
/**
|
|
267
267
|
* Warm again on a session the owner has replaced, inside the deadline the first attempt started.
|
|
268
268
|
*
|
|
@@ -290,6 +290,11 @@ export declare class SharedLiveSource {
|
|
|
290
290
|
* {@link fruitlessReissues} for why one is not enough and why what the stream delivered earlier is not
|
|
291
291
|
* evidence. Once media arrives the keepalive is what is wanted, and an attached camera re-sends a full start
|
|
292
292
|
* either way.
|
|
293
|
+
*
|
|
294
|
+
* A forced start that a whole interval later has still produced nothing is a session that acknowledges and
|
|
295
|
+
* does not serve: a fresh session on the same camera delivers a keyframe within a second, so a warm-up asks
|
|
296
|
+
* for one rather than forcing starts onto this one until its deadline. Wired only: a cold battery camera is
|
|
297
|
+
* slow rather than deaf, measured at about 8 s to first media, and replacing its session would cost a wake.
|
|
293
298
|
*/
|
|
294
299
|
private reissueStart;
|
|
295
300
|
/**
|
|
@@ -438,3 +443,12 @@ export declare class SharedLiveSource {
|
|
|
438
443
|
*/
|
|
439
444
|
dispose(): void;
|
|
440
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Attach a consumer, unless the caller has already abandoned the call.
|
|
448
|
+
*
|
|
449
|
+
* The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
|
|
450
|
+
* somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
|
|
451
|
+
* lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
|
|
452
|
+
* not.
|
|
453
|
+
*/
|
|
454
|
+
export declare function attachUnlessAborted(source: SharedLiveSource, signal?: AbortSignal): Consumer;
|
|
@@ -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. */
|
|
@@ -71,6 +71,8 @@ export declare class VideoFrameDecoder {
|
|
|
71
71
|
}
|
|
72
72
|
/** One whole access unit recovered from the frame (or frames) the station sent it in. */
|
|
73
73
|
export interface AssembledAccessUnit {
|
|
74
|
+
/** Unsigned 32-bit header timestamp from the first frame of this access unit. */
|
|
75
|
+
sourceTimestamp: number;
|
|
74
76
|
/** True when the unit's own header flagged it a keyframe — a point a consumer may begin decoding at. */
|
|
75
77
|
keyframe: boolean;
|
|
76
78
|
/** Frame width the unit's header declared. */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live view of a T8223 doorbell (Video Doorbell C31) over a portal `call` {@link RtcSession}.
|
|
3
|
+
*
|
|
4
|
+
* The doorbell is its own station and streams no PTCS framing: its video arrives as plain RTP on the
|
|
5
|
+
* offer's H.264 video m-line, which a {@link RtcPeerOptions.mediaTrack} peer hands over as `rtp`. This
|
|
6
|
+
* stream depacketizes it into whole Annex-B access units, one per frame. Each stream owns its session:
|
|
7
|
+
* starting it places the call, stopping it hangs up.
|
|
8
|
+
*/
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import type { LiveStreamHandle } from "../../core/contracts.js";
|
|
11
|
+
import { type Logger } from "../../core/logger.js";
|
|
12
|
+
import type { RtcSession } from "./session.js";
|
|
13
|
+
/**
|
|
14
|
+
* H.264 RTP depacketizer (RFC 6184 single NAL / STAP-A / FU-A). Feed whole RTP packets; it calls `onUnit`
|
|
15
|
+
* with each complete Annex-B access unit when the marker bit closes it. A unit with a sequence gap or an
|
|
16
|
+
* unterminated fragment is dropped, never delivered short. RTCP multiplexed on the same transport (PT
|
|
17
|
+
* 200-207, which reads as 72-79 in the RTP payload-type bits) is skipped.
|
|
18
|
+
*/
|
|
19
|
+
export declare class H264Depacketizer {
|
|
20
|
+
private readonly onUnit;
|
|
21
|
+
private nals;
|
|
22
|
+
private fu;
|
|
23
|
+
private lastSeq;
|
|
24
|
+
private broken;
|
|
25
|
+
constructor(onUnit: (unit: Buffer, timestamp: number) => void);
|
|
26
|
+
push(packet: Buffer): void;
|
|
27
|
+
}
|
|
28
|
+
/** A `LiveStreamHandle` over one portal `call` to a T8223 doorbell. */
|
|
29
|
+
export declare class RtcCallLiveStream extends EventEmitter implements LiveStreamHandle {
|
|
30
|
+
private readonly session;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
private running;
|
|
33
|
+
private readonly depacketizer;
|
|
34
|
+
/** `session` is a session on a {@link RtcPeerOptions.mediaTrack} peer, not yet connected. */
|
|
35
|
+
constructor(session: RtcSession, logger?: Logger);
|
|
36
|
+
start(): this;
|
|
37
|
+
stop(): void;
|
|
38
|
+
private onUnit;
|
|
39
|
+
private fail;
|
|
40
|
+
private teardown;
|
|
41
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTC command router: drives a station over the portal's WebRTC data channel instead of P2P.
|
|
3
|
+
*
|
|
4
|
+
* The T9000 answers no P2P lookup, but it accepts the portal's control channel: sign → WS join → scall →
|
|
5
|
+
* SDP answer → ICE over the relay the hub grants → DTLS → SCTP data channels, with commands riding
|
|
6
|
+
* `WebrtcDataChannel` as portal packets (see {@link buildPortalPacket}).
|
|
7
|
+
*
|
|
8
|
+
* `set-payload` rides the `1350` SET_PAYLOAD envelope, inner `{account_id, cmd, mValue3, payload}`;
|
|
9
|
+
* `set-json` rides the `1700` CONTROL_PAYLOAD envelope, inner `{account_id, cmd, commandType, data}`.
|
|
10
|
+
* A command to the station goes on the station channel `255`; an attached device's command shares the
|
|
11
|
+
* station's session and keeps the device's channel. Other command kinds are refused.
|
|
12
|
+
*
|
|
13
|
+
* One session per station, reused across commands and closed after {@link IDLE_CLOSE_MS} without one;
|
|
14
|
+
* a live pull holds it open. Sends on a session are serialised; an ACK is correlated by its outer command
|
|
15
|
+
* and segment, since it carries no inner command. An ACK is not an observation of the resulting state.
|
|
16
|
+
*
|
|
17
|
+
* Live view of an attached camera rides the same session (the hub accepts one RTC session per account),
|
|
18
|
+
* through one shared live source per station: the station serves one camera at a time, so a second
|
|
19
|
+
* camera is refused until the first one's view ends.
|
|
20
|
+
*/
|
|
21
|
+
import { type Command, type MediaProvider } from "../../core/contracts.js";
|
|
22
|
+
import type { Logger } from "../../core/logger.js";
|
|
23
|
+
import { RtcSession, type RtcSessionOptions } from "./session.js";
|
|
24
|
+
export interface RtcIdentity {
|
|
25
|
+
authToken: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
/** The `gtoken` header value the mega session's authed HTTP calls carry. */
|
|
28
|
+
gtoken: string;
|
|
29
|
+
}
|
|
30
|
+
/** Where a command goes: the station's session, and whether it addresses the station or a device on it. */
|
|
31
|
+
export interface RtcRoute {
|
|
32
|
+
/** The station's signaling exchange; defaults to compact SDP. */
|
|
33
|
+
signalingMode?: "call" | "scall";
|
|
34
|
+
/** Candidate policy used by the station's exchange; defaults to relay-only. */
|
|
35
|
+
iceTransportPolicy?: "relay" | "all";
|
|
36
|
+
/**
|
|
37
|
+
* The station streams video as RTP on its offer's video track and serves no data channel, so it takes
|
|
38
|
+
* no portal command; see `RtcPeerOptions.mediaTrack`.
|
|
39
|
+
*/
|
|
40
|
+
mediaTrack?: boolean;
|
|
41
|
+
stationSn: string;
|
|
42
|
+
/**
|
|
43
|
+
* The station's `member.admin_user_id`, the account the session and the payload name; the logged-in
|
|
44
|
+
* user when the station's record carries none.
|
|
45
|
+
*/
|
|
46
|
+
adminUserId?: string;
|
|
47
|
+
/** True for a device attached to the station, which keeps the command's own channel. */
|
|
48
|
+
attached: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** The camera a live view is for, on its station's route. */
|
|
51
|
+
export interface RtcLiveRoute extends RtcRoute {
|
|
52
|
+
cameraSn: string;
|
|
53
|
+
/** The camera's `device_channel` on the station. */
|
|
54
|
+
channel: number;
|
|
55
|
+
}
|
|
56
|
+
export interface RtcCommandRouterDeps {
|
|
57
|
+
/** The logged-in session's credentials; `undefined` while logged out. */
|
|
58
|
+
identity: () => RtcIdentity | undefined;
|
|
59
|
+
/** The mega shard the account signs on (`"ie-pr"`, `"eu-pr"`, `"us-pr"`); picks the smart host. */
|
|
60
|
+
shard: () => string;
|
|
61
|
+
/** ISO country sent on the sign request (default `US`). */
|
|
62
|
+
country?: string;
|
|
63
|
+
logger?: Logger;
|
|
64
|
+
onError?: (e: Error) => void;
|
|
65
|
+
/** ffmpeg for the live still (`ffmpeg` on PATH by default). */
|
|
66
|
+
ffmpegPath?: string;
|
|
67
|
+
createSession?: (opts: RtcSessionOptions) => RtcSession;
|
|
68
|
+
}
|
|
69
|
+
export declare class RtcCommandRouter {
|
|
70
|
+
private readonly deps;
|
|
71
|
+
private readonly sessions;
|
|
72
|
+
private readonly lives;
|
|
73
|
+
private readonly warm;
|
|
74
|
+
private readonly callSources;
|
|
75
|
+
constructor(deps: RtcCommandRouterDeps);
|
|
76
|
+
dispatchCommand(route: RtcRoute, cmd: Command): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* The media of a camera on a T9000: live video, a readable feed and a live still over the station's
|
|
79
|
+
* session. Recording has no wire here and is refused; the optional members it has no wire for are absent.
|
|
80
|
+
*/
|
|
81
|
+
mediaProviderFor(route: RtcLiveRoute): MediaProvider;
|
|
82
|
+
/**
|
|
83
|
+
* Live view of a station on a {@link RtcRoute.mediaTrack} route: a live feed, a readable feed and a live
|
|
84
|
+
* still, from one shared source per station whose every warm-up places a fresh `call` on its own
|
|
85
|
+
* session. Recording has no wire here and is refused.
|
|
86
|
+
*/
|
|
87
|
+
trackMediaProviderFor(route: RtcRoute): MediaProvider;
|
|
88
|
+
/**
|
|
89
|
+
* Tear down every station session (logout / shutdown). Each live source is disposed before its session
|
|
90
|
+
* closes, so its consumers end cleanly and its stop still rides the session.
|
|
91
|
+
*/
|
|
92
|
+
close(): void;
|
|
93
|
+
/**
|
|
94
|
+
* Hand `use` the station's live source for this camera, in the same turn the source is created or reused,
|
|
95
|
+
* so a consumer it attaches is counted before any other caller looks.
|
|
96
|
+
*
|
|
97
|
+
* The station streams one camera at a time. Another camera whose source has consumers refuses this one,
|
|
98
|
+
* before the session is awaited and again after; one with none (never attached or lingering)
|
|
99
|
+
* holds nothing a viewer needs, so it is disposed and the slot taken. The first caller's
|
|
100
|
+
* {@link SharedSourceHints} configure a new source.
|
|
101
|
+
*/
|
|
102
|
+
private withLiveSource;
|
|
103
|
+
/** Refuse a camera while another camera on its station has consumers. */
|
|
104
|
+
private refuseIfWatched;
|
|
105
|
+
private newLiveSource;
|
|
106
|
+
/**
|
|
107
|
+
* Completes on the command ACK or a correlated control-result notification. The notification must
|
|
108
|
+
* match the request channel, parameter and segment and contain a structured payload. Receipt does
|
|
109
|
+
* not establish physical actuation. The command is sent once, bounded by {@link ACK_TIMEOUT_MS}.
|
|
110
|
+
*/
|
|
111
|
+
private sendAwaitAck;
|
|
112
|
+
private newSession;
|
|
113
|
+
private stationSession;
|
|
114
|
+
/**
|
|
115
|
+
* One bounded bring-up: the session is up when its command channel opens, and it fails, with the
|
|
116
|
+
* session closed and every listener gone, when `connect()` throws, the session closes first, or
|
|
117
|
+
* {@link CONNECT_TIMEOUT_MS} passes. A single promise, so an early failure cannot leave a second one
|
|
118
|
+
* rejecting unheard.
|
|
119
|
+
*/
|
|
120
|
+
private bringUp;
|
|
121
|
+
/**
|
|
122
|
+
* Hold a station's command session open: one hold per station, its idle-close ({@link IDLE_CLOSE_MS})
|
|
123
|
+
* suppressed, {@link PORTAL_KEEPALIVE} sent every {@link PORTAL_KEEPALIVE_MS}, and the session dropped
|
|
124
|
+
* on a tick that finds the previous keepalive unechoed. A closed session ends the hold.
|
|
125
|
+
*
|
|
126
|
+
* Answers an idempotent disposer that releases the hold. Throws when no account is logged in.
|
|
127
|
+
*/
|
|
128
|
+
keepWarm(route: RtcRoute): () => void;
|
|
129
|
+
/**
|
|
130
|
+
* Bring a station session up for a {@link keepWarm} hold and start its keepalive. A bring-up that fails,
|
|
131
|
+
* or a hold disposed before it completes, clears the hold and arms no timer. Each tick first checks
|
|
132
|
+
* that the hub echoed the previous keepalive, and drops the session when it did not. The keepalive
|
|
133
|
+
* stops when the session closes, which also clears the hold so the next command or `keepWarm`
|
|
134
|
+
* rebuilds cleanly.
|
|
135
|
+
*/
|
|
136
|
+
private warmUp;
|
|
137
|
+
/** Stop a hold's keepalive and remove its listeners from the session it watches. */
|
|
138
|
+
private unwatch;
|
|
139
|
+
private touch;
|
|
140
|
+
private drop;
|
|
141
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live pull of one camera behind a **HomeBase S1 Pro (T9000)**, over the station's control session.
|
|
3
|
+
*
|
|
4
|
+
* Wire, as the portal sends it on the command data channel:
|
|
5
|
+
*
|
|
6
|
+
* - Start is inner `cmd 1003` in a `1350` SET_PAYLOAD on the station channel `255`. The header byte the
|
|
7
|
+
* codec names `isResponse` carries the portal's `streamId`, `1` for a live view, and `chn_list` is an
|
|
8
|
+
* array of `{index, chn, sensor, isUps, isClicked}` objects. Stop is inner `cmd 1004`, the same way.
|
|
9
|
+
* - Video arrives on the live link as portal packets `1300` on channel `100 + play id`; a single-camera
|
|
10
|
+
* view is play id `1`, whatever the camera's own channel. Each body is a fixed 22-byte media header,
|
|
11
|
+
* then the Annex B video it declares. See {@link parseMediaBody}.
|
|
12
|
+
* - A raw, unframed keepalive (a 20-byte prefix and a bare `XZYH 1139`) goes out every ~29 s; the hub
|
|
13
|
+
* echoes it.
|
|
14
|
+
*
|
|
15
|
+
* The station serves one live view at a time on that play slot, so the router builds at most one pull per
|
|
16
|
+
* station. This is a {@link LiveStreamHandle}: fan-out, backpressure and the start deadline are the
|
|
17
|
+
* shared live source's.
|
|
18
|
+
*/
|
|
19
|
+
import { EventEmitter } from "node:events";
|
|
20
|
+
import type { LiveStreamHandle } from "../../core/contracts.js";
|
|
21
|
+
import type { Logger } from "../../core/logger.js";
|
|
22
|
+
import type { RtcSession } from "./session.js";
|
|
23
|
+
import { type SegmentCounter } from "./portal-packet.js";
|
|
24
|
+
/**
|
|
25
|
+
* Read a `1300` body: a 22-byte media header (u32 LE video length at 0, stream type at 5, u16 LE width
|
|
26
|
+
* and height at 10 and 12), then the Annex B video. This is the layout the portal's own HEVC worker
|
|
27
|
+
* parses. Answers `undefined` for a short body, a stream that is not HEVC, or a declared length the body
|
|
28
|
+
* does not hold.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseMediaBody(body: Buffer): {
|
|
31
|
+
data: Buffer;
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
} | undefined;
|
|
35
|
+
export interface RtcLiveOptions {
|
|
36
|
+
session: RtcSession;
|
|
37
|
+
/** The session's segment counter, shared with the command router so segments never collide. */
|
|
38
|
+
seg: SegmentCounter;
|
|
39
|
+
stationSn: string;
|
|
40
|
+
/** The camera's `device_channel` on the station. */
|
|
41
|
+
channel: number;
|
|
42
|
+
/** The station's `member.admin_user_id`. */
|
|
43
|
+
accountId: string;
|
|
44
|
+
logger?: Logger;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One camera's live pull. `start()` sends the start; a non-zero start ACK or a closed session ends it with
|
|
48
|
+
* `error`, and `stop()` sends the stop. Frames are `h265` Annex B, keyframes being IDRs.
|
|
49
|
+
*/
|
|
50
|
+
export declare class RtcLiveStream extends EventEmitter implements LiveStreamHandle {
|
|
51
|
+
private readonly opts;
|
|
52
|
+
private running;
|
|
53
|
+
private startSegment;
|
|
54
|
+
private keepalive?;
|
|
55
|
+
private width;
|
|
56
|
+
private height;
|
|
57
|
+
private readonly onMedia;
|
|
58
|
+
private readonly onCommand;
|
|
59
|
+
private readonly onClose;
|
|
60
|
+
constructor(opts: RtcLiveOptions);
|
|
61
|
+
private get tag();
|
|
62
|
+
start(): this;
|
|
63
|
+
stop(): void;
|
|
64
|
+
private teardown;
|
|
65
|
+
private fail;
|
|
66
|
+
/** The start's ACK: a non-zero result code ends the pull. */
|
|
67
|
+
private onCommandFrame;
|
|
68
|
+
private onMediaFrame;
|
|
69
|
+
}
|