@mega-yfue/eufy-sdk 0.5.0-beta.8 → 0.5.0-beta.81
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 +75 -6
- package/dist/client/types.d.ts +1 -1
- package/dist/core/contracts.d.ts +80 -33
- package/dist/core/store.d.ts +4 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/index.js +3754 -617
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +39 -4
- package/dist/model/capabilities/camera.d.ts +47 -18
- 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 +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 +12 -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/audio-frame.d.ts +14 -0
- package/dist/transport/p2p/codec.d.ts +16 -0
- package/dist/transport/p2p/command-router.d.ts +19 -30
- package/dist/transport/p2p/fmp4.d.ts +12 -2
- package/dist/transport/p2p/live-trace.d.ts +25 -2
- package/dist/transport/p2p/p2p-session.d.ts +55 -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 +29 -14
- 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 +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 +2 -1
|
@@ -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. */
|
|
@@ -228,6 +230,8 @@ export declare class MegaHttpClient {
|
|
|
228
230
|
/** True while a 2FA code is outstanding: `auth_` holds only the limited pre-verify token, so the
|
|
229
231
|
* restored-session short-circuit must NOT treat it as a usable session. Cleared on Ok/reset. */
|
|
230
232
|
private pending2fa;
|
|
233
|
+
/** An autonomous re-login answered captcha/2FA; see {@link canReauthenticate}. */
|
|
234
|
+
private reloginChallenged;
|
|
231
235
|
private tokenExpiresAt;
|
|
232
236
|
/**
|
|
233
237
|
* Stable per-install device id: `openudid` as configured, as restored from the session store, or as
|
|
@@ -259,10 +263,12 @@ export declare class MegaHttpClient {
|
|
|
259
263
|
constructor(cfg: MegaClientConfig);
|
|
260
264
|
/**
|
|
261
265
|
* Install the session the store holds, if it holds a usable one: the token + its bound ECDH key, skipping
|
|
262
|
-
* estimate/key-exchange/login/2FA entirely. Answers the token adopted, or `undefined`.
|
|
266
|
+
* estimate/key-exchange/login/2FA entirely. Answers the token adopted, or `undefined`. A session logged in
|
|
267
|
+
* with a country other than the configured one is not usable: its shard is the other country's.
|
|
263
268
|
*
|
|
264
269
|
* Read at construction, and again when a rejection is being recovered from — a store SHARED with another
|
|
265
270
|
* client may already hold the session that client obtained, which is cheaper to adopt than to compete with.
|
|
271
|
+
* An adopted session is a complete one, so it ends any 2FA or challenged re-login this client was left with.
|
|
266
272
|
*
|
|
267
273
|
* Note: the device identity (openudid + phone model + media UA) is NOT restored here — it is resolved once
|
|
268
274
|
* in the constructor, where an explicit config wins over the stored value, and must not be re-derived from a
|
|
@@ -275,6 +281,21 @@ export declare class MegaHttpClient {
|
|
|
275
281
|
} | undefined;
|
|
276
282
|
/** The active region shard (e.g. `"eu-pr"`, `"us-pr"`), set after {@link login} or a region override. */
|
|
277
283
|
get regionShard(): RegionShard;
|
|
284
|
+
/**
|
|
285
|
+
* The shard the RTC signalling signs on: the regional prefix of the estimated domain
|
|
286
|
+
* (`…-ie-…`/`…-ie.` → `ie-pr`), else {@link regionShard}. The eu/us classification cannot name shards
|
|
287
|
+
* like `ie-pr`, whose accounts need the IE sign host and cluster.
|
|
288
|
+
*/
|
|
289
|
+
get rtcShard(): string;
|
|
290
|
+
/**
|
|
291
|
+
* The credentials the RTC signalling needs; `undefined` while logged out. The sign is refused
|
|
292
|
+
* ("gtoken not equal userid") unless its gtoken is the one every authed HTTP call carries.
|
|
293
|
+
*/
|
|
294
|
+
rtcIdentity(): {
|
|
295
|
+
authToken: string;
|
|
296
|
+
userId: string;
|
|
297
|
+
gtoken: string;
|
|
298
|
+
} | undefined;
|
|
278
299
|
/**
|
|
279
300
|
* The name commands attribute themselves to — {@link MegaClientConfig.accountName} when the config
|
|
280
301
|
* pins one (trimmed; blank counts as unset), otherwise the logged-in account's display name, which
|
|
@@ -367,6 +388,15 @@ export declare class MegaHttpClient {
|
|
|
367
388
|
* params instead, which also carry `{param_type, param_value, update_time}` and are not owner-gated.
|
|
368
389
|
*/
|
|
369
390
|
getDeviceParamList<T = unknown>(deviceSn: string): Promise<T>;
|
|
391
|
+
/**
|
|
392
|
+
* Fetch the account's devices as the `devicerelation` service lists them, `{attribute: 7, house_id: ""}`.
|
|
393
|
+
*
|
|
394
|
+
* Each entry is `{device: {device_sn, dps, …}}`, and `dps` is the cloud's last-known value of every
|
|
395
|
+
* data point that device has reported, keyed by DP number. Answered for an account a device is shared
|
|
396
|
+
* with as well as for its owner. Sent with `category: eufy_home`, without which the service answers
|
|
397
|
+
* `10000`.
|
|
398
|
+
*/
|
|
399
|
+
getDeviceRelationList<T = unknown>(): Promise<T>;
|
|
370
400
|
/**
|
|
371
401
|
* Fetch one page of a device's **cleaning history** from the mega `clean` service.
|
|
372
402
|
*
|
|
@@ -563,7 +593,9 @@ export declare class MegaHttpClient {
|
|
|
563
593
|
* Structural rather than a list of paths: a login round trip is itself an authenticated call while a limited
|
|
564
594
|
* token is held, so re-logging in from inside one would recurse — and a path list would have to be
|
|
565
595
|
* maintained alongside every request the login flow makes. A 2FA code already outstanding is a login a human
|
|
566
|
-
* is part-way through, and restarting it silently would discard it.
|
|
596
|
+
* is part-way through, and restarting it silently would discard it. A re-login that already came back needing
|
|
597
|
+
* a captcha or a 2FA code would need the same again, and each attempt sends the account another code, so
|
|
598
|
+
* none is tried until a login completes or a stored session is adopted. No credentials means nothing to try,
|
|
567
599
|
* so the rejection surfaces instead.
|
|
568
600
|
*/
|
|
569
601
|
private canReauthenticate;
|
|
@@ -8,8 +8,9 @@ export interface BrokerCredentials {
|
|
|
8
8
|
}
|
|
9
9
|
export interface ProbeResult {
|
|
10
10
|
ip: string;
|
|
11
|
-
/** True iff SUBSCRIBE to the target topic was granted (qos 0/1/2
|
|
11
|
+
/** True iff SUBSCRIBE to the target topic was granted (qos 0/1/2). */
|
|
12
12
|
granted: boolean;
|
|
13
|
+
/** The SUBACK return code for the topic, the 0x80 deny code included. */
|
|
13
14
|
grantedQos?: number;
|
|
14
15
|
error?: string;
|
|
15
16
|
ms: number;
|
|
@@ -136,8 +136,9 @@ export declare class MqttCommandRouter {
|
|
|
136
136
|
* One exception is known: a single garage unit whose own-cert SUBSCRIBE was denied on every candidate
|
|
137
137
|
* while a phone-extracted cert granted on the same one, with a sibling of the same model on the same
|
|
138
138
|
* account granting fine. That is a per-device authorization gap on the vendor's backend, not a
|
|
139
|
-
* broker-instance problem and not something this SDK works around —
|
|
140
|
-
* (
|
|
139
|
+
* broker-instance problem and not something this SDK works around — the throw below names each
|
|
140
|
+
* candidate's outcome (its SUBACK deny code, a timeout or a connection error) until the backend grants
|
|
141
|
+
* the account's own credentials.
|
|
141
142
|
*
|
|
142
143
|
* One fresh, explicitly non-reconnecting (`reconnectPeriod: 0`) connection per call — it repays the
|
|
143
144
|
* full mTLS handshake every command and tears the socket down right after (see
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AudioCodec } from "../../core/contracts.js";
|
|
2
|
+
export declare const CMD_AUDIO_FRAME = 1301;
|
|
3
|
+
/** Length of the audio frame header before the payload. */
|
|
4
|
+
export declare const AUDIO_HEADER_LEN = 16;
|
|
5
|
+
/** Byte offset of the codec id between the frame-size and frame-number fields. */
|
|
6
|
+
export declare const AUDIO_TYPE_OFFSET = 5;
|
|
7
|
+
/**
|
|
8
|
+
* Codec ids the station puts at {@link AUDIO_TYPE_OFFSET}, as the v6 app maps them
|
|
9
|
+
* (`AudioReader.AAC_LC = 0`, `G711A = 2`, `AAC_ELD = 7`).
|
|
10
|
+
*
|
|
11
|
+
* The app's `PCM_SILENCE = -1` is absent on purpose: it is not a wire value. The app injects it
|
|
12
|
+
* itself when a stream produces no audio at all, to feed its decoder silence.
|
|
13
|
+
*/
|
|
14
|
+
export declare const AUDIO_CODECS: Readonly<Record<number, AudioCodec>>;
|
|
@@ -111,6 +111,22 @@ export declare function eciesUnwrap(envelope: Buffer, eccPrivateKeyHex: string,
|
|
|
111
111
|
* Returns the 32-byte key, or undefined if the inputs don't parse.
|
|
112
112
|
*/
|
|
113
113
|
export declare function deriveLevel2KeyFromGatewayInfo(gwPayload: Buffer, eccPrivateKeyHex: string): Buffer | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Derive the P2P **level-2** AES-128-ECB session key from a *decrypted* `CMD_GATEWAYINFO` (1100)
|
|
116
|
+
* payload and the RSA private key of a cipher whose `get_ciphers` answer carries no ECC key.
|
|
117
|
+
*
|
|
118
|
+
* Layout of the decrypted payload: `cipher_id(2 LE) ‖ 0000 ‖ RSA ciphertext ‖ …`, the ciphertext as
|
|
119
|
+
* long as the key's modulus. It is the 16-byte session key under PKCS#1 v1.5 encryption padding
|
|
120
|
+
* (`00 02 ‖ ≥8 non-zero bytes ‖ 00 ‖ key`). The padding is checked here over the raw RSA result so a wrong
|
|
121
|
+
* key fails closed: under OpenSSL's implicit rejection, `RSA_PKCS1_PADDING` answers random bytes for it
|
|
122
|
+
* instead of throwing, and those can be 16 bytes long.
|
|
123
|
+
*
|
|
124
|
+
* `rsaPrivateKey` is the `get_ciphers` `private_key`: base64 PKCS#8 DER.
|
|
125
|
+
*
|
|
126
|
+
* Returns the 16-byte key, or `undefined` when the key does not parse, the payload is short, or the
|
|
127
|
+
* padding or key length is wrong (fails closed, never throws).
|
|
128
|
+
*/
|
|
129
|
+
export declare function deriveLevel2KeyFromRsaGatewayInfo(gwPayload: Buffer, rsaPrivateKey: string): Buffer | undefined;
|
|
114
130
|
/** Local-lookup payload: two zero bytes. */
|
|
115
131
|
export declare function buildLocalLookupPayload(): Buffer;
|
|
116
132
|
/** Cloud-lookup payload (variant 2): `[p2pDid:20][dskKey][0x00000000]`. */
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P2P command router — the transport-side owner of the ThroughTek PPCS sessions and every wire
|
|
3
|
-
* operation over them: opening sessions, resolving a serial to its session + routing params,
|
|
4
|
-
* mapping a transport-neutral {@link Command} to a concrete frame (encryption level, wire shape),
|
|
5
|
-
* the fire-and-forget control senders, request/reply queries, and the media provider.
|
|
6
|
-
*
|
|
7
|
-
* Layering: this module knows P2P bytes; it does NOT know capabilities. Frame → semantic-event
|
|
8
|
-
* decoding is a model concern, so raw frames are handed back to the client via {@link P2PRouterDeps.onFrame}
|
|
9
|
-
* (the client gates them on device capabilities and emits typed events). This keeps transport free of
|
|
10
|
-
* any `model/` import — the capability↔transport decorrelation invariant.
|
|
11
|
-
*/
|
|
12
1
|
import type { MegaHttpClient } from "../http/mega-client.js";
|
|
13
2
|
import type { EufyDevice } from "../../core/types.js";
|
|
14
3
|
import type { Command, AutoLockSnapshot, MediaProvider, SharedSourceHints, AbortableCall } from "../../core/contracts.js";
|
|
@@ -119,7 +108,7 @@ export declare class P2PCommandRouter {
|
|
|
119
108
|
* shared — see {@link P2PCommandRouter.openTalkback}.
|
|
120
109
|
*/
|
|
121
110
|
private readonly talkbacks;
|
|
122
|
-
/** cipher_id → ECC private
|
|
111
|
+
/** cipher_id → its ECC and RSA private keys (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
123
112
|
private readonly cipherKeyCache;
|
|
124
113
|
/** The recording download in flight per station; a station serves one at a time. */
|
|
125
114
|
private readonly recordingDownloads;
|
|
@@ -224,7 +213,7 @@ export declare class P2PCommandRouter {
|
|
|
224
213
|
* Build + wire a {@link P2PSession} for a station (NOT yet connected — the caller awaits `connect()`).
|
|
225
214
|
*
|
|
226
215
|
* `resolveCipherKey` auto-negotiates the level-2 session key from `CMD_GATEWAYINFO` by resolving the
|
|
227
|
-
* cipher's
|
|
216
|
+
* cipher's private keys via cloud `get_ciphers`, so signCode 2/8 frames decrypt live; results are
|
|
228
217
|
* cached on the router instance so a lazy re-open (after idle-detach) reuses the lookup. Only a
|
|
229
218
|
* SUCCESSFUL lookup is cached — caching `undefined` after a transient failure would permanently
|
|
230
219
|
* disable level-2 for the session's life.
|
|
@@ -240,11 +229,12 @@ export declare class P2PCommandRouter {
|
|
|
240
229
|
*/
|
|
241
230
|
private makeSession;
|
|
242
231
|
/**
|
|
243
|
-
* The ECC private
|
|
244
|
-
*
|
|
245
|
-
*
|
|
232
|
+
* The ECC and RSA private keys of `cipherId` (`ecc_private_key`, `private_key`), from the router's cache
|
|
233
|
+
* or one `get_ciphers` call. When the cloud answers with another cipher that carries an ECC key and the
|
|
234
|
+
* asked one does not, that one is used and traced on `session`. Only a successful lookup — one that
|
|
235
|
+
* yields either key — is cached.
|
|
246
236
|
*/
|
|
247
|
-
private
|
|
237
|
+
private cipherKeysFor;
|
|
248
238
|
/**
|
|
249
239
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
250
240
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -311,6 +301,11 @@ export declare class P2PCommandRouter {
|
|
|
311
301
|
* stream costs nothing extra and a talkback on an otherwise idle camera opens the session it needs
|
|
312
302
|
* instead of playing into silence.
|
|
313
303
|
*
|
|
304
|
+
* **On a model in {@link ELD_TALKBACK_MODELS}, the speaker plays the codec the camera sends.** So the first
|
|
305
|
+
* audio frame of the media session this talkback holds decides its codec ({@link speakerCodec}), within
|
|
306
|
+
* {@link TALKBACK_CODEC_WAIT_MS} of the session's first frame, and the claim is re-checked after that wait as
|
|
307
|
+
* after the warm-up. Every other model opens `aac-lc` at once.
|
|
308
|
+
*
|
|
314
309
|
* The level-2 key is waited for softly: only the HomeBase-attached path requires it, and
|
|
315
310
|
* {@link Talkback.start} reports that failure precisely, so a hard wait here would reject an
|
|
316
311
|
* own-session camera that legitimately never negotiates one.
|
|
@@ -456,15 +451,6 @@ export declare class P2PCommandRouter {
|
|
|
456
451
|
* A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
|
|
457
452
|
* unaffected.
|
|
458
453
|
*/
|
|
459
|
-
/**
|
|
460
|
-
* Attach a consumer, unless the caller has already abandoned the call.
|
|
461
|
-
*
|
|
462
|
-
* The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
|
|
463
|
-
* somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
|
|
464
|
-
* lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
|
|
465
|
-
* not.
|
|
466
|
-
*/
|
|
467
|
-
private attachUnlessAborted;
|
|
468
454
|
/**
|
|
469
455
|
* Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
|
|
470
456
|
* on a session goes with it, and a media session opened for this source alone is closed: nothing else
|
|
@@ -524,11 +510,11 @@ export declare class P2PCommandRouter {
|
|
|
524
510
|
*/
|
|
525
511
|
private onLiveStartFailed;
|
|
526
512
|
/**
|
|
527
|
-
* Replace the session under a warming source
|
|
513
|
+
* Replace the session under a warming source it is not serving, and warm again on it.
|
|
528
514
|
*
|
|
529
515
|
* Only a STANDALONE device's session is replaced, for the reason {@link onLiveStartFailed} gives: an attached
|
|
530
516
|
* camera shares its HomeBase session with every other camera on it, and closing that to recover one would
|
|
531
|
-
* drop the rest. Such a source keeps the re-issue it always had.
|
|
517
|
+
* drop the rest. Such a source keeps the re-issue it always had, which the answer (false) tells it.
|
|
532
518
|
*
|
|
533
519
|
* The source is left warming throughout, holding the deadline it started, so this either produces a stream
|
|
534
520
|
* within that window or fails exactly as it would have. A replacement that cannot be opened leaves the
|
|
@@ -562,6 +548,7 @@ export declare class P2PCommandRouter {
|
|
|
562
548
|
*/
|
|
563
549
|
p2pQuery(sn: string, subCmd: number, opts?: {
|
|
564
550
|
timeoutMs?: number;
|
|
551
|
+
payload?: Record<string, unknown>;
|
|
565
552
|
}): Promise<Record<string, unknown>>;
|
|
566
553
|
/**
|
|
567
554
|
* **Generic control-payload request/reply query.** Sends a `CONTROL_PAYLOAD` (1700) `{commandType,
|
|
@@ -690,7 +677,7 @@ export declare class P2PCommandRouter {
|
|
|
690
677
|
* **`set-json-raw` intent** — bare JSON, no envelope: outer P2P cmd = `outerCmd` itself, plaintext
|
|
691
678
|
* exactly `{account_id,...data}` (`session.sendRawLevel2` with no wrapper). Reversed from a live
|
|
692
679
|
* capture of the app's own SET_SNOOZE_TIME (1271) frame — see `param-dictionary.ts`'s `1271` entry
|
|
693
|
-
* (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.
|
|
680
|
+
* (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.SET_ALL_ACTION`) reuses
|
|
694
681
|
* the same bare-JSON shape.
|
|
695
682
|
*/
|
|
696
683
|
private sendJsonRaw;
|
|
@@ -705,7 +692,9 @@ export declare class P2PCommandRouter {
|
|
|
705
692
|
* HomeBase-attached device takes the GCM signCode-8 form, a standalone one the level-1 form. A
|
|
706
693
|
* standalone camera never negotiates a level-2 key, so pinning this to level 2 makes the envelope
|
|
707
694
|
* unreachable on exactly the devices that serve their own RTSP stream. Verified live: a standalone
|
|
708
|
-
* camera accepts the level-1 form. With no `form` (default) it stays level-2 only
|
|
695
|
+
* camera accepts the level-1 form. With no `form` (default) it stays level-2 only and waits for the
|
|
696
|
+
* key every level-2 sender uses; `level2Sender: "set-payload"` also takes an RSA cipher's AES-128-ECB
|
|
697
|
+
* key (see {@link P2PSession.sendControlLevel2}). A `resolved` session keeps the wait its caller gave it.
|
|
709
698
|
*
|
|
710
699
|
* Both seals REPLAY the frame {@link DIRECT_CMD_SENDS}× at 200ms, as every other fire-and-forget
|
|
711
700
|
* control on this router does: these are unacknowledged datagrams, and a level-1 device is the one
|
|
@@ -17,9 +17,12 @@ export declare class Fmp4Muxer {
|
|
|
17
17
|
private baseDecodeTime;
|
|
18
18
|
private fragTicks;
|
|
19
19
|
private samples;
|
|
20
|
-
|
|
20
|
+
/** Ticks from the first video sample's start to the newest sample's end, at its provisional duration. */
|
|
21
|
+
private videoTicks;
|
|
21
22
|
private readonly audioRequested;
|
|
22
23
|
private audioCodec?;
|
|
24
|
+
/** Decoder config of an AAC-ELD track, taken from its frames; absent for AAC-LC, whose `esds` config is fixed. */
|
|
25
|
+
private audioConfig?;
|
|
23
26
|
private audioDisabled;
|
|
24
27
|
private audioSamples;
|
|
25
28
|
private audioBaseDecodeTime;
|
|
@@ -35,9 +38,16 @@ export declare class Fmp4Muxer {
|
|
|
35
38
|
* Push one access unit. Returns any completed segments: the `init` on the first keyframe, and/or a
|
|
36
39
|
* media fragment when this frame closed the open one. Returns `undefined` if nothing is emitted yet
|
|
37
40
|
* (e.g. delta frames before the first keyframe).
|
|
41
|
+
*
|
|
42
|
+
* A sample lasts until the next frame's `timestampMs`, measured from the first keyframe's, so the video
|
|
43
|
+
* timeline follows the arrival clock; a frame that shares the previous one's timestamp leaves that
|
|
44
|
+
* sample one tick long, and the next frame's duration absorbs the difference.
|
|
38
45
|
*/
|
|
39
46
|
push(frame: LiveVideoFrame, timestampMs?: number): MediaFragment | undefined;
|
|
40
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Add one station-declared AAC access unit to the audio track: an ADTS frame for AAC-LC, a raw access unit
|
|
49
|
+
* described by its frame's config for AAC-ELD. G.711 remains available via `live()`.
|
|
50
|
+
*/
|
|
41
51
|
pushAudio(frame: LiveAudioFrame, timestampMs?: number): MediaFragment | undefined;
|
|
42
52
|
/** Permanently omit incompatible audio while allowing the video recording to continue. */
|
|
43
53
|
private disableAudio;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module p2p/live-trace
|
|
13
13
|
*/
|
|
14
|
+
import type { LiveStreamStartFailureReason, LiveStreamStartStage } from "../../core/contracts.js";
|
|
14
15
|
import type { Logger } from "../../core/logger.js";
|
|
15
16
|
/** The message every startup trace is logged under. */
|
|
16
17
|
export declare const LIVE_TRACE_MESSAGE = "[live] start trace";
|
|
@@ -118,11 +119,19 @@ export type LiveTrace =
|
|
|
118
119
|
phase: "level2-wait";
|
|
119
120
|
waitMs: number;
|
|
120
121
|
}
|
|
121
|
-
/** The station's level-2 key was negotiated, under the cipher it selected. */
|
|
122
|
+
/** The station's AES-256-GCM level-2 key was negotiated, under the cipher it selected. */
|
|
122
123
|
| {
|
|
123
124
|
phase: "level2-ready";
|
|
124
125
|
cipherId: number;
|
|
125
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* The station's cipher negotiated only the AES-128-ECB level-2 key of an RSA cipher, under the cipher it
|
|
129
|
+
* selected. That key seals a level-2 set-payload and nothing else, so this is not `level2-ready`.
|
|
130
|
+
*/
|
|
131
|
+
| {
|
|
132
|
+
phase: "level2-set-payload-only";
|
|
133
|
+
cipherId: number;
|
|
134
|
+
}
|
|
126
135
|
/**
|
|
127
136
|
* The station's key is not coming, why, and the cipher where a station named one.
|
|
128
137
|
*
|
|
@@ -145,7 +154,8 @@ export type LiveTrace =
|
|
|
145
154
|
* A station's cipher was answered with material for a DIFFERENT cipher, which was used in its place.
|
|
146
155
|
*
|
|
147
156
|
* The one lookup outcome no other phase accounts for: material for the cipher the station named is followed
|
|
148
|
-
* by `level2-ready` or
|
|
157
|
+
* by `level2-ready`, `level2-set-payload-only` or `level2-unavailable` with `derivation-failed`, an answer
|
|
158
|
+
* holding none by
|
|
149
159
|
* `no-cipher-key`, and a lookup that threw is reported as an error. Substituted material derives to
|
|
150
160
|
* nothing and otherwise reads as a station fault. `cipherId` is the cipher the station asked for,
|
|
151
161
|
* `answeredCipherId` the one whose material was used.
|
|
@@ -236,6 +246,19 @@ export type LiveTrace =
|
|
|
236
246
|
phase: "channel-silent";
|
|
237
247
|
silentMs: number;
|
|
238
248
|
outcome: "reasserted" | "declined";
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A live start failed, with the facts of the `LiveStreamStartError` it was refused with.
|
|
252
|
+
*
|
|
253
|
+
* `errorName` is the upstream error's class name, present for `source-error` only; its message is never
|
|
254
|
+
* carried, because an upstream message can name a station.
|
|
255
|
+
*/
|
|
256
|
+
| {
|
|
257
|
+
phase: "start-failed";
|
|
258
|
+
reason: LiveStreamStartFailureReason;
|
|
259
|
+
stage: LiveStreamStartStage;
|
|
260
|
+
attempts: number;
|
|
261
|
+
errorName?: string;
|
|
239
262
|
};
|
|
240
263
|
/**
|
|
241
264
|
* Record one startup observation at debug level.
|
|
@@ -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
|
/**
|
|
@@ -611,6 +638,18 @@ export declare class P2PSession extends EventEmitter {
|
|
|
611
638
|
accountId?: string;
|
|
612
639
|
channel?: number;
|
|
613
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[]>;
|
|
614
653
|
/**
|
|
615
654
|
* Query one on-station table and answer its rows, once the reply is whole.
|
|
616
655
|
*
|
|
@@ -618,13 +657,15 @@ export declare class P2PSession extends EventEmitter {
|
|
|
618
657
|
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
619
658
|
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
620
659
|
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
621
|
-
* complete reply
|
|
660
|
+
* complete reply, and when the station answers a non-zero `mIntRet`.
|
|
622
661
|
*
|
|
623
662
|
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
624
663
|
* session throws, so a second reply cannot land in this buffer.
|
|
625
664
|
*/
|
|
626
665
|
readDatabase(table: string, opts?: {
|
|
627
666
|
accountId?: string;
|
|
667
|
+
innerCmd?: number;
|
|
668
|
+
query?: Record<string, unknown>;
|
|
628
669
|
timeoutMs?: number;
|
|
629
670
|
signal?: AbortSignal;
|
|
630
671
|
}): Promise<unknown[]>;
|
|
@@ -704,6 +745,7 @@ export declare class P2PSession extends EventEmitter {
|
|
|
704
745
|
* Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
|
|
705
746
|
* of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
|
|
706
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.
|
|
707
749
|
*/
|
|
708
750
|
private reassemble;
|
|
709
751
|
/**
|
|
@@ -718,5 +760,6 @@ export declare class P2PSession extends EventEmitter {
|
|
|
718
760
|
private ackTypeHeader;
|
|
719
761
|
private handleFrame;
|
|
720
762
|
private send;
|
|
763
|
+
/** Finish the END send on the current socket before closing it; send completion is not a peer ACK. */
|
|
721
764
|
close(): Promise<void>;
|
|
722
765
|
}
|
|
@@ -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
|
/**
|
|
@@ -338,7 +343,8 @@ export declare class SharedLiveSource {
|
|
|
338
343
|
* at all, audio without a single video frame, or access units a decoder cannot begin at.
|
|
339
344
|
*
|
|
340
345
|
* Video takes precedence when both arrived: audio alongside video says nothing a caller needs, while video
|
|
341
|
-
* without a keyframe does.
|
|
346
|
+
* without a keyframe does. Every failed start is built here, so this is where its `start-failed` trace is
|
|
347
|
+
* emitted.
|
|
342
348
|
*/
|
|
343
349
|
private startFailure;
|
|
344
350
|
private onVideo;
|
|
@@ -438,3 +444,12 @@ export declare class SharedLiveSource {
|
|
|
438
444
|
*/
|
|
439
445
|
dispose(): void;
|
|
440
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Attach a consumer, unless the caller has already abandoned the call.
|
|
449
|
+
*
|
|
450
|
+
* The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
|
|
451
|
+
* somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
|
|
452
|
+
* lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
|
|
453
|
+
* not.
|
|
454
|
+
*/
|
|
455
|
+
export declare function attachUnlessAborted(source: SharedLiveSource, signal?: AbortSignal): Consumer;
|