@mega-yfue/eufy-sdk 0.5.0-beta.9 → 0.5.0-beta.91
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 +73 -8
- package/dist/client/types.d.ts +1 -1
- package/dist/core/contracts.d.ts +96 -41
- package/dist/core/store.d.ts +4 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/index.js +4135 -704
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +39 -4
- package/dist/model/capabilities/battery.d.ts +6 -1
- package/dist/model/capabilities/camera.d.ts +61 -18
- package/dist/model/capabilities/dp-catalog.d.ts +4 -3
- package/dist/model/capabilities/index.d.ts +17 -4
- package/dist/model/capabilities/keypad.d.ts +20 -23
- package/dist/model/capabilities/light.d.ts +3 -0
- package/dist/model/capabilities/lock.d.ts +42 -16
- package/dist/model/capabilities/motion.d.ts +17 -2
- 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 +24 -0
- package/dist/model/param-dictionary.d.ts +6 -0
- package/dist/model/param-namespace.d.ts +1 -1
- package/dist/model/types.d.ts +20 -12
- package/dist/transport/abortable.d.ts +8 -0
- package/dist/transport/http/mega-client.d.ts +43 -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/audio-frame.d.ts +14 -0
- package/dist/transport/p2p/codec.d.ts +32 -0
- package/dist/transport/p2p/command-router.d.ts +93 -31
- package/dist/transport/p2p/fmp4.d.ts +12 -2
- package/dist/transport/p2p/keyed-payload.d.ts +112 -0
- package/dist/transport/p2p/live-trace.d.ts +25 -2
- package/dist/transport/p2p/media.d.ts +3 -5
- package/dist/transport/p2p/p2p-session.d.ts +81 -12
- package/dist/transport/p2p/readable-egress.d.ts +3 -0
- package/dist/transport/p2p/recording-download.d.ts +4 -3
- package/dist/transport/p2p/shared-live-source.d.ts +44 -56
- package/dist/transport/p2p/talkback.d.ts +21 -10
- package/dist/transport/p2p/video.d.ts +2 -0
- package/dist/transport/push/push-client.d.ts +5 -0
- package/dist/transport/rtc/call-live.d.ts +41 -0
- package/dist/transport/rtc/command-router.d.ts +145 -0
- package/dist/transport/rtc/live.d.ts +76 -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 +99 -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 +7 -6
|
@@ -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?;
|
|
@@ -173,6 +185,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
173
185
|
* change (the level-2 key arrived after an initial level-1 start), otherwise sends the 1139 nudge.
|
|
174
186
|
*/
|
|
175
187
|
private readonly liveStartedChannels;
|
|
188
|
+
/** The `cipher_id` this connection's `CMD_GATEWAYINFO` reply named, once its negotiation has read it. */
|
|
189
|
+
private gatewayCipherId?;
|
|
190
|
+
/** The `n` of the last `[0x01, n]` header this connection sent in the clear — see {@link nextClearCommandBytes}. */
|
|
191
|
+
private clearCommandCount;
|
|
176
192
|
private readonly unackedLiveStarts;
|
|
177
193
|
private liveStartRetransmitTimer?;
|
|
178
194
|
private readonly logger;
|
|
@@ -204,10 +220,18 @@ export declare class P2PSession extends EventEmitter {
|
|
|
204
220
|
private trace;
|
|
205
221
|
/** Provide the negotiated 32-byte session key so level-2 (signCode 2/8) frames can be decrypted. */
|
|
206
222
|
setLevel2Key(key: Buffer): void;
|
|
207
|
-
/** Whether the level-2 session key has been negotiated/set. */
|
|
223
|
+
/** Whether the AES-256-GCM level-2 session key has been negotiated/set. */
|
|
208
224
|
get hasLevel2Key(): boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Whether the held level-2 key can frame for `sender`: every level-2 sender needs the AES-256-GCM key,
|
|
227
|
+
* and `"set-payload"` ({@link sendControlLevel2}) also takes the AES-128-ECB key of an RSA cipher.
|
|
228
|
+
*/
|
|
229
|
+
private level2KeyServes;
|
|
209
230
|
/**
|
|
210
231
|
* Resolve with whether a level-2 key is available, waiting only while waiting can still change that.
|
|
232
|
+
* `sender` names who will frame with it: by default every level-2 sender, which needs the AES-256-GCM
|
|
233
|
+
* key; `"set-payload"` is {@link sendControlLevel2} alone, which also takes the AES-128-ECB key of an RSA
|
|
234
|
+
* cipher, so `true` for it does not imply {@link hasLevel2Key}.
|
|
211
235
|
*
|
|
212
236
|
* A `"session"` grace is measured once, not restarted per call. Best-effort media uses it because every
|
|
213
237
|
* egress asks this same session and can proceed without the key on an own-session camera; a per-call
|
|
@@ -232,7 +256,7 @@ export declare class P2PSession extends EventEmitter {
|
|
|
232
256
|
* `terminal` outcome is the one already stated where the negotiation concluded, since that is where the
|
|
233
257
|
* cipher and the cause are known, and re-stating it here would double every settled negotiation.
|
|
234
258
|
*/
|
|
235
|
-
awaitLevel2Key(graceMs: number, graceFrom?: "call" | "session"): Promise<boolean>;
|
|
259
|
+
awaitLevel2Key(graceMs: number, graceFrom?: "call" | "session", sender?: "every" | "set-payload"): Promise<boolean>;
|
|
236
260
|
/**
|
|
237
261
|
* Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
|
|
238
262
|
*
|
|
@@ -253,9 +277,12 @@ export declare class P2PSession extends EventEmitter {
|
|
|
253
277
|
private settleLevel2;
|
|
254
278
|
/**
|
|
255
279
|
* 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
|
-
*
|
|
280
|
+
* `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
|
|
281
|
+
* `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
|
|
282
|
+
* ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
|
|
283
|
+
* key. One-shot; emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
|
|
284
|
+
* `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` on failure
|
|
285
|
+
* (non-fatal — level-1 traffic keeps working regardless).
|
|
259
286
|
*
|
|
260
287
|
* Every outcome settles the wait in {@link awaitLevel2Key}, because being one-shot is what makes a
|
|
261
288
|
* failure final for this connection generation: nothing will retry it there, so a later command must be
|
|
@@ -373,6 +400,9 @@ export declare class P2PSession extends EventEmitter {
|
|
|
373
400
|
* `video_type` is 12, and `transaction` is the millisecond timestamp as a string. The device answers a
|
|
374
401
|
* start carrying these values with a keyframe; `encryptkey` is the modulus it RSA-wraps each keyframe's
|
|
375
402
|
* AES media key with (unwrapped by {@link decodeVideoFrame}).
|
|
403
|
+
*
|
|
404
|
+
* With `clearMsgId`, the body of a start sent in the clear: `{accountId, encryptkey, msg_id, streamtype: 1}`
|
|
405
|
+
* only, `msg_id` being `clearMsgId`, which changes from one start to the next.
|
|
376
406
|
*/
|
|
377
407
|
private startLiveJson;
|
|
378
408
|
/**
|
|
@@ -380,8 +410,20 @@ export declare class P2PSession extends EventEmitter {
|
|
|
380
410
|
* (GCM, signCode 8, frame type 10) when the session has a negotiated key, else level-1 (AES-128-ECB,
|
|
381
411
|
* signCode 1, frame type 11) — chosen by the session key, not the device family. The camera RSA-wraps
|
|
382
412
|
* each keyframe's AES media key with the `encryptkey` modulus (unwrapped by {@link decodeVideoFrame}).
|
|
413
|
+
* A session that {@link sendsInTheClear} sends it unencrypted instead: signCode 0, frame type 0, header
|
|
414
|
+
* bytes `[0x01, n]` from {@link nextClearCommandBytes}, and `n` as the body's `msg_id`.
|
|
383
415
|
*/
|
|
384
416
|
private sendStartLiveOwnSession;
|
|
417
|
+
/**
|
|
418
|
+
* Whether this session's own-session start and stop go in the clear: its `CMD_GATEWAYINFO` reply named
|
|
419
|
+
* `cipher_id` 0.
|
|
420
|
+
*/
|
|
421
|
+
private sendsInTheClear;
|
|
422
|
+
/**
|
|
423
|
+
* Header bytes 10-11 of this connection's next command sent in the clear: `[0x01, n]`, with `n` counting
|
|
424
|
+
* those commands from 1 and never 0. A retransmit repeats its `n`, and the device's reply echoes it.
|
|
425
|
+
*/
|
|
426
|
+
private nextClearCommandBytes;
|
|
385
427
|
/**
|
|
386
428
|
* Repeat unacknowledged own-session live starts until the device takes one. Runs only while a start is
|
|
387
429
|
* outstanding.
|
|
@@ -421,6 +463,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
421
463
|
* mechanism proven for camera selection in media start. Use for control commands routed through a
|
|
422
464
|
* HomeBase (where level-1 ECB is rejected). Returns `false` if the level-2 key isn't negotiated
|
|
423
465
|
* yet (caller can fall back to {@link sendStringPayloadCommand}).
|
|
466
|
+
*
|
|
467
|
+
* On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
|
|
468
|
+
* layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
|
|
469
|
+
* byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
|
|
424
470
|
*/
|
|
425
471
|
sendControlLevel2(cmd: number, channel: number, accountId: string, payload: Record<string, unknown>, mValue3?: number): boolean;
|
|
426
472
|
/**
|
|
@@ -448,6 +494,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
448
494
|
* `uint32`, the same 4-byte body as the attached talkback frames. The app sends this frame on closing
|
|
449
495
|
* an own-session camera's live view; the camera stops streaming on it within a second, and keeps
|
|
450
496
|
* streaming through the 1350-wrapped form.
|
|
497
|
+
* - own-session on a session that {@link sendsInTheClear}: the same 4-byte body unencrypted, signCode 0,
|
|
498
|
+
* header bytes from {@link nextClearCommandBytes}.
|
|
451
499
|
* - without a level-2 key: the bare 1004 command.
|
|
452
500
|
*/
|
|
453
501
|
stopLiveMedia(channel?: number, accountId?: string, homeBaseAttached?: boolean): void;
|
|
@@ -611,6 +659,18 @@ export declare class P2PSession extends EventEmitter {
|
|
|
611
659
|
accountId?: string;
|
|
612
660
|
channel?: number;
|
|
613
661
|
}): void;
|
|
662
|
+
/**
|
|
663
|
+
* Read a date-ranged page of `history_record_info` from the station. The reply's `data` array groups
|
|
664
|
+
* recording rows with companion tables; each group retains its `table_name` and `payload`.
|
|
665
|
+
* `startTime` is `"0"` for the newest page or the oldest row's `yyyyMMddHHmmss` cursor for older rows.
|
|
666
|
+
*/
|
|
667
|
+
queryRecordPage(opts: {
|
|
668
|
+
accountId?: string;
|
|
669
|
+
startDate: string;
|
|
670
|
+
endDate: string;
|
|
671
|
+
startTime?: string;
|
|
672
|
+
count?: number;
|
|
673
|
+
}): Promise<unknown[]>;
|
|
614
674
|
/**
|
|
615
675
|
* Query one on-station table and answer its rows, once the reply is whole.
|
|
616
676
|
*
|
|
@@ -618,13 +678,15 @@ export declare class P2PSession extends EventEmitter {
|
|
|
618
678
|
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
619
679
|
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
620
680
|
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
621
|
-
* complete reply
|
|
681
|
+
* complete reply, and when the station answers a non-zero `mIntRet`.
|
|
622
682
|
*
|
|
623
683
|
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
624
684
|
* session throws, so a second reply cannot land in this buffer.
|
|
625
685
|
*/
|
|
626
686
|
readDatabase(table: string, opts?: {
|
|
627
687
|
accountId?: string;
|
|
688
|
+
innerCmd?: number;
|
|
689
|
+
query?: Record<string, unknown>;
|
|
628
690
|
timeoutMs?: number;
|
|
629
691
|
signal?: AbortSignal;
|
|
630
692
|
}): Promise<unknown[]>;
|
|
@@ -644,6 +706,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
644
706
|
* Low-level: send a `CMD_SET_PAYLOAD` (1350) wrapping `{account_id, cmd:<subCmd>, mChannel,
|
|
645
707
|
* payload, transaction}` over the level-1 channel. The reply arrives as a `NOTIFY_PAYLOAD`
|
|
646
708
|
* (1351) frame (level-1 decrypted, surfaced via the `data` event / `frame.json`).
|
|
709
|
+
*
|
|
710
|
+
* `opts.key` makes it a keyed envelope: the sealed per-command key rides ahead of the other fields
|
|
711
|
+
* as the value's `key`, and the value goes out unencrypted, encryption-type 0, with `=` written
|
|
712
|
+
* `=` — the form the vendor app sends one in, and the one form the device reads it in.
|
|
647
713
|
*/
|
|
648
714
|
sendSetPayload(subCmd: number, payload?: unknown, opts?: {
|
|
649
715
|
accountId?: string;
|
|
@@ -651,6 +717,7 @@ export declare class P2PSession extends EventEmitter {
|
|
|
651
717
|
omitPayload?: boolean;
|
|
652
718
|
wrapCmd?: number;
|
|
653
719
|
rawValue?: Record<string, unknown>;
|
|
720
|
+
key?: string;
|
|
654
721
|
}): void;
|
|
655
722
|
/**
|
|
656
723
|
* Request the on-device edge-AI face roster over P2P. `COMMAND_GET_LOCAL_FACES` (1194) =
|
|
@@ -704,6 +771,7 @@ export declare class P2PSession extends EventEmitter {
|
|
|
704
771
|
* Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
|
|
705
772
|
* of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
|
|
706
773
|
* it would lose that frame and every frame after it until a datagram happened to begin on a header.
|
|
774
|
+
* Payload fragments retain their original slices and are copied once, when the complete frame is delivered.
|
|
707
775
|
*/
|
|
708
776
|
private reassemble;
|
|
709
777
|
/**
|
|
@@ -718,5 +786,6 @@ export declare class P2PSession extends EventEmitter {
|
|
|
718
786
|
private ackTypeHeader;
|
|
719
787
|
private handleFrame;
|
|
720
788
|
private send;
|
|
789
|
+
/** Finish the END send on the current socket before closing it; send completion is not a peer ACK. */
|
|
721
790
|
close(): Promise<void>;
|
|
722
791
|
}
|
|
@@ -17,11 +17,14 @@ 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, preserving their source-arrival metadata.
|
|
26
29
|
*/
|
|
27
30
|
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,19 +83,19 @@ 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
|
|
95
96
|
* back directly), plus the listener removal and arrival-timed feed the recording and readable egresses use.
|
|
96
97
|
*/
|
|
97
98
|
export interface Consumer extends LiveStreamConsumer {
|
|
98
|
-
/** What this consumer holds the pull for. */
|
|
99
99
|
/** Detach a previously registered listener (mirrors {@link LiveStreamHandle.on}). */
|
|
100
100
|
off(event: "video", listener: (frame: LiveVideoFrame) => void): this;
|
|
101
101
|
off(event: "audio", listener: (frame: LiveAudioFrame) => void): this;
|
|
@@ -103,18 +103,14 @@ export interface Consumer extends LiveStreamConsumer {
|
|
|
103
103
|
off(event: "error", listener: (err: Error) => void): this;
|
|
104
104
|
/** Subscribe to frames carrying the source-captured arrival time used by the prebuffer. */
|
|
105
105
|
onMedia(listener: (item: TimedMediaFrame) => void): this;
|
|
106
|
-
/** True once the source has replayed a cached keyframe to this consumer (no GOP wait on join). */
|
|
107
|
-
readonly primed: boolean;
|
|
108
106
|
/** Leave the source (refcount--). Idempotent. `stop()` is an alias (LiveStreamHandle). */
|
|
109
107
|
detach(): void;
|
|
110
108
|
}
|
|
111
109
|
/**
|
|
112
110
|
* One media frame retained with its transport-arrival time for prebuffer continuity.
|
|
113
111
|
*
|
|
114
|
-
* A video frame carries the coded configuration in force when it
|
|
115
|
-
*
|
|
116
|
-
* drain hands over, and both have to announce the configuration their media was coded under rather than
|
|
117
|
-
* whichever one is current by the time they are delivered.
|
|
112
|
+
* A video frame carries the coded configuration in force when it arrived, so a prebuffer drain
|
|
113
|
+
* announces the configuration its retained media was coded under.
|
|
118
114
|
*/
|
|
119
115
|
export type TimedMediaFrame = {
|
|
120
116
|
kind: "video";
|
|
@@ -144,8 +140,8 @@ export declare class SharedLiveSource {
|
|
|
144
140
|
* {@link LiveStreamStartError}.
|
|
145
141
|
*/
|
|
146
142
|
private delivered;
|
|
147
|
-
/**
|
|
148
|
-
private
|
|
143
|
+
/** Whether the upstream has delivered a keyframe since its last teardown. */
|
|
144
|
+
private hasKeyframe;
|
|
149
145
|
/** Last parameter sets the stream announced — see {@link parameterSets}. */
|
|
150
146
|
private lastParamSets?;
|
|
151
147
|
/**
|
|
@@ -163,14 +159,7 @@ export declare class SharedLiveSource {
|
|
|
163
159
|
private warmRetryTimer?;
|
|
164
160
|
/** Whether this warm-up has already asked its owner to replace the session. */
|
|
165
161
|
private sessionReplacementAsked;
|
|
166
|
-
/**
|
|
167
|
-
* Whether the pending watch is a REUSE watch, which any frame settles.
|
|
168
|
-
*
|
|
169
|
-
* A cold warm-up needs a keyframe: nothing can be decoded without one. A join already holds the retained
|
|
170
|
-
* keyframe, so what its watch is missing is evidence the stream is still being served — and a delta frame is
|
|
171
|
-
* that evidence. Requiring a keyframe there let the deadline outlive an actively delivering stream whose
|
|
172
|
-
* group of pictures is longer than the window, and the timeout fails EVERY consumer.
|
|
173
|
-
*/
|
|
162
|
+
/** A reuse watch settles on upstream media independently of each consumer's keyframe gate. */
|
|
174
163
|
private reuseWatch;
|
|
175
164
|
/** This source's opaque handle for tracing — see {@link SharedLiveSource.trace}. */
|
|
176
165
|
private readonly traceId;
|
|
@@ -178,8 +167,8 @@ export declare class SharedLiveSource {
|
|
|
178
167
|
* How many re-issues this watch has spent with nothing arriving since it was armed.
|
|
179
168
|
*
|
|
180
169
|
* What a stream delivered BEFORE the current watch is no evidence about now — a reused stream's upstream may
|
|
181
|
-
* have served plenty and since been dropped by the station
|
|
182
|
-
*
|
|
170
|
+
* have served plenty and since been dropped by the station. Only a frame arriving after the watch was
|
|
171
|
+
* armed proves current delivery, and {@link delivered}
|
|
183
172
|
* is reset to track exactly that.
|
|
184
173
|
*
|
|
185
174
|
* The first re-issue is therefore a keepalive: a reuse cannot yet know which case it is in, and a keepalive
|
|
@@ -219,13 +208,13 @@ export declare class SharedLiveSource {
|
|
|
219
208
|
get parameterSets(): ParamSets | undefined;
|
|
220
209
|
/**
|
|
221
210
|
* Attach a new consumer. Warms the stream on the first attach (or cancels a pending linger teardown
|
|
222
|
-
* and reuses the warm stream)
|
|
211
|
+
* and reuses the warm stream). Delivery starts at the next live keyframe so every dependent
|
|
212
|
+
* picture follows its reference pictures without a gap from a cached keyframe.
|
|
223
213
|
*/
|
|
224
214
|
attach(): Consumer;
|
|
225
215
|
/**
|
|
226
216
|
* Attach at the same instant a keyframe-aligned prebuffer snapshot is taken. The returned consumer
|
|
227
|
-
*
|
|
228
|
-
* duplicates the newest IDR nor leaves a gap at the handoff.
|
|
217
|
+
* continues after `buffered` without waiting for another keyframe or duplicating the newest IDR.
|
|
229
218
|
*/
|
|
230
219
|
attachWithPrebuffer(seconds: number): {
|
|
231
220
|
consumer: Consumer;
|
|
@@ -251,18 +240,17 @@ export declare class SharedLiveSource {
|
|
|
251
240
|
*/
|
|
252
241
|
private warm;
|
|
253
242
|
/**
|
|
254
|
-
*
|
|
243
|
+
* Ask the owner for a replacement session where nothing has been delivered yet, answering whether one is coming.
|
|
255
244
|
*
|
|
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
|
-
*
|
|
245
|
+
* An unacknowledged start is roughly twenty byte-identical sends with no reply, against acknowledgement latencies
|
|
246
|
+
* 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
|
|
247
|
+
* camera was never told to stream. Where media has already flowed the abandonment means something else and this
|
|
248
|
+
* does nothing. The DEADLINE is left running: the window belongs to the attempt, not to the session it started on.
|
|
260
249
|
*
|
|
261
|
-
* The retry ticker is stopped
|
|
262
|
-
*
|
|
263
|
-
* started on.
|
|
250
|
+
* The retry ticker is stopped only once the owner has said it is replacing the session: an owner that cannot
|
|
251
|
+
* (an attached camera shares its HomeBase session) leaves the source re-issuing as it always did.
|
|
264
252
|
*/
|
|
265
|
-
private
|
|
253
|
+
private askForFreshSession;
|
|
266
254
|
/**
|
|
267
255
|
* Warm again on a session the owner has replaced, inside the deadline the first attempt started.
|
|
268
256
|
*
|
|
@@ -290,27 +278,17 @@ export declare class SharedLiveSource {
|
|
|
290
278
|
* {@link fruitlessReissues} for why one is not enough and why what the stream delivered earlier is not
|
|
291
279
|
* evidence. Once media arrives the keepalive is what is wanted, and an attached camera re-sends a full start
|
|
292
280
|
* either way.
|
|
281
|
+
*
|
|
282
|
+
* A forced start that a whole interval later has still produced nothing is a session that acknowledges and
|
|
283
|
+
* does not serve: a fresh session on the same camera delivers a keyframe within a second, so a warm-up asks
|
|
284
|
+
* for one rather than forcing starts onto this one until its deadline. Wired only: a cold battery camera is
|
|
285
|
+
* slow rather than deaf, measured at about 8 s to first media, and replacing its session would cost a wake.
|
|
293
286
|
*/
|
|
294
287
|
private reissueStart;
|
|
295
288
|
/**
|
|
296
|
-
* Watch
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* it says the stream WAS being served, not that it still is. A caller commits to media on that frame — a
|
|
300
|
-
* process, a negotiated session — so a stream the station has quietly stopped serving strands it with no
|
|
301
|
-
* deadline, because warming is what arms one and a reuse skips warming by definition.
|
|
302
|
-
*
|
|
303
|
-
* The watch is the warm-up's own, deadline and retry alike, and the first frame to arrive AFTER the join
|
|
304
|
-
* clears it, that being the only frame which says the stream is still being served. A stream still serving
|
|
305
|
-
* clears it long before the deadline fires; one that is not fails its consumers exactly as a cold start that
|
|
306
|
-
* never delivered would.
|
|
307
|
-
*
|
|
308
|
-
* The start is re-issued at once and then on the retry's cadence, because a station that stopped serving a
|
|
309
|
-
* channel when its last consumer left is the very case the retry recovers: waiting the whole window to
|
|
310
|
-
* report what one re-issued start can fix is a timeout where a stream was available.
|
|
311
|
-
*
|
|
312
|
-
* Nothing is armed while a watch is already pending, so several consumers joining one reused stream share the
|
|
313
|
-
* watch the first of them started.
|
|
289
|
+
* Watch an existing upstream with the warm-up retry and deadline. Any video or audio received after
|
|
290
|
+
* the watch begins confirms delivery and settles it. Each consumer's keyframe gate remains separate.
|
|
291
|
+
* Concurrent joins share a pending watch; the first join reissues the start immediately.
|
|
314
292
|
*/
|
|
315
293
|
private watchReusedStream;
|
|
316
294
|
/** Arm the battery budget timer (battery/solar sources) — replaces any pending budget/grace. */
|
|
@@ -323,7 +301,7 @@ export declare class SharedLiveSource {
|
|
|
323
301
|
private onBudgetExpire;
|
|
324
302
|
/** Re-push the battery budget (host called `extend()` from the notice), cancelling the auto-stop. */
|
|
325
303
|
private extendBudget;
|
|
326
|
-
/** Settle a reuse watch on any frame
|
|
304
|
+
/** Settle a reuse watch on any upstream frame without changing consumer keyframe gates. */
|
|
327
305
|
private settleReuseWatch;
|
|
328
306
|
/** Stop the warm-up retry + deadline (the stream is confirmed live). */
|
|
329
307
|
private clearWarmWatch;
|
|
@@ -338,7 +316,8 @@ export declare class SharedLiveSource {
|
|
|
338
316
|
* at all, audio without a single video frame, or access units a decoder cannot begin at.
|
|
339
317
|
*
|
|
340
318
|
* Video takes precedence when both arrived: audio alongside video says nothing a caller needs, while video
|
|
341
|
-
* without a keyframe does.
|
|
319
|
+
* without a keyframe does. Every failed start is built here, so this is where its `start-failed` trace is
|
|
320
|
+
* emitted.
|
|
342
321
|
*/
|
|
343
322
|
private startFailure;
|
|
344
323
|
private onVideo;
|
|
@@ -404,7 +383,7 @@ export declare class SharedLiveSource {
|
|
|
404
383
|
/** Refcount hit zero — arm the linger teardown. A new attach in the window cancels it. */
|
|
405
384
|
private arm;
|
|
406
385
|
/**
|
|
407
|
-
* Stop + drop the underlying stream and clear the
|
|
386
|
+
* Stop + drop the underlying stream and clear the keyframe/ring caches. Rebuildable via attach().
|
|
408
387
|
*
|
|
409
388
|
* The stream reference is dropped BEFORE stopping it, because `stop()` emits `"stop"` synchronously and
|
|
410
389
|
* this source listens for that — so stopping re-enters `teardown` through {@link onUpstreamEnd}. Clearing
|
|
@@ -438,3 +417,12 @@ export declare class SharedLiveSource {
|
|
|
438
417
|
*/
|
|
439
418
|
dispose(): void;
|
|
440
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Attach a consumer, unless the caller has already abandoned the call.
|
|
422
|
+
*
|
|
423
|
+
* The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
|
|
424
|
+
* somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
|
|
425
|
+
* lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
|
|
426
|
+
* not.
|
|
427
|
+
*/
|
|
428
|
+
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. */
|
|
@@ -32,6 +32,11 @@ export declare class PushClient extends EventEmitter {
|
|
|
32
32
|
private closing;
|
|
33
33
|
/** Consecutive MCS login rejections — transient ones self-heal via reconnect (see {@link onMessage}). */
|
|
34
34
|
private loginFailures;
|
|
35
|
+
/**
|
|
36
|
+
* How many of {@link persistentIds} the last `LoginRequest` reported; a successful `LoginResponse`
|
|
37
|
+
* acknowledges them, so they are dropped then.
|
|
38
|
+
*/
|
|
39
|
+
private reportedIds;
|
|
35
40
|
constructor(creds: FcmCredentials, logger?: Logger);
|
|
36
41
|
/** Persistent ids already seen (set this from storage to avoid re-delivery). */
|
|
37
42
|
setPersistentIds(ids: string[]): void;
|
|
@@ -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
|
+
}
|