@mega-yfue/eufy-sdk 0.5.0-beta.9 → 0.5.0-beta.90
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 +4152 -719
- 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
|
@@ -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
|
*
|
|
@@ -422,6 +452,15 @@ export declare class MegaHttpClient {
|
|
|
422
452
|
downloadImage(url: string, p2pDid?: string): Promise<Buffer>;
|
|
423
453
|
/** The security-app data host for this region (face recognition, media, etc.). */
|
|
424
454
|
private securityAppHost;
|
|
455
|
+
/**
|
|
456
|
+
* The device's long-lived P-256 public key, as the cloud publishes it for key-wrapped commands
|
|
457
|
+
* (`type=2`): 64 bytes hex with no `04` prefix. A plain authed GET on the security-app host — the
|
|
458
|
+
* one read here that is neither signed nor body-encrypted, matching how the app fetches it.
|
|
459
|
+
* Cached per serial for the client's lifetime; the key does not rotate in normal operation.
|
|
460
|
+
*/
|
|
461
|
+
getDevicePublicKey(deviceSn: string): Promise<string>;
|
|
462
|
+
/** Per-serial cache for {@link getDevicePublicKey}. */
|
|
463
|
+
private readonly devicePublicKeys;
|
|
425
464
|
/**
|
|
426
465
|
* Signed+encrypted POST to the security-app data host (face recognition, etc.).
|
|
427
466
|
* Despite the different host, these endpoints use the SAME algo_ecdh pipeline as
|
|
@@ -563,7 +602,9 @@ export declare class MegaHttpClient {
|
|
|
563
602
|
* Structural rather than a list of paths: a login round trip is itself an authenticated call while a limited
|
|
564
603
|
* token is held, so re-logging in from inside one would recurse — and a path list would have to be
|
|
565
604
|
* 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.
|
|
605
|
+
* is part-way through, and restarting it silently would discard it. A re-login that already came back needing
|
|
606
|
+
* a captcha or a 2FA code would need the same again, and each attempt sends the account another code, so
|
|
607
|
+
* none is tried until a login completes or a stored session is adopted. No credentials means nothing to try,
|
|
567
608
|
* so the rejection surfaces instead.
|
|
568
609
|
*/
|
|
569
610
|
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>>;
|
|
@@ -100,6 +100,22 @@ export interface EciesUnwrapOptions {
|
|
|
100
100
|
* decrypted bytes, or `undefined` on any parse / HMAC / cipher failure (fails closed, never throws).
|
|
101
101
|
*/
|
|
102
102
|
export declare function eciesUnwrap(envelope: Buffer, eccPrivateKeyHex: string, options?: EciesUnwrapOptions): Buffer | undefined;
|
|
103
|
+
/** The two random inputs of {@link eciesWrap}, injectable so a spec can pin an envelope's bytes. */
|
|
104
|
+
export interface EciesWrapOptions {
|
|
105
|
+
/** The ephemeral P-256 private key for the exchange (32 bytes); generated fresh when absent. */
|
|
106
|
+
ephemeralPrivateKey?: Buffer;
|
|
107
|
+
/** The AES-128-CBC IV, which the envelope carries (16 bytes); random when absent. */
|
|
108
|
+
iv?: Buffer;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The sealing counterpart of {@link eciesUnwrap} with `verifyHmac` and `pkcs7` both set.
|
|
112
|
+
*
|
|
113
|
+
* Seals `plaintext` for `peerPublicKey` (a P-256 point, compressed or uncompressed) as
|
|
114
|
+
* `ephemeralPub(33, compressed) ‖ iv(16) ‖ ct ‖ HMAC(32)`: `S = ECDH_X(ephemeral, peer)` →
|
|
115
|
+
* `kdf = eufyKDF(S)` → `ct = AES-128-CBC(kdf[0:16], iv, PKCS7(plaintext))`, tagged
|
|
116
|
+
* `HMAC-SHA256(kdf[16:48], iv ‖ ct)`.
|
|
117
|
+
*/
|
|
118
|
+
export declare function eciesWrap(plaintext: Buffer, peerPublicKey: Buffer, options?: EciesWrapOptions): Buffer;
|
|
103
119
|
/**
|
|
104
120
|
* Derive the P2P **level-2** (signCode 2/8) AES-256-GCM session key from a *decrypted*
|
|
105
121
|
* `CMD_GATEWAYINFO` (1100) payload and the station's ECC private key (from `get_ciphers`).
|
|
@@ -111,6 +127,22 @@ export declare function eciesUnwrap(envelope: Buffer, eccPrivateKeyHex: string,
|
|
|
111
127
|
* Returns the 32-byte key, or undefined if the inputs don't parse.
|
|
112
128
|
*/
|
|
113
129
|
export declare function deriveLevel2KeyFromGatewayInfo(gwPayload: Buffer, eccPrivateKeyHex: string): Buffer | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* Derive the P2P **level-2** AES-128-ECB session key from a *decrypted* `CMD_GATEWAYINFO` (1100)
|
|
132
|
+
* payload and the RSA private key of a cipher whose `get_ciphers` answer carries no ECC key.
|
|
133
|
+
*
|
|
134
|
+
* Layout of the decrypted payload: `cipher_id(2 LE) ‖ 0000 ‖ RSA ciphertext ‖ …`, the ciphertext as
|
|
135
|
+
* long as the key's modulus. It is the 16-byte session key under PKCS#1 v1.5 encryption padding
|
|
136
|
+
* (`00 02 ‖ ≥8 non-zero bytes ‖ 00 ‖ key`). The padding is checked here over the raw RSA result so a wrong
|
|
137
|
+
* key fails closed: under OpenSSL's implicit rejection, `RSA_PKCS1_PADDING` answers random bytes for it
|
|
138
|
+
* instead of throwing, and those can be 16 bytes long.
|
|
139
|
+
*
|
|
140
|
+
* `rsaPrivateKey` is the `get_ciphers` `private_key`: base64 PKCS#8 DER.
|
|
141
|
+
*
|
|
142
|
+
* Returns the 16-byte key, or `undefined` when the key does not parse, the payload is short, or the
|
|
143
|
+
* padding or key length is wrong (fails closed, never throws).
|
|
144
|
+
*/
|
|
145
|
+
export declare function deriveLevel2KeyFromRsaGatewayInfo(gwPayload: Buffer, rsaPrivateKey: string): Buffer | undefined;
|
|
114
146
|
/** Local-lookup payload: two zero bytes. */
|
|
115
147
|
export declare function buildLocalLookupPayload(): Buffer;
|
|
116
148
|
/** 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,10 +108,21 @@ 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;
|
|
115
|
+
/**
|
|
116
|
+
* The `seq_num` last sent to each device over the keyed-payload wire. The device holds a mark for the life
|
|
117
|
+
* of a P2P connection: the first keyed command on a new connection is accepted at whatever number it
|
|
118
|
+
* carries and becomes the mark, and after that a number not above the mark is refused with the mark
|
|
119
|
+
* published ({@link keyedPayloadSeqMark}). Each accepted command moves this to its number, a refusal
|
|
120
|
+
* moves it to the published mark + 1, and a device absent here has not been sent one in this process and
|
|
121
|
+
* seeds from a small random number — which a new connection takes as sent.
|
|
122
|
+
*/
|
|
123
|
+
private readonly keyedSequences;
|
|
124
|
+
/** The keyed-payload command in flight per device, which the next one for that device waits on. */
|
|
125
|
+
private readonly keyedQueue;
|
|
126
126
|
constructor(deps: P2PRouterDeps);
|
|
127
127
|
/** Forward one P2P failure once even when both the session listener and startup waiter observe it. */
|
|
128
128
|
private reportError;
|
|
@@ -224,7 +224,7 @@ export declare class P2PCommandRouter {
|
|
|
224
224
|
* Build + wire a {@link P2PSession} for a station (NOT yet connected — the caller awaits `connect()`).
|
|
225
225
|
*
|
|
226
226
|
* `resolveCipherKey` auto-negotiates the level-2 session key from `CMD_GATEWAYINFO` by resolving the
|
|
227
|
-
* cipher's
|
|
227
|
+
* cipher's private keys via cloud `get_ciphers`, so signCode 2/8 frames decrypt live; results are
|
|
228
228
|
* cached on the router instance so a lazy re-open (after idle-detach) reuses the lookup. Only a
|
|
229
229
|
* SUCCESSFUL lookup is cached — caching `undefined` after a transient failure would permanently
|
|
230
230
|
* disable level-2 for the session's life.
|
|
@@ -240,11 +240,12 @@ export declare class P2PCommandRouter {
|
|
|
240
240
|
*/
|
|
241
241
|
private makeSession;
|
|
242
242
|
/**
|
|
243
|
-
* The ECC private
|
|
244
|
-
*
|
|
245
|
-
*
|
|
243
|
+
* The ECC and RSA private keys of `cipherId` (`ecc_private_key`, `private_key`), from the router's cache
|
|
244
|
+
* or one `get_ciphers` call. When the cloud answers with another cipher that carries an ECC key and the
|
|
245
|
+
* asked one does not, that one is used and traced on `session`. Only a successful lookup — one that
|
|
246
|
+
* yields either key — is cached.
|
|
246
247
|
*/
|
|
247
|
-
private
|
|
248
|
+
private cipherKeysFor;
|
|
248
249
|
/**
|
|
249
250
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
250
251
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -272,7 +273,7 @@ export declare class P2PCommandRouter {
|
|
|
272
273
|
* A {@link MediaProvider} bound to one serial — resolves the session then calls `p2p/media`.
|
|
273
274
|
*
|
|
274
275
|
* Every live egress here is a consumer of the SAME shared pull, so N `live()` calls collapse to one
|
|
275
|
-
* PPCS session and a live snapshot against a warm
|
|
276
|
+
* PPCS session and a live snapshot against a warm shared source costs no extra pull at all; a
|
|
276
277
|
* cold source warms one and waits for a clean keyframe. Each shared egress passes its complete options
|
|
277
278
|
* through because any of them may create the source, whose power and retention hints are fixed for
|
|
278
279
|
* everyone who joins later. The bounded {@link MediaProvider.record} clip is the exception: it opens its
|
|
@@ -311,6 +312,11 @@ export declare class P2PCommandRouter {
|
|
|
311
312
|
* stream costs nothing extra and a talkback on an otherwise idle camera opens the session it needs
|
|
312
313
|
* instead of playing into silence.
|
|
313
314
|
*
|
|
315
|
+
* **On a model in {@link ELD_TALKBACK_MODELS}, the speaker plays the codec the camera sends.** So the first
|
|
316
|
+
* audio frame of the media session this talkback holds decides its codec ({@link speakerCodec}), within
|
|
317
|
+
* {@link TALKBACK_CODEC_WAIT_MS} of the session's first frame, and the claim is re-checked after that wait as
|
|
318
|
+
* after the warm-up. Every other model opens `aac-lc` at once.
|
|
319
|
+
*
|
|
314
320
|
* The level-2 key is waited for softly: only the HomeBase-attached path requires it, and
|
|
315
321
|
* {@link Talkback.start} reports that failure precisely, so a hard wait here would reject an
|
|
316
322
|
* own-session camera that legitimately never negotiates one.
|
|
@@ -456,15 +462,6 @@ export declare class P2PCommandRouter {
|
|
|
456
462
|
* A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
|
|
457
463
|
* unaffected.
|
|
458
464
|
*/
|
|
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
465
|
/**
|
|
469
466
|
* Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
|
|
470
467
|
* on a session goes with it, and a media session opened for this source alone is closed: nothing else
|
|
@@ -524,11 +521,11 @@ export declare class P2PCommandRouter {
|
|
|
524
521
|
*/
|
|
525
522
|
private onLiveStartFailed;
|
|
526
523
|
/**
|
|
527
|
-
* Replace the session under a warming source
|
|
524
|
+
* Replace the session under a warming source it is not serving, and warm again on it.
|
|
528
525
|
*
|
|
529
526
|
* Only a STANDALONE device's session is replaced, for the reason {@link onLiveStartFailed} gives: an attached
|
|
530
527
|
* 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.
|
|
528
|
+
* drop the rest. Such a source keeps the re-issue it always had, which the answer (false) tells it.
|
|
532
529
|
*
|
|
533
530
|
* The source is left warming throughout, holding the deadline it started, so this either produces a stream
|
|
534
531
|
* within that window or fails exactly as it would have. A replacement that cannot be opened leaves the
|
|
@@ -562,6 +559,7 @@ export declare class P2PCommandRouter {
|
|
|
562
559
|
*/
|
|
563
560
|
p2pQuery(sn: string, subCmd: number, opts?: {
|
|
564
561
|
timeoutMs?: number;
|
|
562
|
+
payload?: Record<string, unknown>;
|
|
565
563
|
}): Promise<Record<string, unknown>>;
|
|
566
564
|
/**
|
|
567
565
|
* **Generic control-payload request/reply query.** Sends a `CONTROL_PAYLOAD` (1700) `{commandType,
|
|
@@ -690,7 +688,7 @@ export declare class P2PCommandRouter {
|
|
|
690
688
|
* **`set-json-raw` intent** — bare JSON, no envelope: outer P2P cmd = `outerCmd` itself, plaintext
|
|
691
689
|
* exactly `{account_id,...data}` (`session.sendRawLevel2` with no wrapper). Reversed from a live
|
|
692
690
|
* 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.
|
|
691
|
+
* (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.SET_ALL_ACTION`) reuses
|
|
694
692
|
* the same bare-JSON shape.
|
|
695
693
|
*/
|
|
696
694
|
private sendJsonRaw;
|
|
@@ -705,7 +703,9 @@ export declare class P2PCommandRouter {
|
|
|
705
703
|
* HomeBase-attached device takes the GCM signCode-8 form, a standalone one the level-1 form. A
|
|
706
704
|
* standalone camera never negotiates a level-2 key, so pinning this to level 2 makes the envelope
|
|
707
705
|
* 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
|
|
706
|
+
* camera accepts the level-1 form. With no `form` (default) it stays level-2 only and waits for the
|
|
707
|
+
* key every level-2 sender uses; `level2Sender: "set-payload"` also takes an RSA cipher's AES-128-ECB
|
|
708
|
+
* key (see {@link P2PSession.sendControlLevel2}). A `resolved` session keeps the wait its caller gave it.
|
|
709
709
|
*
|
|
710
710
|
* Both seals REPLAY the frame {@link DIRECT_CMD_SENDS}× at 200ms, as every other fire-and-forget
|
|
711
711
|
* control on this router does: these are unacknowledged datagrams, and a level-1 device is the one
|
|
@@ -715,6 +715,68 @@ export declare class P2PCommandRouter {
|
|
|
715
715
|
* boolean, so the first pass carries the failure and the rest are repeats.
|
|
716
716
|
*/
|
|
717
717
|
private sendSetPayloadEnvelope;
|
|
718
|
+
/**
|
|
719
|
+
* **`keyed-payload-actuate` intent** — the classic Wi-Fi lock's actuation: the sealed fields from
|
|
720
|
+
* {@link buildKeyedActuatePayload}, sent as a keyed envelope through {@link P2PSession.sendSetPayload}
|
|
721
|
+
* on the lock's device channel. No session key is waited for — the envelope seals its own per-command
|
|
722
|
+
* key for the device's public key (fetched from the cloud, cached per serial), so it goes as soon as
|
|
723
|
+
* the session is up. The device shows the acting member by display name, which lives on the device
|
|
724
|
+
* record's `member` rather than in the intent, so it is read here and falls back to the account name
|
|
725
|
+
* the intent carries.
|
|
726
|
+
*
|
|
727
|
+
* The device answers, so the outcome is read rather than assumed: {@link awaitKeyedReply} settles each
|
|
728
|
+
* send on the replies, and resolves a {@link KEYED_PAYLOAD_SEQ_ERROR} refusal with the mark the device
|
|
729
|
+
* publishes in the refusal's `CMD_DOORLOCK_P2P_SEQ` (1916) twin. On that mark the command resends once,
|
|
730
|
+
* at mark + 1; a second refusal, or a refusal that published no mark, throws. Commands run one at a time
|
|
731
|
+
* per device: the replies carry no correlation id, so one command's answer must never be read as
|
|
732
|
+
* another's.
|
|
733
|
+
*/
|
|
734
|
+
private sendKeyedPayloadActuate;
|
|
735
|
+
/** Run `work` once every keyed-payload command already queued for `sn` has settled. */
|
|
736
|
+
private serializeKeyed;
|
|
737
|
+
/**
|
|
738
|
+
* Send one keyed command at the device's next sequence and, on a sequence refusal, resend once at the
|
|
739
|
+
* mark the refusal published.
|
|
740
|
+
*
|
|
741
|
+
* A device's first number in a process is a small random seed — the scale the vendor app's own counters
|
|
742
|
+
* run at — and every later one is the previous plus one. The device takes the first number on a new
|
|
743
|
+
* connection as sent, and from then on refuses one not above the mark it holds, stating the mark in the
|
|
744
|
+
* refusal's `CMD_DOORLOCK_P2P_SEQ` (1916) twin ({@link awaitKeyedReply} resolves the refusal with it).
|
|
745
|
+
* So a refusal is resolved to that mark and the command resent once at mark + 1, the number the device
|
|
746
|
+
* accepts after a refusal; a second refusal, or a refusal that carried no mark, throws. The accepted
|
|
747
|
+
* number is recorded, so the next command starts just above it.
|
|
748
|
+
*
|
|
749
|
+
* @param engage whether the command drives to the engaged (locked) state, which its own report states.
|
|
750
|
+
* @param send dispatches the envelope at the sequence it is given.
|
|
751
|
+
*/
|
|
752
|
+
private sendKeyedAtSequence;
|
|
753
|
+
/**
|
|
754
|
+
* Send one keyed-payload command and read the device's answer for it from the replies on the command's
|
|
755
|
+
* channel: the `SET_PAYLOAD` echo (a `commandResult`), at once, carrying the result as an int32 — or,
|
|
756
|
+
* when the echo was not read and the command changed the device's state, the `NOTIFY_PAYLOAD` about two
|
|
757
|
+
* seconds later carrying the result again as a `P2P_QUERY_STATUS_IN_LOCK` (1955) document. A report
|
|
758
|
+
* is tied to the command by its `slState`: only one stating the state this command drives to, by
|
|
759
|
+
* `engage` ({@link keyedPayloadReportCode}), is read as its answer. A report of the other state is a
|
|
760
|
+
* previous command's, landing after that command settled on its echo, and is ignored.
|
|
761
|
+
*
|
|
762
|
+
* - An accepted result (code `0`) settles the command.
|
|
763
|
+
* - A sequence refusal is stated twice, both about a tenth of a second after the send: the `-151` echo
|
|
764
|
+
* and a `CMD_DOORLOCK_P2P_SEQ` (1916) notify whose `seq_num` is the mark the device holds. The
|
|
765
|
+
* resolved value carries that mark, so the caller resends just above it. Collection runs for
|
|
766
|
+
* {@link KEYED_REFUSAL_TWIN_MS} after the first of the two, so both land and neither is left on the
|
|
767
|
+
* channel for the resend to read as its own answer, and either one alone still resolves the refusal.
|
|
768
|
+
* - No reply within {@link KEYED_REPLY_MS} is a command that never landed, and rejects.
|
|
769
|
+
*
|
|
770
|
+
* Both listeners are armed before the send, and only replies on `channel` are read. The channel names
|
|
771
|
+
* the device, not the command: every other P2P write a lock takes waits for a level-2 key this device
|
|
772
|
+
* never negotiates, so nothing else the SDK sends can draw a reply here, and a future intent on this
|
|
773
|
+
* wrapper has to share {@link serializeKeyed}.
|
|
774
|
+
*/
|
|
775
|
+
private awaitKeyedReply;
|
|
776
|
+
/** How long a keyed-payload command waits for any reply before it is taken as never having landed. */
|
|
777
|
+
private static readonly KEYED_REPLY_MS;
|
|
778
|
+
/** How long a refusal collects its two statements — the `-151` echo and the mark notify — before resolving. */
|
|
779
|
+
private static readonly KEYED_REFUSAL_TWIN_MS;
|
|
718
780
|
/**
|
|
719
781
|
* **`ff09-actuate` intent** — build the `ff09` AES-128-CBC frame ({@link buildFf09Frame}, shared with the
|
|
720
782
|
* MQTT transport — see `transport/ff09.ts`) and dispatch it in the `1940` TRANSFER_PAYLOAD envelope
|
|
@@ -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;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Ff09Identity } from "../../core/contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Keyed-payload envelope — the `SET_PAYLOAD` (1350) shape the classic Wi-Fi smart lock takes for its
|
|
4
|
+
* actuation. There is no session key: every command carries its OWN fresh AES-128 key, sealed for the
|
|
5
|
+
* device's long-lived P-256 public key (published by the cloud, one per device), and the inner JSON is
|
|
6
|
+
* AES-128-CBC/PKCS#7 under that key. This module builds the two sealed fields; `P2PSession.sendSetPayload`
|
|
7
|
+
* wraps them in the ordinary envelope and carries a keyed one in the clear, as the vendor app sends it.
|
|
8
|
+
*
|
|
9
|
+
* ## Wire
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* { key: <keyBlobHex>, account_id: <adminUserId>, cmd: <opcode>, mChannel: <ch>, mValue3: 0,
|
|
13
|
+
* payload: <base64(AES-128-CBC(aesKey, iv, innerJson))> }
|
|
14
|
+
* ```
|
|
15
|
+
* `iv` = ASCII(deviceSn) zero-padded/truncated to 16 bytes. `aesKey` = 16 random bytes, spelled as 32
|
|
16
|
+
* UPPERCASE hex characters — the hex SPELLING is what the key blob seals, not the raw bytes.
|
|
17
|
+
*
|
|
18
|
+
* `key` (hex) is the ECIES envelope {@link eciesWrap} produces for the device's public key over that
|
|
19
|
+
* spelling: `compressedEphemeralPub(33) ‖ nonce(16) ‖ sealedKey(48) ‖ hmac(32)`.
|
|
20
|
+
*
|
|
21
|
+
* Inner JSON for the actuation opcode, `P2P_ON_OFF_LOCK` (1961): `{shortUserId, slOperation: 1|0, userId,
|
|
22
|
+
* userName, seq_num}`.
|
|
23
|
+
*
|
|
24
|
+
* ## Answer
|
|
25
|
+
*
|
|
26
|
+
* The device answers every command at once with an int32 result on the `SET_PAYLOAD` echo — `0`
|
|
27
|
+
* accepted, {@link KEYED_PAYLOAD_SEQ_ERROR} when `seq_num` is not above the mark it holds — and, when the
|
|
28
|
+
* command changed its state, about two seconds later with a `NOTIFY_PAYLOAD` whose JSON carries
|
|
29
|
+
* `P2P_QUERY_STATUS_IN_LOCK` (1955) as `cmd`, the result again under `payload.code` and the state reached
|
|
30
|
+
* under `payload.slState` ({@link keyedPayloadReportCode} reads the code of the report stating the state
|
|
31
|
+
* a command drives to). A command it was already in the state of draws the echo alone.
|
|
32
|
+
*
|
|
33
|
+
* A refusal carries a second `NOTIFY_PAYLOAD`, `CMD_DOORLOCK_P2P_SEQ` (1916), whose `payload.seq_num`
|
|
34
|
+
* ({@link keyedPayloadSeqMark}) is the mark the device holds — so a refused command hands back the exact
|
|
35
|
+
* number to resend above, with no searching. The mark lives with the P2P connection: the first keyed
|
|
36
|
+
* command on a new connection is accepted at whatever number it carries.
|
|
37
|
+
*
|
|
38
|
+
* The command ids are {@link CommandType}'s; the inner field names follow a third-party reverse-engineering
|
|
39
|
+
* project, a name source only.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* The int32 result the device answers a command with when its `seq_num` is not above the mark it holds for
|
|
43
|
+
* the connection. The command is dropped untried, so this is a retry signal rather than a failure: resend
|
|
44
|
+
* above the mark. The refusal publishes that mark in its `CMD_DOORLOCK_P2P_SEQ` (1916) twin
|
|
45
|
+
* ({@link keyedPayloadSeqMark}), so the resend goes to the next number rather than searching for it.
|
|
46
|
+
*/
|
|
47
|
+
export declare const KEYED_PAYLOAD_SEQ_ERROR = -151;
|
|
48
|
+
/** Knobs a spec injects to make the otherwise random envelope deterministic, plus the sequence it carries. */
|
|
49
|
+
export interface KeyedPayloadRandomness {
|
|
50
|
+
/** The per-command AES key (16 bytes). */
|
|
51
|
+
aesKey?: Buffer;
|
|
52
|
+
/** The CBC nonce sealing that key inside the blob (16 bytes). */
|
|
53
|
+
nonce?: Buffer;
|
|
54
|
+
/** The ephemeral P-256 private key for the ECDH exchange (32 bytes). */
|
|
55
|
+
ephemeralPrivateKey?: Buffer;
|
|
56
|
+
/** The `seq_num` the inner JSON carries, issued by the sender. */
|
|
57
|
+
seqNum: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The cipher IV the device derives from its own serial: ASCII bytes, zero-padded to 16. It is the same for
|
|
61
|
+
* every command to a device, which is sound only because {@link buildKeyedActuatePayload} seals each command
|
|
62
|
+
* under a key minted for it alone — no two messages ever share a key with this IV.
|
|
63
|
+
*/
|
|
64
|
+
export declare function serialIv(deviceSn: string): Buffer;
|
|
65
|
+
/** Spell 16 key bytes as the 32 uppercase hex characters the wire seals. */
|
|
66
|
+
export declare function spellAesKey(key: Buffer): string;
|
|
67
|
+
/**
|
|
68
|
+
* Seal a per-command AES key for the device's P-256 public key: the {@link eciesWrap} envelope over the
|
|
69
|
+
* key's hex spelling, as hex.
|
|
70
|
+
* @param aesKeyHex the 32-hex-char key spelling (what gets sealed — as ASCII).
|
|
71
|
+
* @param devicePublicKeyHex the device's uncompressed public key WITHOUT the `04` prefix (64 bytes hex).
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildKeyBlob(aesKeyHex: string, devicePublicKeyHex: string, rnd?: Pick<KeyedPayloadRandomness, "nonce" | "ephemeralPrivateKey">): string;
|
|
74
|
+
/** AES-128-CBC/PKCS#7 the inner JSON under the per-command key, IV from the serial; base64 out. */
|
|
75
|
+
export declare function sealInnerPayload(innerJson: string, aesKeyHex: string, deviceSn: string): string;
|
|
76
|
+
/** The two sealed fields of a keyed envelope; the sender supplies the rest of the `SET_PAYLOAD` value. */
|
|
77
|
+
export interface KeyedActuatePayload {
|
|
78
|
+
/** The sealed per-command key — the value's `key` field, hex. */
|
|
79
|
+
key: string;
|
|
80
|
+
/** The inner JSON sealed under that key — the value's `payload` field, base64. */
|
|
81
|
+
payload: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the sealed fields of the classic Wi-Fi lock's actuation.
|
|
85
|
+
* @param id the acting identity + direction.
|
|
86
|
+
* @param devicePublicKeyHex the device's public key from the cloud (no `04` prefix).
|
|
87
|
+
* @param userName the acting member's display name as the device shows it in its log.
|
|
88
|
+
*/
|
|
89
|
+
export declare function buildKeyedActuatePayload(id: Ff09Identity, devicePublicKeyHex: string, userName: string, rnd: KeyedPayloadRandomness): KeyedActuatePayload;
|
|
90
|
+
/**
|
|
91
|
+
* The result a `NOTIFY_PAYLOAD` document reports for a keyed command driving to a state. The device sends
|
|
92
|
+
* one once a keyed command has changed its state, `{cmd, payload: {code, slState, …}}` with `cmd`
|
|
93
|
+
* `P2P_QUERY_STATUS_IN_LOCK` (1955) in {@link CommandType}, `payload.code` being the command's result and
|
|
94
|
+
* `payload.slState` the state reached. Answers `payload.code` when it is a number and `payload.slState` is
|
|
95
|
+
* the state the command drives to: `"4"` engaged (locked) when `engage`, `"3"` released (unlocked) when
|
|
96
|
+
* not, the values of the lock's state param 6000 as the device spells them. Answers `undefined` for any
|
|
97
|
+
* other document, a report of the other state included: that one answers some other command.
|
|
98
|
+
*/
|
|
99
|
+
export declare function keyedPayloadReportCode(json: {
|
|
100
|
+
cmd?: number;
|
|
101
|
+
payload?: unknown;
|
|
102
|
+
} | undefined, engage: boolean): number | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* The sequence mark a `NOTIFY_PAYLOAD` document publishes. The one that rides a sequence refusal is
|
|
105
|
+
* `{cmd, payload: {lock_cmd, seq_num}}` with `cmd` `CMD_DOORLOCK_P2P_SEQ` (1916) in {@link CommandType},
|
|
106
|
+
* `payload.seq_num` being the mark the device holds on this connection. Answers that number, or
|
|
107
|
+
* `undefined` for any other document.
|
|
108
|
+
*/
|
|
109
|
+
export declare function keyedPayloadSeqMark(json: {
|
|
110
|
+
cmd?: number;
|
|
111
|
+
payload?: unknown;
|
|
112
|
+
} | undefined): number | undefined;
|
|
@@ -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.
|
|
@@ -24,11 +24,9 @@ import type { SharedLiveSource } from "./shared-live-source.js";
|
|
|
24
24
|
export declare function openLiveStream(session: P2PSession, opts?: LiveStreamOptions): Promise<LiveStream>;
|
|
25
25
|
/**
|
|
26
26
|
* **Live snapshot off a SHARED source** (V6) — snapshot as just another consumer of the shared live
|
|
27
|
-
* pull.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* keyframe (the first IDR after a cold start is frequently partial, so skip it by default). Requires
|
|
31
|
-
* `ffmpeg` for the Annex-B → JPEG decode.
|
|
27
|
+
* pull. A warm source reuses its upstream session; collection starts from live keyframes rather than
|
|
28
|
+
* a retained picture. A live source captures its next keyframe by default; a cold source skips the
|
|
29
|
+
* first keyframe because its first IDR is frequently partial. Requires `ffmpeg` for the Annex-B → JPEG decode.
|
|
32
30
|
*
|
|
33
31
|
* The returned dimensions are the ENCODED IMAGE's own, read back out of it — never the frame header's.
|
|
34
32
|
* The header states the stream's geometry when the capture started, and a stream that reconfigures
|